UNPKG

chrome-devtools-frontend

Version:
590 lines (546 loc) 26.2 kB
// Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as 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 * as Bindings from '../../models/bindings/bindings.js'; import * as Trace from '../../models/trace/trace.js'; import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js'; import { addDecorationToEvent, buildGroupStyle, buildTrackHeader, getDurationString, } from './AppenderUtils.js'; import { type CompatibilityTracksAppender, entryIsVisibleInTimeline, type PopoverInfo, type TrackAppender, type TrackAppenderName, VisualLoggingTrackName, } from './CompatibilityTracksAppender.js'; import * as ModificationsManager from './ModificationsManager.js'; import * as Utils from './utils/utils.js'; const UIStrings = { /** * @description Text shown for an entry in the flame chart that is ignored because it matches * a predefined ignore list. * @example {/analytics\.js$} rule */ onIgnoreList: 'On ignore list ({rule})', /** * @description Refers to the "Main frame", meaning the top level frame. See https://www.w3.org/TR/html401/present/frames.html * @example{example.com} PH1 */ mainS: 'Main — {PH1}', /** * @description Refers to the main thread of execution of a program. See https://developer.mozilla.org/en-US/docs/Glossary/Main_thread */ main: 'Main', /** * @description Refers to any frame in the page. See https://www.w3.org/TR/html401/present/frames.html * @example {https://example.com} PH1 */ frameS: 'Frame — {PH1}', /** *@description A web worker in the page. See https://developer.mozilla.org/en-US/docs/Web/API/Worker *@example {https://google.com} PH1 */ workerS: '`Worker` — {PH1}', /** *@description A web worker in the page. See https://developer.mozilla.org/en-US/docs/Web/API/Worker *@example {FormatterWorker} PH1 *@example {https://google.com} PH2 */ workerSS: '`Worker`: {PH1} — {PH2}', /** *@description Label for a web worker exclusively allocated for a purpose. */ dedicatedWorker: 'Dedicated `Worker`', /** *@description A generic name given for a thread running in the browser (sequence of programmed instructions). * The placeholder is an enumeration given to the thread. *@example {1} PH1 */ threadS: 'Thread {PH1}', /** *@description Rasterization in computer graphics. */ raster: 'Raster', /** *@description Threads used for background tasks. */ threadPool: 'Thread pool', /** *@description Name for a thread that rasterizes graphics in a website. *@example {2} PH1 */ rasterizerThreadS: 'Rasterizer thread {PH1}', /** *@description Text in Timeline Flame Chart Data Provider of the Performance panel *@example {2} PH1 */ threadPoolThreadS: 'Thread pool worker {PH1}', /** *@description Title of a bidder auction worklet with known URL in the timeline flame chart of the Performance panel *@example {https://google.com} PH1 */ bidderWorkletS: 'Bidder Worklet — {PH1}', /** *@description Title of a bidder auction worklet in the timeline flame chart of the Performance panel with an unknown URL */ bidderWorklet: 'Bidder Worklet', /** *@description Title of a seller auction worklet in the timeline flame chart of the Performance panel with an unknown URL */ sellerWorklet: 'Seller Worklet', /** *@description Title of an auction worklet in the timeline flame chart of the Performance panel with an unknown URL */ unknownWorklet: 'Auction Worklet', /** *@description Title of control thread of a service process for an auction worklet in the timeline flame chart of the Performance panel with an unknown URL */ workletService: 'Auction Worklet service', /** *@description Title of a seller auction worklet with known URL in the timeline flame chart of the Performance panel *@example {https://google.com} PH1 */ sellerWorkletS: 'Seller Worklet — {PH1}', /** *@description Title of an auction worklet with known URL in the timeline flame chart of the Performance panel *@example {https://google.com} PH1 */ unknownWorkletS: 'Auction Worklet — {PH1}', /** *@description Title of control thread of a service process for an auction worklet with known URL in the timeline flame chart of the Performance panel * @example {https://google.com} PH1 */ workletServiceS: 'Auction Worklet service — {PH1}', } as const; const str_ = i18n.i18n.registerUIStrings('panels/timeline/ThreadAppender.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); // This appender is only triggered when the Renderer handler is run. At // the moment this only happens in the basic component server example. // In the future, once this appender fully supports the behaviour of the // old engine's thread/sync tracks we can always run it by enabling the // Renderer and Samples handler by default. export class ThreadAppender implements TrackAppender { readonly appenderName: TrackAppenderName = 'Thread'; #colorGenerator: Common.Color.Generator; #compatibilityBuilder: CompatibilityTracksAppender; #parsedTrace: Trace.Handlers.Types.ParsedTrace; #entries: readonly Trace.Types.Events.Event[] = []; #tree: Trace.Helpers.TreeHelpers.TraceEntryTree; #processId: Trace.Types.Events.ProcessID; #threadId: Trace.Types.Events.ThreadID; #threadDefaultName: string; #expanded = false; #headerAppended = false; readonly threadType: Trace.Handlers.Threads.ThreadType = Trace.Handlers.Threads.ThreadType.MAIN_THREAD; readonly isOnMainFrame: boolean; #showAllEventsEnabled = Root.Runtime.experiments.isEnabled('timeline-show-all-events'); #url = ''; #headerNestingLevel: number|null = null; constructor( compatibilityBuilder: CompatibilityTracksAppender, parsedTrace: Trace.Handlers.Types.ParsedTrace, processId: Trace.Types.Events.ProcessID, threadId: Trace.Types.Events.ThreadID, threadName: string|null, type: Trace.Handlers.Threads.ThreadType, entries: readonly Trace.Types.Events.Event[], tree: Trace.Helpers.TreeHelpers.TraceEntryTree) { this.#compatibilityBuilder = compatibilityBuilder; // TODO(crbug.com/1456706): // The values for this color generator have been taken from the old // engine to keep the colors the same after the migration. This // generator is used here to create colors for js frames (profile // calls) in the flamechart by hashing the script's url. We might // need to reconsider this generator when migrating to GM3 colors. this.#colorGenerator = new Common.Color.Generator({min: 30, max: 330, count: undefined}, {min: 50, max: 80, count: 3}, 85); // Add a default color for call frames with no url. this.#colorGenerator.setColorForID('', '#f2ecdc'); this.#parsedTrace = parsedTrace; this.#processId = processId; this.#threadId = threadId; if (!entries || !tree) { throw new Error(`Could not find data for thread with id ${threadId} in process with id ${processId}`); } this.#entries = entries; this.#tree = tree; this.#threadDefaultName = threadName || i18nString(UIStrings.threadS, {PH1: threadId}); this.isOnMainFrame = Boolean(this.#parsedTrace.Renderer?.processes.get(processId)?.isOnMainFrame); this.threadType = type; // AuctionWorklets are threads, so we re-use this appender rather than // duplicate it, but we change the name because we want to render these // lower down than other threads. if (this.#parsedTrace.AuctionWorklets.worklets.has(processId)) { this.appenderName = 'Thread_AuctionWorklet'; } this.#url = this.#parsedTrace.Renderer?.processes.get(this.#processId)?.url || ''; } processId(): Trace.Types.Events.ProcessID { return this.#processId; } threadId(): Trace.Types.Events.ThreadID { return this.#threadId; } /** * Appends into the flame chart data the data corresponding to the * this thread. * @param trackStartLevel the horizontal level of the flame chart events where * the track's events will start being appended. * @param expanded wether the track should be rendered expanded. * @returns the first available level to append more data after having * appended the track's events. */ appendTrackAtLevel(trackStartLevel: number, expanded = false): number { if (this.#entries.length === 0) { return trackStartLevel; } this.#expanded = expanded; return this.#appendTreeAtLevel(trackStartLevel); } setHeaderNestingLevel(level: number): void { this.#headerNestingLevel = level; } /** * Track header is appended only if there are events visible on it. * Otherwise we don't append any track. So, instead of preemptively * appending a track before appending its events, we only do so once * we have detected that the track contains an event that is visible. */ #ensureTrackHeaderAppended(trackStartLevel: number): void { if (this.#headerAppended) { return; } if (this.threadType === Trace.Handlers.Threads.ThreadType.RASTERIZER || this.threadType === Trace.Handlers.Threads.ThreadType.THREAD_POOL) { this.#appendGroupedTrackHeaderAndTitle(trackStartLevel, this.threadType); } else { this.#appendTrackHeaderAtLevel(trackStartLevel); } this.#headerAppended = true; } setHeaderAppended(headerAppended: boolean): void { this.#headerAppended = headerAppended; } headerAppended(): boolean { return this.#headerAppended; } /** * Adds into the flame chart data the header corresponding to this * thread. A header is added in the shape of a group in the flame * chart data. A group has a predefined style and a reference to the * definition of the legacy track (which should be removed in the * future). * @param currentLevel the flame chart level at which the header is * appended. */ #appendTrackHeaderAtLevel(currentLevel: number): void { const trackIsCollapsible = this.#entries.length > 0; const style = buildGroupStyle({shareHeaderLine: false, collapsible: trackIsCollapsible}); if (this.#headerNestingLevel !== null) { style.nestingLevel = this.#headerNestingLevel; } const visualLoggingName = this.#visualLoggingNameForThread(); const group = buildTrackHeader( visualLoggingName, currentLevel, this.trackName(), style, /* selectable= */ true, this.#expanded, /* showStackContextMenu= */ true); this.#compatibilityBuilder.registerTrackForGroup(group, this); } #visualLoggingNameForThread(): VisualLoggingTrackName|null { switch (this.threadType) { case Trace.Handlers.Threads.ThreadType.MAIN_THREAD: return this.isOnMainFrame ? VisualLoggingTrackName.THREAD_MAIN : VisualLoggingTrackName.THREAD_FRAME; case Trace.Handlers.Threads.ThreadType.WORKER: return VisualLoggingTrackName.THREAD_WORKER; case Trace.Handlers.Threads.ThreadType.RASTERIZER: return VisualLoggingTrackName.THREAD_RASTERIZER; case Trace.Handlers.Threads.ThreadType.AUCTION_WORKLET: return VisualLoggingTrackName.THREAD_AUCTION_WORKLET; case Trace.Handlers.Threads.ThreadType.OTHER: return VisualLoggingTrackName.THREAD_OTHER; case Trace.Handlers.Threads.ThreadType.CPU_PROFILE: return VisualLoggingTrackName.THREAD_CPU_PROFILE; case Trace.Handlers.Threads.ThreadType.THREAD_POOL: return VisualLoggingTrackName.THREAD_POOL; default: return null; } } /** * Raster threads are rendered under a single header in the * flamechart. However, each thread has a unique title which needs to * be added to the flamechart data. */ #appendGroupedTrackHeaderAndTitle( trackStartLevel: number, threadType: Trace.Handlers.Threads.ThreadType.RASTERIZER|Trace.Handlers.Threads.ThreadType.THREAD_POOL): void { const currentTrackCount = this.#compatibilityBuilder.getCurrentTrackCountForThreadType(threadType); if (currentTrackCount === 0) { const trackIsCollapsible = this.#entries.length > 0; const headerStyle = buildGroupStyle({shareHeaderLine: false, collapsible: trackIsCollapsible}); // Don't set any jslogcontext (first argument) because this is a shared // header group. Each child will have its context set. const headerGroup = buildTrackHeader( null, trackStartLevel, this.trackName(), headerStyle, /* selectable= */ false, this.#expanded); this.#compatibilityBuilder.getFlameChartTimelineData().groups.push(headerGroup); } // Nesting is set to 1 because the track is appended inside the // header for all raster threads. const titleStyle = buildGroupStyle({padding: 2, nestingLevel: 1, collapsible: false}); const rasterizerTitle = this.threadType === Trace.Handlers.Threads.ThreadType.RASTERIZER ? i18nString(UIStrings.rasterizerThreadS, {PH1: currentTrackCount + 1}) : i18nString(UIStrings.threadPoolThreadS, {PH1: currentTrackCount + 1}); const visualLoggingName = this.#visualLoggingNameForThread(); const titleGroup = buildTrackHeader( visualLoggingName, trackStartLevel, rasterizerTitle, titleStyle, /* selectable= */ true, this.#expanded); this.#compatibilityBuilder.registerTrackForGroup(titleGroup, this); } trackName(): string { let threadTypeLabel: string|null = null; switch (this.threadType) { case Trace.Handlers.Threads.ThreadType.MAIN_THREAD: threadTypeLabel = this.isOnMainFrame ? i18nString(UIStrings.mainS, {PH1: this.#url}) : i18nString(UIStrings.frameS, {PH1: this.#url}); break; case Trace.Handlers.Threads.ThreadType.CPU_PROFILE: threadTypeLabel = i18nString(UIStrings.main); break; case Trace.Handlers.Threads.ThreadType.WORKER: threadTypeLabel = this.#buildNameForWorker(); break; case Trace.Handlers.Threads.ThreadType.RASTERIZER: threadTypeLabel = i18nString(UIStrings.raster); break; case Trace.Handlers.Threads.ThreadType.THREAD_POOL: threadTypeLabel = i18nString(UIStrings.threadPool); break; case Trace.Handlers.Threads.ThreadType.OTHER: break; case Trace.Handlers.Threads.ThreadType.AUCTION_WORKLET: threadTypeLabel = this.#buildNameForAuctionWorklet(); break; default: return Platform.assertNever(this.threadType, `Unknown thread type: ${this.threadType}`); } let suffix = ''; if (this.#parsedTrace.Meta.traceIsGeneric) { suffix = suffix + ` (${this.threadId()})`; } return (threadTypeLabel || this.#threadDefaultName) + suffix; } getUrl(): string { return this.#url; } getEntries(): readonly Trace.Types.Events.Event[] { return this.#entries; } #buildNameForAuctionWorklet(): string { const workletMetadataEvent = this.#parsedTrace.AuctionWorklets.worklets.get(this.#processId); // We should always have this event - if we do not, we were instantiated with invalid data. if (!workletMetadataEvent) { return i18nString(UIStrings.unknownWorklet); } // Host could be empty - in which case we do not want to add it. const host = workletMetadataEvent.host ? `https://${workletMetadataEvent.host}` : ''; const shouldAddHost = host.length > 0; // For each Auction Worklet in a page there are two threads we care about on the same process. // 1. The "Worklet Service" which is a generic helper service. This thread // is always named "auction_worklet.CrUtilityMain". // // 2. The "Seller/Bidder" service. This thread is always named // "AuctionV8HelperThread". The AuctionWorkets handler does the job of // figuring this out for us - the metadata event it provides for each // worklet process will have a `type` already set. // // Therefore, for this given thread, which we know is part of // an AuctionWorklet process, we need to figure out if this thread is the // generic service, or a seller/bidder worklet. // // Note that the worklet could also have the "unknown" type - this is not // expected but implemented to prevent trace event changes causing DevTools // to break with unknown worklet types. const isUtilityThread = workletMetadataEvent.args.data.utilityThread.tid === this.#threadId; const isBidderOrSeller = workletMetadataEvent.args.data.v8HelperThread.tid === this.#threadId; if (isUtilityThread) { return shouldAddHost ? i18nString(UIStrings.workletServiceS, {PH1: host}) : i18nString(UIStrings.workletService); } if (isBidderOrSeller) { switch (workletMetadataEvent.type) { case Trace.Types.Events.AuctionWorkletType.SELLER: return shouldAddHost ? i18nString(UIStrings.sellerWorkletS, {PH1: host}) : i18nString(UIStrings.sellerWorklet); case Trace.Types.Events.AuctionWorkletType.BIDDER: return shouldAddHost ? i18nString(UIStrings.bidderWorkletS, {PH1: host}) : i18nString(UIStrings.bidderWorklet); case Trace.Types.Events.AuctionWorkletType.UNKNOWN: return shouldAddHost ? i18nString(UIStrings.unknownWorkletS, {PH1: host}) : i18nString(UIStrings.unknownWorklet); default: Platform.assertNever( workletMetadataEvent.type, `Unexpected Auction Worklet Type ${workletMetadataEvent.type}`); } } // We should never reach here, but just in case! return shouldAddHost ? i18nString(UIStrings.unknownWorkletS, {PH1: host}) : i18nString(UIStrings.unknownWorklet); } #buildNameForWorker(): string { const url = this.#parsedTrace.Renderer?.processes.get(this.#processId)?.url || ''; const workerId = this.#parsedTrace.Workers.workerIdByThread.get(this.#threadId); const workerURL = workerId ? this.#parsedTrace.Workers.workerURLById.get(workerId) : url; // Try to create a name using the worker url if present. If not, use a generic label. let workerName = workerURL ? i18nString(UIStrings.workerS, {PH1: workerURL}) : i18nString(UIStrings.dedicatedWorker); const workerTarget = workerId !== undefined && SDK.TargetManager.TargetManager.instance().targetById(workerId); if (workerTarget) { // Get the worker name from the target, which corresponds to the name // assigned to the worker when it was constructed. workerName = i18nString(UIStrings.workerSS, {PH1: workerTarget.name(), PH2: url}); } return workerName; } /** * Adds into the flame chart data the entries of this thread, which * includes trace events and JS calls. * @param currentLevel the flame chart level from which entries will * be appended. * @returns the next level after the last occupied by the appended * entries (the first available level to append more data). */ #appendTreeAtLevel(trackStartLevel: number): number { // We can not used the tree maxDepth in the tree from the // RendererHandler because ignore listing and visibility of events // alter the final depth of the flame chart. return this.#appendNodesAtLevel(this.#tree.roots, trackStartLevel); } /** * Traverses the trees formed by the provided nodes in breadth first * fashion and appends each node's entry on each iteration. As each * entry is handled, a check for the its visibility or if it's ignore * listed is done before appending. */ #appendNodesAtLevel( nodes: Iterable<Trace.Helpers.TreeHelpers.TraceEntryNode>, startingLevel: number, parentIsIgnoredListed = false): number { const invisibleEntries = ModificationsManager.ModificationsManager.activeManager()?.getEntriesFilter().invisibleEntries() ?? []; let maxDepthInTree = startingLevel; for (const node of nodes) { let nextLevel = startingLevel; const entry = node.entry; const entryIsIgnoreListed = Utils.IgnoreList.isIgnoreListedEntry(entry); // Events' visibility is determined from their predefined styles, // which is something that's not available in the engine data. // Thus it needs to be checked in the appenders, but preemptively // checking if there are visible events and returning early if not // is potentially expensive since, in theory, we would be adding // another traversal to the entries array (which could grow // large). To avoid the extra cost we add the check in the // traversal we already need to append events. const entryIsVisible = !invisibleEntries.includes(entry) && (entryIsVisibleInTimeline(entry, this.#parsedTrace) || this.#showAllEventsEnabled); // For ignore listing support, these two conditions need to be met // to not append a profile call to the flame chart: // 1. It is ignore listed // 2. It is NOT the bottom-most call in an ignore listed stack (a // set of chained profile calls that belong to ignore listed // URLs). // This means that all of the ignore listed calls are ignored (not // appended), except if it is the bottom call of an ignored stack. // This is becaue to represent ignore listed stack frames, we add // a flame chart entry with the length and position of the bottom // frame, which is distictively marked to denote an ignored listed // stack. const skipEventDueToIgnoreListing = entryIsIgnoreListed && parentIsIgnoredListed; if (entryIsVisible && !skipEventDueToIgnoreListing) { this.#appendEntryAtLevel(entry, startingLevel); nextLevel++; } const depthInChildTree = this.#appendNodesAtLevel(node.children, nextLevel, entryIsIgnoreListed); maxDepthInTree = Math.max(depthInChildTree, maxDepthInTree); } return maxDepthInTree; } #appendEntryAtLevel(entry: Trace.Types.Events.Event, level: number): void { this.#ensureTrackHeaderAppended(level); const index = this.#compatibilityBuilder.appendEventAtLevel(entry, level, this); this.#addDecorationsToEntry(entry, index); } #addDecorationsToEntry(entry: Trace.Types.Events.Event, index: number): void { const flameChartData = this.#compatibilityBuilder.getFlameChartTimelineData(); if (ModificationsManager.ModificationsManager.activeManager()?.getEntriesFilter().isEntryExpandable(entry)) { addDecorationToEvent( flameChartData, index, {type: PerfUI.FlameChart.FlameChartDecorationType.HIDDEN_DESCENDANTS_ARROW}); } const warnings = this.#parsedTrace.Warnings.perEvent.get(entry); if (!warnings) { return; } addDecorationToEvent(flameChartData, index, {type: PerfUI.FlameChart.FlameChartDecorationType.WARNING_TRIANGLE}); if (!warnings.includes('LONG_TASK')) { return; } addDecorationToEvent(flameChartData, index, { type: PerfUI.FlameChart.FlameChartDecorationType.CANDY, startAtTime: Trace.Handlers.ModelHandlers.Warnings.LONG_MAIN_THREAD_TASK_THRESHOLD, }); } /* ------------------------------------------------------------------------------------ The following methods are invoked by the flame chart renderer to query features about events on rendering. ------------------------------------------------------------------------------------ */ /** * Gets the color an event added by this appender should be rendered with. */ colorForEvent(event: Trace.Types.Events.Event): string { if (this.#parsedTrace.Meta.traceIsGeneric) { return event.name ? `hsl(${Platform.StringUtilities.hashCode(event.name) % 300 + 30}, 40%, 70%)` : '#ccc'; } if (Trace.Types.Events.isProfileCall(event)) { if (event.callFrame.functionName === '(idle)') { return Utils.EntryStyles.getCategoryStyles().idle.getComputedColorValue(); } if (event.callFrame.functionName === '(program)') { return Utils.EntryStyles.getCategoryStyles().other.getComputedColorValue(); } if (event.callFrame.scriptId === '0') { // If we can not match this frame to a script, return the // generic "scripting" color. return Utils.EntryStyles.getCategoryStyles().scripting.getComputedColorValue(); } // Otherwise, return a color created based on its URL. return this.#colorGenerator.colorForID(event.callFrame.url); } const defaultColor = Utils.EntryStyles.getEventStyle(event.name as Trace.Types.Events.Name)?.category.getComputedColorValue(); return defaultColor || Utils.EntryStyles.getCategoryStyles().other.getComputedColorValue(); } /** * Gets the title an event added by this appender should be rendered with. */ titleForEvent(entry: Trace.Types.Events.Event): string { if (Utils.IgnoreList.isIgnoreListedEntry(entry)) { const rule = Utils.IgnoreList.getIgnoredReasonString(entry); return i18nString(UIStrings.onIgnoreList, {rule}); } return Utils.EntryName.nameForEntry(entry, this.#parsedTrace); } setPopoverInfo(event: Trace.Types.Events.Event, info: PopoverInfo): void { if (Trace.Types.Events.isParseHTML(event)) { const startLine = event.args['beginData']['startLine']; const endLine = event.args['endData']?.['endLine']; const eventURL = event.args['beginData']['url'] as Platform.DevToolsPath.UrlString; const url = Bindings.ResourceUtils.displayNameForURL(eventURL); const range = (endLine !== -1 || endLine === startLine) ? `${startLine}...${endLine}` : startLine; info.title += ` - ${url} [${range}]`; } const selfTime = this.#parsedTrace.Renderer.entryToNode.get(event)?.selfTime; info.formattedTime = getDurationString(event.dur, selfTime); } }