blob: 710df3cd5b1605523a35f92bf588067c79492566 [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright 2016 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/base/statistics.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/timed_event.html">
<link rel="import" href="/tracing/value/histogram.html">
<link rel="import" href="/tracing/value/numeric.html">
<script>
'use strict';
tr.exportTo('tr.metrics.sh', function() {
var RESPONSIVENESS_THRESHOLD = 50;
var INTERACTIVE_WINDOW_SIZE = 5 * 1000;
var timeDurationInMs_smallerIsBetter =
tr.b.Unit.byName.timeDurationInMs_smallerIsBetter;
var RelatedEventSet = tr.v.d.RelatedEventSet;
// TODO(ksakamoto): This should be a method of tr.model.Event or one of its
// subclasses.
function hasCategoryAndName(event, category, title) {
return event.title === title && event.category &&
tr.b.getCategoryParts(event.category).indexOf(category) !== -1;
}
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 createBreakdownDiagnostic(rendererHelper, start, end) {
var breakdownDict = rendererHelper.generateTimeBreakdownTree(
start, end);
var breakdownDiagnostic = new tr.v.d.Breakdown();
breakdownDiagnostic.colorScheme =
tr.v.d.COLOR_SCHEME_CHROME_USER_FRIENDLY_CATEGORY_DRIVER;
for (var label in breakdownDict) {
breakdownDiagnostic.set(label, breakdownDict[label].total);
}
return breakdownDiagnostic;
}
/**
* A utility class for finding navigationStart event for given frame and
* timestamp.
* @constructor
*/
function NavigationStartFinder(rendererHelper) {
this.navigationStartsForFrameId_ = {};
for (var ev of rendererHelper.mainThread.sliceGroup.childEvents()) {
if (!hasCategoryAndName(ev, 'blink.user_timing', 'navigationStart'))
continue;
var frameIdRef = ev.args['frame'];
var list = this.navigationStartsForFrameId_[frameIdRef];
if (list === undefined)
this.navigationStartsForFrameId_[frameIdRef] = list = [];
list.unshift(ev);
}
}
NavigationStartFinder.prototype = {
findNavigationStartEventForFrameBeforeTimestamp: function(frameIdRef, ts) {
var list = this.navigationStartsForFrameId_[frameIdRef];
if (list === undefined) {
console.warn('No navigationStartEvent found for frame id "' +
frameIdRef + '"');
return undefined;
}
var eventBeforeTimestamp;
for (var ev of list) {
if (ev.start > ts)
continue;
if (eventBeforeTimestamp === undefined)
eventBeforeTimestamp = ev;
}
if (eventBeforeTimestamp === undefined) {
console.warn('Failed to find navigationStartEvent.');
return undefined;
}
return eventBeforeTimestamp;
}
};
var FIRST_PAINT_BOUNDARIES = tr.v.HistogramBinBoundaries
.createLinear(0, 1e3, 20) // 50ms step to 1s
.addLinearBins(3e3, 20) // 100ms step to 3s
.addExponentialBins(20e3, 20);
function createHistogram(name) {
var histogram = new tr.v.Histogram(name,
timeDurationInMs_smallerIsBetter, FIRST_PAINT_BOUNDARIES);
histogram.customizeSummaryOptions({
avg: true,
count: false,
max: true,
min: true,
std: true,
sum: false,
percentile: [0.90, 0.95, 0.99],
});
return histogram;
}
function findFrameLoaderSnapshotAt(rendererHelper, frameIdRef, ts) {
var snapshot;
var objects = rendererHelper.process.objects;
var frameLoaderInstances = objects.instancesByTypeName_['FrameLoader'];
if (frameLoaderInstances === undefined) {
console.warn('Failed to find FrameLoader for frameId "' + frameIdRef +
'" at ts ' + ts + ', the trace maybe incomplete or from an old' +
'Chrome.');
return undefined;
}
var snapshot;
for (var instance of frameLoaderInstances) {
if (!instance.isAliveAt(ts))
continue;
var maybeSnapshot = instance.getSnapshotAt(ts);
if (frameIdRef !== maybeSnapshot.args['frame']['id_ref'])
continue;
snapshot = maybeSnapshot;
}
return snapshot;
}
function findAllUserTimingEvents(rendererHelper, title) {
var targetEvents = [];
for (var ev of rendererHelper.process.getDescendantEvents()) {
if (!hasCategoryAndName(ev, 'blink.user_timing', title))
continue;
targetEvents.push(ev);
}
return targetEvents;
}
function findFirstMeaningfulPaintCandidates(rendererHelper) {
var isTelemetryInternalEvent =
prepareTelemetryInternalEventPredicate(rendererHelper);
var candidatesForFrameId = {};
for (var ev of rendererHelper.process.getDescendantEvents()) {
if (!hasCategoryAndName(ev, 'loading', 'firstMeaningfulPaintCandidate'))
continue;
if (isTelemetryInternalEvent(ev))
continue;
var frameIdRef = ev.args['frame'];
if (frameIdRef === undefined)
continue;
var list = candidatesForFrameId[frameIdRef];
if (list === undefined)
candidatesForFrameId[frameIdRef] = list = [];
list.push(ev);
}
return candidatesForFrameId;
}
function prepareTelemetryInternalEventPredicate(rendererHelper) {
var ignoreRegions = [];
var internalRegionStart;
for (var slice of
rendererHelper.mainThread.asyncSliceGroup.getDescendantEvents()) {
if (!!slice.title.match(/^telemetry\.internal\.[^.]*\.start$/))
internalRegionStart = slice.start;
if (!!slice.title.match(/^telemetry\.internal\.[^.]*\.end$/)) {
var timedEvent = new tr.model.TimedEvent(internalRegionStart);
timedEvent.duration = slice.end - internalRegionStart;
ignoreRegions.push(timedEvent);
}
}
return function isTelemetryInternalEvent(slice) {
for (var region of ignoreRegions)
if (region.bounds(slice))
return true;
return false;
}
}
var URL_BLACKLIST = [
'about:blank',
// Chrome on Android creates main frames with the below URL for plugins.
'data:text/html,pluginplaceholderdata'
];
function shouldIgnoreURL(url) {
return URL_BLACKLIST.indexOf(url) >= 0;
}
var METRICS = [
{
valueName: 'timeToFirstContentfulPaint',
title: 'firstContentfulPaint',
description: 'time to first contentful paint'
},
{
valueName: 'timeToOnload',
title: 'loadEventStart',
description: 'time to onload. ' +
'This is temporary metric used for PCv1/v2 sanity checking'
}];
function timeToFirstContentfulPaintMetric(values, model) {
var chromeHelper = model.getOrCreateHelper(
tr.model.helpers.ChromeModelHelper);
var rendererHelper = findTargetRendererHelper(chromeHelper);
var isTelemetryInternalEvent =
prepareTelemetryInternalEventPredicate(rendererHelper);
var navigationStartFinder = new NavigationStartFinder(rendererHelper);
for (var metric of METRICS) {
var histogram = createHistogram(metric.valueName);
histogram.description = metric.description;
var targetEvents = findAllUserTimingEvents(rendererHelper, metric.title);
for (var ev of targetEvents) {
if (isTelemetryInternalEvent(ev))
continue;
var frameIdRef = ev.args['frame'];
var snapshot =
findFrameLoaderSnapshotAt(rendererHelper, frameIdRef, ev.start);
if (snapshot === undefined || !snapshot.args.isLoadingMainFrame)
continue;
var url = snapshot.args.documentLoaderURL;
if (shouldIgnoreURL(url))
continue;
var navigationStartEvent = navigationStartFinder.
findNavigationStartEventForFrameBeforeTimestamp(frameIdRef, ev.start);
// Ignore layout w/o preceding navigationStart, as they are not
// attributed to any time-to-X metric.
if (navigationStartEvent === undefined)
continue;
var timeToEvent = ev.start - navigationStartEvent.start;
histogram.addSample(timeToEvent, {url: new tr.v.d.Generic(url)});
}
values.addHistogram(histogram);
}
}
function addTimeToInteractiveSampleToHistogram(histogram, rendererHelper,
navigationStart, firstMeaningfulPaint, url) {
if (shouldIgnoreURL(url))
return;
var navigationStartTime = navigationStart.start;
var firstInteractive = Infinity;
var firstInteractiveCandidate = firstMeaningfulPaint;
var lastLongTaskEvent = undefined;
// Find the first interactive point X after firstMeaningfulPaint so that
// range [X, X + INTERACTIVE_WINDOW_SIZE] contains no
// 'TaskQueueManager::ProcessTaskFromWorkQueues' slice which takes more than
// RESPONSIVENESS_THRESHOLD.
// For more details on why TaskQueueManager::ProcessTaskFromWorkQueue is
// chosen as a proxy for all un-interruptable task on renderer thread, see
// https://github.com/GoogleChrome/lighthouse/issues/489
// TODO(nedn): replace this with just "var ev of rendererHelper..." once
// canary binary is updated.
// (https://github.com/catapult-project/catapult/issues/2586)
for (var ev of[...rendererHelper.mainThread.sliceGroup.childEvents()]) {
if (ev.start < firstInteractiveCandidate)
continue;
var interactiveDurationSoFar = ev.start - firstInteractiveCandidate;
if (interactiveDurationSoFar >= INTERACTIVE_WINDOW_SIZE) {
firstInteractive = firstInteractiveCandidate;
break;
}
if (ev.title === 'TaskQueueManager::ProcessTaskFromWorkQueue' &&
ev.duration > RESPONSIVENESS_THRESHOLD) {
firstInteractiveCandidate = ev.end - 50;
lastLongTaskEvent = ev;
}
}
var breakdownDiagnostic = createBreakdownDiagnostic(
rendererHelper, navigationStartTime, firstInteractive);
var timeToFirstInteractive = firstInteractive - navigationStartTime;
histogram.addSample(
timeToFirstInteractive,
{
"Start": new RelatedEventSet(navigationStart),
"Last long task": new RelatedEventSet(lastLongTaskEvent),
"Navigation infos": new tr.v.d.Generic(
{url: url, pid: rendererHelper.pid,
start: navigationStartTime, interactive: firstInteractive}),
"Breakdown of [navStart, Interactive]": breakdownDiagnostic,
});
}
/**
* Computes Time to first meaningful paint (TTFMP) & time to interactive (TTI)
* from |model| and add it to |value|.
*
* First meaningful paint is the paint following the layout with the highest
* "Layout Significance". The Layout Significance is computed inside Blink,
* by FirstMeaningfulPaintDetector class. It logs
* "firstMeaningfulPaintCandidate" event every time the Layout Significance
* marks a record. TTFMP is the time between NavigationStart and the last
* firstMeaningfulPaintCandidate event.
*
* Design doc: https://goo.gl/vpaxv6
*
* TTI is computed as the starting time of the timed window with size
* INTERACTIVE_WINDOW_SIZE that happens after FMP in which there is no
* uninterruptable task on the main thread with size more than
* RESPONSIVENESS_THRESHOLD.
*
* Design doc: https://goo.gl/ISWndc
*/
function timeToFirstMeaningfulPaintAndTimeToInteractiveMetrics(
values, model) {
var chromeHelper = model.getOrCreateHelper(
tr.model.helpers.ChromeModelHelper);
var rendererHelper = findTargetRendererHelper(chromeHelper);
var navigationStartFinder = new NavigationStartFinder(rendererHelper);
var firstMeaningfulPaintHistogram = createHistogram(
'timeToFirstMeaningfulPaint');
firstMeaningfulPaintHistogram.description =
'time to first meaningful paint';
var firstInteractiveHistogram = createHistogram('timeToFirstInteractive');
firstInteractiveHistogram.description = 'time to first interactive';
function addFirstMeaningfulPaintSampleToHistogram(
frameIdRef, navigationStart, fmpMarkerEvent) {
var snapshot = findFrameLoaderSnapshotAt(
rendererHelper, frameIdRef, fmpMarkerEvent.start);
if (snapshot === undefined || !snapshot.args.isLoadingMainFrame)
return;
var url = snapshot.args.documentLoaderURL;
if (shouldIgnoreURL(url))
return;
var timeToFirstMeaningfulPaint =
fmpMarkerEvent.start - navigationStart.start;
var extraDiagnostic = {
url: url,
pid: rendererHelper.pid
};
var breakdownDiagnostic = createBreakdownDiagnostic(
rendererHelper, navigationStart.start, fmpMarkerEvent.start);
firstMeaningfulPaintHistogram.addSample(timeToFirstMeaningfulPaint,
{
"Breakdown of [navStart, FMP]": breakdownDiagnostic,
"Start": new RelatedEventSet(navigationStart),
"End": new RelatedEventSet(fmpMarkerEvent),
"Navigation infos": new tr.v.d.Generic(
{url: url, pid: rendererHelper.pid,
start: navigationStart.start, fmp: fmpMarkerEvent.start}),
});
return {firstMeaningfulPaint: fmpMarkerEvent.start, url: url};
}
var candidatesForFrameId =
findFirstMeaningfulPaintCandidates(rendererHelper);
for (var frameIdRef in candidatesForFrameId) {
var navigationStart;
var lastCandidate;
// Iterate over the FMP candidates, remembering the last one.
for (var ev of candidatesForFrameId[frameIdRef]) {
var navigationStartForThisCandidate = navigationStartFinder.
findNavigationStartEventForFrameBeforeTimestamp(frameIdRef, ev.start);
// Ignore candidate w/o preceding navigationStart, as they are not
// attributed to any TTFMP.
if (navigationStartForThisCandidate === undefined)
continue;
if (navigationStart !== navigationStartForThisCandidate) {
// New navigation is found. Compute TTFMP for current navigation, and
// reset the state variables.
if (navigationStart !== undefined && lastCandidate !== undefined) {
data = addFirstMeaningfulPaintSampleToHistogram(
frameIdRef, navigationStart, lastCandidate);
if (data !== undefined)
addTimeToInteractiveSampleToHistogram(
firstInteractiveHistogram, rendererHelper,
navigationStart,
data.firstMeaningfulPaint, data.url);
}
navigationStart = navigationStartForThisCandidate;
}
lastCandidate = ev;
}
// Emit TTFMP for the last navigation.
if (lastCandidate !== undefined) {
var data = addFirstMeaningfulPaintSampleToHistogram(
frameIdRef, navigationStart, lastCandidate);
if (data !== undefined)
addTimeToInteractiveSampleToHistogram(
firstInteractiveHistogram, rendererHelper, navigationStart,
data.firstMeaningfulPaint, data.url);
}
}
values.addHistogram(firstMeaningfulPaintHistogram);
values.addHistogram(firstInteractiveHistogram);
}
function loadingMetric(values, model) {
timeToFirstContentfulPaintMetric(values, model);
timeToFirstMeaningfulPaintAndTimeToInteractiveMetrics(values, model);
}
tr.metrics.MetricRegistry.register(loadingMetric);
return {
loadingMetric: loadingMetric
};
});
</script>