chrome-devtools-frontend
Version:
Chrome DevTools UI
298 lines (279 loc) • 13.2 kB
text/typescript
// Copyright 2024 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.
import * as Helpers from '../helpers/helpers.js';
import * as Types from '../types/types.js';
import type {HandlerName} from './types.js';
import {data as userTimingsData} from './UserTimingsHandler.js';
const extensionTrackEntries: Types.Extensions.SyntheticExtensionTrackEntry[] = [];
const extensionTrackData: Types.Extensions.ExtensionTrackData[] = [];
const extensionMarkers: Types.Extensions.SyntheticExtensionMarker[] = [];
const entryToNode = new Map<Types.Events.Event, Helpers.TreeHelpers.TraceEntryNode>();
const timeStampByName = new Map<string, Types.Events.ConsoleTimeStamp>();
const syntheticConsoleEntriesForTimingsTrack: Types.Events.SyntheticConsoleTimeStamp[] = [];
export interface ExtensionTraceData {
extensionTrackData: readonly Types.Extensions.ExtensionTrackData[];
extensionMarkers: readonly Types.Extensions.SyntheticExtensionMarker[];
// TODO(andoli): Can we augment Renderer's entryToNode instead? To avoid the split of TimelineUIUtils's getEventSelfTime()?
entryToNode: Map<Types.Events.Event, Helpers.TreeHelpers.TraceEntryNode>;
syntheticConsoleEntriesForTimingsTrack: Types.Events.SyntheticConsoleTimeStamp[];
}
export function handleEvent(_event: Types.Events.Event): void {
// Implementation not needed because data is sourced from UserTimingsHandler
}
export function reset(): void {
extensionTrackEntries.length = 0;
syntheticConsoleEntriesForTimingsTrack.length = 0;
extensionTrackData.length = 0;
extensionMarkers.length = 0;
entryToNode.clear();
timeStampByName.clear();
}
export async function finalize(): Promise<void> {
createExtensionFlameChartEntries();
}
function createExtensionFlameChartEntries(): void {
const pairedMeasures: readonly Types.Events.SyntheticUserTimingPair[] = userTimingsData().performanceMeasures;
const marks: readonly Types.Events.PerformanceMark[] = userTimingsData().performanceMarks;
const mergedRawExtensionEvents = Helpers.Trace.mergeEventsInOrder(pairedMeasures, marks);
extractPerformanceAPIExtensionEntries(mergedRawExtensionEvents);
extractConsoleAPIExtensionEntries();
// extensionTrackEntries is filled by the above two calls.
Helpers.Trace.sortTraceEventsInPlace(extensionTrackEntries);
Helpers.Extensions.buildTrackDataFromExtensionEntries(extensionTrackEntries, extensionTrackData, entryToNode);
}
/**
* Extracts extension entries from console.timeStamp events.
*
* Entries are built by pairing `console.timeStamp` events based on
* their names. When a `console.timeStamp` event includes a `start`
* argument (and optionally an `end` argument), it attempts to find
* previously recorded `console.timeStamp` events with names matching
* the `start` and `end` values. These matching events are then used to
* determine the start and end times of the new entry.
*
* If a `console.timeStamp` event includes data for a custom track
* (specified by the `track` argument), an extension track entry is
* created and added to the `extensionTrackEntries` array. These entries
* are used to visualize custom tracks in the Performance panel.
*
* If a `console.timeStamp` event includes data for a custom track
* (specified by the `track` argument), an extension track entry is
* created and added to the `extensionTrackEntries` array. These entries
* are used to visualize custom tracks in the Performance panel.
*
* If a `console.timeStamp` event does not specify a custom track but
* includes a start and/or end time (referencing other
* `console.timeStamp` names), a synthetic console time stamp entry is
* created and added to the `syntheticConsoleEntriesForTimingsTrack`
* array. These entries are displayed in the "Timings" track.
*/
export function extractConsoleAPIExtensionEntries(): void {
const consoleTimeStamps: readonly Types.Events.ConsoleTimeStamp[] = userTimingsData().timestampEvents;
for (const currentTimeStamp of consoleTimeStamps) {
if (!currentTimeStamp.args.data) {
continue;
}
const timeStampName = String(currentTimeStamp.args.data.name ?? currentTimeStamp.args.data.message);
timeStampByName.set(timeStampName, currentTimeStamp);
const extensionData = extensionDataInConsoleTimeStamp(currentTimeStamp);
const start = currentTimeStamp.args.data.start;
const end = currentTimeStamp.args.data.end;
if (!extensionData && !start && !end) {
continue;
}
// If the start or end is a number, it's assumed to be a timestamp
// from the tracing clock, so we use that directly, otherwise we
// assume it's the label of a previous console timestamp, in which
// case we use its corresponding timestamp.
const startTimeStamp =
typeof start === 'number' ? Types.Timing.Micro(start) : timeStampByName.get(String(start))?.ts;
const endTimeStamp = typeof end === 'number' ? Types.Timing.Micro(end) : timeStampByName.get(String(end))?.ts;
if (endTimeStamp !== undefined && startTimeStamp === undefined) {
// Invalid data
continue;
}
const entryStartTime = startTimeStamp ?? currentTimeStamp.ts;
const entryEndTime = endTimeStamp ?? currentTimeStamp.ts;
if (extensionData) {
const unregisteredExtensionEntry: Omit<Types.Extensions.SyntheticExtensionTrackEntry, '_tag'> = {
...currentTimeStamp,
name: timeStampName,
cat: 'devtools.extension',
args: extensionData,
rawSourceEvent: currentTimeStamp,
dur: Types.Timing.Micro(entryEndTime - entryStartTime),
ts: entryStartTime,
ph: Types.Events.Phase.COMPLETE,
};
const extensionEntry =
Helpers.SyntheticEvents.SyntheticEventsManager.getActiveManager()
.registerSyntheticEvent<Types.Extensions.SyntheticExtensionTrackEntry>(unregisteredExtensionEntry);
extensionTrackEntries.push(extensionEntry);
continue;
}
// If no extension data is found in the entry (no custom track name
// was passed), but the entry has a duration. we still save it here
// to be added in the timings track. Note that timings w/o duration
// and extension data are already handled by the UserTimingsHandler.
const unregisteredSyntheticTimeStamp: Omit<Types.Events.SyntheticConsoleTimeStamp, '_tag'> = {
...currentTimeStamp,
name: timeStampName,
cat: 'disabled-by-default-v8.inspector',
ph: Types.Events.Phase.COMPLETE,
ts: entryStartTime,
dur: Types.Timing.Micro(entryEndTime - entryStartTime),
rawSourceEvent: currentTimeStamp
};
const syntheticTimeStamp =
Helpers.SyntheticEvents.SyntheticEventsManager.getActiveManager()
.registerSyntheticEvent<Types.Events.SyntheticConsoleTimeStamp>(unregisteredSyntheticTimeStamp);
syntheticConsoleEntriesForTimingsTrack.push(syntheticTimeStamp);
}
}
/**
* Extracts extension entries from Performance API events (marks and
* measures).
* It specifically looks for events that contain extension-specific data
* within their `detail` property.
*
* If an event's `detail` property can be parsed as a JSON object and
* contains a `devtools` field with a valid extension payload, a
* synthetic extension entry is created. The type of extension entry
* created depends on the payload:
*
* - If the payload conforms to `ExtensionPayloadMarker`, a
* `SyntheticExtensionMarker` is created and added to the
* `extensionMarkers` array. These markers represent single points in
* time.
* - If the payload conforms to `ExtensionPayloadTrackEntry`, a
* `SyntheticExtensionTrackEntry` is created and added to the
* `extensionTrackEntries` array. These entries represent events with
* a duration and are displayed on custom tracks in the Performance
* panel.
*
* **Note:** Only events with a `detail` property that contains valid
* extension data are processed. Other `performance.mark` and
* `performance.measure` events are ignored.
*
* @param timings An array of `SyntheticUserTimingPair` or
* `PerformanceMark` events, typically obtained from the
* `UserTimingsHandler`.
*/
export function extractPerformanceAPIExtensionEntries(
timings: Array<Types.Events.SyntheticUserTimingPair|Types.Events.PerformanceMark>): void {
for (const timing of timings) {
const extensionPayload = extensionDataInPerformanceTiming(timing);
if (!extensionPayload) {
// Not an extension user timing.
continue;
}
const extensionSyntheticEntry = {
name: timing.name,
ph: Types.Extensions.isExtensionPayloadMarker(extensionPayload) ? Types.Events.Phase.INSTANT :
Types.Events.Phase.COMPLETE,
pid: timing.pid,
tid: timing.tid,
ts: timing.ts,
dur: timing.dur as Types.Timing.Micro,
cat: 'devtools.extension',
args: extensionPayload,
rawSourceEvent: Types.Events.isSyntheticUserTiming(timing) ? timing.rawSourceEvent : timing,
};
if (Types.Extensions.isExtensionPayloadMarker(extensionPayload)) {
const extensionMarker =
Helpers.SyntheticEvents.SyntheticEventsManager.getActiveManager()
.registerSyntheticEvent<Types.Extensions.SyntheticExtensionMarker>(
extensionSyntheticEntry as Omit<Types.Extensions.SyntheticExtensionMarker, '_tag'>);
extensionMarkers.push(extensionMarker);
continue;
}
if (Types.Extensions.isExtensionPayloadTrackEntry(extensionSyntheticEntry.args)) {
const extensionTrackEntry =
Helpers.SyntheticEvents.SyntheticEventsManager.getActiveManager()
.registerSyntheticEvent<Types.Extensions.SyntheticExtensionTrackEntry>(
extensionSyntheticEntry as Omit<Types.Extensions.SyntheticExtensionTrackEntry, '_tag'>);
extensionTrackEntries.push(extensionTrackEntry);
continue;
}
}
}
export function extensionDataInPerformanceTiming(
timing: Types.Events.SyntheticUserTimingPair|Types.Events.PerformanceMark): Types.Extensions.ExtensionDataPayload|
null {
const timingDetail =
Types.Events.isPerformanceMark(timing) ? timing.args.data?.detail : timing.args.data.beginEvent.args.detail;
if (!timingDetail) {
return null;
}
try {
// Attempt to parse the detail as an object that might be coming from a
// DevTools Perf extension.
// Wrapped in a try-catch because timingDetail might either:
// 1. Not be `json.parse`-able (it should, but just in case...)
// 2.Not be an object - in which case the `in` check will error.
// If we hit either of these cases, we just ignore this mark and move on.
const detailObj = JSON.parse(timingDetail);
if (!('devtools' in detailObj)) {
return null;
}
if (!Types.Extensions.isValidExtensionPayload(detailObj.devtools)) {
return null;
}
return detailObj.devtools;
} catch {
// No need to worry about this error, just discard this event and don't
// treat it as having any useful information for the purposes of extensions
return null;
}
}
/**
* Extracts extension data from a `console.timeStamp` event.
*
* Checks if a `console.timeStamp` event contains data intended for
* creating a custom track entry in the DevTools Performance panel. It
* specifically looks for a `track` argument within the event's data.
*
* If a `track` argument is present (and not an empty string), the
* function constructs an `ExtensionTrackEntryPayload` object containing
* the track name, an optional color, an optional track group. This
* payload is then used to create a `SyntheticExtensionTrackEntry`.
*
* **Note:** The `color` argument is optional and its type is validated
* against a predefined palette (see
* `ExtensionUI::extensionEntryColor`).
*
* @param timeStamp The `ConsoleTimeStamp` event to extract data from.
* @return An `ExtensionTrackEntryPayload` object if the event contains
* valid extension data for a track entry, or `null` otherwise.
*/
export function extensionDataInConsoleTimeStamp(timeStamp: Types.Events.ConsoleTimeStamp):
Types.Extensions.ExtensionTrackEntryPayload|null {
if (!timeStamp.args.data) {
return null;
}
const trackName = timeStamp.args.data.track;
if (trackName === '' || trackName === undefined) {
return null;
}
return {
// the color is defaulted to primary if it's value isn't one from
// the defined palette (see ExtensionUI::extensionEntryColor) so
// we don't need to check the value is valid here.
color: String(timeStamp.args.data.color) as Types.Extensions.ExtensionTrackEntryPayload['color'],
track: String(trackName),
dataType: 'track-entry',
trackGroup: timeStamp.args.data.trackGroup !== undefined ? String(timeStamp.args.data.trackGroup) : undefined
};
}
export function data(): ExtensionTraceData {
return {
entryToNode,
extensionTrackData,
extensionMarkers,
syntheticConsoleEntriesForTimingsTrack,
};
}
export function deps(): HandlerName[] {
return ['UserTimings'];
}