| <!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> |