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 |