chrome-devtools-frontend
Version:
Chrome DevTools UI
271 lines (229 loc) • 10.7 kB
text/typescript
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Bindings from '../bindings/bindings.js';
import * as Formatter from '../formatter/formatter.js';
import * as TextUtils from '../text_utils/text_utils.js';
import * as Workspace from '../workspace/workspace.js';
/** Represents the source code for a given function, including additional context of surrounding lines. */
export interface FunctionCode {
functionBounds: Workspace.UISourceCode.UIFunctionBounds;
/** The text of `uiSourceCode`. */
text: TextUtils.Text.Text;
/** The function text. */
code: string;
/** The range of `code` within `text`. */
range: TextUtils.TextRange.TextRange;
/** The function text, plus some additional context before and after. The actual function is wrapped in <FUNCTION_START>...<FUNCTION_END> */
codeWithContext: string;
/** The range of `codeWithContext` within `text`. */
rangeWithContext: TextUtils.TextRange.TextRange;
}
export interface CreateFunctionCodeOptions {
/** Number of characters to include before and after the function. Stacks with `contextLineLength`. */
contextLength?: number;
/** Number of lines to include before and after the function. Stacks with `contextLength`. */
contextLineLength?: number;
/** If true, appends profile data from the trace at the end of every line of the function in `codeWithContext`. This should match what is seen in the formatted view in the Sources panel. */
appendProfileData?: boolean;
}
interface InputData {
text: TextUtils.Text.Text;
formattedContent: Formatter.ScriptFormatter.FormattedContent|null;
performanceData: Workspace.UISourceCode.LineColumnProfileMap|undefined;
}
const inputCache = new WeakMap<Workspace.UISourceCode.UISourceCode, Promise<InputData>>();
async function prepareInput(uiSourceCode: Workspace.UISourceCode.UISourceCode, content: string): Promise<InputData> {
const formattedContent = await format(uiSourceCode, content);
const text = new TextUtils.Text.Text(formattedContent ? formattedContent.formattedContent : content);
let performanceData = uiSourceCode.getDecorationData(Workspace.UISourceCode.DecoratorType.PERFORMANCE) as
Workspace.UISourceCode.LineColumnProfileMap |
undefined;
// Map profile data to the formatted view of the text.
if (formattedContent && performanceData) {
performanceData = Workspace.UISourceCode.createMappedProfileData(performanceData, (line, column) => {
return formattedContent.formattedMapping.originalToFormatted(line, column);
});
}
return {text, formattedContent, performanceData};
}
/** Formatting and parsing line endings for Text is expensive, so cache it. */
async function prepareInputAndCache(
uiSourceCode: Workspace.UISourceCode.UISourceCode, content: string): Promise<InputData> {
let cachedPromise = inputCache.get(uiSourceCode);
if (cachedPromise) {
return await cachedPromise;
}
cachedPromise = prepareInput(uiSourceCode, content);
inputCache.set(uiSourceCode, cachedPromise);
return await cachedPromise;
}
function extractPerformanceDataByLine(
textRange: TextUtils.TextRange.TextRange, performanceData: Workspace.UISourceCode.LineColumnProfileMap): number[] {
const {startLine, startColumn, endLine, endColumn} = textRange;
const byLine = new Array(endLine - startLine + 1).fill(0);
for (let line = startLine; line <= endLine; line++) {
const lineData = performanceData.get(line + 1);
if (!lineData) {
continue;
}
// Fast-path for when the entire line's data is relevant.
if (line !== startLine && line !== endLine) {
byLine[line - startLine] = lineData.values().reduce((acc, cur) => acc + cur);
continue;
}
const column0 = line === startLine ? startColumn + 1 : 0;
const column1 = line === endLine ? endColumn + 1 : Number.POSITIVE_INFINITY;
let totalData = 0;
for (const [column, data] of lineData) {
if (column >= column0 && column <= column1) {
totalData += data;
}
}
byLine[line - startLine] = totalData;
}
return byLine.map(data => Math.round(data * 10) / 10);
}
function createFunctionCode(
inputData: InputData, functionBounds: Workspace.UISourceCode.UIFunctionBounds,
options?: CreateFunctionCodeOptions): FunctionCode {
let {startLine, startColumn, endLine, endColumn} = functionBounds.range;
if (inputData.formattedContent) {
const startMapped = inputData.formattedContent.formattedMapping.originalToFormatted(startLine, startColumn);
startLine = startMapped[0];
startColumn = startMapped[1];
const endMapped = inputData.formattedContent.formattedMapping.originalToFormatted(endLine, endColumn);
endLine = endMapped[0];
endColumn = endMapped[1];
}
const text = inputData.text;
const content = text.value();
// Define two ranges - the first is just the function bounds, the second includes
// that plus some surrounding context as dictated by the options.
const range = new TextUtils.TextRange.TextRange(startLine, startColumn, endLine, endColumn);
const functionStartOffset = text.offsetFromPosition(startLine, startColumn);
const functionEndOffset = text.offsetFromPosition(endLine, endColumn);
let contextStartOffset = 0;
if (options?.contextLength !== undefined) {
const contextLength = options.contextLength;
contextStartOffset = Math.max(contextStartOffset, functionStartOffset - contextLength);
}
if (options?.contextLineLength !== undefined) {
const contextLineLength = options.contextLineLength;
const position = text.offsetFromPosition(Math.max(startLine - contextLineLength, 0), 0);
contextStartOffset = Math.max(contextStartOffset, position);
}
let contextEndOffset = content.length;
if (options?.contextLength !== undefined) {
const contextLength = options.contextLength;
contextEndOffset = Math.min(contextEndOffset, functionEndOffset + contextLength);
}
if (options?.contextLineLength !== undefined) {
const contextLineLength = options.contextLineLength;
const position =
text.offsetFromPosition(Math.min(endLine + contextLineLength, text.lineCount() - 1), Number.POSITIVE_INFINITY);
contextEndOffset = Math.min(contextEndOffset, position);
}
const contextStart = text.positionFromOffset(contextStartOffset);
const contextEnd = text.positionFromOffset(contextEndOffset);
const rangeWithContext = new TextUtils.TextRange.TextRange(
contextStart.lineNumber, contextStart.columnNumber, contextEnd.lineNumber, contextEnd.columnNumber);
// Grab substrings for the function range, and for the context range.
const code = content.substring(functionStartOffset, functionEndOffset);
const before = content.substring(contextStartOffset, functionStartOffset);
const after = content.substring(functionEndOffset, contextEndOffset);
let codeWithContext;
if (options?.appendProfileData && inputData.performanceData) {
const performanceDataByLine = extractPerformanceDataByLine(range, inputData.performanceData);
const lines = performanceDataByLine.map((data, i) => {
let line = text.lineAt(startLine + i);
const isLastLine = i === performanceDataByLine.length - 1;
if (i === 0) {
if (isLastLine) {
line = line.substring(startColumn, endColumn);
} else {
line = line.substring(startColumn);
}
} else if (isLastLine) {
line = line.substring(0, endColumn);
}
if (isLastLine) {
// Don't ever annotate the last line - it could make the rest of the code on
// that line get commented out.
data = 0;
}
return data ? `${line} // ${data} ms` : line;
});
const annotatedCode = lines.join('\n');
codeWithContext = before + `<FUNCTION_START>${annotatedCode}<FUNCTION_END>` + after;
} else {
codeWithContext = before + `<FUNCTION_START>${code}<FUNCTION_END>` + after;
}
return {
functionBounds,
text,
code,
range,
codeWithContext,
rangeWithContext,
};
}
/**
* The input location may be a source mapped location or a raw location.
*/
export async function getFunctionCodeFromLocation(
target: SDK.Target.Target, url: Platform.DevToolsPath.UrlString, line: number, column: number,
options?: CreateFunctionCodeOptions): Promise<FunctionCode|null> {
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
if (!debuggerModel) {
throw new Error('missing debugger model');
}
let uiSourceCode;
const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance();
const projects = debuggerWorkspaceBinding.workspace.projectsForType(Workspace.Workspace.projectTypes.Network);
for (const project of projects) {
uiSourceCode = project.uiSourceCodeForURL(url);
if (uiSourceCode) {
break;
}
}
if (!uiSourceCode) {
return null;
}
const rawLocations = await debuggerWorkspaceBinding.uiLocationToRawLocations(uiSourceCode, line, column);
const rawLocation = rawLocations.at(-1);
if (!rawLocation) {
return null;
}
return await getFunctionCodeFromRawLocation(rawLocation, options);
}
async function format(uiSourceCode: Workspace.UISourceCode.UISourceCode, content: string):
Promise<Formatter.ScriptFormatter.FormattedContent|null> {
const contentType = uiSourceCode.contentType();
const shouldFormat = !contentType.isFromSourceMap() && (contentType.isDocument() || contentType.isScript()) &&
TextUtils.TextUtils.isMinified(content);
if (!shouldFormat) {
return null;
}
return await Formatter.ScriptFormatter.formatScriptContent(contentType.canonicalMimeType(), content, '\t');
}
/**
* Returns a {@link FunctionCode} for the given raw location.
*/
export async function getFunctionCodeFromRawLocation(
rawLocation: SDK.DebuggerModel.Location, options?: CreateFunctionCodeOptions): Promise<FunctionCode|null> {
const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance();
const functionBounds = await debuggerWorkspaceBinding.functionBoundsAtRawLocation(rawLocation);
if (!functionBounds) {
return null;
}
await functionBounds.uiSourceCode.requestContentData();
const content = functionBounds.uiSourceCode.content();
if (!content) {
return null;
}
const inputData = await prepareInputAndCache(functionBounds.uiSourceCode, content);
return createFunctionCode(inputData, functionBounds, options);
}