chrome-devtools-frontend
Version:
Chrome DevTools UI
1,113 lines (1,051 loc) • 113 kB
text/typescript
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable @devtools/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 TextUtils from '../../models/text_utils/text_utils.js';
import * as Trace from '../../models/trace/trace.js';
import * as SourceMapsResolver from '../../models/trace_source_maps_resolver/trace_source_maps_resolver.js';
import * as TraceBounds from '../../services/trace_bounds/trace_bounds.js';
import * as Tracing from '../../services/tracing/tracing.js';
import * as CodeHighlighter from '../../ui/components/code_highlighter/code_highlighter.js';
// eslint-disable-next-line @devtools/es-modules-import
import codeHighlighterStyles from '../../ui/components/code_highlighter/codeHighlighter.css.js';
import * as uiI18n from '../../ui/i18n/i18n.js';
import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
// eslint-disable-next-line @devtools/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 * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js';
import * as PanelsCommon from '../common/common.js';
import {getDurationString} from './AppenderUtils.js';
import * as TimelineComponents from './components/components.js';
import * as Extensions from './extensions/extensions.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 @devtools/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 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 refer to a positive timeout value that schedules the idle callback once elapsed, even if no idle time is available.
*/
requestIdleCallbackTimeout: '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 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". It is not technically a stack trace, because it points to the beginning of each function
* and not to each call site, so we call it a function stack instead to avoid confusion.
*/
functionStack: 'Function stack',
/**
* @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_);
/** Look for scheme:// plus text and exclude any punctuation at the end. **/
export const URL_REGEX = /(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\/\/)[^\s"]{2,}[^\s"'\)\}\],:;.!?]/u;
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;
isFreshOrEnhanced?: 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 = 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, handlerData?: Trace.Handlers.Types.HandlerData): 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 (!handlerData?.Samples) {
tokens.push(traceEvent.callFrame.functionName);
} else {
tokens.push(Trace.Handlers.ModelHandlers.Samples.getProfileCallFunctionName(handlerData.Samples, traceEvent));
}
}
if (handlerData) {
const url = Trace.Handlers.Helpers.getNonResolvedURL(traceEvent, handlerData);
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): Trace.Styles.TimelineRecordStyle {
if (Trace.Types.Events.isProfileCall(event) && event.callFrame.functionName === '(idle)') {
return new Trace.Styles.TimelineRecordStyle(event.name, Trace.Styles.getCategoryStyles().idle);
}
if (event.cat === Trace.Types.Events.Categories.Console || event.cat === Trace.Types.Events.Categories.UserTiming) {
return new Trace.Styles.TimelineRecordStyle(event.name, Trace.Styles.getCategoryStyles()['scripting']);
}
return Trace.Styles.getEventStyle(event.name as Trace.Types.Events.Name) ??
new Trace.Styles.TimelineRecordStyle(event.name, Trace.Styles.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);
}
const themeSupport = ThemeSupport.ThemeSupport.instance();
let parsedColor = themeSupport.getComputedValue(TimelineUIUtils.eventStyle(event).category.cssVariable);
// 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 = themeSupport.getComputedValue(Trace.Styles.getCategoryStyles().scripting.cssVariable);
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 = 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 Trace.Name.forEntry(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 buildDetailsNodeForTraceEvent(
event: Trace.Types.Events.Event, target: SDK.Target.Target|null, linkifier: LegacyComponents.Linkifier.Linkifier,
isFreshOrEnhanced = false, parsedTrace: Trace.TraceModel.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.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.data);
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,
isFreshOrEnhanced,
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,
isFreshOrEnhanced,
linkifier,
});
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,
isFreshOrEnhanced,
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, isFreshOrEnhanced) ?? null;
}
break;
}
}
if (!details && detailsText) {
details = document.createTextNode(detailsText);
}
return details;
}
static linkifyLocation(linkifyOptions: LinkifyLocationOptions): Element|null {
const {scriptId, url, lineNumber, columnNumber, isFreshOrEnhanced, linkifier, target, omitOrigin} = linkifyOptions;
const options = {
lineNumber,
columnNumber,
showColumnNumber: true,
inlineFrameIndex: 0,
className: 'timeline-details',
tabStop: true,
omitOrigin,
};
if (isFreshOrEnhanced) {
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,
isFreshOrEnhanced = 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 (isFreshOrEnhanced) {
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_LCP_CANDIDATE_FOR_SOFT_NAVIGATION:
link = 'https://developer.chrome.com/docs/web-platform/soft-navigations-experiment';
name = 'largest contentful paint (soft navigation)';
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 maybeCreateLinkElement(url: string): Element|null {
const parsedURL = new Common.ParsedURL.ParsedURL(url);
if (!parsedURL.scheme) {
return null;
}
const splitResult = Common.ParsedURL.ParsedURL.splitLineAndColumn(url);
if (!splitResult) {
return null;
}
const {url: rawURL, lineNumber, columnNumber} = splitResult;
const options = {
lineNumber,
columnNumber,
showColumnNumber: true,
omitOrigin: true,
};
return LegacyComponents.Linkifier.Linkifier.linkifyURL(rawURL as Platform.DevToolsPath.UrlString, options);
}
/**
* Takes an input string and parses it to look for links. It does this by
* looking for URLs in the input string. The returned fragment will contain
* the same string but with any links wrapped in clickable links. The text
* of the link is the URL, so the visible string to the user is unchanged.
*/
static parseStringForLinks(rawString: string): DocumentFragment {
const results = TextUtils.TextUtils.Utils.splitStringByRegexes(rawString, [URL_REGEX]);
const nodes = results.map(result => {
if (result.regexIndex === -1) {
return result.value;
}
return TimelineUIUtils.maybeCreateLinkElement(result.value) ?? result.value;
});
const frag = document.createDocumentFragment();
frag.append(...nodes);
return frag;
}
static async buildTraceEventDetails(
parsedTrace: Trace.TraceModel.ParsedTrace,
event: Trace.Types.Events.Event,
linkifier: LegacyComponents.Linkifier.Linkifier,
canShowPieChart: boolean,
entityMapper: Trace.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 Utils.EntryNodes.relatedDOMNodesForEvent(
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.data);
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, event);
// 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.data.Initiators.eventToInitiator.get(event) ?? null;
const initiatorFor = parsedTrace.data.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, including custom extensibility markers
if (Trace.Helpers.Trace.eventHasCategory(event, Trace.Types.Events.Categories.UserTiming) ||
Trace.Types.Extensions.isSyntheticExtensionEntry(event)) {
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.renderObjectJson(JSON.parse(event.args.data?.detail));
contentHelper.appendElementRow(i18nString(UIStrings.details), detailContainer);
}
if (Trace.Types.Events.isSyntheticUserTiming(event) && event.args?.data?.beginEvent.args.detail) {
const detailContainer = TimelineUIUtils.renderObjectJson(JSON.parse(event.args?.data?.beginEvent.args.detail));
contentHelper.appendElementRow(i18nString(UIStrings.details), detailContainer);
}
if (parsedTrace.data.Meta.traceIsGeneric) {
TimelineUIUtils.renderEventJson(event, contentHelper);
return contentHelper.fragment;
}
if (Trace.Types.Events.isNavigationStart(event)) {
url = (event.args.data?.documentLoaderURL ?? event.args.data?.url) as Platform.DevToolsPath.UrlString;
if (url) {
contentHelper.appendElementRow(i18nString(UIStrings.url), LegacyComponents.Linkifier.Linkifier.linkifyURL(url));
}
}
if (Trace.Types.Events.isSoftNavigationStart(event)) {
url = event.args.context.URL as Platform.DevToolsPath.UrlString;
if (url) {
contentHelper.appendElementRow(i18nString(UIStrings.url), LegacyComponents.Linkifier.Linkifier.linkifyURL(url));
}
}
if (Trace.Types.Events.isV8Compile(event)) {
url = event.args.data?.url as Platform.DevToolsPath.UrlString;
if (url) {
const {lineNumber, columnNumber} = Trace.Helpers.Trace.getZeroIndexedLineAndColumnForEvent(event);
contentHelper.appendLocationRow(
i18nString(UIStrings.script), url, lineNumber || 0, columnNumber, undefined, true);
const originWithEntity = this.getOriginWithEntity(entityMapper, parsedTrace, event);
if (originWithEntity) {
contentHelper.appendElementRow(i18nString(UIStrings.origin), originWithEntity);
}
entityAppended = true;
}
const isEager = Boolean(event.args.data?.eager);
if (isEager) {
contentHelper.appendTextRow(i18nString(UIStrings.eagerCompile), true);
}
const isStreamed = Boolean(event.args.data?.streamed);
contentHelper.appendTextRow(
i18nString(UIStrings.streamed),
isStreamed + (isStreamed ? '' : `: ${event.args.data?.notStreamedReason || ''}`));
if (event.args.data) {
TimelineUIUtils.buildConsumeCacheDetails(event.args.data, contentHelper);
}
}
if (Trace.Types.Extensions.isSyntheticExtensionEntry(event)) {
// isSyntheticExtensionEntries can be any of: perf.measure, perf.mark or console.timeStamp.
const userDetail = structuredClone(event.userDetail) as Record<string, Trace.Types.Extensions.JsonValue>;
if (userDetail && Object.keys(userDetail).length) {
// E.g. console.timeStamp(name, start, end, track, trackGroup, color,
// {url: 'foo-extension://node/1', description: 'Node'});
const hasExclusiveLink = typeof userDetail === 'object' && typeof userDetail.url === 'string' &&
typeof userDetail.description === 'string';
if (hasExclusiveLink && Boolean(Root.Runtime.hostConfig.devToolsDeepLinksViaExtensibilityApi?.enabled)) {
const linkElement = this.maybeCreateLinkElement(String(userDetail.url));
if (linkElement) {
contentHelper.appendElementRow(String(userDetail.description), linkElement);
// Now remove so we don't render them in renderObjectJson.
delete userDetail.url;
delete userDetail.description;
}
}
if (Object.keys(userDetail).length) {
// E.g., performance.measure(name, {detail: {important: 42, devtools: {}}})
const detailContainer = TimelineUIUtils.renderObjectJson(userDetail);
contentHelper.appendElementRow(i18nString(UIStrings.details), detailContainer);
}
}
// E.g. performance.measure(name, {detail: {devtools: {properties: ['Key', 'Value']}}})
if (event.devtoolsObj.properties) {
for (const [key, value] of event.devtoolsObj.properties || []) {
const renderedValue = typeof value === 'string' ? TimelineUIUtils.parseStringForLinks(value) :
TimelineUIUtils.renderObjectJson(value);
contentHelper.appendElementRow(key, renderedValue);
}
}
}
const isFreshOrEnhanced =
Boolean(parsedTrace && Tracing.FreshRecording.Tracker.instance().recordingIsFreshOrEnhanced(parsedTrace));
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'];
contentHelper.appendTextRow(i18nString(UIStrings.collected), i18n.ByteUtilities.bytesToString(delta));
break;
}
case Trace.Types.Events.Name.PROFILE_CALL: {
const profileCall = event as Trace.Types.Events.SyntheticProfileCall;
const resolvedURL = SourceMapsResolver.SourceMapsResolver.resolvedURLForEntry(parsedTrace, profileCall);
if (!resolvedURL) {
break;
}
const callFrame = profileCall.callFrame;
// Render the URL with its location content.
contentHelper.appendLocationRow(
i18nString(UIStrings.source), resolvedURL, callFrame.lineNumber || 0, callFrame.columnNumber, undefined,
true);
const originWithEntity = this.getOriginWithEntity(entityMapper, parsedTrace, profileCall);
if (originWithEntity) {
contentHelper.appendElementRow(i18nString(UIStrings.origin), originWithEntity);
}
entityAppended = true;
break;
}
case Trace.Types.Events.Name.FUNCTION_CALL: {
const detailsNode