chrome-devtools-frontend
Version:
Chrome DevTools UI
1,150 lines (1,092 loc) • 115 kB
text/typescript
// Copyright 2021 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.
/* eslint-disable rulesdir/no-imperative-dom-api */
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
* Copyright (C) 2012 Intel 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 SDK from '../../core/sdk/sdk.js';
import type * as Protocol from '../../generated/protocol.js';
import * as Bindings from '../../models/bindings/bindings.js';
import * as Trace from '../../models/trace/trace.js';
import * as TraceBounds from '../../services/trace_bounds/trace_bounds.js';
import * as CodeHighlighter from '../../ui/components/code_highlighter/code_highlighter.js';
// eslint-disable-next-line rulesdir/es-modules-import
import codeHighlighterStyles from '../../ui/components/code_highlighter/codeHighlighter.css.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
// eslint-disable-next-line rulesdir/es-modules-import
import imagePreviewStyles from '../../ui/legacy/components/utils/imagePreview.css.js';
import * as LegacyComponents from '../../ui/legacy/components/utils/utils.js';
import * as UI from '../../ui/legacy/legacy.js';
import {getDurationString} from './AppenderUtils.js';
import * as TimelineComponents from './components/components.js';
import * as Extensions from './extensions/extensions.js';
import {Tracker} from './FreshRecording.js';
import {ModificationsManager} from './ModificationsManager.js';
import {targetForEvent} from './TargetForEvent.js';
import * as ThirdPartyTreeView from './ThirdPartyTreeView.js';
import {TimelinePanel} from './TimelinePanel.js';
import {selectionFromEvent} from './TimelineSelection.js';
import * as Utils from './utils/utils.js';
const UIStrings = {
/**
*@description Text that only contain a placeholder
*@example {100ms (at 200ms)} PH1
*/
emptyPlaceholder: '{PH1}', // eslint-disable-line rulesdir/l10n-no-locked-or-placeholder-only-phrase
/**
*@description Text for timestamps of items
*/
timestamp: 'Timestamp',
/**
*@description Text shown next to the interaction event's ID in the detail view.
*/
interactionID: 'ID',
/**
*@description Text shown next to the interaction event's input delay time in the detail view.
*/
inputDelay: 'Input delay',
/**
*@description Text shown next to the interaction event's thread processing duration in the detail view.
*/
processingDuration: 'Processing duration',
/**
*@description Text shown next to the interaction event's presentation delay time in the detail view.
*/
presentationDelay: 'Presentation delay',
/**
*@description Text shown when the user has selected an event that represents script compiliation.
*/
compile: 'Compile',
/**
*@description Text shown when the user selects an event that represents script parsing.
*/
parse: 'Parse',
/**
*@description Text with two placeholders separated by a colon
*@example {Node removed} PH1
*@example {div#id1} PH2
*/
sS: '{PH1}: {PH2}',
/**
*@description Details text used to show the amount of data collected.
*@example {30 MB} PH1
*/
sCollected: '{PH1} collected',
/**
*@description Text used to show a URL to a script and the relevant line numbers.
*@example {https://example.com/foo.js} PH1
*@example {2} PH2
*@example {4} PH3
*/
sSs: '{PH1} [{PH2}…{PH3}]',
/**
*@description Text used to show a URL to a script and the starting line
* number - used when there is no end line number available.
*@example {https://example.com/foo.js} PH1
*@example {2} PH2
*/
sSSquareBrackets: '{PH1} [{PH2}…]',
/**
*@description Text that is usually a hyperlink to more documentation
*/
learnMore: 'Learn more',
/**
*@description Text referring to the status of the browser's compilation cache.
*/
compilationCacheStatus: 'Compilation cache status',
/**
*@description Text referring to the size of the browser's compiliation cache.
*/
compilationCacheSize: 'Compilation cache size',
/**
*@description Text in Timeline UIUtils of the Performance panel. "Compilation
* cache" refers to the code cache described at
* https://v8.dev/blog/code-caching-for-devs . This label is followed by the
* type of code cache data used, either "normal" or "full" as described in the
* linked article.
*/
compilationCacheKind: 'Compilation cache kind',
/**
*@description Text used to inform the user that the script they are looking
* at was loaded from the browser's cache.
*/
scriptLoadedFromCache: 'script loaded from cache',
/**
*@description Text to inform the user that the script they are looking at
* was unable to be loaded from the browser's cache.
*/
failedToLoadScriptFromCache: 'failed to load script from cache',
/**
*@description Text to inform the user that the script they are looking at was not eligible to be loaded from the browser's cache.
*/
scriptNotEligibleToBeLoadedFromCache: 'script not eligible',
/**
*@description Label in the summary view in the Performance panel for a number which indicates how much managed memory has been reclaimed by performing Garbage Collection
*/
collected: 'Collected',
/**
*@description Text for a programming function
*/
function: 'Function',
/**
*@description Text for referring to the ID of a timer.
*/
timerId: 'Timer ID',
/**
*@description Text for referring to a timer that has timed-out and therefore is being removed.
*/
timeout: 'Timeout',
/**
*@description Text used to indicate that a timer is repeating (e.g. every X seconds) rather than a one off.
*/
repeats: 'Repeats',
/**
*@description Text for referring to the ID of a callback function installed by an event.
*/
callbackId: 'Callback ID',
/**
*@description Text for a module, the programming concept
*/
module: 'Module',
/**
*@description Label for a group of JavaScript files
*/
script: 'Script',
/**
*@description Text used to tell a user that a compilation trace event was streamed.
*/
streamed: 'Streamed',
/**
*@description Text to indicate if a compilation event was eager.
*/
eagerCompile: 'Compiling all functions eagerly',
/**
*@description Text to refer to the URL associated with a given event.
*/
url: 'Url',
/**
*@description Text to indicate to the user the size of the cache (as a filesize - e.g. 5mb).
*/
producedCacheSize: 'Produced cache size',
/**
*@description Text to indicate to the user the amount of the cache (as a filesize - e.g. 5mb) that has been used.
*/
consumedCacheSize: 'Consumed cache size',
/**
*@description Title for a group of cities
*/
location: 'Location',
/**
*@description Text used to show a coordinate pair (e.g. (3, 2)).
*@example {2} PH1
*@example {2} PH2
*/
sSCurlyBrackets: '({PH1}, {PH2})',
/**
*@description Text used to indicate to the user they are looking at the physical dimensions of a shape that was drawn by the browser.
*/
dimensions: 'Dimensions',
/**
*@description Text used to show the user the dimensions of a shape and indicate its area (e.g. 3x2).
*@example {2} PH1
*@example {2} PH2
*/
sSDimensions: '{PH1} × {PH2}',
/**
*@description Related node label in Timeline UIUtils of the Performance panel
*/
layerRoot: 'Layer root',
/**
*@description Related node label in Timeline UIUtils of the Performance panel
*/
ownerElement: 'Owner element',
/**
*@description Text used to show the user the URL of the image they are viewing.
*/
imageUrl: 'Image URL',
/**
*@description Text used to show the user that the URL they are viewing is loading a CSS stylesheet.
*/
stylesheetUrl: 'Stylesheet URL',
/**
*@description Text used next to a number to show the user how many elements were affected.
*/
elementsAffected: 'Elements affected',
/**
*@description Text used next to a number to show the user how many nodes required the browser to update and re-layout the page.
*/
nodesThatNeedLayout: 'Nodes that need layout',
/**
*@description Text used to show the amount in a subset - e.g. "2 of 10".
*@example {2} PH1
*@example {10} PH2
*/
sOfS: '{PH1} of {PH2}',
/**
*@description Related node label in Timeline UIUtils of the Performance panel
*/
layoutRoot: 'Layout root',
/**
*@description Text used when viewing an event that can have a custom message attached.
*/
message: 'Message',
/**
*@description Text used to tell the user they are viewing an event that has a function embedded in it, which is referred to as the "callback function".
*/
callbackFunction: 'Callback function',
/**
*@description Text used to show the relevant range of a file - e.g. "lines 2-10".
*/
range: 'Range',
/**
*@description Text used to refer to the amount of time some event or code was given to complete within.
*/
allottedTime: 'Allotted time',
/**
*@description Text used to tell a user that a particular event or function was automatically run by a timeout.
*/
invokedByTimeout: 'Invoked by timeout',
/**
*@description Text that refers to some types
*/
type: 'Type',
/**
*@description Text for the size of something
*/
size: 'Size',
/**
*@description Text for the details of something
*/
details: 'Details',
/**
*@description Text to indicate an item is a warning
*/
warning: 'Warning',
/**
*@description Text that indicates a particular HTML element or node is related to what the user is viewing.
*/
relatedNode: 'Related node',
/**
*@description Text for previewing items
*/
preview: 'Preview',
/**
*@description Text used to refer to the total time summed up across multiple events.
*/
aggregatedTime: 'Aggregated time',
/**
*@description Text for the duration of something
*/
duration: 'Duration',
/**
*@description Text for the stack trace of the initiator of something. The Initiator is the event or factor that directly triggered or precipitated a subsequent action.
*/
initiatorStackTrace: 'Initiator stack trace',
/**
*@description Text for the event initiated by another one
*/
initiatedBy: 'Initiated by',
/**
*@description Text for the event that is an initiator for another one
*/
initiatorFor: 'Initiator for',
/**
*@description Text for the underlying data behing a specific flamechart selection. Trace events are the browser instrumentation that are emitted as JSON objects.
*/
traceEvent: 'Trace event',
/**
*@description Call site stack label in Timeline UIUtils of the Performance panel
*/
timerInstalled: 'Timer installed',
/**
*@description Call site stack label in Timeline UIUtils of the Performance panel
*/
animationFrameRequested: 'Animation frame requested',
/**
*@description Call site stack label in Timeline UIUtils of the Performance panel
*/
idleCallbackRequested: 'Idle callback requested',
/**
*@description Stack label in Timeline UIUtils of the Performance panel
*/
recalculationForced: 'Recalculation forced',
/**
*@description Call site stack label in Timeline UIUtils of the Performance panel
*/
firstLayoutInvalidation: 'First layout invalidation',
/**
*@description Stack label in Timeline UIUtils of the Performance panel
*/
layoutForced: 'Layout forced',
/**
*@description Label in front of CSS property (eg `opacity`) being animated or a CSS animation name (eg `layer-4-fade-in-out`)
*/
animating: 'Animating',
/**
*@description Label in front of reasons why a CSS animation wasn't composited (aka hardware accelerated)
*/
compositingFailed: 'Compositing failed',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to accelerated animations being disabled. Shown in a table with a list of other potential failure reasons. */
compositingFailedAcceleratedAnimationsDisabled: 'Accelerated animations disabled',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to DevTools suppressing the effect. Shown in a table with a list of other potential failure reasons. */
compositingFailedEffectSuppressedByDevtools: 'Effect suppressed by DevTools ',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to the animation or effect being invalid. Shown in a table with a list of other potential failure reasons. */
compositingFailedInvalidAnimationOrEffect: 'Invalid animation or effect',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to an effect having unsupported timing parameters. Shown in a table with a list of other potential failure reasons. */
compositingFailedEffectHasUnsupportedTimingParams: 'Effect has unsupported timing parameters',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to an effect having a composite mode which is not `replace`. Shown in a table with a list of other potential failure reasons. */
compositingFailedEffectHasNonReplaceCompositeMode: 'Effect has composite mode other than "replace"',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to the target being in an invalid compositing state. Shown in a table with a list of other potential failure reasons. */
compositingFailedTargetHasInvalidCompositingState: 'Target has invalid compositing state',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to another animation on the same target being incompatible. Shown in a table with a list of other potential failure reasons. */
compositingFailedTargetHasIncompatibleAnimations: 'Target has another animation which is incompatible',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to the target having a CSS offset. Shown in a table with a list of other potential failure reasons. */
compositingFailedTargetHasCSSOffset: 'Target has CSS offset',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to the animation affecting non-CSS properties. Shown in a table with a list of other potential failure reasons. */
compositingFailedAnimationAffectsNonCSSProperties: 'Animation affects non-CSS properties',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to the transform-related property not being able to be animated on the target. Shown in a table with a list of other potential failure reasons. */
compositingFailedTransformRelatedPropertyCannotBeAcceleratedOnTarget:
'Transform-related property cannot be accelerated on target',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to a `transform` property being dependent on the size of the element itself. Shown in a table with a list of other potential failure reasons. */
compositingFailedTransformDependsBoxSize: 'Transform-related property depends on box size',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to a `filter` property possibly moving pixels. Shown in a table with a list of other potential failure reasons. */
compositingFailedFilterRelatedPropertyMayMovePixels: 'Filter-related property may move pixels',
/**
* @description [ICU Syntax] Descriptive reason for why a user-provided animation failed to be optimized by the browser due to the animated CSS property not being supported on the compositor. Shown in a table with a list of other potential failure reasons.
* @example {height, width} properties
*/
compositingFailedUnsupportedCSSProperty: `{propertyCount, plural,
=1 {Unsupported CSS property: {properties}}
other {Unsupported CSS properties: {properties}}
}`,
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to mixing keyframe value types. Shown in a table with a list of other potential failure reasons. */
compositingFailedMixedKeyframeValueTypes: 'Mixed keyframe value types',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to the timeline source being in an invalid compositing state. Shown in a table with a list of other potential failure reasons. */
compositingFailedTimelineSourceHasInvalidCompositingState: 'Timeline source has invalid compositing state',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to the animation having no visible change. Shown in a table with a list of other potential failure reasons. */
compositingFailedAnimationHasNoVisibleChange: 'Animation has no visible change',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to an effect affecting an important property. Shown in a table with a list of other potential failure reasons. */
compositingFailedAffectsImportantProperty: 'Effect affects a property with !important',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to the SVG target having an independent transfrom property. Shown in a table with a list of other potential failure reasons. */
compositingFailedSVGTargetHasIndependentTransformProperty: 'SVG target has independent transform property',
/** Descriptive reason for why a user-provided animation failed to be optimized by the browser due to an unknown reason. Shown in a table with a list of other potential failure reasons. */
compositingFailedUnknownReason: 'Unknown Reason',
/**
*@description Text for the execution stack trace
*/
stackTrace: 'Stack trace',
/**
*@description Text used to show any invalidations for a particular event that caused the browser to have to do more work to update the page.
* @example {2} PH1
*/
invalidations: 'Invalidations ({PH1} total)',
/**
* @description Text in Timeline UIUtils of the Performance panel. Phrase is followed by a number of milliseconds.
* Some events or tasks might have been only started, but have not ended yet. Such events or tasks are considered
* "pending".
*/
pendingFor: 'Pending for',
/**
*@description Noun label for a stack trace which indicates the first time some condition was invalidated.
*/
firstInvalidated: 'First invalidated',
/**
*@description Title of the paint profiler, old name of the performance pane
*/
paintProfiler: 'Paint profiler',
/**
*@description Text in Timeline Flame Chart View of the Performance panel
*@example {Frame} PH1
*@example {10ms} PH2
*/
sAtS: '{PH1} at {PH2}',
/**
*@description Text used next to a time to indicate that the particular event took that much time itself. In context this might look like "3ms blink.console (self)"
*@example {blink.console} PH1
*/
sSelf: '{PH1} (self)',
/**
*@description Text used next to a time to indicate that the event's children took that much time. In context this might look like "3ms blink.console (children)"
*@example {blink.console} PH1
*/
sChildren: '{PH1} (children)',
/**
*@description Text used to show the user how much time the browser spent on rendering (drawing the page onto the screen).
*/
timeSpentInRendering: 'Time spent in rendering',
/**
*@description Text for a rendering frame
*/
frame: 'Frame',
/**
*@description Text used to refer to the duration of an event at a given offset - e.g. "2ms at 10ms" which can be read as "2ms starting after 10ms".
*@example {10ms} PH1
*@example {10ms} PH2
*/
sAtSParentheses: '{PH1} (at {PH2})',
/**
*@description Text of a DOM element in Timeline UIUtils of the Performance panel
*/
UnknownNode: '[ unknown node ]',
/**
*@description Text used to refer to a particular element and the file it was referred to in.
*@example {node} PH1
*@example {app.js} PH2
*/
invalidationWithCallFrame: '{PH1} at {PH2}',
/**
*@description Text indicating that something is outside of the Performace Panel Timeline Minimap range
*/
outsideBreadcrumbRange: '(outside of the breadcrumb range)',
/**
*@description Text indicating that something is hidden from the Performace Panel Timeline
*/
entryIsHidden: '(entry is hidden)',
/**
* @description Title of a row in the details view for a `Recalculate Styles` event that contains more info about selector stats tracing.
*/
selectorStatsTitle: 'Selector stats',
/**
* @description Info text that explains to the user how to enable selector stats tracing.
* @example {Setting Name} PH1
*/
sSelectorStatsInfo: 'Select "{PH1}" to collect detailed CSS selector matching statistics.',
/**
* @description Label for a numeric value that was how long to wait before a function was run.
*/
delay: 'Delay',
/**
* @description Label for a string that describes the priority at which a task was scheduled, like 'background' for low-priority tasks, and 'user-blocking' for high priority.
*/
priority: 'Priority',
/**
* @description Label for third party table.
*/
thirdPartyTable: '1st / 3rd party table',
/**
* @description Label for the a source URL.
*/
source: 'Source',
/**
* @description Label for a URL origin.
*/
origin: 'Origin',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineUIUtils.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
let eventDispatchDesciptors: EventDispatchTypeDescriptor[];
let colorGenerator: Common.Color.Generator;
interface LinkifyLocationOptions {
scriptId: Protocol.Runtime.ScriptId|null;
url: string;
lineNumber: number;
target: SDK.Target.Target|null;
linkifier: LegacyComponents.Linkifier.Linkifier;
isFreshRecording?: boolean;
columnNumber?: number;
omitOrigin?: boolean;
}
type TimeRangeCategoryStats = Record<string, number>;
const {SamplesIntegrator} = Trace.Helpers.SamplesIntegrator;
export class TimelineUIUtils {
/**
* use getGetDebugModeEnabled() to query this variable.
*/
static debugModeEnabled: boolean|undefined = undefined;
static getGetDebugModeEnabled(): boolean {
if (TimelineUIUtils.debugModeEnabled === undefined) {
TimelineUIUtils.debugModeEnabled =
Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.TIMELINE_DEBUG_MODE);
}
return TimelineUIUtils.debugModeEnabled;
}
static frameDisplayName(frame: Protocol.Runtime.CallFrame): string {
const maybeResolvedData = Utils.SourceMapsResolver.SourceMapsResolver.resolvedCodeLocationForCallFrame(frame);
const functionName = maybeResolvedData?.name || frame.functionName;
if (!SamplesIntegrator.isNativeRuntimeFrame(frame)) {
return UI.UIUtils.beautifyFunctionName(functionName);
}
const nativeGroup = SamplesIntegrator.nativeGroup(functionName);
switch (nativeGroup) {
case SamplesIntegrator.NativeGroups.COMPILE:
return i18nString(UIStrings.compile);
case SamplesIntegrator.NativeGroups.PARSE:
return i18nString(UIStrings.parse);
}
return functionName;
}
static testContentMatching(
traceEvent: Trace.Types.Events.Event, regExp: RegExp, parsedTrace?: Trace.Handlers.Types.ParsedTrace): boolean {
const title = TimelineUIUtils.eventStyle(traceEvent).title;
const tokens = [title];
if (Trace.Types.Events.isProfileCall(traceEvent)) {
// In the future this case will not be possible - wherever we call this
// function we will be able to pass in the data from the new engine. But
// currently this is called in a variety of places including from the
// legacy model which does not have a reference to the new engine's data.
// So if we are missing the data, we just fallback to the name from the
// callFrame.
if (!parsedTrace?.Samples) {
tokens.push(traceEvent.callFrame.functionName);
} else {
tokens.push(Trace.Handlers.ModelHandlers.Samples.getProfileCallFunctionName(parsedTrace.Samples, traceEvent));
}
}
if (parsedTrace) {
const url = Trace.Handlers.Helpers.getNonResolvedURL(traceEvent, parsedTrace);
if (url) {
tokens.push(url);
}
}
if (TimelineUIUtils.getGetDebugModeEnabled()) {
// When in debug mode append top level properties (like timestamp)
// and deeply nested properties.
appendObjectProperties(traceEvent as unknown as ContentObject, 4);
} else {
appendObjectProperties(traceEvent.args as ContentObject, 2);
}
const result = tokens.join('|').match(regExp);
return result ? result.length > 0 : false;
interface ContentObject {
[x: string]: number|string|ContentObject;
}
function appendObjectProperties(object: ContentObject, depth: number): void {
if (!depth) {
return;
}
for (const key in object) {
const value = object[key];
if (typeof value === 'string') {
tokens.push(value);
} else if (typeof value === 'number') {
tokens.push(String(value));
} else if (typeof value === 'object' && value !== null) {
appendObjectProperties(value, depth - 1);
}
}
}
}
static eventStyle(event: Trace.Types.Events.Event): Utils.EntryStyles.TimelineRecordStyle {
if (Trace.Types.Events.isProfileCall(event) && event.callFrame.functionName === '(idle)') {
return new Utils.EntryStyles.TimelineRecordStyle(event.name, Utils.EntryStyles.getCategoryStyles().idle);
}
if (event.cat === Trace.Types.Events.Categories.Console || event.cat === Trace.Types.Events.Categories.UserTiming) {
return new Utils.EntryStyles.TimelineRecordStyle(event.name, Utils.EntryStyles.getCategoryStyles()['scripting']);
}
return Utils.EntryStyles.getEventStyle(event.name as Trace.Types.Events.Name) ??
new Utils.EntryStyles.TimelineRecordStyle(event.name, Utils.EntryStyles.getCategoryStyles().other);
}
static eventColor(event: Trace.Types.Events.Event): string {
if (Trace.Types.Events.isProfileCall(event)) {
const frame = event.callFrame;
if (TimelineUIUtils.isUserFrame(frame)) {
// TODO(andoli): This should use the resolved (sourcemapped) URL
return TimelineUIUtils.colorForId(frame.url);
}
}
if (Trace.Types.Extensions.isSyntheticExtensionEntry(event)) {
return Extensions.ExtensionUI.extensionEntryColor(event);
}
let parsedColor = TimelineUIUtils.eventStyle(event).category.getComputedColorValue();
// This event is considered idle time but still rendered as a scripting event here
// to connect the StreamingCompileScriptParsing events it belongs to.
if (event.name === Trace.Types.Events.Name.STREAMING_COMPILE_SCRIPT_WAITING) {
parsedColor = Utils.EntryStyles.getCategoryStyles().scripting.getComputedColorValue();
if (!parsedColor) {
throw new Error('Unable to parse color from getCategoryStyles().scripting.color');
}
}
return parsedColor;
}
static eventTitle(event: Trace.Types.Events.Event): string {
// Profile call events do not have a args.data property, thus, we
// need to check for profile calls in the beginning of this
// function.
if (Trace.Types.Events.isProfileCall(event)) {
const maybeResolvedData = Utils.SourceMapsResolver.SourceMapsResolver.resolvedCodeLocationForEntry(event);
const displayName = maybeResolvedData?.name || TimelineUIUtils.frameDisplayName(event.callFrame);
return displayName;
}
if (event.name === 'EventTiming' && Trace.Types.Events.isSyntheticInteraction(event)) {
// TODO(crbug.com/365047728): replace this entire method with this call.
return Utils.EntryName.nameForEntry(event);
}
const title = TimelineUIUtils.eventStyle(event).title;
if (Trace.Helpers.Trace.eventHasCategory(event, Trace.Types.Events.Categories.Console)) {
return title;
}
if (Trace.Types.Events.isConsoleTimeStamp(event) && event.args.data) {
return i18nString(UIStrings.sS, {PH1: title, PH2: event.args.data.name ?? event.args.data.message});
}
if (Trace.Types.Events.isAnimation(event) && event.args.data.name) {
return i18nString(UIStrings.sS, {PH1: title, PH2: event.args.data.name});
}
if (Trace.Types.Events.isDispatch(event)) {
return i18nString(UIStrings.sS, {PH1: title, PH2: event.args.data.type});
}
return title;
}
static isUserFrame(frame: Protocol.Runtime.CallFrame): boolean {
return frame.scriptId !== '0' && !(frame.url?.startsWith('native '));
}
static async buildDetailsTextForTraceEvent(
event: Trace.Types.Events.Event, parsedTrace: Trace.Handlers.Types.ParsedTrace): Promise<string|null> {
let detailsText;
// TODO(40287735): update this code with type-safe data checks.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const unsafeEventArgs = event.args as Record<string, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const unsafeEventData = event.args?.data as Record<string, any>;
switch (event.name) {
case Trace.Types.Events.Name.GC:
case Trace.Types.Events.Name.MAJOR_GC:
case Trace.Types.Events.Name.MINOR_GC: {
const delta = unsafeEventArgs['usedHeapSizeBefore'] - unsafeEventArgs['usedHeapSizeAfter'];
detailsText = i18nString(UIStrings.sCollected, {PH1: i18n.ByteUtilities.bytesToString(delta)});
break;
}
case Trace.Types.Events.Name.FUNCTION_CALL: {
const {lineNumber, columnNumber} = Trace.Helpers.Trace.getZeroIndexedLineAndColumnForEvent(event);
if (lineNumber !== undefined && columnNumber !== undefined) {
detailsText = unsafeEventData.url + ':' + (lineNumber + 1) + ':' + (columnNumber + 1);
}
break;
}
case Trace.Types.Events.Name.EVENT_DISPATCH:
detailsText = unsafeEventData ? unsafeEventData['type'] : null;
break;
case Trace.Types.Events.Name.PAINT: {
const width = TimelineUIUtils.quadWidth(unsafeEventData.clip);
const height = TimelineUIUtils.quadHeight(unsafeEventData.clip);
if (width && height) {
detailsText = i18nString(UIStrings.sSDimensions, {PH1: width, PH2: height});
}
break;
}
case Trace.Types.Events.Name.PARSE_HTML: {
const startLine = unsafeEventArgs['beginData']['startLine'];
const endLine = unsafeEventArgs['endData']?.['endLine'];
const url = Bindings.ResourceUtils.displayNameForURL(unsafeEventArgs['beginData']['url']);
if (endLine >= 0) {
detailsText = i18nString(UIStrings.sSs, {PH1: url, PH2: startLine + 1, PH3: endLine + 1});
} else {
detailsText = i18nString(UIStrings.sSSquareBrackets, {PH1: url, PH2: startLine + 1});
}
break;
}
case Trace.Types.Events.Name.COMPILE_MODULE:
case Trace.Types.Events.Name.CACHE_MODULE:
detailsText = Bindings.ResourceUtils.displayNameForURL(unsafeEventArgs['fileName']);
break;
case Trace.Types.Events.Name.COMPILE_SCRIPT:
case Trace.Types.Events.Name.CACHE_SCRIPT:
case Trace.Types.Events.Name.EVALUATE_SCRIPT: {
const {lineNumber} = Trace.Helpers.Trace.getZeroIndexedLineAndColumnForEvent(event);
const url = unsafeEventData?.['url'];
if (url) {
detailsText = Bindings.ResourceUtils.displayNameForURL(url) + ':' + ((lineNumber || 0) + 1);
}
break;
}
case Trace.Types.Events.Name.WASM_COMPILED_MODULE:
case Trace.Types.Events.Name.WASM_MODULE_CACHE_HIT: {
const url = unsafeEventArgs['url'];
if (url) {
detailsText = Bindings.ResourceUtils.displayNameForURL(url);
}
break;
}
case Trace.Types.Events.Name.STREAMING_COMPILE_SCRIPT:
case Trace.Types.Events.Name.BACKGROUND_DESERIALIZE:
case Trace.Types.Events.Name.XHR_READY_STATE_CHANGED:
case Trace.Types.Events.Name.XHR_LOAD: {
const url = unsafeEventData['url'];
if (url) {
detailsText = Bindings.ResourceUtils.displayNameForURL(url);
}
break;
}
case Trace.Types.Events.Name.TIME_STAMP:
detailsText = unsafeEventData['message'];
break;
case Trace.Types.Events.Name.WEB_SOCKET_CREATE:
case Trace.Types.Events.Name.WEB_SOCKET_SEND_HANDSHAKE_REQUEST:
case Trace.Types.Events.Name.WEB_SOCKET_RECEIVE_HANDSHAKE_REQUEST:
case Trace.Types.Events.Name.WEB_SOCKET_SEND:
case Trace.Types.Events.Name.WEB_SOCKET_RECEIVE:
case Trace.Types.Events.Name.WEB_SOCKET_DESTROY:
case Trace.Types.Events.Name.RESOURCE_WILL_SEND_REQUEST:
case Trace.Types.Events.Name.RESOURCE_SEND_REQUEST:
case Trace.Types.Events.Name.RESOURCE_RECEIVE_DATA:
case Trace.Types.Events.Name.RESOURCE_RECEIVE_RESPONSE:
case Trace.Types.Events.Name.RESOURCE_FINISH:
case Trace.Types.Events.Name.PAINT_IMAGE:
case Trace.Types.Events.Name.DECODE_IMAGE:
case Trace.Types.Events.Name.DECODE_LAZY_PIXEL_REF: {
const url = Trace.Handlers.Helpers.getNonResolvedURL(event, parsedTrace);
if (url) {
detailsText = Bindings.ResourceUtils.displayNameForURL(url);
}
break;
}
case Trace.Types.Events.Name.EMBEDDER_CALLBACK:
detailsText = unsafeEventData['callbackName'];
break;
case Trace.Types.Events.Name.ASYNC_TASK:
detailsText = unsafeEventData ? unsafeEventData['name'] : null;
break;
default:
if (Trace.Helpers.Trace.eventHasCategory(event, Trace.Types.Events.Categories.Console)) {
detailsText = null;
} else {
detailsText = linkifyTopCallFrameAsText();
}
break;
}
return detailsText;
function linkifyTopCallFrameAsText(): string|null {
const frame = Trace.Helpers.Trace.getZeroIndexedStackTraceInEventPayload(event)?.at(0) ?? null;
if (!frame) {
return null;
}
return frame.url + ':' + (frame.lineNumber + 1) + ':' + (frame.columnNumber + 1);
}
}
static async buildDetailsNodeForTraceEvent(
event: Trace.Types.Events.Event, target: SDK.Target.Target|null, linkifier: LegacyComponents.Linkifier.Linkifier,
isFreshRecording = false, parsedTrace: Trace.Handlers.Types.ParsedTrace): Promise<Node|null> {
let details: HTMLElement|HTMLSpanElement|(Element | null)|Text|null = null;
let detailsText;
// TODO(40287735): update this code with type-safe data checks.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const unsafeEventArgs = event.args as Record<string, any>;
// TODO(40287735): update this code with type-safe data checks.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const unsafeEventData = event.args?.data as Record<string, any>;
switch (event.name) {
case Trace.Types.Events.Name.GC:
case Trace.Types.Events.Name.MAJOR_GC:
case Trace.Types.Events.Name.MINOR_GC:
case Trace.Types.Events.Name.EVENT_DISPATCH:
case Trace.Types.Events.Name.PAINT:
case Trace.Types.Events.Name.ANIMATION:
case Trace.Types.Events.Name.EMBEDDER_CALLBACK:
case Trace.Types.Events.Name.PARSE_HTML:
case Trace.Types.Events.Name.WASM_STREAM_FROM_RESPONSE_CALLBACK:
case Trace.Types.Events.Name.WASM_COMPILED_MODULE:
case Trace.Types.Events.Name.WASM_MODULE_CACHE_HIT:
case Trace.Types.Events.Name.WASM_CACHED_MODULE:
case Trace.Types.Events.Name.WASM_MODULE_CACHE_INVALID:
case Trace.Types.Events.Name.WEB_SOCKET_CREATE:
case Trace.Types.Events.Name.WEB_SOCKET_SEND_HANDSHAKE_REQUEST:
case Trace.Types.Events.Name.WEB_SOCKET_RECEIVE_HANDSHAKE_REQUEST:
case Trace.Types.Events.Name.WEB_SOCKET_SEND:
case Trace.Types.Events.Name.WEB_SOCKET_RECEIVE:
case Trace.Types.Events.Name.WEB_SOCKET_DESTROY: {
detailsText = await TimelineUIUtils.buildDetailsTextForTraceEvent(event, parsedTrace);
break;
}
case Trace.Types.Events.Name.PAINT_IMAGE:
case Trace.Types.Events.Name.DECODE_IMAGE:
case Trace.Types.Events.Name.DECODE_LAZY_PIXEL_REF:
case Trace.Types.Events.Name.XHR_READY_STATE_CHANGED:
case Trace.Types.Events.Name.XHR_LOAD:
case Trace.Types.Events.Name.RESOURCE_WILL_SEND_REQUEST:
case Trace.Types.Events.Name.RESOURCE_SEND_REQUEST:
case Trace.Types.Events.Name.RESOURCE_RECEIVE_DATA:
case Trace.Types.Events.Name.RESOURCE_RECEIVE_RESPONSE:
case Trace.Types.Events.Name.RESOURCE_FINISH: {
const url = Trace.Handlers.Helpers.getNonResolvedURL(event, parsedTrace);
if (url) {
const options = {
tabStop: true,
showColumnNumber: false,
inlineFrameIndex: 0,
};
details = LegacyComponents.Linkifier.Linkifier.linkifyURL(url, options);
}
break;
}
case Trace.Types.Events.Name.FUNCTION_CALL: {
details = document.createElement('span');
// FunctionCall events have an args.data that could be a CallFrame, if all the details are present, so we check for that.
const callFrame = Trace.Helpers.Trace.getZeroIndexedStackTraceInEventPayload(event)?.at(0);
if (Trace.Types.Events.isFunctionCall(event) && callFrame) {
UI.UIUtils.createTextChild(
details,
TimelineUIUtils.frameDisplayName(
{...callFrame, scriptId: String(callFrame.scriptId) as Protocol.Runtime.ScriptId}));
}
const location = this.linkifyLocation({
scriptId: unsafeEventData['scriptId'],
url: unsafeEventData['url'],
lineNumber: callFrame?.lineNumber || 0,
columnNumber: callFrame?.columnNumber,
target,
isFreshRecording,
linkifier,
omitOrigin: true,
});
if (location) {
UI.UIUtils.createTextChild(details, ' @ ');
details.appendChild(location);
}
break;
}
case Trace.Types.Events.Name.COMPILE_MODULE:
case Trace.Types.Events.Name.CACHE_MODULE: {
details = this.linkifyLocation({
scriptId: null,
url: unsafeEventArgs['fileName'],
lineNumber: 0,
columnNumber: 0,
target,
isFreshRecording,
linkifier,
});
break;
}
case Trace.Types.Events.Name.COMPILE_SCRIPT:
case Trace.Types.Events.Name.CACHE_SCRIPT:
case Trace.Types.Events.Name.EVALUATE_SCRIPT: {
const url = unsafeEventData['url'];
if (url) {
const {lineNumber} = Trace.Helpers.Trace.getZeroIndexedLineAndColumnForEvent(event);
details = this.linkifyLocation({
scriptId: null,
url,
lineNumber: lineNumber || 0,
columnNumber: 0,
target,
isFreshRecording,
linkifier,
omitOrigin: true,
});
}
break;
}
case Trace.Types.Events.Name.BACKGROUND_DESERIALIZE:
case Trace.Types.Events.Name.STREAMING_COMPILE_SCRIPT: {
const url = unsafeEventData['url'];
if (url) {
details = this.linkifyLocation({
scriptId: null,
url,
lineNumber: 0,
columnNumber: 0,
target,
isFreshRecording,
linkifier,
omitOrigin: true,
});
}
break;
}
default: {
/**
* Some events have a stack trace which is extracted by default at @see TimelineUIUtils.generateCauses
* thus, we prevent extracting the stack trace again here.
*/
if (Trace.Helpers.Trace.eventHasCategory(event, Trace.Types.Events.Categories.Console) ||
Trace.Types.Events.isUserTiming(event) || Trace.Types.Extensions.isSyntheticExtensionEntry(event) ||
Trace.Types.Events.isProfileCall(event)) {
detailsText = null;
} else {
details = this.linkifyTopCallFrame(event, target, linkifier, isFreshRecording) ?? null;
}
break;
}
}
if (!details && detailsText) {
details = document.createTextNode(detailsText);
}
return details;
}
static linkifyLocation(linkifyOptions: LinkifyLocationOptions): Element|null {
const {scriptId, url, lineNumber, columnNumber, isFreshRecording, linkifier, target, omitOrigin} = linkifyOptions;
const options = {
lineNumber,
columnNumber,
showColumnNumber: true,
inlineFrameIndex: 0,
className: 'timeline-details',
tabStop: true,
omitOrigin,
};
if (isFreshRecording) {
return linkifier.linkifyScriptLocation(
target, scriptId, url as Platform.DevToolsPath.UrlString, lineNumber, options);
}
return LegacyComponents.Linkifier.Linkifier.linkifyURL(url as Platform.DevToolsPath.UrlString, options);
}
static linkifyTopCallFrame(
event: Trace.Types.Events.Event, target: SDK.Target.Target|null, linkifier: LegacyComponents.Linkifier.Linkifier,
isFreshRecording = false): Element|null {
let frame = Trace.Helpers.Trace.getZeroIndexedStackTraceInEventPayload(event)?.[0];
if (Trace.Types.Events.isProfileCall(event)) {
frame = event.callFrame;
}
if (!frame) {
return null;
}
const options = {
className: 'timeline-details',
tabStop: true,
inlineFrameIndex: 0,
showColumnNumber: true,
columnNumber: frame.columnNumber,
lineNumber: frame.lineNumber,
};
if (isFreshRecording) {
return linkifier.maybeLinkifyConsoleCallFrame(target, frame, {showColumnNumber: true, inlineFrameIndex: 0});
}
return LegacyComponents.Linkifier.Linkifier.linkifyURL(frame.url as Platform.DevToolsPath.UrlString, options);
}
static buildDetailsNodeForMarkerEvents(event: Trace.Types.Events.MarkerEvent): HTMLElement {
let link = 'https://web.dev/user-centric-performance-metrics/';
let name = 'page performance metrics';
switch (event.name) {
case Trace.Types.Events.Name.MARK_LCP_CANDIDATE:
link = 'https://web.dev/lcp/';
name = 'largest contentful paint';
break;
case Trace.Types.Events.Name.MARK_FCP:
link = 'https://web.dev/first-contentful-paint/';
name = 'first contentful paint';
break;
default:
break;
}
const html = UI.Fragment.html`<div>${
UI.XLink.XLink.create(
link, i18nString(UIStrings.learnMore), undefined, undefined, 'learn-more')} about ${name}.</div>`;
return html as HTMLElement;
}
static buildConsumeCacheDetails(
eventData: {
consumedCacheSize?: number,
cacheRejected?: boolean,
cacheKind?: string,
},
contentHelper: TimelineDetailsContentHelper): void {
if (typeof eventData.consumedCacheSize === 'number') {
contentHelper.appendTextRow(
i18nString(UIStrings.compilationCacheStatus), i18nString(UIStrings.scriptLoadedFromCache));
contentHelper.appendTextRow(
i18nString(UIStrings.compilationCacheSize), i18n.ByteUtilities.bytesToString(eventData.consumedCacheSize));
const cacheKind = eventData.cacheKind;
if (cacheKind) {
contentHelper.appendTextRow(i18nString(UIStrings.compilationCacheKind), cacheKind);
}
} else if ('cacheRejected' in eventData && eventData['cacheRejected']) {
// Version mismatch or similar.
contentHelper.appendTextRow(
i18nString(UIStrings.compilationCacheStatus), i18nString(UIStrings.failedToLoadScriptFromCache));
} else {
contentHelper.appendTextRow(
i18nString(UIStrings.compilationCacheStatus), i18nString(UIStrings.scriptNotEligibleToBeLoadedFromCache));
}
}
static async buildTraceEventDetails(
parsedTrace: Trace.Handlers.Types.ParsedTrace,
event: Trace.Types.Events.Event,
linkifier: LegacyComponents.Linkifier.Linkifier,
canShowPieChart: boolean,
entityMapper: Utils.EntityMapper.EntityMapper|null,
): Promise<DocumentFragment> {
const maybeTarget = targetForEvent(parsedTrace, event);
const {duration} = Trace.Helpers.Timing.eventTimingsMicroSeconds(event);
const selfTime = getEventSelfTime(event, parsedTrace);
const relatedNodesMap = await Trace.Extras.FetchNodes.extractRelatedDOMNodesFromEvent(
parsedTrace,
event,
);
let entityAppended = false;
if (maybeTarget) {
// @ts-expect-error TODO(crbug.com/1011811): Remove symbol usage.
if (typeof event[previewElementSymbol] === 'undefined') {
let previewElement: (Element|null)|null = null;
const url = Trace.Handlers.Helpers.getNonResolvedURL(event, parsedTrace);
if (url) {
previewElement = await LegacyComponents.ImagePreview.ImagePreview.build(url, false, {
imageAltText: LegacyComponents.ImagePreview.ImagePreview.defaultAltTextForImageURL(url),
precomputedFeatures: undefined,
align: LegacyComponents.ImagePreview.Align.START,
});
} else if (Trace.Types.Events.isPaint(event)) {
previewElement = await TimelineUIUtils.buildPicturePreviewContent(parsedTrace, event, maybeTarget);
}
// @ts-expect-error TODO(crbug.com/1011811): Remove symbol usage.
event[previewElementSymbol] = previewElement;
}
}
// This message may vary per event.name;
let relatedNodeLabel;
const contentHelper = new TimelineDetailsContentHelper(targetForEvent(parsedTrace, event), linkifier);
const defaultColorForEvent = this.eventColor(event);
const isMarker = parsedTrace && isMarkerEvent(parsedTrace, event);
const color = isMarker ? TimelineUIUtils.markerStyleForEvent(event).color : defaultColorForEvent;
contentHelper.addSection(TimelineUIUtils.eventTitle(event), color);
// TODO: as part of the removal of the old engine, produce a typesafe way
// to look up args and data for events.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const unsafeEventArgs = event.args as Record<string, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const unsafeEventData = event.args?.data as Record<string, any>;
const initiator = parsedTrace.Initiators.eventToInitiator.get(event) ?? null;
const initiatorFor = parsedTrace.Initiators.initiatorToEvents.get(event) ?? null;
let url: Platform.DevToolsPath.UrlString|null = null;
if (parsedTrace) {
const warnings = TimelineComponents.DetailsView.buildWarningElementsForEvent(event, parsedTrace);
for (const warning of warnings) {
contentHelper.appendElementRow(i18nString(UIStrings.warning), warning, true);
}
}
// Add timestamp to user timings.
if (Trace.Helpers.Trace.eventHasCategory(event, Trace.Types.Events.Categories.UserTiming)) {
const adjustedEventTimeStamp = timeStampForEventAdjustedForClosestNavigationIfPossible(
event,
parsedTrace,
);
contentHelper.appendTextRow(
i18nString(UIStrings.timestamp), i18n.TimeUtilities.preciseMillisToString(adjustedEventTimeStamp, 1));
}
// Only show total time and self time for events with non-zero durations.
if (duration !== 0 && !Number.isNaN(duration)) {
const timeStr = getDurationString(duration, selfTime);
contentHelper.appendTextRow(i18nString(UIStrings.duration), timeStr);
}
if (Trace.Types.Events.isPerformanceMark(event) && event.args.data?.detail) {
const detailContainer = TimelineUIUtils.renderObje