UNPKG

chrome-devtools-frontend

Version:
289 lines (258 loc) • 10.8 kB
// Copyright 2016 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // This file supports the gutter decorations visible in the Sources panel when a // performance trace is active, showing either the runtime sample measures or the // memory sampling (memory is behind the LiveHeapProfile experiment). // // When profiles are added, the associated UISourceCodes are given the profile data // as decorations. The raw profile locations are mapped to original source files in // this way. // // Note, while this is called "LineLevel", it's the profile data is actually granular // to the column. import type * as Platform from '../../../../core/platform/platform.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 type * as CPUProfile from '../../../../models/cpu_profile/cpu_profile.js'; import * as Workspace from '../../../../models/workspace/workspace.js'; let performanceInstance: Performance; export class Performance { private readonly helper: Helper; private constructor() { this.helper = new Helper(Workspace.UISourceCode.DecoratorType.PERFORMANCE); } static instance(opts: { forceNew: boolean|null, } = {forceNew: null}): Performance { const {forceNew} = opts; if (!performanceInstance || forceNew) { performanceInstance = new Performance(); } return performanceInstance; } initialize(profiles: CPUProfile.CPUProfileDataModel.CPUProfileDataModel[], target: SDK.Target.Target|null): void { this.helper.reset(); for (const profile of profiles) { this.appendCPUProfile(profile, target); } void this.helper.update(); } private appendLegacyCPUProfile( profile: CPUProfile.CPUProfileDataModel.CPUProfileDataModel, target: SDK.Target.Target|null): void { const nodesToGo: CPUProfile.CPUProfileDataModel.CPUProfileNode[] = [profile.profileHead]; const sampleDuration = (profile.profileEndTime - profile.profileStartTime) / profile.totalHitCount; while (nodesToGo.length) { const nodes: CPUProfile.CPUProfileDataModel.CPUProfileNode[] = nodesToGo.pop()?.children ?? []; for (let i = 0; i < nodes.length; ++i) { const node = nodes[i]; nodesToGo.push(node); if (!node.url || !node.positionTicks) { continue; } for (let j = 0; j < node.positionTicks.length; ++j) { const lineInfo = node.positionTicks[j]; const line = lineInfo.line; const time = lineInfo.ticks * sampleDuration; // Since no column number is provided by legacy profile, default to 1 (beginning of line). this.helper.addLocationData(target, node.url, {line, column: 1}, time); } } } } private appendCPUProfile(profile: CPUProfile.CPUProfileDataModel.CPUProfileDataModel, target: SDK.Target.Target|null): void { if (!profile.lines) { this.appendLegacyCPUProfile(profile, target); return; } if (!profile.samples || !profile.columns) { return; } for (let i = 1; i < profile.samples.length; ++i) { const line = profile.lines[i]; const column = profile.columns?.[i]; if (!line || !column) { continue; } const node = profile.nodeByIndex(i); if (!node) { continue; } const scriptIdOrUrl = Number(node.scriptId) || node.url; if (!scriptIdOrUrl) { continue; } const time = profile.timestamps[i] - profile.timestamps[i - 1]; this.helper.addLocationData(target, scriptIdOrUrl, {line, column}, time); } } } let memoryInstance: Memory; // Note: this is used only by LiveHeapProfile (a drawer panel) if the experiment is enabled. export class Memory { private readonly helper: Helper; private constructor() { this.helper = new Helper(Workspace.UISourceCode.DecoratorType.MEMORY); } static instance(opts: { forceNew: boolean|null, } = {forceNew: null}): Memory { const {forceNew} = opts; if (!memoryInstance || forceNew) { memoryInstance = new Memory(); } return memoryInstance; } reset(): void { this.helper.reset(); void this.helper.update(); } initialize(profilesAndTargets: Array<{ profile: Protocol.HeapProfiler.SamplingHeapProfile, target: SDK.Target.Target, }>): void { this.helper.reset(); for (const {profile, target} of profilesAndTargets) { this.appendHeapProfile(profile, target); } void this.helper.update(); } private appendHeapProfile(profile: Protocol.HeapProfiler.SamplingHeapProfile, target: SDK.Target.Target|null): void { const helper = this.helper; processNode(profile.head); function processNode(node: Protocol.HeapProfiler.SamplingHeapProfileNode): void { node.children.forEach(processNode); if (!node.selfSize) { return; } const script = Number(node.callFrame.scriptId) || node.callFrame.url as Platform.DevToolsPath.UrlString; if (!script) { return; } const line = node.callFrame.lineNumber + 1; const column = node.callFrame.columnNumber + 1; helper.addLocationData(target, script, {line, column}, node.selfSize); } } } export class Helper { private readonly type: Workspace.UISourceCode.DecoratorType; private readonly locationPool = new Bindings.LiveLocation.LiveLocationPool(); /** * Given a location in a script (with line and column numbers being 1-based) stores * the time spent at that location in a performance profile. */ private locationData = new Map< SDK.Target.Target|null, Map<Platform.DevToolsPath.UrlString|number, Workspace.UISourceCode.LineColumnProfileMap>>(); constructor(type: Workspace.UISourceCode.DecoratorType) { this.type = type; this.reset(); } reset(): void { // The second map uses string keys for script URLs and numbers for scriptId. this.locationData = new Map(); } /** * Stores the time taken running a given script location (line and column) */ addLocationData( target: SDK.Target.Target|null, scriptIdOrUrl: Platform.DevToolsPath.UrlString|number, {line, column}: {line: number, column: number}, data: number): void { let targetData = this.locationData.get(target); if (!targetData) { targetData = new Map(); this.locationData.set(target, targetData); } let scriptData = targetData.get(scriptIdOrUrl); if (!scriptData) { scriptData = new Map(); targetData.set(scriptIdOrUrl, scriptData); } let lineData = scriptData.get(line); if (!lineData) { lineData = new Map(); scriptData.set(line, lineData); } lineData.set(column, (lineData.get(column) || 0) + data); } async update(): Promise<void> { this.locationPool.disposeAll(); // Map from sources to line->value profile maps. const decorationsBySource: Workspace.UISourceCode.ProfileDataMap = new Map(); const pending: Array<Promise<void>> = []; for (const [target, scriptToLineMap] of this.locationData) { const debuggerModel = target ? target.model(SDK.DebuggerModel.DebuggerModel) : null; for (const [scriptIdOrUrl, lineToDataMap] of scriptToLineMap) { // debuggerModel is null when the profile is loaded from file. // Try to get UISourceCode by the URL in this case. const workspace = Workspace.Workspace.WorkspaceImpl.instance(); if (debuggerModel) { const workspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance(); for (const [lineNumber, lineData] of lineToDataMap) { // lineData contains profiling data by column. for (const [columnNumber, data] of lineData) { const zeroBasedLine = lineNumber - 1; const zeroBasedColumn = columnNumber - 1; if (target) { const rawLocation = typeof scriptIdOrUrl === 'string' ? debuggerModel.createRawLocationByURL(scriptIdOrUrl, zeroBasedLine, zeroBasedColumn || 0) : debuggerModel.createRawLocationByScriptId( String(scriptIdOrUrl) as Protocol.Runtime.ScriptId, zeroBasedLine, zeroBasedColumn || 0); if (rawLocation) { pending.push(workspaceBinding.rawLocationToUILocation(rawLocation).then(uiLocation => { if (!uiLocation) { return; } this.addLineColumnData( decorationsBySource, uiLocation.uiSourceCode, uiLocation.lineNumber + 1, (uiLocation.columnNumber ?? 0) + 1, data); // If the above was a source mapped UILocation, then we also need to add it to the generated UILocation. if (uiLocation.uiSourceCode.contentType().isFromSourceMap()) { const script = rawLocation.script(); const uiSourceCode = script ? workspaceBinding.uiSourceCodeForScript(script) : null; if (uiSourceCode) { this.addLineColumnData(decorationsBySource, uiSourceCode, lineNumber, columnNumber, data); } } })); } } } } } else if (typeof scriptIdOrUrl === 'string') { const uiSourceCode = workspace.uiSourceCodeForURL(scriptIdOrUrl); if (uiSourceCode) { decorationsBySource.set(uiSourceCode, lineToDataMap); } } } await Promise.all(pending); for (const [uiSourceCode, lineMap] of decorationsBySource) { uiSourceCode.setDecorationData(this.type, lineMap); } } for (const uiSourceCode of Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodes()) { if (!decorationsBySource.has(uiSourceCode)) { uiSourceCode.setDecorationData(this.type, undefined); } } } private addLineColumnData( decorationsBySource: Workspace.UISourceCode.ProfileDataMap, uiSourceCode: Workspace.UISourceCode.UISourceCode, lineOneIndexed: number, columnOneIndexed: number, data: number): void { let lineMap = decorationsBySource.get(uiSourceCode); if (!lineMap) { lineMap = new Map(); decorationsBySource.set(uiSourceCode, lineMap); } let columnMap = lineMap.get(lineOneIndexed); if (!columnMap) { columnMap = new Map(); lineMap.set(lineOneIndexed, columnMap); } columnMap.set(columnOneIndexed, (columnMap.get(columnOneIndexed) ?? 0) + data); } }