blob: cccedf0e06f1481b69b59e9523d3f06453c6d5f3 [file] [log] [blame]
<!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/math/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,
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.math.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.math.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>