chrome-devtools-frontend
Version:
Chrome DevTools UI
448 lines (409 loc) • 18.4 kB
text/typescript
// Copyright 2023 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 TraceEngine from '../../models/trace/trace.js';
import type * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as TimelineModel from '../../models/timeline_model/timeline_model.js';
import * as Common from '../../core/common/common.js';
import {
type TimelineFlameChartEntry,
EntryType,
InstantEventVisibleDurationMs,
} from './TimelineFlameChartDataProvider.js';
import {TimingsTrackAppender} from './TimingsTrackAppender.js';
import {InteractionsTrackAppender} from './InteractionsTrackAppender.js';
import {GPUTrackAppender} from './GPUTrackAppender.js';
import {LayoutShiftsTrackAppender} from './LayoutShiftsTrackAppender.js';
import {getEventLevel} from './AppenderUtils.js';
import {TimelineUIUtils} from './TimelineUIUtils.js';
export type HighlightedEntryInfo = {
title: string,
formattedTime: string,
warning?: string,
};
/**
* Track appenders add the data of each track into the timeline flame
* chart. Each track appender also implements functions tha allow the
* canvas renderer to gather more information about an event in a track,
* like its display name or color.
*
* At the moment, tracks in the timeline flame chart are appended in
* two locations: in the TimelineFlameChartDataProvider and in the track
* appenders exported by this module. As part of the work to use a new
* trace parsing engine, a track appender will be defined with this API
* for each of the tracks in the timeline. With this implementation in
* place its counterpart in the TimelineFlameChartDataProvider can be
* removed. This processes of doing this for a track is referred to as
* "migrating the track" to the new system.
*
* The migration implementation will result beneficial among other
* things because the complexity of rendering the details of each track
* is distributed among multiple standalone modules.
* Read more at go/rpp-flamechart-arch
*/
export interface TrackAppender {
/**
* The unique name given to the track appender.
*/
appenderName: TrackAppenderName;
/**
* Appends into the flame chart data the data corresponding to a track.
* @param level the horizontal level of the flame chart events where the
* track's events will start being appended.
* @param expanded wether the track should be rendered expanded.
* @returns the first available level to append more data after having
* appended the track's events.
*/
appendTrackAtLevel(level: number, expanded?: boolean): number;
/**
* Returns the color an event is shown with in the timeline.
*/
colorForEvent(event: TraceEngine.Types.TraceEvents.TraceEventData): string;
/**
* Returns the title an event is shown with in the timeline.
*/
titleForEvent(event: TraceEngine.Types.TraceEvents.TraceEventData): string;
/**
* Returns the info shown when an event in the timeline is hovered.
*/
highlightedEntryInfo(event: TraceEngine.Types.TraceEvents.TraceEventData): HighlightedEntryInfo;
}
export const TrackNames = ['Timings', 'Interactions', 'GPU', 'LayoutShifts'] as const;
export type TrackAppenderName = typeof TrackNames[number];
export class CompatibilityTracksAppender {
#trackForLevel = new Map<number, TrackAppender>();
#trackForGroup = new Map<PerfUI.FlameChart.Group, TrackAppender>();
#eventsForTrack = new Map<TrackAppenderName, TraceEngine.Types.TraceEvents.TraceEventData[]>();
#trackEventsForTreeview = new Map<TrackAppenderName, TraceEngine.Types.TraceEvents.TraceEventData[]>();
#flameChartData: PerfUI.FlameChart.FlameChartTimelineData;
#traceParsedData: TraceEngine.TraceModel.PartialTraceParseDataDuringMigration;
#entryData: TimelineFlameChartEntry[];
#colorGenerator: Common.Color.Generator;
#indexForEvent = new WeakMap<TraceEngine.Types.TraceEvents.TraceEventData, number>();
#allTrackAppenders: TrackAppender[] = [];
#visibleTrackNames: Set<TrackAppenderName> = new Set([...TrackNames]);
// TODO(crbug.com/1416533)
// These are used only for compatibility with the legacy flame chart
// architecture of the panel. Once all tracks have been migrated to
// use the new engine and flame chart architecture, the reference can
// be removed.
#legacyTimelineModel: TimelineModel.TimelineModel.TimelineModelImpl;
#legacyEntryTypeByLevel: EntryType[];
#timingsTrackAppender: TimingsTrackAppender;
#interactionsTrackAppender: InteractionsTrackAppender;
#gpuTrackAppender: GPUTrackAppender;
#layoutShiftsTrackAppender: LayoutShiftsTrackAppender;
/**
* @param flameChartData the data used by the flame chart renderer on
* which the track data will be appended.
* @param traceParsedData the trace parsing engines output.
* @param entryData the array containing all event to be rendered in
* the flamechart.
* @param legacyEntryTypeByLevel an array containing the type of
* each entry in the entryData array. Indexed by the position the
* corresponding entry occupies in the entryData array. This reference
* is needed only for compatibility with the legacy flamechart
* architecture and should be removed once all tracks use the new
* system.
*/
constructor(
flameChartData: PerfUI.FlameChart.FlameChartTimelineData,
traceParsedData: TraceEngine.TraceModel.PartialTraceParseDataDuringMigration,
entryData: TimelineFlameChartEntry[], legacyEntryTypeByLevel: EntryType[],
legacyTimelineModel: TimelineModel.TimelineModel.TimelineModelImpl) {
this.#flameChartData = flameChartData;
this.#traceParsedData = traceParsedData;
this.#entryData = entryData;
this.#colorGenerator = new Common.Color.Generator(
/* hueSpace= */ {min: 30, max: 55, count: undefined},
/* satSpace= */ {min: 70, max: 100, count: 6},
/* lightnessSpace= */ 50,
/* alphaSpace= */ 0.7);
this.#legacyEntryTypeByLevel = legacyEntryTypeByLevel;
this.#legacyTimelineModel = legacyTimelineModel;
this.#timingsTrackAppender =
new TimingsTrackAppender(this, this.#flameChartData, this.#traceParsedData, this.#colorGenerator);
this.#allTrackAppenders.push(this.#timingsTrackAppender);
this.#interactionsTrackAppender =
new InteractionsTrackAppender(this, this.#flameChartData, this.#traceParsedData, this.#colorGenerator);
this.#allTrackAppenders.push(this.#interactionsTrackAppender);
this.#gpuTrackAppender = new GPUTrackAppender(this, this.#traceParsedData);
this.#allTrackAppenders.push(this.#gpuTrackAppender);
// Layout Shifts track in OPP was called the "Experience" track even though
// all it shows are layout shifts.
this.#layoutShiftsTrackAppender = new LayoutShiftsTrackAppender(this, this.#flameChartData, this.#traceParsedData);
this.#allTrackAppenders.push(this.#layoutShiftsTrackAppender);
}
/**
* Given a trace event returns instantiates a legacy SDK.Event. This should
* be used for compatibility purposes only.
*/
getLegacyEvent(event: TraceEngine.Types.TraceEvents.TraceEventData): SDK.TracingModel.Event|null {
const process = this.#legacyTimelineModel.tracingModel()?.getProcessById(event.pid);
const thread = process?.threadById(event.tid);
if (!thread) {
return null;
}
return SDK.TracingModel.PayloadEvent.fromPayload(event as unknown as SDK.TracingManager.EventPayload, thread);
}
timingsTrackAppender(): TimingsTrackAppender {
return this.#timingsTrackAppender;
}
interactionsTrackAppender(): InteractionsTrackAppender {
return this.#interactionsTrackAppender;
}
gpuTrackAppender(): GPUTrackAppender {
return this.#gpuTrackAppender;
}
layoutShiftsTrackAppender(): LayoutShiftsTrackAppender {
return this.#layoutShiftsTrackAppender;
}
/**
* Get the index of the event.
* This ${index}-th elements in entryData, flameChartData.entryLevels, flameChartData.entryTotalTimes,
* flameChartData.entryStartTimes are all related to this event.
*/
indexForEvent(event: TraceEngine.Types.TraceEvents.TraceEventData): number|undefined {
return this.#indexForEvent.get(event);
}
eventsInTrack(trackAppenderName: TrackAppenderName): TraceEngine.Types.TraceEvents.TraceEventData[] {
const cachedData = this.#eventsForTrack.get(trackAppenderName);
if (cachedData) {
return cachedData;
}
// Calculate the levels occupied by a track.
let trackStartLevel = null;
let trackEndLevel = null;
for (const [level, track] of this.#trackForLevel) {
if (track.appenderName !== trackAppenderName) {
continue;
}
if (trackStartLevel === null) {
trackStartLevel = level;
}
trackEndLevel = level;
}
if (trackStartLevel === null || trackEndLevel === null) {
throw new Error(`Could not find events for track: ${trackAppenderName}`);
}
const entryLevels = this.#flameChartData.entryLevels;
const events = [];
for (let i = 0; i < entryLevels.length; i++) {
if (trackStartLevel <= entryLevels[i] && entryLevels[i] <= trackEndLevel) {
events.push(this.#entryData[i] as TraceEngine.Types.TraceEvents.TraceEventData);
}
}
events.sort((a, b) => a.ts - b.ts);
this.#eventsForTrack.set(trackAppenderName, events);
return events;
}
/**
* Determines if the given events, which are assumed to be ordered can
* be organized into tree structures.
* This condition is met if there is *not* a pair of async events
* e1 and e2 where:
*
* e1.startTime <= e2.startTime && e1.endTime > e2.startTime && e1.endTime > e2.endTime.
* or, graphically:
* |------- e1 ------|
* |------- e2 --------|
*
* Because a parent-child relationship cannot be made from the example
* above, a tree cannot be made from the set of events.
*
* Note that this will also return true if multiple trees can be
* built, for example if none of the events overlap with each other.
*/
canBuildTreesFromEvents(events: readonly TraceEngine.Types.TraceEvents.TraceEventData[]): boolean {
const stack: TraceEngine.Types.TraceEvents.TraceEventData[] = [];
for (const event of events) {
const startTime = event.ts;
const endTime = event.ts + (event.dur || 0);
let parent = stack.at(-1);
if (parent === undefined) {
stack.push(event);
continue;
}
let parentEndTime = parent.ts + (parent.dur || 0);
// Discard events that are not parents for this event. The parent
// is one whose end time is after this event start time.
while (stack.length && startTime >= parentEndTime) {
stack.pop();
parent = stack.at(-1);
if (parent === undefined) {
break;
}
parentEndTime = parent.ts + (parent.dur || 0);
}
if (stack.length && endTime > parentEndTime) {
// If such an event exists but its end time is before this
// event's end time, then a tree cannot be made using this
// events.
return false;
}
stack.push(event);
}
return true;
}
/**
* Gets the events to be shown in the tree views of the details pane
* (Bottom-up, Call tree, etc.). These are the events from the track
* that can be arranged in a tree shape.
*/
eventsForTreeView(trackAppenderName: TrackAppenderName): TraceEngine.Types.TraceEvents.TraceEventData[] {
const cachedData = this.#trackEventsForTreeview.get(trackAppenderName);
if (cachedData) {
return cachedData;
}
let trackEvents = this.eventsInTrack(trackAppenderName);
if (!this.canBuildTreesFromEvents(trackEvents)) {
// Some tracks can include both async and sync events. When this
// happens, we use all events for the tree views if a trees can be
// built from both sync and async events. If this is not possible,
// async events are filtered out and only sync events are used
// (it's assumed a tree can always be built using a tracks sync
// events).
trackEvents = trackEvents.filter(e => !TraceEngine.Types.TraceEvents.isAsyncPhase(e.ph));
}
this.#trackEventsForTreeview.set(trackAppenderName, trackEvents);
return trackEvents;
}
/**
* Caches the track appender that owns a flame chart group. FlameChart
* groups are created for each track in the timeline. When an user
* selects a track in the UI, the track's group is passed to the model
* layer to inform about the selection.
*/
registerTrackForGroup(group: PerfUI.FlameChart.Group, appender: TrackAppender): void {
this.#flameChartData.groups.push(group);
this.#trackForGroup.set(group, appender);
}
/**
* Given a FlameChart group, gets the events to be shown in the tree
* views if that group was registered by the appender system.
*/
groupEventsForTreeView(group: PerfUI.FlameChart.Group): TraceEngine.Types.TraceEvents.TraceEventData[]|null {
const track = this.#trackForGroup.get(group);
if (!track) {
return null;
}
return this.eventsForTreeView(track.appenderName);
}
/**
* Caches the track appender that owns a level. An appender takes
* ownership of a level when it appends data to it.
* The cache is useful to determine what appender should handle a
* query from the flame chart renderer when an event's feature (like
* style, title, etc.) is needed.
*/
registerTrackForLevel(level: number, appender: TrackAppender): void {
// TODO(crbug.com/1442454) Figure out how to avoid the circular calls.
this.#trackForLevel.set(level, appender);
}
/**
* Adds an event to the flame chart data at a defined level.
* @param event the event to be appended,
* @param level the level to append the event,
* @param appender the track which the event belongs to.
* @returns the index of the event in all events to be rendered in the flamechart.
*/
appendEventAtLevel(event: TraceEngine.Types.TraceEvents.TraceEventData, level: number, appender: TrackAppender):
number {
// TODO(crbug.com/1442454) Figure out how to avoid the circular calls.
this.#trackForLevel.set(level, appender);
const index = this.#entryData.length;
this.#entryData.push(event);
this.#indexForEvent.set(event, index);
this.#legacyEntryTypeByLevel[level] = EntryType.TrackAppender;
this.#flameChartData.entryLevels[index] = level;
this.#flameChartData.entryStartTimes[index] = TraceEngine.Helpers.Timing.microSecondsToMilliseconds(event.ts);
const msDuration = event.dur ||
TraceEngine.Helpers.Timing.millisecondsToMicroseconds(
InstantEventVisibleDurationMs as TraceEngine.Types.Timing.MilliSeconds);
this.#flameChartData.entryTotalTimes[index] = TraceEngine.Helpers.Timing.microSecondsToMilliseconds(msDuration);
return index;
}
/**
* Adds into the flame chart data a list of trace events.
* @param events the trace events that will be appended to the flame chart.
* The events should be taken straight from the trace handlers. The handlers
* should sort the events by start time, and the parent event is before the
* child.
* @param trackStartLevel the flame chart level from which the events will
* be appended.
* @param appender the track that the trace events belong to.
* @returns the next level after the last occupied by the appended these
* trace events (the first available level to append next track).
*/
appendEventsAtLevel(
events: readonly TraceEngine.Types.TraceEvents.TraceEventData[], trackStartLevel: number,
appender: TrackAppender): number {
const lastUsedTimeByLevel: number[] = [];
for (let i = 0; i < events.length; ++i) {
const event = events[i];
const eventAsLegacy = this.getLegacyEvent(event);
// Default styles are globally defined for each event name. Some
// events are hidden by default.
const visibleNames = new Set(TimelineUIUtils.visibleTypes());
const eventIsVisible = eventAsLegacy &&
visibleNames.has(TimelineModel.TimelineModelFilter.TimelineVisibleEventsFilter.eventType(eventAsLegacy));
if (!eventIsVisible) {
continue;
}
const level = getEventLevel(event, lastUsedTimeByLevel);
this.appendEventAtLevel(event, trackStartLevel + level, appender);
}
this.#legacyEntryTypeByLevel.length = trackStartLevel + lastUsedTimeByLevel.length;
this.#legacyEntryTypeByLevel.fill(EntryType.TrackAppender, trackStartLevel);
return trackStartLevel + lastUsedTimeByLevel.length;
}
/**
* Gets the all track appenders that have been set to be visible.
*/
allVisibleTrackAppenders(): TrackAppender[] {
return this.#allTrackAppenders.filter(track => this.#visibleTrackNames.has(track.appenderName));
}
/**
* Sets the visible tracks internally
* @param visibleTracks set with the names of the visible track
* appenders. If undefined, all tracks are set to be visible.
*/
setVisibleTracks(visibleTracks?: Set<TrackAppenderName>): void {
if (!visibleTracks) {
this.#visibleTrackNames = new Set([...TrackNames]);
return;
}
this.#visibleTrackNames = visibleTracks;
}
/**
* Returns the color an event is shown with in the timeline.
*/
colorForEvent(event: TraceEngine.Types.TraceEvents.TraceEventData, level: number): string {
const track = this.#trackForLevel.get(level);
if (!track) {
throw new Error('Track not found for level');
}
return track.colorForEvent(event);
}
/**
* Returns the title an event is shown with in the timeline.
*/
titleForEvent(event: TraceEngine.Types.TraceEvents.TraceEventData, level: number): string {
const track = this.#trackForLevel.get(level);
if (!track) {
throw new Error('Track not found for level');
}
return track.titleForEvent(event);
}
/**
* Returns the info shown when an event in the timeline is hovered.
*/
highlightedEntryInfo(event: TraceEngine.Types.TraceEvents.TraceEventData, level: number): HighlightedEntryInfo {
const track = this.#trackForLevel.get(level);
if (!track) {
throw new Error('Track not found for level');
}
return track.highlightedEntryInfo(event);
}
}