| Index: tracing/tracing/metrics/system_health/loading_metric.html
|
| diff --git a/tracing/tracing/metrics/system_health/loading_metric.html b/tracing/tracing/metrics/system_health/loading_metric.html
|
| index e9e5a4cbdd177917b81e3920efd6dda6fe4daec7..2adf697a5bd599cc815c1a227c39c88e526a8ba3 100644
|
| --- a/tracing/tracing/metrics/system_health/loading_metric.html
|
| +++ b/tracing/tracing/metrics/system_health/loading_metric.html
|
| @@ -19,12 +19,156 @@ found in the LICENSE file.
|
| 'use strict';
|
|
|
| tr.exportTo('tr.metrics.sh', function() {
|
| + const PLUGIN_URL = 'data:text/html,pluginplaceholderdata';
|
| const RESPONSIVENESS_THRESHOLD_MS = 50;
|
| const INTERACTIVE_WINDOW_SIZE_MS = 5 * 1000;
|
| const timeDurationInMs_smallerIsBetter =
|
| tr.b.Unit.byName.timeDurationInMs_smallerIsBetter;
|
| const RelatedEventSet = tr.v.d.RelatedEventSet;
|
|
|
| +
|
| + /**
|
| + * Helper class for detecting whether a frame is the main frame.
|
| + *
|
| + * The class uses frame loader snapshots and special main frame markers in the
|
| + * trace to compute the isMainFrame predicate. A frame with the given frame id
|
| + * F is the main frame at the given time T if the following conditions hold:
|
| + * 1) There is a frame loader with the frame id F that is alive at time T.
|
| + * 2) There is an event (category='loading', title='markAsMainFrame') with the
|
| + * frame id F that occurs during the lifetime of the frame loader.
|
| + * 3) The frame is not a plug-in frame, i.e. the frame loader does not have a
|
| + * snapshot with the URL equal to the PLUGIN_URL.
|
| + * See https://bugs.chromium.org/p/chromium/issues/detail?id=692112#c10 for
|
| + * more info on main frame detection.
|
| + */
|
| + class MainFrameHelper {
|
| + /**
|
| + * @param {!tr.model.helpers.ChromeRendererHelper} rendererHelper
|
| + */
|
| + constructor(rendererHelper) {
|
| + this.frameLoaders_ = rendererHelper.process.objects
|
| + .getAllInstancesNamed('FrameLoader') || [];
|
| + this.mainFrameMarkers_ = this.findMainFrameMarkers_(rendererHelper);
|
| + this.mainFrameLiveRanges_ = this.findMainFrameLiveRanges_(rendererHelper);
|
| + }
|
| +
|
| + /**
|
| + * Returns true if the frame with the given frameId is the main frame at
|
| + * given time.
|
| + * @param {string} frameId
|
| + * @param {number} timestamp
|
| + * @returns {boolean}
|
| + */
|
| + isMainFrame(frameId, timestamp) {
|
| + for (const {frameId: mainFrameId, liveRange} of
|
| + this.mainFrameLiveRanges_) {
|
| + if (mainFrameId === frameId &&
|
| + liveRange.containsExplicitRangeInclusive(timestamp, timestamp)) {
|
| + return true;
|
| + }
|
| + }
|
| + return false;
|
| + }
|
| +
|
| + /**
|
| + * Returns the document URL that this being loaded in the given frame at the
|
| + * given time.
|
| + * @param {string} frameId
|
| + * @param {number} timestamp
|
| + * @returns {string}
|
| + */
|
| + getURL(frameId, timestamp) {
|
| + for (const frameLoader of this.frameLoaders_) {
|
| + if (!frameLoader.isAliveAt(timestamp)) continue;
|
| + const snapshot = frameLoader.getSnapshotAt(timestamp);
|
| + if (frameId === snapshot.args.frame.id_ref) {
|
| + return snapshot.args.documentLoaderURL;
|
| + }
|
| + }
|
| + return undefined;
|
| + }
|
| +
|
| + /**
|
| + * Returns a list of main frame markers.
|
| + * @private
|
| + * @param {!tr.model.helpers.ChromeRendererHelper} rendererHelper
|
| + * @returns {!Array.<{frameId: string, timestamp: number}>}
|
| + */
|
| + findMainFrameMarkers_(rendererHelper) {
|
| + return findAllEvents(rendererHelper, 'loading', 'markAsMainFrame').
|
| + map(event => {
|
| + return {frameId: event.args.frame, timestamp: event.start};
|
| + });
|
| + }
|
| +
|
| + /**
|
| + * Returns a list of main frame ids and live ranges.
|
| + * @private
|
| + * @returns {!Array.<{frameId: string, liveRange: !tr.b.Range}>}
|
| + */
|
| + findMainFrameLiveRanges_() {
|
| + const result = [];
|
| + for (const frameLoader of this.frameLoaders_) {
|
| + if (frameLoader.snapshots.length > 0 &&
|
| + this.isMarkedAsMainFrame_(frameLoader) &&
|
| + !this.isPluginFrame_(frameLoader)) {
|
| + result.push({
|
| + frameId: this.getFrameId_(frameLoader),
|
| + liveRange: frameLoader.bounds
|
| + });
|
| + }
|
| + }
|
| + return result;
|
| + }
|
| +
|
| + /**
|
| + * Checks if the given frame loader has a main frame marker event.
|
| + * @private
|
| + * @param {!FrameLoader} frameLoader
|
| + * @returns {boolean}
|
| + */
|
| + isMarkedAsMainFrame_(frameLoader) {
|
| + const currentFrameId = this.getFrameId_(frameLoader);
|
| + frameLoader.updateBounds();
|
| + const liveRange = frameLoader.bounds;
|
| + for (const {frameId, timestamp} of this.mainFrameMarkers_) {
|
| + if (currentFrameId === frameId &&
|
| + liveRange.containsExplicitRangeInclusive(timestamp, timestamp)) {
|
| + return true;
|
| + }
|
| + }
|
| + return false;
|
| + }
|
| +
|
| + /**
|
| + * Checks if the given frame loader loads the PLUGIN_URL.
|
| + * @private
|
| + * @param {!FrameLoader} frameLoader
|
| + * @returns {boolean}
|
| + */
|
| + isPluginFrame_(frameLoader) {
|
| + for (const snapshot of frameLoader.snapshots) {
|
| + if (snapshot.args.documentLoaderURL === PLUGIN_URL) return true;
|
| + }
|
| + return false;
|
| + }
|
| +
|
| + /**
|
| + * @private
|
| + * @param {!FrameLoader} frameLoader
|
| + * @returns {number} the frame id corresponding to the given frame loader.
|
| + */
|
| + getFrameId_(frameLoader) {
|
| + const result = frameLoader.snapshots[0].args.frame.id_ref;
|
| + for (const snapshot of frameLoader.snapshots) {
|
| + if (snapshot.args.frame.id_ref !== result) {
|
| + throw new Error('Snapshots have different frame ids.');
|
| + }
|
| + }
|
| + return result;
|
| + }
|
| + }
|
| +
|
| /**
|
| * @param {!tr.model.Process} process
|
| * @param {!tr.b.math.Range} range
|
| @@ -79,18 +223,18 @@ tr.exportTo('tr.metrics.sh', function() {
|
| if (!hasCategoryAndName(ev, 'blink.user_timing', 'navigationStart')) {
|
| continue;
|
| }
|
| - const frameIdRef = ev.args.frame;
|
| - let list = this.navigationStartsForFrameId_[frameIdRef];
|
| + const frameId = ev.args.frame;
|
| + let list = this.navigationStartsForFrameId_[frameId];
|
| if (list === undefined) {
|
| - this.navigationStartsForFrameId_[frameIdRef] = list = [];
|
| + this.navigationStartsForFrameId_[frameId] = list = [];
|
| }
|
| list.unshift(ev);
|
| }
|
| }
|
|
|
| NavigationStartFinder.prototype = {
|
| - findNavigationStartEventForFrameBeforeTimestamp(frameIdRef, ts) {
|
| - const list = this.navigationStartsForFrameId_[frameIdRef];
|
| + findNavigationStartEventForFrameBeforeTimestamp(frameId, ts) {
|
| + const list = this.navigationStartsForFrameId_[frameId];
|
| if (list === undefined) return undefined;
|
| let eventBeforeTimestamp;
|
| for (const ev of list) {
|
| @@ -123,22 +267,6 @@ tr.exportTo('tr.metrics.sh', function() {
|
| return histogram;
|
| }
|
|
|
| - function findFrameLoaderSnapshotAt(rendererHelper, frameIdRef, ts) {
|
| - const objects = rendererHelper.process.objects;
|
| - const frameLoaderInstances = objects.instancesByTypeName_.FrameLoader;
|
| - if (frameLoaderInstances === undefined) return undefined;
|
| -
|
| - let snapshot;
|
| - for (const instance of frameLoaderInstances) {
|
| - if (!instance.isAliveAt(ts)) continue;
|
| - const maybeSnapshot = instance.getSnapshotAt(ts);
|
| - if (frameIdRef !== maybeSnapshot.args.frame.id_ref) continue;
|
| - snapshot = maybeSnapshot;
|
| - }
|
| -
|
| - return snapshot;
|
| - }
|
| -
|
| function findAllEvents(rendererHelper, category, title) {
|
| const targetEvents = [];
|
|
|
| @@ -157,21 +285,21 @@ tr.exportTo('tr.metrics.sh', function() {
|
| continue;
|
| }
|
| if (rendererHelper.isTelemetryInternalEvent(ev)) continue;
|
| - const frameIdRef = ev.args.frame;
|
| - if (frameIdRef === undefined) continue;
|
| - let list = candidatesForFrameId[frameIdRef];
|
| + const frameId = ev.args.frame;
|
| + if (frameId === undefined) continue;
|
| + let list = candidatesForFrameId[frameId];
|
| if (list === undefined) {
|
| - candidatesForFrameId[frameIdRef] = list = [];
|
| + candidatesForFrameId[frameId] = list = [];
|
| }
|
| list.push(ev);
|
| }
|
| return candidatesForFrameId;
|
| }
|
|
|
| + // TODO(ulan): Remove URL blacklist.
|
| const URL_BLACKLIST = [
|
| 'about:blank',
|
| // Chrome on Android creates main frames with the below URL for plugins.
|
| - 'data:text/html,pluginplaceholderdata',
|
| // Special URL used to start a navigation to an unreachable error page.
|
| 'data:text/html,chromewebdata'
|
| ];
|
| @@ -180,20 +308,18 @@ tr.exportTo('tr.metrics.sh', function() {
|
| }
|
|
|
| function collectTimeToEvent(
|
| - category, eventName, rendererHelper, navigationStartFinder) {
|
| + category, eventName, rendererHelper,
|
| + navigationStartFinder, mainFrameHelper) {
|
| const targetEvents = findAllEvents(rendererHelper, category, eventName);
|
| const samples = [];
|
| for (const ev of targetEvents) {
|
| if (rendererHelper.isTelemetryInternalEvent(ev)) continue;
|
| - const frameIdRef = ev.args.frame;
|
| - const snapshot =
|
| - findFrameLoaderSnapshotAt(rendererHelper, frameIdRef, ev.start);
|
| - if (snapshot === undefined || !snapshot.args.isLoadingMainFrame) continue;
|
| - const url = snapshot.args.documentLoaderURL;
|
| - if (shouldIgnoreURL(url)) continue;
|
| + const frameId = ev.args.frame;
|
| + if (!mainFrameHelper.isMainFrame(frameId, ev.start)) continue;
|
| + const url = mainFrameHelper.getURL(frameId, ev.start);
|
| + if (url === undefined || shouldIgnoreURL(url)) continue;
|
| const navigationStartEvent = navigationStartFinder.
|
| - findNavigationStartEventForFrameBeforeTimestamp(
|
| - frameIdRef, ev.start);
|
| + findNavigationStartEventForFrameBeforeTimestamp(frameId, ev.start);
|
| // Ignore layout w/o preceding navigationStart, as they are not
|
| // attributed to any time-to-X metric.
|
| if (navigationStartEvent === undefined) continue;
|
| @@ -208,13 +334,7 @@ tr.exportTo('tr.metrics.sh', function() {
|
| }
|
|
|
| function addFirstMeaningfulPaintSample(samples, rendererHelper,
|
| - frameIdRef, navigationStart, fmpMarkerEvent) {
|
| - const snapshot = findFrameLoaderSnapshotAt(
|
| - rendererHelper, frameIdRef, fmpMarkerEvent.start);
|
| - if (!snapshot || !snapshot.args.isLoadingMainFrame) return;
|
| - const url = snapshot.args.documentLoaderURL;
|
| - if (shouldIgnoreURL(url)) return;
|
| -
|
| + navigationStart, fmpMarkerEvent, url) {
|
| const navStartToFMPRange = tr.b.math.Range.fromExplicitRange(
|
| navigationStart.start, fmpMarkerEvent.start);
|
| const networkEvents = getNetworkEventsInRange(
|
| @@ -241,15 +361,9 @@ tr.exportTo('tr.metrics.sh', function() {
|
| }
|
|
|
| function addFirstMeaningfulPaintCpuTimeSample(samples, rendererHelper,
|
| - frameIdRef, navigationStart, fmpMarkerEvent) {
|
| + navigationStart, fmpMarkerEvent, url) {
|
| const navStartToFMPCpuRange = tr.b.math.Range.fromExplicitRange(
|
| navigationStart.cpuStart, fmpMarkerEvent.cpuStart);
|
| - const snapshot = findFrameLoaderSnapshotAt(
|
| - rendererHelper, frameIdRef, fmpMarkerEvent.start);
|
| - if (!snapshot || !snapshot.args.isLoadingMainFrame) return;
|
| - const url = snapshot.args.documentLoaderURL;
|
| - if (shouldIgnoreURL(url)) return;
|
| -
|
| const mainThreadCpuTime = getMainThreadCpuTime(
|
| rendererHelper, navStartToFMPCpuRange);
|
|
|
| @@ -287,7 +401,6 @@ tr.exportTo('tr.metrics.sh', function() {
|
|
|
| function addFirstInteractiveSample(samples, rendererHelper,
|
| navigationStart, firstMeaningfulPaint, url) {
|
| - if (shouldIgnoreURL(url)) return;
|
| const navigationStartTime = navigationStart.start;
|
| let firstInteractive = Infinity;
|
| let firstInteractiveCandidate = firstMeaningfulPaint;
|
| @@ -364,18 +477,21 @@ tr.exportTo('tr.metrics.sh', function() {
|
| * Design doc: https://goo.gl/ISWndc
|
| */
|
| function collectFirstMeaningfulPaintAndTimeToInteractiveForRenderer(
|
| - rendererHelper, navigationStartFinder) {
|
| + rendererHelper, navigationStartFinder, mainFrameHelper) {
|
| const firstMeaningfulPaintSamples = [];
|
| const firstMeaningfulPaintCpuTimeSamples = [];
|
| const firstInteractiveSamples = [];
|
|
|
| - function addSamples(frameIdRef, navigationStart, fmpMarkerEvent) {
|
| + function addSamples(frameId, navigationStart, fmpMarkerEvent) {
|
| + if (!mainFrameHelper.isMainFrame(frameId, fmpMarkerEvent.start)) return;
|
| + const url = mainFrameHelper.getURL(frameId, fmpMarkerEvent.start);
|
| + if (url === undefined || shouldIgnoreURL(url)) return;
|
| const data = addFirstMeaningfulPaintSample(
|
| firstMeaningfulPaintSamples, rendererHelper,
|
| - frameIdRef, navigationStart, fmpMarkerEvent);
|
| + navigationStart, fmpMarkerEvent, url);
|
| addFirstMeaningfulPaintCpuTimeSample(
|
| firstMeaningfulPaintCpuTimeSamples, rendererHelper,
|
| - frameIdRef, navigationStart, fmpMarkerEvent);
|
| + navigationStart, fmpMarkerEvent, url);
|
| if (data !== undefined) {
|
| addFirstInteractiveSample(
|
| firstInteractiveSamples, rendererHelper,
|
| @@ -386,15 +502,15 @@ tr.exportTo('tr.metrics.sh', function() {
|
| const candidatesForFrameId =
|
| findFirstMeaningfulPaintCandidates(rendererHelper);
|
|
|
| - for (const frameIdRef in candidatesForFrameId) {
|
| + for (const frameId in candidatesForFrameId) {
|
| let navigationStart = undefined;
|
| let lastCandidate = undefined;
|
|
|
| // Iterate over the FMP candidates, remembering the last one.
|
| - for (const ev of candidatesForFrameId[frameIdRef]) {
|
| + for (const ev of candidatesForFrameId[frameId]) {
|
| const navigationStartForThisCandidate = navigationStartFinder.
|
| findNavigationStartEventForFrameBeforeTimestamp(
|
| - frameIdRef, ev.start);
|
| + frameId, ev.start);
|
| // Ignore candidate w/o preceding navigationStart, as they are not
|
| // attributed to any TTFMP.
|
| if (navigationStartForThisCandidate === undefined) continue;
|
| @@ -403,7 +519,7 @@ tr.exportTo('tr.metrics.sh', function() {
|
| // New navigation is found. Compute TTFMP for current navigation,
|
| // and reset the state variables.
|
| if (navigationStart !== undefined && lastCandidate !== undefined) {
|
| - addSamples(frameIdRef, navigationStart, lastCandidate);
|
| + addSamples(frameId, navigationStart, lastCandidate);
|
| }
|
| navigationStart = navigationStartForThisCandidate;
|
| }
|
| @@ -412,7 +528,7 @@ tr.exportTo('tr.metrics.sh', function() {
|
|
|
| // Compute TTFMP for the last navigation.
|
| if (lastCandidate !== undefined) {
|
| - addSamples(frameIdRef, navigationStart, lastCandidate);
|
| + addSamples(frameId, navigationStart, lastCandidate);
|
| }
|
| }
|
| return {
|
| @@ -424,17 +540,18 @@ tr.exportTo('tr.metrics.sh', function() {
|
|
|
| function collectLoadingMetricsForRenderer(rendererHelper) {
|
| const navigationStartFinder = new NavigationStartFinder(rendererHelper);
|
| + const mainFrameHelper = new MainFrameHelper(rendererHelper);
|
|
|
| const firstContentfulPaintSamples = collectTimeToEvent(
|
| 'loading', 'firstContentfulPaint',
|
| - rendererHelper, navigationStartFinder);
|
| + rendererHelper, navigationStartFinder, mainFrameHelper);
|
| const onLoadSamples = collectTimeToEvent(
|
| 'blink.user_timing', 'loadEventStart',
|
| - rendererHelper, navigationStartFinder);
|
| + rendererHelper, navigationStartFinder, mainFrameHelper);
|
| const {firstMeaningfulPaintSamples, firstMeaningfulPaintCpuTimeSamples,
|
| firstInteractiveSamples} =
|
| collectFirstMeaningfulPaintAndTimeToInteractiveForRenderer(
|
| - rendererHelper, navigationStartFinder);
|
| + rendererHelper, navigationStartFinder, mainFrameHelper);
|
| return {
|
| firstContentfulPaintSamples,
|
| onLoadSamples,
|
| @@ -506,6 +623,7 @@ tr.exportTo('tr.metrics.sh', function() {
|
| collectLoadingMetricsForRenderer,
|
| RESPONSIVENESS_THRESHOLD_MS,
|
| INTERACTIVE_WINDOW_SIZE_MS,
|
| + MainFrameHelper, // For testing.
|
| };
|
| });
|
| </script>
|
|
|