@quick-game/cli
Version:
Command line interface for rapid qg development
1,114 lines • 52.3 kB
JavaScript
/*
* Copyright (C) 2014 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as Root from '../../core/root/root.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as TimelineModel from '../../models/timeline_model/timeline_model.js';
import * as TraceEngine from '../../models/trace/trace.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
import * as UI from '../../ui/legacy/legacy.js';
import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js';
import { CompatibilityTracksAppender } from './CompatibilityTracksAppender.js';
import timelineFlamechartPopoverStyles from './timelineFlamechartPopover.css.js';
import { FlameChartStyle, Selection } from './TimelineFlameChartView.js';
import { TimelineSelection } from './TimelineSelection.js';
import { TimelineUIUtils } from './TimelineUIUtils.js';
const UIStrings = {
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*/
onIgnoreList: 'On ignore list',
/**
* @description Text in Timeline Flame Chart Data Provider of the Performance panel *
* @example{example.com} PH1
*/
mainS: 'Main — {PH1}',
/**
* @description Text that refers to the main target
*/
main: 'Main',
/**
* @description Text in Timeline Flame Chart Data Provider of the Performance panel * @example {https://example.com} PH1
*/
frameS: 'Frame — {PH1}',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*/
subframe: 'Subframe',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*/
raster: 'Raster',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*@example {2} PH1
*/
rasterizerThreadS: 'Rasterizer Thread {PH1}',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*/
thread: 'Thread',
/**
*@description Text for rendering frames
*/
frames: 'Frames',
/**
* @description Text in the Performance panel to show how long was spent in a particular part of the code.
* The first placeholder is the total time taken for this node and all children, the second is the self time
* (time taken in this node, without children included).
*@example {10ms} PH1
*@example {10ms} PH2
*/
sSelfS: '{PH1} (self {PH2})',
/**
*@description Text in Timeline Flame Chart Data Provider of the Performance panel
*/
idleFrame: 'Idle Frame',
/**
*@description Text in Timeline Frame Chart Data Provider of the Performance panel
*/
droppedFrame: 'Dropped Frame',
/**
*@description Text in Timeline Frame Chart Data Provider of the Performance panel
*/
partiallyPresentedFrame: 'Partially Presented Frame',
/**
*@description Text for a rendering frame
*/
frame: 'Frame',
};
const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineFlameChartDataProvider.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const LONG_MAIN_THREAD_TASK_THRESHOLD = TraceEngine.Types.Timing.MilliSeconds(50);
export class TimelineFlameChartDataProvider extends Common.ObjectWrapper.ObjectWrapper {
droppedFramePatternCanvas;
partialFramePatternCanvas;
timelineDataInternal;
currentLevel;
// The Performance and the Timeline models are expected to be
// deprecated in favor of using traceEngineData (new RPP engine) only
// as part of the work in crbug.com/1386091. For this reason they
// have the "legacy" prefix on their name.
legacyPerformanceModel;
compatibilityTracksAppender;
legacyTimelineModel;
traceEngineData;
/**
* Raster threads are tracked and enumerated with this property. This is also
* used to group all raster threads together in the same track, instead of
* rendering a track for thread.
*/
#rasterCount = 0;
minimumBoundaryInternal;
timeSpan;
headerLevel1;
headerLevel2;
staticHeader;
framesHeader;
screenshotsHeader;
flowEventIndexById;
entryData;
entryTypeByLevel;
screenshotImageCache;
entryIndexToTitle;
asyncColorByCategory;
lastInitiatorEntry;
entryParent;
lastSelection;
colorForEvent;
#eventToDisallowRoot = new WeakMap();
#indexForEvent = new WeakMap();
#font;
constructor() {
super();
this.reset();
this.#font = `${PerfUI.Font.DEFAULT_FONT_SIZE} ${PerfUI.Font.getFontFamilyForCanvas()}`;
this.droppedFramePatternCanvas = document.createElement('canvas');
this.partialFramePatternCanvas = document.createElement('canvas');
this.preparePatternCanvas();
this.timelineDataInternal = null;
this.currentLevel = 0;
this.legacyPerformanceModel = null;
this.legacyTimelineModel = null;
this.compatibilityTracksAppender = null;
this.traceEngineData = null;
this.minimumBoundaryInternal = 0;
this.timeSpan = 0;
this.headerLevel1 = this.buildGroupStyle({ shareHeaderLine: false });
this.headerLevel2 = this.buildGroupStyle({ padding: 2, nestingLevel: 1, collapsible: false });
this.staticHeader = this.buildGroupStyle({ collapsible: false });
this.framesHeader = this.buildGroupStyle({ useFirstLineForOverview: true });
this.screenshotsHeader =
this.buildGroupStyle({ useFirstLineForOverview: true, nestingLevel: 1, collapsible: false, itemsHeight: 150 });
ThemeSupport.ThemeSupport.instance().addEventListener(ThemeSupport.ThemeChangeEvent.eventName, () => {
const headers = [
this.headerLevel1,
this.headerLevel2,
this.staticHeader,
this.framesHeader,
this.screenshotsHeader,
];
for (const header of headers) {
header.color = ThemeSupport.ThemeSupport.instance().getComputedValue('--color-text-primary');
header.backgroundColor = ThemeSupport.ThemeSupport.instance().getComputedValue('--color-background');
}
});
this.flowEventIndexById = new Map();
}
buildGroupStyle(extra) {
const defaultGroupStyle = {
padding: 4,
height: 17,
collapsible: true,
color: ThemeSupport.ThemeSupport.instance().getComputedValue('--color-text-primary'),
backgroundColor: ThemeSupport.ThemeSupport.instance().getComputedValue('--color-background'),
nestingLevel: 0,
shareHeaderLine: true,
};
return Object.assign(defaultGroupStyle, extra);
}
setModel(performanceModel, newTraceEngineData) {
this.reset();
this.legacyPerformanceModel = performanceModel;
this.legacyTimelineModel = performanceModel && performanceModel.timelineModel();
this.traceEngineData = newTraceEngineData;
if (this.legacyTimelineModel) {
this.minimumBoundaryInternal = this.legacyTimelineModel.minimumRecordTime();
this.timeSpan = this.legacyTimelineModel.isEmpty() ?
1000 :
this.legacyTimelineModel.maximumRecordTime() - this.minimumBoundaryInternal;
}
else if (this.traceEngineData) {
this.setTimingBoundsData(this.traceEngineData);
}
}
/**
* Sets the minimum time and total time span of a trace using the
* new engine data.
*/
setTimingBoundsData(newTraceEngineData) {
const { traceBounds } = newTraceEngineData.Meta;
const minTime = TraceEngine.Helpers.Timing.microSecondsToMilliseconds(traceBounds.min);
const maxTime = TraceEngine.Helpers.Timing.microSecondsToMilliseconds(traceBounds.max);
this.minimumBoundaryInternal = minTime;
this.timeSpan = minTime === maxTime ? 1000 : maxTime - this.minimumBoundaryInternal;
}
/**
* Instances and caches a CompatibilityTracksAppender using the
* internal flame chart data and the trace parsed data coming from the
* trace engine.
* The model data must have been set to the data provider instance before
* attempting to instance the CompatibilityTracksAppender.
*/
compatibilityTracksAppenderInstance(forceNew = false) {
if (!this.compatibilityTracksAppender || forceNew) {
if (!this.traceEngineData || !this.legacyTimelineModel) {
throw new Error('Attempted to instantiate a CompatibilityTracksAppender without having set the trace parse data first.');
}
this.timelineDataInternal = this.#instantiateTimelineData();
this.compatibilityTracksAppender = new CompatibilityTracksAppender(this.timelineDataInternal, this.traceEngineData, this.entryData, this.entryTypeByLevel, this.legacyTimelineModel);
}
return this.compatibilityTracksAppender;
}
/**
* Returns the instance of the timeline flame chart data, without
* adding data to it. In case the timeline data hasn't been instanced
* creates a new instance and returns it.
*/
#instantiateTimelineData() {
if (!this.timelineDataInternal) {
this.timelineDataInternal = PerfUI.FlameChart.FlameChartTimelineData.createEmpty();
}
return this.timelineDataInternal;
}
/**
* Builds the flame chart data using the track appenders
*/
buildFromTrackAppenders(expandedTracks) {
if (!this.compatibilityTracksAppender) {
return;
}
const appenders = this.compatibilityTracksAppender.allVisibleTrackAppenders();
for (const appender of appenders) {
const expanded = expandedTracks?.has(appender.appenderName);
this.currentLevel = appender.appendTrackAtLevel(this.currentLevel, expanded);
}
}
groupTrack(group) {
return group.track || null;
}
groupTreeEvents(group) {
const eventsFromAppenderSystem = this.compatibilityTracksAppender?.groupEventsForTreeView(group);
return eventsFromAppenderSystem || group.track?.eventsForTreeView() || null;
}
mainFrameNavigationStartEvents() {
if (!this.traceEngineData) {
return [];
}
return this.traceEngineData.Meta.mainFrameNavigations;
}
entryTitle(entryIndex) {
const entryTypes = EntryType;
const entryType = this.entryType(entryIndex);
if (entryType === entryTypes.Event) {
const event = this.entryData[entryIndex];
if (event.phase === "T" /* TraceEngine.Types.TraceEvents.Phase.ASYNC_STEP_INTO */ ||
event.phase === "p" /* TraceEngine.Types.TraceEvents.Phase.ASYNC_STEP_PAST */) {
return event.name + ':' + event.args['step'];
}
if (this.#eventToDisallowRoot.get(event)) {
return i18nString(UIStrings.onIgnoreList);
}
return TimelineUIUtils.eventTitle(event);
}
if (entryType === entryTypes.Screenshot) {
return '';
}
if (entryType === entryTypes.TrackAppender) {
const timelineData = this.timelineDataInternal;
const eventLevel = timelineData.entryLevels[entryIndex];
const event = this.entryData[entryIndex];
return this.compatibilityTracksAppender?.titleForEvent(event, eventLevel) || null;
}
let title = this.entryIndexToTitle[entryIndex];
if (!title) {
title = `Unexpected entryIndex ${entryIndex}`;
console.error(title);
}
return title;
}
textColor(index) {
const event = this.entryData[index];
return event && this.#eventToDisallowRoot.get(event) ? '#888' :
FlameChartStyle.textColor;
}
entryFont(_index) {
return this.#font;
}
reset() {
this.currentLevel = 0;
this.timelineDataInternal = null;
this.entryData = [];
this.entryParent = [];
this.entryTypeByLevel = [];
this.entryIndexToTitle = [];
this.asyncColorByCategory = new Map();
this.screenshotImageCache = new Map();
this.compatibilityTracksAppender = null;
this.#eventToDisallowRoot = new WeakMap();
this.#indexForEvent = new WeakMap();
}
maxStackDepth() {
return this.currentLevel;
}
/**
* Builds the flame chart data using the tracks appender (which use
* the new trace engine) and the legacy code paths present in this
* file. The result built data is cached and returned.
*/
timelineData() {
if (this.timelineDataInternal && this.timelineDataInternal.entryLevels.length !== 0) {
// The flame chart data is built already, so return the cached
// data.
return this.timelineDataInternal;
}
this.timelineDataInternal = PerfUI.FlameChart.FlameChartTimelineData.createEmpty();
if (!this.legacyTimelineModel) {
return this.timelineDataInternal;
}
this.flowEventIndexById.clear();
this.currentLevel = 0;
if (this.traceEngineData) {
this.compatibilityTracksAppender = this.compatibilityTracksAppenderInstance();
}
if (this.legacyTimelineModel.isGenericTrace()) {
this.processGenericTrace();
}
else {
this.processInspectorTrace();
}
return this.timelineDataInternal;
}
processGenericTrace() {
const processGroupStyle = this.buildGroupStyle({ shareHeaderLine: false });
const threadGroupStyle = this.buildGroupStyle({ padding: 2, nestingLevel: 1, shareHeaderLine: false });
const eventEntryType = EntryType.Event;
const tracksByProcess = new Platform.MapUtilities.Multimap();
if (!this.legacyTimelineModel) {
return;
}
for (const track of this.legacyTimelineModel.tracks()) {
if (track.thread !== null) {
tracksByProcess.set(track.thread.process(), track);
}
else {
// The Timings track can reach this point, so we should probably do something more useful.
console.error('Failed to process track');
}
}
for (const process of tracksByProcess.keysArray()) {
if (tracksByProcess.size > 1) {
const name = `${process.name()} ${process.id()}`;
this.appendHeader(name, processGroupStyle, false /* selectable */);
}
for (const track of tracksByProcess.get(process)) {
const group = this.appendSyncEvents(track, track.events, track.name, threadGroupStyle, eventEntryType, true /* selectable */);
if (this.timelineDataInternal &&
(!this.timelineDataInternal.selectedGroup ||
track.name === TimelineModel.TimelineModel.TimelineModelImpl.BrowserMainThreadName)) {
this.timelineDataInternal.selectedGroup = group;
}
}
}
}
processInspectorTrace() {
this.appendFrames();
const weight = (track) => {
if (track.appenderName !== undefined) {
switch (track.appenderName) {
case 'Animations':
return 0;
case 'Timings':
return 1;
case 'Interactions':
return 2;
case 'LayoutShifts':
return 3;
case 'GPU':
return 8;
case 'Thread':
return 4;
default:
return -1;
}
}
switch (track.type) {
case TimelineModel.TimelineModel.TrackType.MainThread:
return track.forMainFrame ? 5 : 6;
case TimelineModel.TimelineModel.TrackType.Worker:
return 7;
case TimelineModel.TimelineModel.TrackType.Raster:
return 9;
case TimelineModel.TimelineModel.TrackType.Other:
return 10;
default:
return -1;
}
};
if (!this.legacyTimelineModel) {
return;
}
const trackAppenders = this.compatibilityTracksAppender ? this.compatibilityTracksAppender.allVisibleTrackAppenders() : [];
// Due to tracks having a predefined order, we cannot render legacy
// and new tracks separately.
const tracksAndAppenders = [...this.legacyTimelineModel.tracks(), ...trackAppenders].slice();
tracksAndAppenders.sort((a, b) => weight(a) - weight(b));
// TODO(crbug.com/1386091) Remove interim state to use only new track
// appenders.
for (const trackOrAppender of tracksAndAppenders) {
if ('type' in trackOrAppender) {
// Legacy track
this.appendLegacyTrackData(trackOrAppender);
continue;
}
// Track rendered with new engine data.
if (!this.traceEngineData) {
continue;
}
this.currentLevel = trackOrAppender.appendTrackAtLevel(this.currentLevel);
}
if (this.timelineDataInternal && this.timelineDataInternal.selectedGroup) {
this.timelineDataInternal.selectedGroup.expanded = true;
}
this.flowEventIndexById.clear();
}
#addDecorationToEvent(eventIndex, decoration) {
if (!this.timelineDataInternal) {
return;
}
const decorationsForEvent = this.timelineDataInternal.entryDecorations[eventIndex] || [];
decorationsForEvent.push(decoration);
this.timelineDataInternal.entryDecorations[eventIndex] = decorationsForEvent;
}
/**
* Appends a track in the flame chart using the legacy system.
* @param track the legacy track to be rendered.
* @param expanded if the track is expanded.
*/
appendLegacyTrackData(track, expanded) {
this.#instantiateTimelineData();
const eventEntryType = EntryType.Event;
switch (track.type) {
case TimelineModel.TimelineModel.TrackType.MainThread: {
if (track.forMainFrame) {
const group = this.appendSyncEvents(track, track.events, track.url ? i18nString(UIStrings.mainS, { PH1: track.url }) : i18nString(UIStrings.main), this.headerLevel1, eventEntryType, true /* selectable */, expanded);
if (group && this.timelineDataInternal) {
this.timelineDataInternal.selectedGroup = group;
}
}
else {
this.appendSyncEvents(track, track.events, track.url ? i18nString(UIStrings.frameS, { PH1: track.url }) : i18nString(UIStrings.subframe), this.headerLevel1, eventEntryType, true /* selectable */, expanded);
}
break;
}
case TimelineModel.TimelineModel.TrackType.Worker: {
this.appendSyncEvents(track, track.events, track.name, this.headerLevel1, eventEntryType, true /* selectable */, expanded);
break;
}
case TimelineModel.TimelineModel.TrackType.Raster: {
if (!this.#rasterCount) {
this.appendHeader(i18nString(UIStrings.raster), this.headerLevel1, false /* selectable */, expanded);
}
++this.#rasterCount;
this.appendSyncEvents(track, track.events, i18nString(UIStrings.rasterizerThreadS, { PH1: this.#rasterCount }), this.headerLevel2, eventEntryType, true /* selectable */, expanded);
break;
}
case TimelineModel.TimelineModel.TrackType.Other: {
this.appendSyncEvents(track, track.events, track.name || i18nString(UIStrings.thread), this.headerLevel1, eventEntryType, true /* selectable */, expanded);
this.appendAsyncEventsGroup(track, track.name, track.asyncEvents, this.headerLevel1, eventEntryType, true /* selectable */, expanded);
break;
}
}
}
minimumBoundary() {
return this.minimumBoundaryInternal;
}
totalTime() {
return this.timeSpan;
}
/**
* Narrows an entry of type TimelineFlameChartEntry to the 2 types of
* simple trace events (legacy and new engine definitions).
*/
isEntryRegularEvent(entry) {
return 'name' in entry;
}
search(startTime, endTime, filter) {
const result = [];
this.timelineData();
for (let i = 0; i < this.entryData.length; ++i) {
const entry = this.entryData[i];
if (!this.isEntryRegularEvent(entry)) {
continue;
}
let event;
// The search features are implemented for SDK Event types only. Until we haven't fully
// transitioned to use the types of the new engine, we need to use legacy representation
// for events coming from the new engine.
if (entry instanceof TraceEngine.Legacy.Event) {
event = entry;
}
else {
if (!this.compatibilityTracksAppender) {
// This should not happen.
console.error('compatibilityTracksAppender was unexpectedly not set.');
continue;
}
event = this.compatibilityTracksAppender.getLegacyEvent(entry);
}
if (!event) {
continue;
}
if (event.startTime > endTime) {
continue;
}
if ((event.endTime || event.startTime) < startTime) {
continue;
}
if (filter.accept(event)) {
result.push(i);
}
}
result.sort((a, b) => {
let firstEvent = this.entryData[a];
let secondEvent = this.entryData[b];
if (!this.isEntryRegularEvent(firstEvent) || !this.isEntryRegularEvent(secondEvent)) {
return 0;
}
firstEvent = firstEvent instanceof TraceEngine.Legacy.Event ?
firstEvent :
(this.compatibilityTracksAppender?.getLegacyEvent(firstEvent) || null);
secondEvent = secondEvent instanceof TraceEngine.Legacy.Event ?
secondEvent :
(this.compatibilityTracksAppender?.getLegacyEvent(secondEvent) || null);
if (!firstEvent || !secondEvent) {
return 0;
}
return TraceEngine.Legacy.Event.compareStartTime(firstEvent, secondEvent);
});
return result;
}
appendSyncEvents(track, events, title, style, entryType, selectable, expanded) {
if (!events.length) {
return null;
}
if (!this.legacyPerformanceModel || !this.legacyTimelineModel) {
return null;
}
const openEvents = [];
const ignoreListingEnabled = Root.Runtime.experiments.isEnabled('ignoreListJSFramesOnTimeline');
let maxStackDepth = 0;
let group = null;
if (track && track.type === TimelineModel.TimelineModel.TrackType.MainThread) {
group = this.appendHeader(title, style, selectable, expanded);
group.track = track;
}
for (let i = 0; i < events.length; ++i) {
const event = events[i];
const { duration: eventDuration } = TraceEngine.Legacy.timesForEventInMilliseconds(event);
// TODO(crbug.com/1386091) this check should happen at the model level.
// Skip Layout Shifts and TTI events when dealing with the main thread.
if (this.legacyPerformanceModel) {
const isInteractiveTime = this.legacyPerformanceModel.timelineModel().isInteractiveTimeEvent(event);
const isLayoutShift = this.legacyPerformanceModel.timelineModel().isLayoutShiftEvent(event);
const skippableEvent = isInteractiveTime || isLayoutShift;
if (track && track.type === TimelineModel.TimelineModel.TrackType.MainThread && skippableEvent) {
continue;
}
}
if (!TraceEngine.Types.TraceEvents.isFlowPhase(event.phase)) {
if (!event.endTime && event.phase !== "I" /* TraceEngine.Types.TraceEvents.Phase.INSTANT */) {
continue;
}
if (TraceEngine.Types.TraceEvents.isAsyncPhase(event.phase)) {
continue;
}
if (!this.legacyPerformanceModel.isVisible(event)) {
continue;
}
}
// Handle events belonging to a stack. E.g. A call stack in the main thread flame chart.
while (openEvents.length &&
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// @ts-expect-error
(openEvents[openEvents.length - 1].endTime) <= event.startTime) {
openEvents.pop();
}
this.#eventToDisallowRoot.set(event, false);
if (ignoreListingEnabled && this.isIgnoreListedEvent(event)) {
const parent = openEvents[openEvents.length - 1];
if (parent && this.#eventToDisallowRoot.get(parent)) {
continue;
}
this.#eventToDisallowRoot.set(event, true);
}
if (!group && title) {
group = this.appendHeader(title, style, selectable, expanded);
if (selectable) {
group.track = track;
}
}
const level = this.currentLevel + openEvents.length;
const index = this.appendEvent(event, level);
if (openEvents.length) {
this.entryParent[index] = openEvents[openEvents.length - 1];
}
const trackIsMainThreadMainFrame = Boolean(track?.forMainFrame && track?.type === TimelineModel.TimelineModel.TrackType.MainThread);
// If we are dealing with the Main Thread, find any long tasks and add
// the candy striping to them. Doing it here avoids having to do another
// pass through the events at a later point.
if (trackIsMainThreadMainFrame && event.name === TimelineModel.TimelineModel.RecordType.Task &&
eventDuration > LONG_MAIN_THREAD_TASK_THRESHOLD) {
this.#addDecorationToEvent(index, {
type: 'CANDY',
startAtTime: TraceEngine.Helpers.Timing.millisecondsToMicroseconds(LONG_MAIN_THREAD_TASK_THRESHOLD),
});
}
if (entryType === EntryType.Event) {
if (TimelineModel.TimelineModel.EventOnTimelineData.forEvent(event).warning) {
this.#addDecorationToEvent(index, { type: 'WARNING_TRIANGLE' });
}
}
maxStackDepth = Math.max(maxStackDepth, openEvents.length + 1);
if (event.endTime) {
openEvents.push(event);
}
}
this.entryTypeByLevel.length = this.currentLevel + maxStackDepth;
this.entryTypeByLevel.fill(entryType, this.currentLevel);
this.currentLevel += maxStackDepth;
return group;
}
isIgnoreListedEvent(event) {
if (!TimelineModel.TimelineModel.TimelineModelImpl.isJsFrameEvent(event)) {
return false;
}
const url = event.args['data']['url'];
return url && this.isIgnoreListedURL(url);
}
isIgnoreListedURL(url) {
return Bindings.IgnoreListManager.IgnoreListManager.instance().isUserIgnoreListedURL(url);
}
appendAsyncEventsGroup(track, title, events, style, entryType, selectable, expanded) {
if (!events.length) {
return null;
}
const lastUsedTimeByLevel = [];
let group = null;
for (let i = 0; i < events.length; ++i) {
const asyncEvent = events[i];
if (!this.legacyPerformanceModel || !this.legacyPerformanceModel.isVisible(asyncEvent)) {
continue;
}
if (!group && title) {
group = this.appendHeader(title, style, selectable, expanded);
if (selectable) {
group.track = track;
}
}
const startTime = asyncEvent.startTime;
let level;
for (level = 0; level < lastUsedTimeByLevel.length && lastUsedTimeByLevel[level] > startTime; ++level) {
}
this.appendAsyncEvent(asyncEvent, this.currentLevel + level);
lastUsedTimeByLevel[level] = asyncEvent.endTime;
}
this.entryTypeByLevel.length = this.currentLevel + lastUsedTimeByLevel.length;
this.entryTypeByLevel.fill(entryType, this.currentLevel);
this.currentLevel += lastUsedTimeByLevel.length;
return group;
}
getEntryTypeForLevel(level) {
return this.entryTypeByLevel[level];
}
appendFrames() {
if (!this.legacyPerformanceModel || !this.timelineDataInternal || !this.legacyTimelineModel ||
!this.traceEngineData) {
return;
}
// TODO: Long term we want to move both the Frames track and the screenshots
// track into the TrackAppender system. However right now the frames track
// expects data in a different form to how the new engine parses frame
// information. Therefore we have migrated the screenshots to use the new
// data model in place without creating a new TrackAppender. When we can
// migrate the frames track to the new appender system, we can migrate the
// screnshots then as well.
const filmStrip = TraceEngine.Extras.FilmStrip.fromTraceData(this.traceEngineData);
const hasScreenshots = filmStrip.frames.length > 0;
this.framesHeader.collapsible = hasScreenshots;
const expanded = Root.Runtime.Runtime.queryParam('flamechart-force-expand') === 'frames';
this.appendHeader(i18nString(UIStrings.frames), this.framesHeader, false /* selectable */, expanded);
this.entryTypeByLevel[this.currentLevel] = EntryType.Frame;
for (const frame of this.legacyPerformanceModel.frames()) {
this.appendFrame(frame);
}
++this.currentLevel;
if (!hasScreenshots) {
return;
}
this.#appendScreenshots(filmStrip);
}
#appendScreenshots(filmStrip) {
if (!this.timelineDataInternal || !this.legacyTimelineModel) {
return;
}
this.appendHeader('', this.screenshotsHeader, false /* selectable */);
this.entryTypeByLevel[this.currentLevel] = EntryType.Screenshot;
let prevTimestamp = undefined;
for (const filmStripFrame of filmStrip.frames) {
const screenshotTimeInMilliSeconds = TraceEngine.Helpers.Timing.microSecondsToMilliseconds(filmStripFrame.screenshotEvent.ts);
this.entryData.push(filmStripFrame.screenshotEvent);
this.timelineDataInternal.entryLevels.push(this.currentLevel);
this.timelineDataInternal.entryStartTimes.push(screenshotTimeInMilliSeconds);
if (prevTimestamp) {
this.timelineDataInternal.entryTotalTimes.push(screenshotTimeInMilliSeconds - prevTimestamp);
}
prevTimestamp = screenshotTimeInMilliSeconds;
}
if (filmStrip.frames.length && prevTimestamp !== undefined) {
// Set the total time of the final screenshot so it takes up the remainder of the trace.
this.timelineDataInternal.entryTotalTimes
.push(this.legacyTimelineModel.maximumRecordTime() - prevTimestamp);
}
++this.currentLevel;
}
entryType(entryIndex) {
return this.entryTypeByLevel[this.timelineDataInternal
.entryLevels[entryIndex]];
}
prepareHighlightedEntryInfo(entryIndex) {
let time = '';
let title;
let warning;
let nameSpanTimelineInfoTime = 'timeline-info-time';
const entryType = this.entryType(entryIndex);
if (entryType === EntryType.TrackAppender) {
if (!this.compatibilityTracksAppender) {
return null;
}
const event = this.entryData[entryIndex];
const timelineData = this.timelineDataInternal;
const eventLevel = timelineData.entryLevels[entryIndex];
const highlightedEntryInfo = this.compatibilityTracksAppender.highlightedEntryInfo(event, eventLevel);
title = highlightedEntryInfo.title;
time = highlightedEntryInfo.formattedTime;
}
else if (entryType === EntryType.Event) {
const event = this.entryData[entryIndex];
const totalTime = event.duration;
const selfTime = event.selfTime;
const eps = 1e-6;
if (typeof totalTime === 'number') {
time = Math.abs(totalTime - selfTime) > eps && selfTime > eps ?
i18nString(UIStrings.sSelfS, {
PH1: i18n.TimeUtilities.millisToString(totalTime, true),
PH2: i18n.TimeUtilities.millisToString(selfTime, true),
}) :
i18n.TimeUtilities.millisToString(totalTime, true);
}
title = this.entryTitle(entryIndex);
warning = TimelineUIUtils.buildEventWarningElement(event);
if (this.legacyTimelineModel && this.legacyTimelineModel.isParseHTMLEvent(event)) {
const startLine = event.args['beginData']['startLine'];
const endLine = event.args['endData'] && event.args['endData']['endLine'];
const url = Bindings.ResourceUtils.displayNameForURL(event.args['beginData']['url']);
const range = (endLine !== -1 || endLine === startLine) ? `${startLine}...${endLine}` : startLine;
title += ` - ${url} [${range}]`;
}
}
else if (entryType === EntryType.Frame) {
const frame = this.entryData[entryIndex];
time = i18n.TimeUtilities.preciseMillisToString(frame.duration, 1);
if (frame.idle) {
title = i18nString(UIStrings.idleFrame);
}
else if (frame.dropped) {
if (frame.isPartial) {
title = i18nString(UIStrings.partiallyPresentedFrame);
}
else {
title = i18nString(UIStrings.droppedFrame);
}
nameSpanTimelineInfoTime = 'timeline-info-warning';
}
else {
title = i18nString(UIStrings.frame);
}
}
else {
return null;
}
const element = document.createElement('div');
const root = UI.Utils.createShadowRootWithCoreStyles(element, {
cssFile: [timelineFlamechartPopoverStyles],
delegatesFocus: undefined,
});
const contents = root.createChild('div', 'timeline-flamechart-popover');
contents.createChild('span', nameSpanTimelineInfoTime).textContent = time;
contents.createChild('span', 'timeline-info-title').textContent = title;
if (warning) {
warning.classList.add('timeline-info-warning');
contents.appendChild(warning);
}
return element;
}
entryColor(entryIndex) {
function patchColorAndCache(cache, key, lookupColor) {
let color = cache.get(key);
if (color) {
return color;
}
const parsedColor = Common.Color.parse(lookupColor(key));
if (!parsedColor) {
throw new Error('Could not parse color from entry');
}
color = parsedColor.setAlpha(0.7).asString("rgba" /* Common.Color.Format.RGBA */) || '';
cache.set(key, color);
return color;
}
if (!this.legacyPerformanceModel || !this.legacyTimelineModel) {
return '';
}
const entryTypes = EntryType;
const entryType = this.entryType(entryIndex);
if (entryType === entryTypes.Event) {
const event = this.entryData[entryIndex];
if (this.legacyTimelineModel.isGenericTrace()) {
return this.genericTraceEventColor(event);
}
if (this.legacyPerformanceModel.timelineModel().isMarkerEvent(event)) {
return TimelineUIUtils.markerStyleForEvent(event).color;
}
if (!TraceEngine.Types.TraceEvents.isAsyncPhase(event.phase) && this.colorForEvent) {
return this.colorForEvent(event);
}
const category = TimelineUIUtils.eventStyle(event).category;
return patchColorAndCache(this.asyncColorByCategory, category, () => category.color);
}
if (entryType === entryTypes.Frame) {
return 'white';
}
if (entryType === entryTypes.TrackAppender) {
const timelineData = this.timelineDataInternal;
const eventLevel = timelineData.entryLevels[entryIndex];
const event = this.entryData[entryIndex];
return this.compatibilityTracksAppender?.colorForEvent(event, eventLevel) || '';
}
return '';
}
genericTraceEventColor(event) {
const key = event.categoriesString || event.name;
return key ? `hsl(${Platform.StringUtilities.hashCode(key) % 300 + 30}, 40%, 70%)` : '#ccc';
}
preparePatternCanvas() {
// Set the candy stripe pattern to 17px so it repeats well.
const size = 17;
this.droppedFramePatternCanvas.width = size;
this.droppedFramePatternCanvas.height = size;
this.partialFramePatternCanvas.width = size;
this.partialFramePatternCanvas.height = size;
const ctx = this.droppedFramePatternCanvas.getContext('2d');
if (ctx) {
// Make a dense solid-line pattern.
ctx.translate(size * 0.5, size * 0.5);
ctx.rotate(Math.PI * 0.25);
ctx.translate(-size * 0.5, -size * 0.5);
ctx.fillStyle = 'rgb(255, 255, 255)';
for (let x = -size; x < size * 2; x += 3) {
ctx.fillRect(x, -size, 1, size * 3);
}
}
const ctx2 = this.partialFramePatternCanvas.getContext('2d');
if (ctx2) {
// Make a sparse dashed-line pattern.
ctx2.strokeStyle = 'rgb(255, 255, 255)';
ctx2.lineWidth = 2;
ctx2.beginPath();
ctx2.moveTo(17, 0);
ctx2.lineTo(10, 7);
ctx2.moveTo(8, 9);
ctx2.lineTo(2, 15);
ctx2.stroke();
}
}
drawFrame(entryIndex, context, text, barX, barY, barWidth, barHeight) {
const hPadding = 1;
const frame = this.entryData[entryIndex];
barX += hPadding;
barWidth -= 2 * hPadding;
if (frame.idle) {
context.fillStyle = 'white';
}
else if (frame.dropped) {
if (frame.isPartial) {
// For partially presented frame boxes, paint a yellow background with
// a sparse white dashed-line pattern overlay.
context.fillStyle = '#f0e442';
context.fillRect(barX, barY, barWidth, barHeight);
const overlay = context.createPattern(this.partialFramePatternCanvas, 'repeat');
context.fillStyle = overlay || context.fillStyle;
}
else {
// For dropped frame boxes, paint a red background with a dense white
// solid-line pattern overlay.
context.fillStyle = '#f08080';
context.fillRect(barX, barY, barWidth, barHeight);
const overlay = context.createPattern(this.droppedFramePatternCanvas, 'repeat');
context.fillStyle = overlay || context.fillStyle;
}
}
else {
context.fillStyle = '#d7f0d1';
}
context.fillRect(barX, barY, barWidth, barHeight);
const frameDurationText = i18n.TimeUtilities.preciseMillisToString(frame.duration, 1);
const textWidth = context.measureText(frameDurationText).width;
if (textWidth <= barWidth) {
context.fillStyle = this.textColor(entryIndex);
context.fillText(frameDurationText, barX + (barWidth - textWidth) / 2, barY + barHeight - 4);
}
}
async drawScreenshot(entryIndex, context, barX, barY, barWidth, barHeight) {
const screenshot = this.entryData[entryIndex];
if (!this.screenshotImageCache.has(screenshot)) {
this.screenshotImageCache.set(screenshot, null);
const data = screenshot.args.snapshot;
const image = await UI.UIUtils.loadImageFromData(data);
this.screenshotImageCache.set(screenshot, image);
this.dispatchEventToListeners(Events.DataChanged);
return;
}
const image = this.screenshotImageCache.get(screenshot);
if (!image) {
return;
}
const imageX = barX + 1;
const imageY = barY + 1;
const imageHeight = barHeight - 2;
const scale = imageHeight / image.naturalHeight;
const imageWidth = Math.floor(image.naturalWidth * scale);
context.save();
context.beginPath();
context.rect(barX, barY, barWidth, barHeight);
context.clip();
context.drawImage(image, imageX, imageY, imageWidth, imageHeight);
context.strokeStyle = '#ccc';
context.strokeRect(imageX - 0.5, imageY - 0.5, Math.min(barWidth - 1, imageWidth + 1), imageHeight);
context.restore();
}
decorateEntry(entryIndex, context, text, barX, barY, barWidth, barHeight, _unclippedBarX, _timeToPixels) {
const entryType = this.entryType(entryIndex);
if (entryType === EntryType.Frame) {
this.drawFrame(entryIndex, context, text, barX, barY, barWidth, barHeight);
return true;
}
if (entryType === EntryType.Screenshot) {
void this.drawScreenshot(entryIndex, context, barX, barY, barWidth, barHeight);
return true;
}
return false;
}
forceDecoration(entryIndex) {
const entryTypes = EntryType;
const entryType = this.entryType(entryIndex);
if (entryType === entryTypes.Frame) {
return true;
}
if (entryType === entryTypes.Screenshot) {
return true;
}
if (entryType === entryTypes.Event) {
const event = this.entryData[entryIndex];
return Boolean(TimelineModel.TimelineModel.EventOnTimelineData.forEvent(event).warning);
}
return false;
}
appendHeader(title, style, selectable, expanded) {
const group = { startLevel: this.currentLevel, name: title, style: style, selectable: selectable, expanded };
this.timelineDataInternal.groups.push(group);
return group;
}
appendEvent(event, level) {
const index = this.entryData.length;
this.entryData.push(event);
const timelineData = this.timelineDataInternal;
timelineData.entryLevels[index] = level;
timelineData.entryTotalTimes[index] = event.duration || InstantEventVisibleDurationMs;
timelineData.entryStartTimes[index] = event.startTime;
this.#indexForEvent.set(event, index);
return index;
}
appendAsyncEvent(asyncEvent, level) {
const steps = asyncEvent.steps;
// If we have past steps, put the end event for each range rather than start one.
const eventOffset = steps.length > 1 && steps[1].phase === "p" /* TraceEngine.Types.TraceEvents.Phase.ASYNC_STEP_PAST */ ? 1 : 0;
for (let i = 0; i < steps.length - 1; ++i) {
const index = this.entryData.length;
this.entryData.push(steps[i + eventOffset]);
const startTime = steps[i].startTime;
const timelineData = this.timelineDataInternal;
timelineData.entryLevels[index] = level;
timelineData.entryTotalTimes[index] = steps[i + 1].startTime - startTime;
timelineData.entryStartTimes[index] = startTime;
}
}
appendFrame(frame) {
const index = this.entryData.length;
this.entryData.push(frame);
this.entryIndexToTitle[index] = i18n.TimeUtilities.millisToString(frame.duration, true);
if (!this.timelineDataInternal) {
return;
}
this.timelineDataInternal.entryLevels[index] = this.currentLevel;
this.timelineDataInternal.entryTotalTimes[index] = frame.duration;
this.timelineDataInternal.entryStartTimes[index] = frame.startTime;
}
createSelection(entryIndex) {
const entryType = this.entryType(entryIndex);
let timelineSelection = null;
const entry = this.entryData[entryIndex];
if (entry && this.isEntryRegularEvent(entry)) {
timelineSelection = TimelineSelection.fromTraceEvent(entry);
}
else if (entryType === EntryType.Frame) {
timelineSelection =
TimelineSelection.fromFrame(this.entryData[entryIndex]);
}
if (timelineSelection) {
this.lastSelection = new Selection(timelineSelection, entryIndex);
}
return timelineSelection;
}
formatValue(value, precision) {
return i18n.TimeUtilities.preciseMillisToString(value, precision);
}
canJumpToEntry(_entryIndex) {
return false;
}
entryIndexForSelection(selection) {
if (!selection || TimelineSelection.isRangeSelection(selection.object) ||
TimelineSelection.isSyntheticNetworkRequestDetailsEventSelection(selection.object)) {
return -1;
}
if (this.lastSelection && this.lastSelection.timelineSelection.object === selection.object) {
return this.lastSelection.entryIndex;
}
const index = this.entryData.indexOf(selection.object);
if (index !== -1) {
this.lastSelection = new Selection(selection, index);
}
return index;
}
buildFlowForInitiator(entryIndex) {
if (this.lastInitiatorEntry === entryIndex) {
return false;
}
this.lastInitiatorEntry = entryIndex;
let event = this.eventByIndex(entryIndex);
if (TraceEngine.Legacy.eventIsFromNewEngine(event)) {
// TODO(crbug.com/1434596): Add support for this use case in the
// new engine.
return false;
}
const td = this.timelineDataInternal;
if (!td) {
return false;
}
td.flowStartTimes = [];
td.flowStartLevels = [];
td.flowEndTimes = [];
td.flowEndLevels = [];
while (event) {
// Find the closest ancestor with an initiator.
let initiator;
for (; event; event = this.eventParent(event)) {
initiator = TimelineModel.TimelineModel.EventOnTimelineData.forEvent(event).initiator();
if (initiator) {
break;
}
}
if (!initiator || !event) {
break;
}
const eventIndex = this.#indexForEvent.get(event);
const initiatorIndex = this.#indexForEvent.get(initiator);
td.flowStartTimes.push(initiator.endTime || initi