Chromium Code Reviews| Index: tracing/tracing/metrics/webrtc/webrtc_rendering_metric.html |
| diff --git a/tracing/tracing/metrics/webrtc/webrtc_rendering_metric.html b/tracing/tracing/metrics/webrtc/webrtc_rendering_metric.html |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..8747508f2c6add43801ccbf29b5802fbbcb95a6f |
| --- /dev/null |
| +++ b/tracing/tracing/metrics/webrtc/webrtc_rendering_metric.html |
| @@ -0,0 +1,339 @@ |
| +<!DOCTYPE html> |
| +<!-- |
| +Copyright 2017 The Chromium Authors. All rights reserved. |
| +Use of this source code is governed by a BSD-style license that can be |
| +found in the LICENSE file. |
| +--> |
| + |
| +<link rel="import" href="/tracing/base/range.html"> |
| +<link rel="import" href="/tracing/base/unit.html"> |
| +<link rel="import" href="/tracing/base/utils.html"> |
| +<link rel="import" href="/tracing/metrics/metric_registry.html"> |
| +<link rel="import" href="/tracing/metrics/v8/utils.html"> |
| +<link rel="import" href="/tracing/value/histogram.html"> |
| + |
| +<script> |
| +'use strict'; |
| + |
| +tr.exportTo('tr.metrics.webrtc', function() { |
| + const DISPLAY_HERTZ = 60.0; |
| + const VSYNC_DURATION_US = 1e6 / DISPLAY_HERTZ; |
| + // How much more severe is a 'Badly out of sync' render event compared to an |
| + // 'Out of sync' one when calculating the smoothness score. |
| + const SEVERITY = 3; |
| + // How many vsyncs a frame should be displayed to be considered frozen. |
| + const FROZEN_FRAME_VSYNC_COUNT_THRESHOLD = 6; |
| + |
| + const WEB_MEDIA_PLAYER_UPDATE_TITLE = 'WebMediaPlayerMS::UpdateCurrentFrame'; |
| + // These four are args for WebMediaPlayerMS update events. |
| + const IDEAL_RENDER_INSTANT_NAME = 'Ideal Render Instant'; |
| + const ACTUAL_RENDER_BEGIN_NAME = 'Actual Render Begin'; |
| + const ACTUAL_RENDER_END_NAME = 'Actual Render End'; |
| + // The events of interest have a 'Serial' argument which represents the |
| + // stream ID. |
| + const STREAM_ID_NAME = 'Serial'; |
| + |
| + const REQUIRED_EVENT_ARGS_NAMES = [ |
| + IDEAL_RENDER_INSTANT_NAME, ACTUAL_RENDER_BEGIN_NAME, ACTUAL_RENDER_END_NAME, |
| + STREAM_ID_NAME |
| + ]; |
| + |
| + const count_smallerIsBetter = |
| + tr.b.Unit.byName.count_smallerIsBetter; |
| + const percentage_biggerIsBetter = |
| + tr.b.Unit.byName.normalizedPercentage_biggerIsBetter; |
| + const percentage_smallerIsBetter = |
| + tr.b.Unit.byName.normalizedPercentage_smallerIsBetter; |
| + const timeDurationInMs_smallerIsBetter = |
| + tr.b.Unit.byName.timeDurationInMs_smallerIsBetter; |
| + const unitlessNumber_biggerIsBetter = |
| + tr.b.Unit.byName.unitlessNumber_biggerIsBetter; |
| + |
| + /* |
| + * Verify that the event is a valid event. |
| + * |
| + * An event is valid if it is a WebMediaPlayerMS::UpdateCurrentFrame event, |
| + * and has all of the mandatory arguments. See MANDATORY above. |
| + */ |
| + function isValidEvent(event) { |
| + if (event.title !== WEB_MEDIA_PLAYER_UPDATE_TITLE || !event.args) { |
| + return false; |
| + } |
| + for (let parameter of REQUIRED_EVENT_ARGS_NAMES) { |
| + if (!(parameter in event.args)) { |
| + return false; |
| + } |
| + } |
| + return true; |
| + } |
| + |
| + function webrtcRenderingMetric(histograms, model) { |
| + tr.metrics.v8.utils.groupAndProcessEvents(model, |
|
nednguyen
2017/03/09 19:21:56
Actually, why is this metric reusing a v8 metric u
|
| + isValidEvent, |
| + event => event.args[STREAM_ID_NAME], |
| + (streamName, events) => getTimeStats(histograms, streamName, events) |
| + ); |
| + } |
| + |
| + tr.metrics.MetricRegistry.register(webrtcRenderingMetric); |
| + |
| + function addHistogram(samples, histograms, name, unit, opt_summaryOptions) { |
| + let summaryOptions = opt_summaryOptions; |
| + if (!summaryOptions) { |
| + // By default, we store a single value, so we only need one of the |
| + // statistics to keep track. We choose the average for that. |
| + summaryOptions = { |
| + count: false, |
| + max: false, |
| + min: false, |
| + std: false, |
| + sum: false, |
| + }; |
| + } |
| + let histogram = new tr.v.Histogram(name, unit); |
| + for (let sample of samples) { |
| + histogram.addSample(sample); |
| + } |
| + histogram.customizeSummaryOptions(summaryOptions); |
| + histograms.addHistogram(histogram); |
| + } |
| + |
| + function getTimeStats(histograms, streamName, events) { |
| + let frameHist = getFrameDistribution(histograms, events); |
| + addFpsFromFrameDistribution(histograms, frameHist); |
| + addFreezingScore(histograms, frameHist); |
| + let driftTimeStats = getDriftStats(events); |
| + addHistogram(driftTimeStats.driftTime, histograms, |
| + 'WebRTCRendering_drift_time', timeDurationInMs_smallerIsBetter, |
| + {count: false, min: false, percentile: [0.75, 0.9]}); |
| + addHistogram([driftTimeStats.renderingLengthError], histograms, |
| + 'WebRTCRendering_rendering_length_error', percentage_smallerIsBetter); |
| + let smoothnessStats = getSmoothnessStats(driftTimeStats.driftTime); |
| + addHistogram([smoothnessStats.percentBadlyOutOfSync], histograms, |
| + 'WebRTCRendering_percent_badly_out_of_sync', |
| + percentage_smallerIsBetter); |
| + addHistogram([smoothnessStats.percentOutOfSync], histograms, |
| + 'WebRTCRendering_percent_out_of_sync', percentage_smallerIsBetter); |
| + addHistogram([smoothnessStats.smoothnessScore], histograms, |
| + 'WebRTCRendering_smoothness_score', percentage_biggerIsBetter); |
| + addHistogram([smoothnessStats.framesOutOfSync], histograms, |
| + 'WebRTCRendering_frames_out_of_sync', count_smallerIsBetter); |
| + addHistogram([smoothnessStats.framesSeverelyOutOfSync], histograms, |
| + 'WebRTCRendering_frames_badly_out_of_sync', count_smallerIsBetter); |
| + } |
| + |
| + /** |
| + * Create the frame distribution. |
| + * |
| + * If the overall display distribution is A1:A2:..:An, this will tell how |
| + * many times a frame stays displayed during Ak*VSYNC_DURATION_US, also known |
| + * as 'source to output' distribution. |
| + * |
| + * In other terms, a distribution B where |
| + * B[k] = number of frames that are displayed k times. |
| + * |
| + * @param {tr.v.HistogramSet} histograms |
| + * @param {Array.<event>} events - An array of events. |
| + * @returns {tr.v.Histogram} frameHist - The frame distribution. |
| + */ |
| + function getFrameDistribution(histograms, events) { |
| + const cadence = tr.b.runLengthEncoding( |
| + events.map(e => e.args[IDEAL_RENDER_INSTANT_NAME])); |
| + const frameHist = new tr.v.Histogram('WebRTCRendering_frame_distribution', |
| + count_smallerIsBetter, |
| + tr.v.HistogramBinBoundaries.createLinear(1, 50, 49)); |
| + for (const ticks of cadence) { |
| + frameHist.addSample(ticks.count); |
| + } |
| + frameHist.customizeSummaryOptions({percentile: [0.75, 0.9]}); |
| + histograms.addHistogram(frameHist); |
| + return frameHist; |
| + } |
| + |
| + /** |
| + * Calculate the apparent FPS from frame distribution. |
| + * |
| + * Knowing the display frequency and the frame distribution, it is possible to |
| + * calculate the video apparent frame rate as played by WebMediaPlayerMs |
| + * module. |
| + * |
| + * @param {tr.v.HistogramSet} histograms |
| + * @param {tr.v.Histogram} frameHist - The frame distribution. See |
| + * getFrameDistribution. |
| + */ |
| + function addFpsFromFrameDistribution(histograms, frameHist) { |
| + let numberFrames = 0; |
| + let numberVsyncs = 0; |
| + for (let ticks = 1; ticks < frameHist.allBins.length; ++ticks) { |
| + const count = frameHist.allBins[ticks].count; |
| + numberFrames += count; |
| + numberVsyncs += ticks * count; |
| + } |
| + let meanRatio = numberVsyncs / numberFrames; |
| + addHistogram([DISPLAY_HERTZ / meanRatio], histograms, 'WebRTCRendering_fps', |
| + unitlessNumber_biggerIsBetter); |
| + } |
| + |
| + /** |
| + * Returns the weighted penalty for a number of frozen frames. |
| + * |
| + * In a series of repeated frames of length > 5, all frames after the first |
| + * are considered frozen. Conversely, no frames in a series of repeated frames |
| + * of length <= 5 will be considered frozen. |
| + * |
| + * This means the weight for 0 to 4 frozen frames is 0. |
| + * |
| + * @param {Number} numberFrozenFrames - The number of frozen frames. |
| + * @returns {Number} - The weight penalty for the number of frozen frames. |
| + */ |
| + function frozenPenaltyWeight(numberFrozenFrames) { |
| + const penalty = { |
| + 5: 1, |
| + 6: 5, |
| + 7: 15, |
| + 8: 25 |
| + }; |
| + return penalty[numberFrozenFrames] || (8 * (numberFrozenFrames - 4)); |
| + } |
| + |
| + /** |
| + * Adds the freezing score. |
| + * |
| + * @param {tr.v.HistogramSet} histograms |
| + * @param {tr.v.Histogram} frameHist - The frame distribution. |
| + * See getFrameDistribution. |
| + */ |
| + function addFreezingScore(histograms, frameHist) { |
| + let numberVsyncs = 0; |
| + let freezingScore = 0; |
| + let frozenFramesCount = 0; |
| + for (let ticks = 1; ticks < frameHist.allBins.length; ++ticks) { |
| + const count = frameHist.allBins[ticks].count; |
| + numberVsyncs += ticks * count; |
| + if (ticks >= FROZEN_FRAME_VSYNC_COUNT_THRESHOLD) { |
| + // The first frame of the series is not considered frozen. |
| + frozenFramesCount += count * (ticks - 1); |
| + freezingScore += count * frozenPenaltyWeight(ticks - 1); |
| + } |
| + } |
| + freezingScore = 1 - freezingScore / numberVsyncs; |
| + if (freezingScore < 0) { |
| + freezingScore = 0; |
| + } |
| + addHistogram([frozenFramesCount], histograms, |
| + 'WebRTCRendering_frozen_frames_count', count_smallerIsBetter); |
| + addHistogram([freezingScore], histograms, 'WebRTCRendering_freezing_score', |
| + percentage_biggerIsBetter); |
| + } |
| + |
| + /** |
| + * Get the drift time statistics. |
| + * |
| + * This method will calculate: |
| + * - Drift Time: The difference between the Actual Render Begin and the Ideal |
| + * Render Instant for each event. |
| + * - Rendering Length Error: The alignment error of the Ideal Render |
| + * Instants. The Ideal Render Instants should be equally spaced by |
| + * intervals of length VSYNC_DURATION_US. The Rendering Length error |
| + * measures how much they are misaligned. |
| + * |
| + * @param {Array.<event>} events - An array of events. |
| + * @returns {Object.<Array.<Number>, Number>} - The drift time and rendering |
| + * length error. |
| + */ |
| + function getDriftStats(events) { |
| + let driftTime = []; |
| + let discrepancy = []; |
| + let oldIdealRender = 0; |
| + let expectedIdealRender = 0; |
| + |
| + for (let event of events) { |
| + let currentIdealRender = event.args[IDEAL_RENDER_INSTANT_NAME]; |
| + // The expected time of the next 'Ideal Render' event begins as the |
| + // current 'Ideal Render' time and increases by VSYNC_DURATION_US on every |
| + // frame. |
| + expectedIdealRender += VSYNC_DURATION_US; |
| + if (currentIdealRender === oldIdealRender) { |
| + continue; |
| + } |
| + let actualRenderBegin = event.args[ACTUAL_RENDER_BEGIN_NAME]; |
| + // When was the frame rendered vs. when it would've been ideal. |
| + driftTime.push(actualRenderBegin - currentIdealRender); |
| + // The discrepancy is the absolute difference between the current Ideal |
| + // Render and the expected Ideal Render. |
| + discrepancy.push(Math.abs(currentIdealRender - expectedIdealRender)); |
| + expectedIdealRender = currentIdealRender; |
| + oldIdealRender = currentIdealRender; |
| + } |
| + |
| + let discrepancySum = tr.b.Statistics.sum(discrepancy) - discrepancy[0]; |
| + let lastIdealRender = |
| + events[events.length - 1].args[IDEAL_RENDER_INSTANT_NAME]; |
| + let firstIdealRender = events[0].args[IDEAL_RENDER_INSTANT_NAME]; |
| + let idealRenderSpan = lastIdealRender - firstIdealRender; |
| + |
| + let renderingLengthError = discrepancySum / idealRenderSpan; |
| + |
| + return {driftTime, renderingLengthError}; |
| + } |
| + |
| + /** |
| + * Get the smoothness stats from the normalized drift time. |
| + * |
| + * This method will calculate the smoothness score, along with the percentage |
| + * of frames badly out of sync and the percentage of frames out of sync. |
| + * To be considered badly out of sync, a frame has to have missed rendering by |
| + * at least 2 * VSYNC_DURATION_US. |
| + * To be considered out of sync, a frame has to have missed rendering by at |
| + * least one VSYNC_DURATION_US. |
| + * The smoothness score is a measure of how out of sync the frames are. |
| + * |
| + * @param {Array.<Number>} driftTimes - See getDriftStats. |
| + * @returns {Object.<Number, Number, Number>} - The percentBadlyOutOfSync, |
| + * percentOutOfSync and smoothnesScore calculated from the driftTimes array. |
| + */ |
| + function getSmoothnessStats(driftTimes) { |
| + let meanDriftTime = tr.b.Statistics.mean(driftTimes); |
| + let normDriftTimes = driftTimes.map(driftTime => |
| + Math.abs(driftTime - meanDriftTime)); |
| + |
| + // How many times is a frame later/earlier than T=2*VSYNC_DURATION_US. Time |
| + // is in microseconds |
| + let framesSeverelyOutOfSync = normDriftTimes |
| + .filter(driftTime => driftTime > 2 * VSYNC_DURATION_US) |
| + .length; |
| + // How many times is a frame later/earlier than VSYNC_DURATION_US. |
| + let framesOutOfSync = normDriftTimes |
| + .filter(driftTime => driftTime > VSYNC_DURATION_US) |
| + .length; |
| + |
| + let percentBadlyOutOfSync = framesSeverelyOutOfSync / |
| + driftTimes.length; |
| + let percentOutOfSync = framesOutOfSync / driftTimes.length; |
| + |
| + let framesOutOfSyncOnlyOnce = framesOutOfSync - framesSeverelyOutOfSync; |
| + |
| + // Calculate smoothness metric. From the formula, we can see that smoothness |
| + // score can be negative. |
| + let smoothnessScore = 1 - (framesOutOfSyncOnlyOnce + |
| + SEVERITY * framesSeverelyOutOfSync) / driftTimes.length; |
| + |
| + // Minimum smoothness_score value allowed is zero. |
| + if (smoothnessScore < 0) { |
| + smoothnessScore = 0; |
| + } |
| + |
| + return { |
| + framesOutOfSync, |
| + framesSeverelyOutOfSync, |
| + percentBadlyOutOfSync, |
| + percentOutOfSync, |
| + smoothnessScore |
| + }; |
| + } |
| + |
| + return { |
| + webrtcRenderingMetric, |
| + }; |
| +}); |
| +</script> |