chrome-devtools-frontend
Version:
Chrome DevTools UI
289 lines (258 loc) • 10.8 kB
text/typescript
// 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);
}
}