UNPKG

chrome-devtools-frontend

Version:
234 lines (214 loc) • 8.39 kB
// Copyright 2022 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as Helpers from '../helpers/helpers.js'; import * as Types from '../types/types.js'; /** * IMPORTANT! * See UserTimings.md in this directory for some handy documentation on * UserTimings and the trace events we parse currently. **/ let syntheticEvents: Array<Types.Events.SyntheticEventPair<Types.Events.PairableAsync>> = []; // There are two events dispatched for performance.measure calls: one to // represent the measured timing in the tracing clock (which we type as // PerformanceMeasure) and another one for the call itself (which we // type as UserTimingMeasure). The two events corresponding to the same // call are linked together by a common trace_id. The reason two events // are dispatched is because the first was originally added with the // implementation of the performance.measure API and it uses an // overridden timestamp and duration. To prevent breaking potential deps // created since then, a second event was added instead of changing the // params of the first. let measureTraceByTraceId = new Map<number, Types.Events.UserTimingMeasure>(); let performanceMeasureEvents: Types.Events.PerformanceMeasure[] = []; let performanceMarkEvents: Types.Events.PerformanceMark[] = []; let consoleTimings: Array<Types.Events.ConsoleTimeBegin|Types.Events.ConsoleTimeEnd> = []; let timestampEvents: Types.Events.ConsoleTimeStamp[] = []; export interface UserTimingsData { /** * Events triggered with the performance.measure() API. * https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure */ performanceMeasures: readonly Types.Events.SyntheticUserTimingPair[]; /** * Events triggered with the performance.mark() API. * https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark */ performanceMarks: readonly Types.Events.PerformanceMark[]; /** * Events triggered with the console.time(), console.timeEnd() and * console.timeLog() API. * https://developer.mozilla.org/en-US/docs/Web/API/console/time */ consoleTimings: readonly Types.Events.SyntheticConsoleTimingPair[]; /** * Events triggered with the console.timeStamp() API * https://developer.mozilla.org/en-US/docs/Web/API/console/timeStamp */ timestampEvents: readonly Types.Events.ConsoleTimeStamp[]; /** * Events triggered to trace the call to performance.measure itself, * cached by trace_id. */ measureTraceByTraceId: Map<number, Types.Events.UserTimingMeasure>; } export function reset(): void { syntheticEvents = []; performanceMeasureEvents = []; performanceMarkEvents = []; consoleTimings = []; timestampEvents = []; measureTraceByTraceId = new Map(); } const resourceTimingNames = [ 'workerStart', 'redirectStart', 'redirectEnd', 'fetchStart', 'domainLookupStart', 'domainLookupEnd', 'connectStart', 'connectEnd', 'secureConnectionStart', 'requestStart', 'responseStart', 'responseEnd', ]; const navTimingNames = [ 'navigationStart', 'unloadEventStart', 'unloadEventEnd', 'redirectStart', 'redirectEnd', 'fetchStart', 'commitNavigationEnd', 'domainLookupStart', 'domainLookupEnd', 'connectStart', 'connectEnd', 'secureConnectionStart', 'requestStart', 'responseStart', 'responseEnd', 'domLoading', 'domInteractive', 'domContentLoadedEventStart', 'domContentLoadedEventEnd', 'domComplete', 'loadEventStart', 'loadEventEnd', ]; // These are events dispatched under the blink.user_timing category // but that the user didn't add. Filter them out so that they do not // Appear in the timings track (they still appear in the main thread // flame chart). const ignoredNames = [...resourceTimingNames, ...navTimingNames]; function getEventTimings(event: Types.Events.SyntheticEventPair|Types.Events.ConsoleTimeStamp): {start: Types.Timing.Micro, end: Types.Timing.Micro} { if ('dur' in event) { // It's a SyntheticEventPair. return {start: event.ts, end: Types.Timing.Micro(event.ts + (event.dur ?? 0))}; } if (Types.Events.isConsoleTimeStamp(event)) { const {start, end} = event.args.data || {}; if (typeof start === 'number' && typeof end === 'number') { return {start: Types.Timing.Micro(start), end: Types.Timing.Micro(end)}; } } // A ConsoleTimeStamp without start/end is just a point in time, so dur is 0. return {start: event.ts, end: event.ts}; } function getEventTrack(event: Types.Events.SyntheticEventPair|Types.Events.ConsoleTimeStamp): string|undefined { if (event.cat === 'blink.user_timing') { // This is a SyntheticUserTimingPair const detailString = ((event as Types.Events.SyntheticUserTimingPair).args.data.beginEvent.args as {detail?: string})?.detail; if (detailString) { const details = Helpers.Trace.parseDevtoolsDetails(detailString, 'devtools'); if (details && 'track' in details) { return details.track; } } } else if (Types.Events.isConsoleTimeStamp(event)) { const track = event.args.data?.track; return typeof track === 'string' ? track : undefined; } // SyntheticConsoleTimingPair does not have track info. return undefined; } /** * Similar to the default {@link Helpers.Trace.eventTimeComparator} * but with a twist: * In case of equal start and end times, put the second event (within a * track) first. * * Explanation: * User timing entries come as trace events dispatched when * performance.measure/mark is called. The trace events buffered in * devtools frontend are sorted by the start time. If their start time * is the same, then the event for the first call will appear first. * * When entries are meant to be stacked, the corresponding * performance.measure calls usually are done in bottom-up direction: * calls for children first and for parent later (because the call * is usually done when the measured task is over). This means that * when two user timing events have the same start and end time, usually * the second event is the parent of the first. Hence the switch. * */ export function userTimingComparator<T extends Types.Events.SyntheticEventPair|Types.Events.ConsoleTimeStamp>( a: T, b: T, originalArray: readonly T[]): number { const {start: aStart, end: aEnd} = getEventTimings(a); const {start: bStart, end: bEnd} = getEventTimings(b); const timeDifference = Helpers.Trace.compareBeginAndEnd(aStart, bStart, aEnd, bEnd); if (timeDifference) { return timeDifference; } // Never re-order entries across different tracks. const aTrack = getEventTrack(a); const bTrack = getEventTrack(b); if (aTrack !== bTrack) { return 0; // Preserve current positions. } // Prefer the event located in a further position in the original array. const aIndex = originalArray.indexOf(a); const bIndex = originalArray.indexOf(b); return bIndex - aIndex; } export function handleEvent(event: Types.Events.Event): void { if (ignoredNames.includes(event.name)) { return; } if (Types.Events.isUserTimingMeasure(event)) { measureTraceByTraceId.set(event.args.traceId, event); } if (Types.Events.isPerformanceMeasure(event)) { performanceMeasureEvents.push(event); return; } if (Types.Events.isPerformanceMark(event)) { performanceMarkEvents.push(event); } if (Types.Events.isConsoleTime(event)) { consoleTimings.push(event); } if (Types.Events.isConsoleTimeStamp(event)) { timestampEvents.push(event); } } export async function finalize(): Promise<void> { const asyncEvents = [...performanceMeasureEvents, ...consoleTimings]; syntheticEvents = Helpers.Trace.createMatchedSortedSyntheticEvents(asyncEvents); syntheticEvents = syntheticEvents.sort((a, b) => userTimingComparator(a, b, [...syntheticEvents])); timestampEvents = timestampEvents.sort((a, b) => userTimingComparator(a, b, [...timestampEvents])); } export function data(): UserTimingsData { return { consoleTimings: syntheticEvents.filter(e => e.cat === 'blink.console') as Types.Events.SyntheticConsoleTimingPair[], performanceMeasures: syntheticEvents.filter(e => e.cat === 'blink.user_timing') as Types.Events.SyntheticUserTimingPair[], performanceMarks: performanceMarkEvents, timestampEvents, measureTraceByTraceId, }; }