chrome-devtools-frontend
Version:
Chrome DevTools UI
238 lines (218 loc) • 10 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 i18n from '../../core/i18n/i18n.js';
import * as Trace from '../../models/trace/trace.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js';
import {
addDecorationToEvent,
buildGroupStyle,
buildTrackHeader,
getEventLevel,
type LastTimestampByLevel,
} from './AppenderUtils.js';
import {
type TrackAppender,
type TrackAppenderName,
VisualLoggingTrackName,
} from './CompatibilityTracksAppender.js';
import * as Components from './components/components.js';
import {InstantEventVisibleDurationMs} from './TimelineFlameChartDataProvider.js';
const UIStrings = {
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*/
network: 'Network',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/timeline/NetworkTrackAppender.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export type NetworkTrackEvent = Trace.Types.Events.SyntheticNetworkRequest|Trace.Types.Events.WebSocketEvent;
export class NetworkTrackAppender implements TrackAppender {
readonly appenderName: TrackAppenderName = 'Network';
#flameChartData: PerfUI.FlameChart.FlameChartTimelineData;
webSocketIdToLevel = new Map<number, number>();
#events: NetworkTrackEvent[] = [];
#font: string;
#group?: PerfUI.FlameChart.Group;
constructor(flameChartData: PerfUI.FlameChart.FlameChartTimelineData, events: NetworkTrackEvent[]) {
this.#flameChartData = flameChartData;
this.#events = events;
this.#font = `${PerfUI.Font.DEFAULT_FONT_SIZE} ${PerfUI.Font.getFontFamilyForCanvas()}`;
ThemeSupport.ThemeSupport.instance().addEventListener(ThemeSupport.ThemeChangeEvent.eventName, () => {
if (this.#group) {
// We only need to update the color here, because FlameChart will call `scheduleUpdate()` when theme is changed.
this.#group.style.color = ThemeSupport.ThemeSupport.instance().getComputedValue('--sys-color-on-surface');
this.#group.style.backgroundColor =
ThemeSupport.ThemeSupport.instance().getComputedValue('--sys-color-cdt-base-container');
}
});
}
group(): PerfUI.FlameChart.Group|undefined {
return this.#group;
}
font(): string {
return this.#font;
}
/**
* Appends into the flame chart data the data corresponding to the
* Network track.
* @param trackStartLevel 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(trackStartLevel: number, expanded?: boolean|undefined): number {
if (this.#events.length === 0) {
return trackStartLevel;
}
this.#appendTrackHeaderAtLevel(trackStartLevel, expanded);
return this.#appendEventsAtLevel(this.#events, trackStartLevel);
}
/**
* Adds into the flame chart data the header corresponding to the
* Network track. A header is added in the shape of a group in the
* flame chart data. A group has a predefined style and a reference
* to the definition of the legacy track (which should be removed
* in the future).
* @param currentLevel the flame chart level at which the header is
* appended.
* @param expanded wether the track should be rendered expanded.
*/
#appendTrackHeaderAtLevel(_currentLevel: number, expanded?: boolean): void {
const style = buildGroupStyle({
shareHeaderLine: false,
useFirstLineForOverview: false,
useDecoratorsForOverview: true,
});
this.#group = buildTrackHeader(
VisualLoggingTrackName.NETWORK, 0, i18nString(UIStrings.network), style, /* selectable= */ true, expanded,
/* showStackContextMenu= */ false);
this.#flameChartData.groups.push(this.#group);
}
/**
* 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.
* @returns the next level after the last occupied by the appended these
* trace events (the first available level to append next track).
*/
#appendEventsAtLevel(events: NetworkTrackEvent[], trackStartLevel: number): number {
// Appending everything to the same level isn't "correct", but relayoutEntriesWithinBounds() will handle that
// before anything is rendered.
for (let i = 0; i < events.length; ++i) {
const event = events[i];
this.#appendEventAtLevel(event, trackStartLevel);
// Decorate render blocking
if (Trace.Types.Events.isSyntheticNetworkRequest(event) &&
Trace.Helpers.Network.isSyntheticNetworkRequestEventRenderBlocking(event)) {
addDecorationToEvent(this.#flameChartData, i, {
type: PerfUI.FlameChart.FlameChartDecorationType.WARNING_TRIANGLE,
customStartTime: event.args.data.syntheticData.sendStartTime,
customEndTime: event.args.data.syntheticData.finishTime,
});
}
}
return this.relayoutEntriesWithinBounds(
events, Trace.Types.Timing.Milli(-Infinity), Trace.Types.Timing.Milli(Infinity));
}
/**
* 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,
* @returns the index of the event in all events to be rendered in the flamechart.
*/
#appendEventAtLevel(event: Trace.Types.Events.Event, level: number): number {
const index = this.#flameChartData.entryLevels.length;
this.#flameChartData.entryLevels[index] = level;
this.#flameChartData.entryStartTimes[index] = Trace.Helpers.Timing.microToMilli(event.ts);
const dur = event.dur || Trace.Helpers.Timing.milliToMicro(InstantEventVisibleDurationMs);
this.#flameChartData.entryTotalTimes[index] = Trace.Helpers.Timing.microToMilli(dur);
return level;
}
/**
* Update the flame chart data.
* When users zoom in the flamechart, we only want to show them the network
* requests between minTime and maxTime. This function will append those
* invisible events to the last level, and hide them.
* @returns the number of levels used by this track
*/
relayoutEntriesWithinBounds(
events: NetworkTrackEvent[], minTime: Trace.Types.Timing.Milli, maxTime: Trace.Types.Timing.Milli): number {
if (!this.#flameChartData || events.length === 0) {
return 0;
}
const lastTimestampByLevel: LastTimestampByLevel = [];
this.webSocketIdToLevel = new Map<number, number>();
let maxLevel = 0;
for (let i = 0; i < events.length; ++i) {
const event = events[i];
const beginTime = Trace.Helpers.Timing.microToMilli(event.ts);
const dur = event.dur ? Trace.Helpers.Timing.microToMilli(event.dur) : InstantEventVisibleDurationMs;
const endTime = beginTime + dur;
const isBetweenTimes = beginTime < maxTime && endTime > minTime;
// Exclude events outside the the specified timebounds
if (!isBetweenTimes) {
this.#flameChartData.entryLevels[i] = -1;
continue;
}
// Layout the entries by assigning levels.
let level: number;
if ('identifier' in event.args.data && Trace.Types.Events.isWebSocketEvent(event)) {
level = this.getWebSocketLevel(event, lastTimestampByLevel);
} else {
level = getEventLevel(event, lastTimestampByLevel);
}
this.#flameChartData.entryLevels[i] = level;
maxLevel = Math.max(maxLevel, lastTimestampByLevel.length, level);
}
for (let i = 0; i < events.length; ++i) {
// -1 means this event is invisible.
if (this.#flameChartData.entryLevels[i] === -1) {
// The maxLevel is an invisible level.
this.#flameChartData.entryLevels[i] = maxLevel;
}
}
return maxLevel;
}
getWebSocketLevel(event: Trace.Types.Events.WebSocketEvent, lastTimestampByLevel: LastTimestampByLevel): number {
const webSocketIdentifier = event.args.data.identifier;
let level: number;
if (this.webSocketIdToLevel.has(webSocketIdentifier)) {
// We're placing an instant event on top of its parent websocket
level = this.webSocketIdToLevel.get(webSocketIdentifier) || 0;
} else {
// We're placing the parent websocket
level = getEventLevel(event, lastTimestampByLevel);
this.webSocketIdToLevel.set(webSocketIdentifier, level);
}
return level;
}
/*
------------------------------------------------------------------------------------
The following methods are invoked by the flame chart renderer to query features about
events on rendering.
------------------------------------------------------------------------------------
*/
/**
* Gets the color an event added by this appender should be rendered with.
*/
colorForEvent(event: Trace.Types.Events.Event): string {
if (Trace.Types.Events.isSyntheticWebSocketConnection(event)) {
// the synthetic WebSocket events are not selectable, so we don't need to set the color.
return '';
}
if (Trace.Types.Events.isWebSocketTraceEvent(event)) {
return Components.Utils.colorForNetworkCategory(Components.Utils.NetworkCategory.JS);
}
if (!Trace.Types.Events.isSyntheticNetworkRequest(event)) {
throw new Error(`Unexpected Network Request: The event's type is '${event.name}'`);
}
return Components.Utils.colorForNetworkRequest(event);
}
}