Index: tracing/tracing/metrics/estimated_input_latency_metric.html |
diff --git a/tracing/tracing/metrics/estimated_input_latency_metric.html b/tracing/tracing/metrics/estimated_input_latency_metric.html |
new file mode 100644 |
index 0000000000000000000000000000000000000000..c6fe5c9c114b25e093ac127f3f981ea25cad6468 |
--- /dev/null |
+++ b/tracing/tracing/metrics/estimated_input_latency_metric.html |
@@ -0,0 +1,303 @@ |
+<!DOCTYPE html> |
+<!-- |
+Copyright (c) 2015 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/category_util.html"> |
+<link rel="import" href="/tracing/metrics/metric_registry.html"> |
+<link rel="import" href="/tracing/metrics/system_health/utils.html"> |
+<link rel="import" href="/tracing/model/helpers/chrome_model_helper.html"> |
+<link rel="import" href="/tracing/model/user_model/load_expectation.html"> |
+<link rel="import" href="/tracing/value/histogram.html"> |
+<link rel="import" href="/tracing/value/value.html"> |
+ |
+<script> |
+'use strict'; |
+ |
+tr.exportTo('tr.metrics', function() { |
+ |
+ var TOPLEVEL_CATEGORY_NAME = 'toplevel'; |
+ |
+ // TODO(dproy): Figure out if this is really necessary |
+ var BASE_RESPONSE_LATENCY = 0; |
+ |
+ // 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
|
+ function hasCategoryAndName(event, category, title) { |
+ return event.title === title && event.category && |
+ tr.b.getCategoryParts(event.category).indexOf(category) !== -1; |
+ } |
+ |
+ // 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
|
+ function findTargetRendererHelper(chromeHelper) { |
+ var largestPid = -1; |
+ for (var pid in chromeHelper.rendererHelpers) { |
+ var rendererHelper = chromeHelper.rendererHelpers[pid]; |
+ if (rendererHelper.isChromeTracingUI) |
+ continue; |
+ if (pid > largestPid) |
+ largestPid = pid; |
+ } |
+ |
+ if (largestPid === -1) |
+ return undefined; |
+ |
+ return chromeHelper.rendererHelpers[largestPid]; |
+ } |
+ |
+ function runConditionallyOnInnermostDescendants(slice, predicate, cb) { |
+ var succeededOnSomeDescendant = false; |
+ for (var child of slice.subSlices) { |
+ var succeededOnThisChild = runConditionallyOnInnermostDescendants( |
+ child, predicate, cb); |
+ succeededOnSomeDescendant = |
+ succeededOnThisChild || succeededOnSomeDescendant; |
+ } |
+ |
+ 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
|
+ |
+ if (predicate(slice)) { |
+ cb(slice); |
+ return true; |
+ } else { |
+ return false; |
+ } |
+ } |
+ |
+ 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.
|
+ return tr.b.getCategoryParts(slice.category) |
+ .includes(TOPLEVEL_CATEGORY_NAME); |
+ } |
+ |
+ function forEachInnermostTopLevelSlices(thread, cb) { |
+ 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
|
+ runConditionallyOnInnermostDescendants(slice, isTopLevelSlice, cb); |
+ } |
+ } |
+ |
+ function getAllInteractiveTimestampsSorted(model) { |
+ // 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!
|
+ // update this function to use the new LoadExpectation rather |
+ // than calling loading_metric.html. |
+ |
+ var values = new tr.v.ValueSet(); |
+ tr.metrics.sh.loadingMetric(values, model); |
+ var ttiValues = values.getValuesNamed('timeToFirstInteractive'); |
+ var interactiveTsList = []; |
+ for (var bin of tr.b.getOnlyElement(ttiValues).numeric.allBins) |
+ for (var diagnostics of bin.diagnosticMaps) { |
+ var breakdown = diagnostics.get('breakdown'); |
+ interactiveTsList.push(breakdown.value.interactive); |
+ } |
+ return interactiveTsList.sort((x, y) => x - y); |
+ } |
+ |
+ function getAllNavStartTimesSorted(rendererHelper) { |
+ var list = []; |
+ for (var ev of rendererHelper.mainThread.sliceGroup.childEvents()) { |
+ if (!hasCategoryAndName(ev, 'blink.user_timing', 'navigationStart')) |
+ continue; |
+ list.push(ev.start); |
+ } |
+ return list.sort((x, y) => x - y); |
+ } |
+ |
+ // A task window is defined as time from TTI until either |
+ // 1. beginning of next navigationStart event, or |
+ // 2. end of traces |
+ function getTaskWindows(interactiveTsList, navStartTimeList, endOfTraces) { |
+ var curNavStartTimeIndex = 0; |
+ var endOfLastWindow = -Infinity; |
+ var taskWindows = []; |
+ for (var curTTI of interactiveTsList) { |
+ var curNavStartTime = navStartTimeList[curNavStartTimeIndex]; |
+ while (curNavStartTime !== undefined && curNavStartTime < curTTI) { |
+ // There are possibly multiple navigationStart timestamps between |
+ // two interactive timestamps - the previous page load could |
+ // never reach interactive status. |
+ curNavStartTimeIndex++; |
+ curNavStartTime = navStartTimeList[curNavStartTimeIndex]; |
+ } |
+ |
+ if (curNavStartTime === endOfLastWindow) { |
+ // This is a violation of core assumption. |
+ // TODO: When two pages share a render process, we can possibly |
+ // have two interactive time stamps between two navigation events. |
+ // If both interactive timestamps are reported, it is not clear how |
+ // to define estimated input latency. |
+ throw new Error("Two TTI timestamps with no navigation between them"); |
+ } |
+ |
+ if (curNavStartTime === undefined) { |
+ taskWindows.push({start: curTTI, end: endOfTraces}); |
+ endOfLastWindow = endOfTraces; |
+ continue; |
+ } |
+ |
+ taskWindows.push({start: curTTI, end: curNavStartTime}); |
+ endOfLastWindow = curNavStartTime; |
+ } |
+ return taskWindows; |
+ } |
+ |
+ /** |
+ * Note: Taken from |
+ * https://github.com/GoogleChrome/lighthouse/blob/a5bbe2338fa474c94bb875849408704c81fec3df/lighthouse-core/lib/traces/tracing-processor.js#L121 |
+ * |
+ * Calculate duration at specified percentiles for given population of |
+ * durations. |
+ * If one of the durations overlaps the end of the window, the full |
+ * duration should be in the duration array, but the length not included |
+ * within the window should be given as `clippedLength`. For instance, if a |
+ * 50ms duration occurs 10ms before the end of the window, `50` should be in |
+ * the `durations` array, and `clippedLength` should be set to 40. |
+ * @see https://docs.google.com/document/d/18gvP-CBA2BiBpi3Rz1I1ISciKGhniTSZ9TY0XCnXS7E/preview |
+ */ |
+ function calculateRiskPercentiles( |
+ durations, totalTime, percentiles, clippedLength) { |
+ clippedLength = clippedLength || 0; |
+ |
+ var busyTime = 0; |
+ for (var i = 0; i < durations.length; i++) { |
+ busyTime += durations[i]; |
+ |
+ } |
+ busyTime -= clippedLength; |
+ |
+ // Start with idle time already compvare. |
+ var compvaredTime = totalTime - busyTime; |
+ var duration = 0; |
+ var cdfTime = compvaredTime; |
+ var results = []; |
+ |
+ var durationIndex = -1; |
+ var remainingCount = durations.length + 1; |
+ if (clippedLength > 0) { |
+ // If there was a clipped duration, one less in count |
+ // since one hasn't started yet. |
+ remainingCount--; |
+ |
+ } |
+ |
+ // Find percentiles of interest, in order. |
+ for (var percentile of percentiles) { |
+ // Loop over durations, calculating a CDF value for each until it is above |
+ // the target percentile. |
+ var percentivarime = percentile * totalTime; |
+ while (cdfTime < percentivarime && durationIndex < durations.length - 1) { |
+ compvaredTime += duration; |
+ remainingCount -= (duration < 0 ? -1 : 1); |
+ |
+ if (clippedLength > 0 && clippedLength < durations[durationIndex + 1]) { |
+ duration = -clippedLength; |
+ clippedLength = 0; |
+ } else { |
+ durationIndex++; |
+ duration = durations[durationIndex]; |
+ } |
+ |
+ // Calculate value of CDF (multiplied by totalTime) for the end of |
+ // this duration. |
+ cdfTime = compvaredTime + Math.abs(duration) * remainingCount; |
+ } |
+ |
+ // Negative results are within idle time (0ms wait by definition), |
+ // so clamp at zero. |
+ results.push({ |
+ percentile, |
+ time: Math.max(0, (percentivarime - compvaredTime) / remainingCount) + |
+ BASE_RESPONSE_LATENCY |
+ }); |
+ } |
+ |
+ return results; |
+ } |
+ |
+ /** |
+ * Note: This is adapted from |
+ * https://github.com/GoogleChrome/lighthouse/blob/a5bbe2338fa474c94bb875849408704c81fec3df/lighthouse-core/lib/traces/tracing-processor.js#L185 |
+ */ |
+ function getEQTPercentilesForWindow( |
+ percentiles, rendererHelper, startTime, endTime) { |
+ var totalTime = endTime - startTime; |
+ percentiles.sort((a, b) => a - b); |
+ |
+ var durations = []; |
+ var clippedLength = 0; |
+ forEachInnermostTopLevelSlices(rendererHelper.mainThread, slice => { |
+ // Discard slices outside range. |
+ if (slice.end <= startTime || slice.start >= endTime) { |
+ return; |
+ } |
+ |
+ // Clip any at edges of range. |
+ let duration = slice.duration; |
+ let sliceStart = slice.start; |
+ if (sliceStart < startTime) { |
+ // Any part of task before window can be discarded. |
+ sliceStart = startTime; |
+ duration = slice.end - sliceStart; |
+ |
+ } |
+ if (slice.end > endTime) { |
+ // Any part of task after window must be clipped but accounted for. |
+ clippedLength = duration - (endTime - sliceStart); |
+ } |
+ durations.push(duration); |
+ |
+ }); |
+ durations.sort((a, b) => a - b); |
+ return calculateRiskPercentiles( |
+ durations, totalTime, percentiles, clippedLength); |
+ } |
+ |
+ /** |
+ * @param {!tr.v.ValueSet} values |
+ * @param {!tr.model.Model} model |
+ * @param {!Object=} opt_options |
+ */ |
+ function estimatedInputLatencyMetric(values, model, opt_options) { |
+ |
+ var chromeHelper = model.getOrCreateHelper( |
+ tr.model.helpers.ChromeModelHelper); |
+ var rendererHelper = findTargetRendererHelper(chromeHelper); |
+ |
+ var interactiveTimestamps = getAllInteractiveTimestampsSorted(model); |
+ |
+ // We're assuming children iframes will never emit navigationStart events |
+ var navStartTimeList = getAllNavStartTimesSorted(rendererHelper); |
+ var taskWindowList = getTaskWindows( |
+ interactiveTimestamps, |
+ navStartTimeList, |
+ rendererHelper.mainThread.bounds.max); |
+ |
+ var eqtTargetPercentiles = [.9]; |
+ |
+ var EQT90thPercentile = new tr.v.Histogram( |
+ tr.v.Unit.byName.timeDurationInMs_smallerIsBetter, |
+ tr.v.HistogramBinBoundaries.createExponential(0.1, 1e3, 100)); |
+ |
+ for (var taskWindow of taskWindowList) { |
+ var eqtPercentiles = getEQTPercentilesForWindow( |
+ eqtTargetPercentiles, rendererHelper, taskWindow.start, taskWindow.end); |
+ var filtered = eqtPercentiles.filter(res => res.percentile === 0.9) |
+ var EQT90th = eqtPercentiles.filter(res => res.percentile === 0.9)[0]; |
+ if (EQT90th !== undefined) EQT90thPercentile.addSample(EQT90th.time); |
+ } |
+ |
+ values.addValue(new tr.v.NumericValue( |
+ 'Expected Queueing Time 90th Percentile', EQT90thPercentile, |
+ { description: '90th percetile of expected queueing time for ' + |
+ 'uniformly distributed random input event after TTI' })); |
+ } |
+ |
+ tr.metrics.MetricRegistry.register(estimatedInputLatencyMetric, { |
+ supportsRangeOfInterest: false |
+ }); |
+ |
+ return { |
+ estimatedInputLatencyMetric: estimatedInputLatencyMetric, |
+ }; |
+}); |
+</script> |