Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 <!DOCTYPE html> | |
| 2 <!-- | |
| 3 Copyright (c) 2015 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/category_util.html"> | |
| 9 <link rel="import" href="/tracing/metrics/metric_registry.html"> | |
| 10 <link rel="import" href="/tracing/metrics/system_health/utils.html"> | |
| 11 <link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> | |
| 12 <link rel="import" href="/tracing/model/user_model/load_expectation.html"> | |
| 13 <link rel="import" href="/tracing/value/histogram.html"> | |
| 14 <link rel="import" href="/tracing/value/value.html"> | |
| 15 | |
| 16 <script> | |
| 17 'use strict'; | |
| 18 | |
| 19 tr.exportTo('tr.metrics', function() { | |
| 20 | |
| 21 var TOPLEVEL_CATEGORY_NAME = 'toplevel'; | |
| 22 | |
| 23 // TODO(dproy): Figure out if this is really necessary | |
| 24 var BASE_RESPONSE_LATENCY = 0; | |
| 25 | |
| 26 // TODO(dproy): This is copy pasta from loading_metric.html. Factor out. | |
|
benjhayden
2016/09/08 16:03:26
https://github.com/catapult-project/catapult/issue
dproy
2016/09/08 19:45:53
Ok I'm changing the TODO here to wait until that i
| |
| 27 function hasCategoryAndName(event, category, title) { | |
| 28 return event.title === title && event.category && | |
| 29 tr.b.getCategoryParts(event.category).indexOf(category) !== -1; | |
| 30 } | |
| 31 | |
| 32 // TODO (dproy): This is copy pasta from loading_metric.html. Factor out. | |
|
benjhayden
2016/09/08 16:03:26
Feel free to move it to ChromeBrowserHelper.
dproy
2016/09/08 19:45:53
Done. I moved it to ChromeModelHelper because it f
| |
| 33 function findTargetRendererHelper(chromeHelper) { | |
| 34 var largestPid = -1; | |
| 35 for (var pid in chromeHelper.rendererHelpers) { | |
| 36 var rendererHelper = chromeHelper.rendererHelpers[pid]; | |
| 37 if (rendererHelper.isChromeTracingUI) | |
| 38 continue; | |
| 39 if (pid > largestPid) | |
| 40 largestPid = pid; | |
| 41 } | |
| 42 | |
| 43 if (largestPid === -1) | |
| 44 return undefined; | |
| 45 | |
| 46 return chromeHelper.rendererHelpers[largestPid]; | |
| 47 } | |
| 48 | |
| 49 function runConditionallyOnInnermostDescendants(slice, predicate, cb) { | |
| 50 var succeededOnSomeDescendant = false; | |
| 51 for (var child of slice.subSlices) { | |
| 52 var succeededOnThisChild = runConditionallyOnInnermostDescendants( | |
| 53 child, predicate, cb); | |
| 54 succeededOnSomeDescendant = | |
| 55 succeededOnThisChild || succeededOnSomeDescendant; | |
| 56 } | |
| 57 | |
| 58 if (succeededOnSomeDescendant) return true; | |
|
benjhayden
2016/09/08 16:03:26
Why not move this early return up into the loop so
dproy
2016/09/08 19:45:53
I actually want it to run on all siblings. The pri
| |
| 59 | |
| 60 if (predicate(slice)) { | |
| 61 cb(slice); | |
| 62 return true; | |
| 63 } else { | |
| 64 return false; | |
| 65 } | |
| 66 } | |
| 67 | |
| 68 function isTopLevelSlice(slice) { | |
|
benjhayden
2016/09/08 16:03:26
This name is confusing. Slice and AsyncSlice have
dproy
2016/09/08 19:45:53
Done.
| |
| 69 return tr.b.getCategoryParts(slice.category) | |
| 70 .includes(TOPLEVEL_CATEGORY_NAME); | |
| 71 } | |
| 72 | |
| 73 function forEachInnermostTopLevelSlices(thread, cb) { | |
| 74 for (var slice of thread.sliceGroup.topLevelSlices) { | |
|
benjhayden
2016/09/08 16:03:26
I seem to recall the existence of tracing categori
dproy
2016/09/08 19:45:53
That should be ok. I actually want the biggest and
| |
| 75 runConditionallyOnInnermostDescendants(slice, isTopLevelSlice, cb); | |
| 76 } | |
| 77 } | |
| 78 | |
| 79 function getAllInteractiveTimestampsSorted(model) { | |
| 80 // TODO(dproy): When LoadExpectation v.1.0 is released, | |
|
benjhayden
2016/09/08 16:03:26
I'll take care of this refactor. You can either ch
dproy
2016/09/08 19:45:53
Ok I'll remove the TODO for me here. Thanks!
| |
| 81 // update this function to use the new LoadExpectation rather | |
| 82 // than calling loading_metric.html. | |
| 83 | |
| 84 var values = new tr.v.ValueSet(); | |
| 85 tr.metrics.sh.loadingMetric(values, model); | |
| 86 var ttiValues = values.getValuesNamed('timeToFirstInteractive'); | |
| 87 var interactiveTsList = []; | |
| 88 for (var bin of tr.b.getOnlyElement(ttiValues).numeric.allBins) | |
| 89 for (var diagnostics of bin.diagnosticMaps) { | |
| 90 var breakdown = diagnostics.get('breakdown'); | |
| 91 interactiveTsList.push(breakdown.value.interactive); | |
| 92 } | |
| 93 return interactiveTsList.sort((x, y) => x - y); | |
| 94 } | |
| 95 | |
| 96 function getAllNavStartTimesSorted(rendererHelper) { | |
| 97 var list = []; | |
| 98 for (var ev of rendererHelper.mainThread.sliceGroup.childEvents()) { | |
| 99 if (!hasCategoryAndName(ev, 'blink.user_timing', 'navigationStart')) | |
| 100 continue; | |
| 101 list.push(ev.start); | |
| 102 } | |
| 103 return list.sort((x, y) => x - y); | |
| 104 } | |
| 105 | |
| 106 // A task window is defined as time from TTI until either | |
| 107 // 1. beginning of next navigationStart event, or | |
| 108 // 2. end of traces | |
| 109 function getTaskWindows(interactiveTsList, navStartTimeList, endOfTraces) { | |
| 110 var curNavStartTimeIndex = 0; | |
| 111 var endOfLastWindow = -Infinity; | |
| 112 var taskWindows = []; | |
| 113 for (var curTTI of interactiveTsList) { | |
| 114 var curNavStartTime = navStartTimeList[curNavStartTimeIndex]; | |
| 115 while (curNavStartTime !== undefined && curNavStartTime < curTTI) { | |
| 116 // There are possibly multiple navigationStart timestamps between | |
| 117 // two interactive timestamps - the previous page load could | |
| 118 // never reach interactive status. | |
| 119 curNavStartTimeIndex++; | |
| 120 curNavStartTime = navStartTimeList[curNavStartTimeIndex]; | |
| 121 } | |
| 122 | |
| 123 if (curNavStartTime === endOfLastWindow) { | |
| 124 // This is a violation of core assumption. | |
| 125 // TODO: When two pages share a render process, we can possibly | |
| 126 // have two interactive time stamps between two navigation events. | |
| 127 // If both interactive timestamps are reported, it is not clear how | |
| 128 // to define estimated input latency. | |
| 129 throw new Error("Two TTI timestamps with no navigation between them"); | |
| 130 } | |
| 131 | |
| 132 if (curNavStartTime === undefined) { | |
| 133 taskWindows.push({start: curTTI, end: endOfTraces}); | |
| 134 endOfLastWindow = endOfTraces; | |
| 135 continue; | |
| 136 } | |
| 137 | |
| 138 taskWindows.push({start: curTTI, end: curNavStartTime}); | |
| 139 endOfLastWindow = curNavStartTime; | |
| 140 } | |
| 141 return taskWindows; | |
| 142 } | |
| 143 | |
| 144 /** | |
| 145 * Note: Taken from | |
| 146 * https://github.com/GoogleChrome/lighthouse/blob/a5bbe2338fa474c94bb87584940 8704c81fec3df/lighthouse-core/lib/traces/tracing-processor.js#L121 | |
| 147 * | |
| 148 * Calculate duration at specified percentiles for given population of | |
| 149 * durations. | |
| 150 * If one of the durations overlaps the end of the window, the full | |
| 151 * duration should be in the duration array, but the length not included | |
| 152 * within the window should be given as `clippedLength`. For instance, if a | |
| 153 * 50ms duration occurs 10ms before the end of the window, `50` should be in | |
| 154 * the `durations` array, and `clippedLength` should be set to 40. | |
| 155 * @see https://docs.google.com/document/d/18gvP-CBA2BiBpi3Rz1I1ISciKGhniTSZ9T Y0XCnXS7E/preview | |
| 156 */ | |
| 157 function calculateRiskPercentiles( | |
| 158 durations, totalTime, percentiles, clippedLength) { | |
| 159 clippedLength = clippedLength || 0; | |
| 160 | |
| 161 var busyTime = 0; | |
| 162 for (var i = 0; i < durations.length; i++) { | |
| 163 busyTime += durations[i]; | |
| 164 | |
| 165 } | |
| 166 busyTime -= clippedLength; | |
| 167 | |
| 168 // Start with idle time already compvare. | |
| 169 var compvaredTime = totalTime - busyTime; | |
| 170 var duration = 0; | |
| 171 var cdfTime = compvaredTime; | |
| 172 var results = []; | |
| 173 | |
| 174 var durationIndex = -1; | |
| 175 var remainingCount = durations.length + 1; | |
| 176 if (clippedLength > 0) { | |
| 177 // If there was a clipped duration, one less in count | |
| 178 // since one hasn't started yet. | |
| 179 remainingCount--; | |
| 180 | |
| 181 } | |
| 182 | |
| 183 // Find percentiles of interest, in order. | |
| 184 for (var percentile of percentiles) { | |
| 185 // Loop over durations, calculating a CDF value for each until it is above | |
| 186 // the target percentile. | |
| 187 var percentivarime = percentile * totalTime; | |
| 188 while (cdfTime < percentivarime && durationIndex < durations.length - 1) { | |
| 189 compvaredTime += duration; | |
| 190 remainingCount -= (duration < 0 ? -1 : 1); | |
| 191 | |
| 192 if (clippedLength > 0 && clippedLength < durations[durationIndex + 1]) { | |
| 193 duration = -clippedLength; | |
| 194 clippedLength = 0; | |
| 195 } else { | |
| 196 durationIndex++; | |
| 197 duration = durations[durationIndex]; | |
| 198 } | |
| 199 | |
| 200 // Calculate value of CDF (multiplied by totalTime) for the end of | |
| 201 // this duration. | |
| 202 cdfTime = compvaredTime + Math.abs(duration) * remainingCount; | |
| 203 } | |
| 204 | |
| 205 // Negative results are within idle time (0ms wait by definition), | |
| 206 // so clamp at zero. | |
| 207 results.push({ | |
| 208 percentile, | |
| 209 time: Math.max(0, (percentivarime - compvaredTime) / remainingCount) + | |
| 210 BASE_RESPONSE_LATENCY | |
| 211 }); | |
| 212 } | |
| 213 | |
| 214 return results; | |
| 215 } | |
| 216 | |
| 217 /** | |
| 218 * Note: This is adapted from | |
| 219 * https://github.com/GoogleChrome/lighthouse/blob/a5bbe2338fa474c94bb87584940 8704c81fec3df/lighthouse-core/lib/traces/tracing-processor.js#L185 | |
| 220 */ | |
| 221 function getEQTPercentilesForWindow( | |
| 222 percentiles, rendererHelper, startTime, endTime) { | |
| 223 var totalTime = endTime - startTime; | |
| 224 percentiles.sort((a, b) => a - b); | |
| 225 | |
| 226 var durations = []; | |
| 227 var clippedLength = 0; | |
| 228 forEachInnermostTopLevelSlices(rendererHelper.mainThread, slice => { | |
| 229 // Discard slices outside range. | |
| 230 if (slice.end <= startTime || slice.start >= endTime) { | |
| 231 return; | |
| 232 } | |
| 233 | |
| 234 // Clip any at edges of range. | |
| 235 let duration = slice.duration; | |
| 236 let sliceStart = slice.start; | |
| 237 if (sliceStart < startTime) { | |
| 238 // Any part of task before window can be discarded. | |
| 239 sliceStart = startTime; | |
| 240 duration = slice.end - sliceStart; | |
| 241 | |
| 242 } | |
| 243 if (slice.end > endTime) { | |
| 244 // Any part of task after window must be clipped but accounted for. | |
| 245 clippedLength = duration - (endTime - sliceStart); | |
| 246 } | |
| 247 durations.push(duration); | |
| 248 | |
| 249 }); | |
| 250 durations.sort((a, b) => a - b); | |
| 251 return calculateRiskPercentiles( | |
| 252 durations, totalTime, percentiles, clippedLength); | |
| 253 } | |
| 254 | |
| 255 /** | |
| 256 * @param {!tr.v.ValueSet} values | |
| 257 * @param {!tr.model.Model} model | |
| 258 * @param {!Object=} opt_options | |
| 259 */ | |
| 260 function estimatedInputLatencyMetric(values, model, opt_options) { | |
| 261 | |
| 262 var chromeHelper = model.getOrCreateHelper( | |
| 263 tr.model.helpers.ChromeModelHelper); | |
| 264 var rendererHelper = findTargetRendererHelper(chromeHelper); | |
| 265 | |
| 266 var interactiveTimestamps = getAllInteractiveTimestampsSorted(model); | |
| 267 | |
| 268 // We're assuming children iframes will never emit navigationStart events | |
| 269 var navStartTimeList = getAllNavStartTimesSorted(rendererHelper); | |
| 270 var taskWindowList = getTaskWindows( | |
| 271 interactiveTimestamps, | |
| 272 navStartTimeList, | |
| 273 rendererHelper.mainThread.bounds.max); | |
| 274 | |
| 275 var eqtTargetPercentiles = [.9]; | |
| 276 | |
| 277 var EQT90thPercentile = new tr.v.Histogram( | |
| 278 tr.v.Unit.byName.timeDurationInMs_smallerIsBetter, | |
| 279 tr.v.HistogramBinBoundaries.createExponential(0.1, 1e3, 100)); | |
| 280 | |
| 281 for (var taskWindow of taskWindowList) { | |
| 282 var eqtPercentiles = getEQTPercentilesForWindow( | |
| 283 eqtTargetPercentiles, rendererHelper, taskWindow.start, taskWindow.end); | |
| 284 var filtered = eqtPercentiles.filter(res => res.percentile === 0.9) | |
| 285 var EQT90th = eqtPercentiles.filter(res => res.percentile === 0.9)[0]; | |
| 286 if (EQT90th !== undefined) EQT90thPercentile.addSample(EQT90th.time); | |
| 287 } | |
| 288 | |
| 289 values.addValue(new tr.v.NumericValue( | |
| 290 'Expected Queueing Time 90th Percentile', EQT90thPercentile, | |
| 291 { description: '90th percetile of expected queueing time for ' + | |
| 292 'uniformly distributed random input event after TTI' })); | |
| 293 } | |
| 294 | |
| 295 tr.metrics.MetricRegistry.register(estimatedInputLatencyMetric, { | |
| 296 supportsRangeOfInterest: false | |
| 297 }); | |
| 298 | |
| 299 return { | |
| 300 estimatedInputLatencyMetric: estimatedInputLatencyMetric, | |
| 301 }; | |
| 302 }); | |
| 303 </script> | |
| OLD | NEW |