Chromium Code Reviews| 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> |