| OLD | NEW |
| (Empty) | |
| 1 <!DOCTYPE html> |
| 2 <!-- |
| 3 Copyright 2017 The Chromium Authors. All rights reserved. |
| 4 Use of this source code is governed by a BSD-style license that can be |
| 5 found in the LICENSE file. |
| 6 --> |
| 7 |
| 8 <link rel="import" href="/tracing/base/range.html"> |
| 9 <link rel="import" href="/tracing/base/unit.html"> |
| 10 <link rel="import" href="/tracing/base/utils.html"> |
| 11 <link rel="import" href="/tracing/metrics/metric_registry.html"> |
| 12 <link rel="import" href="/tracing/metrics/v8/utils.html"> |
| 13 <link rel="import" href="/tracing/value/histogram.html"> |
| 14 |
| 15 <script> |
| 16 'use strict'; |
| 17 |
| 18 tr.exportTo('tr.metrics.webrtc', function() { |
| 19 const DISPLAY_HERTZ = 60.0; |
| 20 const VSYNC_DURATION_US = 1e6 / DISPLAY_HERTZ; |
| 21 // When to consider a frame frozen (in VSYNC units) meaning 1 initial |
| 22 // frame + 5 repeats of that frame. |
| 23 const FROZEN_FRAME_VSYNC_COUNT_THRESHOLD = 6; |
| 24 // How much more severe is a 'Badly out of sync' render event compared to an |
| 25 // 'Out of sync' one when calculating the smoothness score. |
| 26 const SEVERITY = 3; |
| 27 |
| 28 const WEB_MEDIA_PLAYER_UPDATE_TITLE = 'WebMediaPlayerMS::UpdateCurrentFrame'; |
| 29 // These four are args for WebMediaPlayerMS update events. |
| 30 const IDEAL_RENDER_INSTANT_NAME = 'Ideal Render Instant'; |
| 31 const ACTUAL_RENDER_BEGIN_NAME = 'Actual Render Begin'; |
| 32 const ACTUAL_RENDER_END_NAME = 'Actual Render End'; |
| 33 // The events of interest have a 'Serial' argument which represents the |
| 34 // stream ID. |
| 35 const STREAM_ID_NAME = 'Serial'; |
| 36 |
| 37 const REQUIRED_EVENT_ARGS_NAMES = [ |
| 38 IDEAL_RENDER_INSTANT_NAME, ACTUAL_RENDER_BEGIN_NAME, ACTUAL_RENDER_END_NAME, |
| 39 STREAM_ID_NAME |
| 40 ]; |
| 41 |
| 42 const percentage_biggerIsBetter = |
| 43 tr.b.Unit.byName.normalizedPercentage_biggerIsBetter; |
| 44 const percentage_smallerIsBetter = |
| 45 tr.b.Unit.byName.normalizedPercentage_smallerIsBetter; |
| 46 const timeDurationInMs_smallerIsBetter = |
| 47 tr.b.Unit.byName.timeDurationInMs_smallerIsBetter; |
| 48 const unitlessNumber_biggerIsBetter = |
| 49 tr.b.Unit.byName.unitlessNumber_biggerIsBetter; |
| 50 |
| 51 /* |
| 52 * Verify that the event is a valid event. |
| 53 * |
| 54 * An event is valid if it is a WebMediaPlayerMS::UpdateCurrentFrame event, |
| 55 * and has all of the mandatory arguments. See MANDATORY above. |
| 56 */ |
| 57 function isValidEvent(event) { |
| 58 if (event.title !== WEB_MEDIRA_PLAYER_UPDATE_TITLE || !event.args) { |
| 59 return false; |
| 60 } |
| 61 for (let parameter of REQUIRED_EVENT_ARGS_NAMES) { |
| 62 if (!(parameter in event.args)) { |
| 63 return false; |
| 64 } |
| 65 } |
| 66 return true; |
| 67 } |
| 68 |
| 69 function webrtcRenderingMetric(values, model) { |
| 70 tr.metrics.v8.utils.groupAndProcessEvents(model, |
| 71 isValidEvent, |
| 72 event => event.args[STREAM_ID_NAME], |
| 73 eventStream, |
| 74 (streamName, events) => getTimeStats(values, streamName, events) |
| 75 ); |
| 76 } |
| 77 |
| 78 tr.metrics.MetricRegistry.register(webrtcRenderingMetric); |
| 79 |
| 80 function addHistogram(scalarValues, values, name, unit) { |
| 81 let histogram = new tr.v.Histogram(name, unit); |
| 82 for (let scalarValue of scalarValues) { |
| 83 histogram.addSample(scalarValue); |
| 84 } |
| 85 values.addHistogram(histogram); |
| 86 } |
| 87 |
| 88 function getTimeStats(values, streamName, events) { |
| 89 let frameDistribution = getSourceToOutputDistribution(events); |
| 90 addFpsFromCadence(values, frameDistribution); |
| 91 addFreezeScore(values, frameDistribution); |
| 92 let driftTimeStats = getDriftTimeStats(events); |
| 93 addHistogram(driftTimeStats.driftTime, values, |
| 94 'WebRTCRendering_drift_time', timeDurationInMs_smallerIsBetter); |
| 95 addHistogram([driftTimeStats.renderingLengthError], values, |
| 96 'WebRTCRendering_rendering_length_error', percentage_smallerIsBetter); |
| 97 let smoothnessStats = getSmoothnessStats(driftTimeStats.driftTime); |
| 98 addHistogram([smoothnessStats.percentBadlyOutOfSync], values, |
| 99 'WebRTCRendering_percent_badly_out_of_sync', |
| 100 percentage_smallerIsBetter); |
| 101 addHistogram([smoothnessStats.percentOutOfSync], values, |
| 102 'WebRTCRendering_percent_out_of_sync', percentage_smallerIsBetter); |
| 103 addHistogram([smoothnessStats.smoothnessScore], values, |
| 104 'WebRTCRendering_smoothness_score', percentage_biggerIsBetter); |
| 105 } |
| 106 |
| 107 /** |
| 108 * Create the source to output distribution. |
| 109 * |
| 110 * If the overall display distribution is A1:A2:..:An, this will tell how |
| 111 * many times a frame stays displayed during Ak*VSYNC_DURATION_US, also known |
| 112 * as 'source to output' distribution. |
| 113 * |
| 114 * In other terms, a distribution B where |
| 115 * B[k] = number of frames that are displayed k times. |
| 116 * |
| 117 * @param {Array.<event>} cadence - An array of events. |
| 118 * @returns {Map} frameDistribution - The source to output distribution. |
| 119 */ |
| 120 function getSourceToOutputDistribution(events) { |
| 121 let cadence = tr.b.runLengthEncoding( |
| 122 events.map(e => e.args[IDEAL_RENDER_INSTANT])); |
| 123 let frameDistribution = new Map(); |
| 124 for (let ticks of cadence) { |
| 125 frameDistribution.set(ticks.count, |
| 126 1 + (frameDistribution.get(ticks.count) || 0)); |
| 127 } |
| 128 return frameDistribution; |
| 129 } |
| 130 |
| 131 /** |
| 132 * Calculate the apparent FPS from frame distribution. |
| 133 * |
| 134 * Knowing the display frequency and the frame distribution, it is possible to |
| 135 * calculate the video apparent frame rate as played by WebMediaPlayerMs |
| 136 * module. |
| 137 * |
| 138 * @param values |
| 139 * @param {Map} frameDistribution - See getSourceToOutputDistribution. |
| 140 */ |
| 141 function addFpsFromCadence(values, frameDistribution) { |
| 142 let numberFrames = 0; |
| 143 let numberVsyncs = 0; |
| 144 for (let [ticks, count] of frameDistribution) { |
| 145 numberFrames += count; |
| 146 numberVsyncs += ticks * count; |
| 147 } |
| 148 let meanRatio = numberVsyncs / numberFrames; |
| 149 addHistogram([DISPLAY_HERTZ / meanRatio], values, 'WebRTCRendering_fps', |
| 150 unitlessNumber_biggerIsBetter); |
| 151 } |
| 152 |
| 153 /** |
| 154 * Find evidence of frozen frames in distribution. |
| 155 * |
| 156 * For simplicity we count as freezing the frames that are repeated at least |
| 157 * five times in a row counted from 'Ideal Render Instant' perspective. |
| 158 * So let's say for 1 source frame, we rendered 6 frames, then we consider 5 |
| 159 * of these rendered frames as frozen. |
| 160 * We mitigate this by saying anything unde 5 frozen frames will not be |
| 161 * counted as frozen. |
| 162 * |
| 163 * In other terms, if C is the source to output distribution, the result is a |
| 164 * distribution B where: |
| 165 * B[k] = C[k+1] for each k in C greater than the frozen frame threshold (6). |
| 166 * |
| 167 * @param {Map} frameDistribution - See getSourceToOutputDistribution. |
| 168 * @return {Map} frozenFrames - The frozen frames distribution. |
| 169 */ |
| 170 function getFrozenFramesReports(frameDistribution) { |
| 171 frozenFrames = {}; |
| 172 for (let [ticks, count] of frameDistribution) { |
| 173 if (ticks >= FROZEN_FRAME_VSYNC_COUNT_THRESHOLD) { |
| 174 frozenFrames.set(ticks - 1, count); |
| 175 } |
| 176 } |
| 177 return frozenFrames; |
| 178 } |
| 179 |
| 180 /** |
| 181 * Returns the weighted penalty for a number of frozen frames. |
| 182 * |
| 183 * We count for frozen anything above 6 vsync display duration for the same |
| 184 * 'Initial Render Instant', which is five frozen frames. |
| 185 * |
| 186 * @param {Number} numberFrozenFrames - The number of frozen frames. |
| 187 * @return {Number} - The weight penalty for the number of frozen frames. |
| 188 */ |
| 189 function frozenPenaltyWeight(numberFrozenFrames) { |
| 190 const penalty = { |
| 191 0: 0, |
| 192 1: 0, |
| 193 2: 0, |
| 194 3: 0, |
| 195 4: 0, |
| 196 5: 1, |
| 197 6: 5, |
| 198 7: 15, |
| 199 8: 25 |
| 200 }; |
| 201 return penalty[numberFrozenFrames] || (8 * (numberFrozenFrames - 4)); |
| 202 } |
| 203 |
| 204 /** |
| 205 * Adds the freezing score. |
| 206 * |
| 207 * @param values |
| 208 * @param {Map} frameDistribution - See getSourceToOutputDistribution. |
| 209 */ |
| 210 function addFreezingScore(values, frameDistribution) { |
| 211 let numberVsyncs = 0; |
| 212 for (let [ticks, count] of frameDistribution) { |
| 213 numberVsyncs += ticks * count; |
| 214 } |
| 215 let frozenFrames = getFrozenFramesReports(frameDistribution); |
| 216 let freezingScore = 100; |
| 217 for (let [ticks, count] of frozenFrames) { |
| 218 let weight = frozenPenaltyWeight(ticks); |
| 219 freezingScore -= 100 * count / numberVsyncs * weight; |
| 220 if (freezingScore < 0) { |
| 221 freezingScore = 0; |
| 222 } |
| 223 } |
| 224 addHistogram([freezingScore], values, 'WebRTCRendering_freezing_score', |
| 225 percentage_biggerIsBetter); |
| 226 } |
| 227 |
| 228 /** |
| 229 * Get the drift time statistics. |
| 230 * |
| 231 * This method will calculate: |
| 232 * - driftTime = list(actual render begin - ideal render). |
| 233 * - renderingLengthError |
| 234 * |
| 235 * @param {Array.<Number>} events - An array of events. |
| 236 * @returns {Object} - The driftTime and renderingLengthError calculated |
| 237 * from the cadence. |
| 238 */ |
| 239 function getDriftStats(events) { |
| 240 let driftTime = []; |
| 241 let discrepancy = []; |
| 242 let oldIdealRender = 0; |
| 243 let expectedIdealRender = 0; |
| 244 |
| 245 for (let event of events) { |
| 246 let currentIdealRender = event.args[IDEAL_RENDER_INSTANT_NAME]; |
| 247 // The expected time of the next 'Ideal Render' event begins as the |
| 248 // current 'Ideal Render' time and increases by VSYNC_DURATION_US on every |
| 249 // frame. |
| 250 expectedIdealRender += VSYNC_DURATION_US; |
| 251 if (currentIdealRender === oldIdealRender) { |
| 252 continue; |
| 253 } |
| 254 let actualRenderBegin = event.args[ACTUAL_RENDER_BEGIN_NAME]; |
| 255 // When was the frame rendered vs. when it would've been ideal. |
| 256 driftTime.push(actualRenderBegin - currentIdealRender); |
| 257 // The discrepancy is the absolute difference between the current Ideal |
| 258 // Render and the expected Ideal Render. |
| 259 discrepancy.push(Math.abs(currentIdealRender - expectedIdealRender)); |
| 260 expectedIdealRender = currentIdealRender; |
| 261 oldIdealRender = currentIdealRender; |
| 262 } |
| 263 |
| 264 let discrepancySum = tr.b.Statistics.sum(discrepancy) - discrepancy[0]; |
| 265 let lastIdealRender = |
| 266 events[events.length - 1].args[IDEAL_RENDER_INSTANT_NAME]; |
| 267 let firstIdealRender = events[0].args[IDEAL_RENDER_INSTANT_NAME]; |
| 268 let idealRenderSpan = lastIdealRender - firstIdealRender; |
| 269 |
| 270 let renderingLengthError = 100 * discrepancySum / idealRenderSpan; |
| 271 |
| 272 return { |
| 273 driftTime: driftTime, |
| 274 renderingLengthError: renderingLengthError, |
| 275 }; |
| 276 } |
| 277 |
| 278 /** |
| 279 * Get the smoothness stats from the normalized drift time. |
| 280 * |
| 281 * This method will calculate the smoothness score, along with the percentage |
| 282 * of frames badly out of sync and the percentage of frames out of sync. |
| 283 * To be considered badly out of sync, a frame has to have missed rendering by |
| 284 * at least 2 * VSYNC_DURATION_US. |
| 285 * To be considered out of sync, a frame has to have missed rendering by at |
| 286 * least one VSYNC_DURATION_US. |
| 287 * The smoothness score is a measure of how out of sync the frames are. |
| 288 * |
| 289 * @param {Array.<Number>} driftTimes - See getDriftStats. |
| 290 * @returns {Object} - The percentBadlyOutOfSync, percentOutOfSync and |
| 291 * smoothnesScore calculated from the driftTimes. |
| 292 */ |
| 293 function getSmoothnessStats(driftTimes) { |
| 294 let meanDriftTime = 0; |
| 295 for (let driftTime of driftTimes) { |
| 296 meanDriftTime += driftTime; |
| 297 } |
| 298 meanDriftTime /= driftTimes.length; |
| 299 let normDriftTimes = []; |
| 300 for (let driftTime of driftTimes) { |
| 301 normDriftTimes.push(Math.abs(driftTime - meanDriftTime)); |
| 302 } |
| 303 |
| 304 // How many times is a frame later/earlier than T=2*VSYNC_DURATION_US. Time |
| 305 // is in microseconds |
| 306 let framesSeverelyOutOfSync = 0; |
| 307 // How many times is a frame later/earlier than VSYNC_DURATION_US. |
| 308 let framesOutOfSync = 0; |
| 309 for (let driftTime of normDriftTimes) { |
| 310 if (Math.abs(driftTime) > VSYNC_DURATION_US) { |
| 311 framesOutOfSync += 1; |
| 312 } |
| 313 if (Math.abs(driftTime) > 2 * VSYNC_DURATION_US) { |
| 314 framesSeverelyOutOfSync += 1; |
| 315 } |
| 316 } |
| 317 let percentBadlyOutOfSync = 100 * framesSeverelyOutOfSync / |
| 318 driftTimes.length; |
| 319 let percentOutOfSync = 100 * framesOutOfSync / driftTimes.length; |
| 320 |
| 321 let framesOutOfSyncOnlyOnce = framesOutOfSync - framesSeverelyOutOfSync; |
| 322 |
| 323 // Calculate smoothness metric. From the formula, we can see that smoothness |
| 324 // score can be negative. |
| 325 let smoothnessScore = 100 - 100 * (framesOutOfSyncOnlyOnce + |
| 326 SEVERITY * framesSeverelyOutOfSync) / driftTimes.length; |
| 327 |
| 328 // Minimum smoothness_score value allowed is zero. |
| 329 if (smoothnessScore < 0) { |
| 330 smoothnessScore = 0; |
| 331 } |
| 332 |
| 333 return { |
| 334 percentBadlyOutOfSync: percentBadlyOutOfSync, |
| 335 percentOutOfSync: percentOutOfSync, |
| 336 smoothnessScore: smoothnessScore, |
| 337 }; |
| 338 } |
| 339 |
| 340 return { |
| 341 webrtcRenderingMetric, |
| 342 }; |
| 343 }); |
| 344 </script> |
| OLD | NEW |