UNPKG

chrome-devtools-frontend

Version:
1,150 lines (1,092 loc) 115 kB
// 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