Skip to content

Commit 512f3c2

Browse files
Connor ClarkDevtools-frontend LUCI CQ
authored andcommitted
[AI] Annotate code given to Performance agent with profile data
Example: ``` function <FUNCTION_START>fibonacci(num) { // 67 ms if (num <= 1) return 1; // 23 ms return fibonacci(num - 1) + fibonacci(num - 2); // 1999 ms } // 13 ms <FUNCTION_END>const btn = document.querySele ``` Bug: 462212096 Change-Id: Ide5c368b5dae60a2f177c68dd4536b8cb27a7794 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7205508 Reviewed-by: Paul Irish <[email protected]> Auto-Submit: Connor Clark <[email protected]> Commit-Queue: Connor Clark <[email protected]>
1 parent 81e6a23 commit 512f3c2

File tree

8 files changed

+236
-83
lines changed

8 files changed

+236
-83
lines changed

front_end/models/ai_assistance/agents/PerformanceAgent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -936,7 +936,7 @@ export class PerformanceAgent extends AiAgent<AgentFocus> {
936936

937937
const url = args.scriptUrl as Platform.DevToolsPath.UrlString;
938938
const code = await SourceMapScopes.FunctionCodeResolver.getFunctionCodeFromLocation(
939-
target, url, args.line, args.column, {contextLength: 200, contextLineLength: 5});
939+
target, url, args.line, args.column, {contextLength: 200, contextLineLength: 5, appendProfileData: true});
940940
if (!code) {
941941
return {error: 'Could not find code'};
942942
}

front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,7 @@ The order of headers corresponds to an internal fixed list. If a header is not p
830830
parts.push(`${name} @ ${url}:${startLine}:${startColumn}. With added context, chunk is from ${contextStartLine}:${
831831
contextStartColumn} to ${contextEndLine}:${contextEndColumn}`);
832832
parts.push(
833-
'\nThe following is a markdown block of JavaScript. <FUNCTION_START> and <FUNCTION_END> marks the exact function declaration, and everything outside that is provided for additional context. Do not show the user the function markers or the additional context.\n');
833+
'\nThe following is a markdown block of JavaScript. <FUNCTION_START> and <FUNCTION_END> marks the exact function declaration, and everything outside that is provided for additional context. Comments at the end of each line indicate the runtime performance cost of that code. Do not show the user the function markers or the additional context.\n');
834834
parts.push('```');
835835
parts.push(code.codeWithContext);
836836
parts.push('```');

front_end/models/source_map_scopes/FunctionCodeResolver.snapshot.txt

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,67 @@
11
Title: FunctionCodeResolver getFunctionCodeFromLocation [no source maps] lookup named function
22
Content:
33
se strict";
4-
function fibonacci<FUNCTION_START>(e) {
5-
return e <= 1 ? 1 : fibonacci(e - 1) + fibonacci(e - 2)
6-
}
4+
function fibonacci<FUNCTION_START>(e) { // 67 ms
5+
return e <= 1 ? 1 : fibonacci(e - 1) + fibonacci(e - 2) // 2022 ms
6+
} // 13 ms
77
<FUNCTION_END>const btn = document.querySele
88
=== end content
99

1010
Title: FunctionCodeResolver getFunctionCodeFromLocation [no source maps] lookup anonymous function
1111
Content:
1212
btn.addEventListener("click", <FUNCTION_START>() => {
13-
console.log(fibonacci(Number(params.get("x")))),
14-
btn.style.backgroundColor = "red"
13+
console.log(fibonacci(Number(params.get("x")))), // 5000 ms
14+
btn.style.backgroundColor = "red" // 333 ms
1515
}
1616
<FUNCTION_END>);
1717
const input = document.quer
1818
=== end content
1919

2020
Title: FunctionCodeResolver getFunctionCodeFromLocation [source maps] lookup named function with generated location
2121
Content:
22-
function <FUNCTION_START>fibonacci(num) {
23-
if (num <= 1) return 1;
22+
function <FUNCTION_START>fibonacci(num) { // 67 ms
23+
if (num <= 1) return 1; // 23 ms
2424

25-
return fibonacci(num - 1) + fibonacci(num - 2);
26-
}
25+
return fibonacci(num - 1) + fibonacci(num - 2); // 1999 ms
26+
} // 13 ms
2727

2828
<FUNCTION_END>const btn = document.querySele
2929
=== end content
3030

3131
Title: FunctionCodeResolver getFunctionCodeFromLocation [source maps] lookup named function with original location
3232
Content:
33-
function <FUNCTION_START>fibonacci(num) {
34-
if (num <= 1) return 1;
33+
function <FUNCTION_START>fibonacci(num) { // 67 ms
34+
if (num <= 1) return 1; // 23 ms
3535

36-
return fibonacci(num - 1) + fibonacci(num - 2);
37-
}
36+
return fibonacci(num - 1) + fibonacci(num - 2); // 1999 ms
37+
} // 13 ms
3838

3939
<FUNCTION_END>const btn = document.querySele
4040
=== end content
4141

4242
Title: FunctionCodeResolver getFunctionCodeFromLocation [source maps, no source contents] lookup named function with generated location
4343
Content:
4444
se strict";
45-
function fibonacci<FUNCTION_START>(e) {
46-
return e <= 1 ? 1 : fibonacci(e - 1) + fibonacci(e - 2)
47-
}
45+
function fibonacci<FUNCTION_START>(e) { // 67 ms
46+
return e <= 1 ? 1 : fibonacci(e - 1) + fibonacci(e - 2) // 2022 ms
47+
} // 13 ms
4848
<FUNCTION_END>const btn = document.querySele
4949
=== end content
5050

5151
Title: FunctionCodeResolver getFunctionCodeFromLocation [source maps, no source contents] lookup named function with original location
5252
Content:
5353
se strict";
54-
function fibonacci<FUNCTION_START>(e) {
55-
return e <= 1 ? 1 : fibonacci(e - 1) + fibonacci(e - 2)
56-
}
54+
function fibonacci<FUNCTION_START>(e) { // 67 ms
55+
return e <= 1 ? 1 : fibonacci(e - 1) + fibonacci(e - 2) // 2022 ms
56+
} // 13 ms
5757
<FUNCTION_END>const btn = document.querySele
5858
=== end content
5959

6060
Title: FunctionCodeResolver getFunctionCodeFromLocation [source maps] lookup anonymous function with generated location
6161
Content:
6262
btn.addEventListener('click', <FUNCTION_START>() => {
63-
console.log(fibonacci(Number(params.get('x'))));
64-
btn.style.backgroundColor = 'red';
63+
console.log(fibonacci(Number(params.get('x')))); // 5000 ms
64+
btn.style.backgroundColor = 'red'; // 333 ms
6565
}<FUNCTION_END>);
6666

6767
const input = document.que
@@ -70,8 +70,8 @@ const input = document.que
7070
Title: FunctionCodeResolver getFunctionCodeFromLocation [source maps] lookup anonymous function with original location
7171
Content:
7272
btn.addEventListener('click', <FUNCTION_START>() => {
73-
console.log(fibonacci(Number(params.get('x'))));
74-
btn.style.backgroundColor = 'red';
73+
console.log(fibonacci(Number(params.get('x')))); // 5000 ms
74+
btn.style.backgroundColor = 'red'; // 333 ms
7575
}<FUNCTION_END>);
7676

7777
const input = document.que
@@ -80,8 +80,8 @@ const input = document.que
8080
Title: FunctionCodeResolver getFunctionCodeFromLocation [source maps, no source contents] lookup anonymous function with generated location
8181
Content:
8282
btn.addEventListener("click", <FUNCTION_START>() => {
83-
console.log(fibonacci(Number(params.get("x")))),
84-
btn.style.backgroundColor = "red"
83+
console.log(fibonacci(Number(params.get("x")))), // 5000 ms
84+
btn.style.backgroundColor = "red" // 333 ms
8585
}
8686
<FUNCTION_END>);
8787
const input = document.quer
@@ -90,8 +90,8 @@ const input = document.quer
9090
Title: FunctionCodeResolver getFunctionCodeFromLocation [source maps, no source contents] lookup anonymous function with original location
9191
Content:
9292
btn.addEventListener("click", <FUNCTION_START>() => {
93-
console.log(fibonacci(Number(params.get("x")))),
94-
btn.style.backgroundColor = "red"
93+
console.log(fibonacci(Number(params.get("x")))), // 5000 ms
94+
btn.style.backgroundColor = "red" // 333 ms
9595
}
9696
<FUNCTION_END>);
9797
const input = document.quer

front_end/models/source_map_scopes/FunctionCodeResolver.test.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ describeWithMockConnection('FunctionCodeResolver', function() {
4141
// This was minified with 'esbuild --sourcemap=linked --minify' v0.25.9.
4242
const exampleSource =
4343
`"use strict";function fibonacci(e){return e<=1?1:fibonacci(e-1)+fibonacci(e-2)}const btn=document.querySelector("button"),params=new URLSearchParams(location.search);btn.addEventListener("click",()=>{console.log(fibonacci(Number(params.get("x")))),btn.style.backgroundColor="red"});const input=document.querySelector('input[type="text"]');input.addEventListener("input",()=>{console.log(fibonacci(Number(params.get("x"))))});\n//# sourceMappingURL=file:///tmp/example.js.min.map`;
44+
const exampleRawPerformanceData = new Map([
45+
[
46+
1, new Map([
47+
[1, 1], // "use strict"
48+
[35, 67], // starting } of fibonacci
49+
[43, 23], // e<=1
50+
[50, 1000], // fibonacci(e-1)
51+
[65, 999], // fibonacci(e-2)
52+
[79, 13], // ending } of fibonacci
53+
[213, 5000], // fibonacci(Number(params.get("x")))
54+
[274, 333], // btn.style.backgroundColor="red"
55+
])
56+
],
57+
]);
4458
const exampleSourceMap = {
4559
version: 3,
4660
sources: ['index.js'],
@@ -165,8 +179,37 @@ describeWithMockConnection('FunctionCodeResolver', function() {
165179
await sourceMap.scopesFallbackPromiseForTest;
166180
}
167181

182+
// Add raw performance data to script's UISourceCode.
183+
const uiSourceCode =
184+
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().uiSourceCodeForScript(script);
185+
assert.isOk(uiSourceCode);
186+
uiSourceCode.setDecorationData(Workspace.UISourceCode.DecoratorType.PERFORMANCE, exampleRawPerformanceData);
187+
188+
// Add mapped performance data to source map url's UISourceCode.
189+
if (sourceMap) {
190+
const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel);
191+
assert.isOk(debuggerModel);
192+
const url = sourceMap.sourceURLForSourceIndex(0);
193+
assert.isOk(url);
194+
const uiSourceCode =
195+
Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().uiSourceCodeForSourceMapSourceURL(
196+
debuggerModel, url, false);
197+
assert.isOk(uiSourceCode);
198+
199+
const mappedPerformanceData =
200+
Workspace.UISourceCode.createMappedProfileData(exampleRawPerformanceData, (line, column) => {
201+
const entry = sourceMap.findEntry(line, column);
202+
if (entry?.sourceURL) {
203+
return [entry.sourceLineNumber, entry.sourceColumnNumber];
204+
}
205+
206+
return null;
207+
});
208+
uiSourceCode.setDecorationData(Workspace.UISourceCode.DecoratorType.PERFORMANCE, mappedPerformanceData);
209+
}
210+
168211
const code = await SourceMapScopes.FunctionCodeResolver.getFunctionCodeFromLocation(
169-
target, testCase.url, testCase.line, testCase.column, {contextLength: 30});
212+
target, testCase.url, testCase.line, testCase.column, {contextLength: 30, appendProfileData: true});
170213
assert.isOk(code);
171214
assert.strictEqual(code.code, testCase.expectedCode);
172215
snapshotTester.assert(this, code.codeWithContext);

front_end/models/source_map_scopes/FunctionCodeResolver.ts

Lines changed: 114 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,27 +29,96 @@ export interface CreateFunctionCodeOptions {
2929
contextLength?: number;
3030
/** Number of lines to include before and after the function. Stacks with `contextLength`. */
3131
contextLineLength?: number;
32+
/** 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. */
33+
appendProfileData?: boolean;
34+
}
35+
36+
interface InputData {
37+
text: TextUtils.Text.Text;
38+
formattedContent: Formatter.ScriptFormatter.FormattedContent|null;
39+
performanceData: Workspace.UISourceCode.LineColumnProfileMap|undefined;
40+
}
41+
42+
const inputCache = new WeakMap<Workspace.UISourceCode.UISourceCode, Promise<InputData>>();
43+
44+
async function prepareInput(uiSourceCode: Workspace.UISourceCode.UISourceCode, content: string): Promise<InputData> {
45+
const formattedContent = await format(uiSourceCode, content);
46+
const text = new TextUtils.Text.Text(formattedContent ? formattedContent.formattedContent : content);
47+
let performanceData = uiSourceCode.getDecorationData(Workspace.UISourceCode.DecoratorType.PERFORMANCE) as
48+
Workspace.UISourceCode.LineColumnProfileMap |
49+
undefined;
50+
51+
// Map profile data to the formatted view of the text.
52+
if (formattedContent && performanceData) {
53+
performanceData = Workspace.UISourceCode.createMappedProfileData(performanceData, (line, column) => {
54+
return formattedContent.formattedMapping.originalToFormatted(line, column);
55+
});
56+
}
57+
58+
return {text, formattedContent, performanceData};
59+
}
60+
61+
/** Formatting and parsing line endings for Text is expensive, so cache it. */
62+
async function prepareInputAndCache(
63+
uiSourceCode: Workspace.UISourceCode.UISourceCode, content: string): Promise<InputData> {
64+
let cachedPromise = inputCache.get(uiSourceCode);
65+
if (cachedPromise) {
66+
return await cachedPromise;
67+
}
68+
69+
cachedPromise = prepareInput(uiSourceCode, content);
70+
inputCache.set(uiSourceCode, cachedPromise);
71+
return await cachedPromise;
72+
}
73+
74+
function extractPerformanceDataByLine(
75+
textRange: TextUtils.TextRange.TextRange, performanceData: Workspace.UISourceCode.LineColumnProfileMap): number[] {
76+
const {startLine, startColumn, endLine, endColumn} = textRange;
77+
const byLine = new Array(endLine - startLine + 1).fill(0);
78+
79+
for (let line = startLine; line <= endLine; line++) {
80+
const lineData = performanceData.get(line + 1);
81+
if (!lineData) {
82+
continue;
83+
}
84+
85+
// Fast-path for when the entire line's data is relevant.
86+
if (line !== startLine && line !== endLine) {
87+
byLine[line - startLine] = lineData.values().reduce((acc, cur) => acc + cur);
88+
continue;
89+
}
90+
91+
const column0 = line === startLine ? startColumn + 1 : 0;
92+
const column1 = line === endLine ? endColumn + 1 : Number.POSITIVE_INFINITY;
93+
94+
let totalData = 0;
95+
for (const [column, data] of lineData) {
96+
if (column >= column0 && column <= column1) {
97+
totalData += data;
98+
}
99+
}
100+
101+
byLine[line - startLine] = totalData;
102+
}
103+
104+
return byLine.map(data => Math.round(data * 10) / 10);
32105
}
33106

34107
function createFunctionCode(
35-
uiSourceCodeContent: string, formattedContent: Formatter.ScriptFormatter.FormattedContent|null,
36-
functionBounds: Workspace.UISourceCode.UIFunctionBounds, options?: CreateFunctionCodeOptions): FunctionCode {
108+
inputData: InputData, functionBounds: Workspace.UISourceCode.UIFunctionBounds,
109+
options?: CreateFunctionCodeOptions): FunctionCode {
37110
let {startLine, startColumn, endLine, endColumn} = functionBounds.range;
38-
let text;
39-
if (formattedContent) {
40-
text = new TextUtils.Text.Text(formattedContent.formattedContent);
41-
42-
const startMapped = formattedContent.formattedMapping.originalToFormatted(startLine, startColumn);
111+
if (inputData.formattedContent) {
112+
const startMapped = inputData.formattedContent.formattedMapping.originalToFormatted(startLine, startColumn);
43113
startLine = startMapped[0];
44114
startColumn = startMapped[1];
45115

46-
const endMapped = formattedContent.formattedMapping.originalToFormatted(endLine, endColumn);
116+
const endMapped = inputData.formattedContent.formattedMapping.originalToFormatted(endLine, endColumn);
47117
endLine = endMapped[0];
48118
endColumn = endMapped[1];
49-
} else {
50-
text = new TextUtils.Text.Text(uiSourceCodeContent);
51119
}
52120

121+
const text = inputData.text;
53122
const content = text.value();
54123

55124
// Define two ranges - the first is just the function bounds, the second includes
@@ -91,7 +160,37 @@ function createFunctionCode(
91160
const code = content.substring(functionStartOffset, functionEndOffset);
92161
const before = content.substring(contextStartOffset, functionStartOffset);
93162
const after = content.substring(functionEndOffset, contextEndOffset);
94-
const codeWithContext = before + `<FUNCTION_START>${code}<FUNCTION_END>` + after;
163+
164+
let codeWithContext;
165+
if (options?.appendProfileData && inputData.performanceData) {
166+
const performanceDataByLine = extractPerformanceDataByLine(range, inputData.performanceData);
167+
const lines = performanceDataByLine.map((data, i) => {
168+
let line = text.lineAt(startLine + i);
169+
170+
const isLastLine = i === performanceDataByLine.length - 1;
171+
if (i === 0) {
172+
if (isLastLine) {
173+
line = line.substring(startColumn, endColumn);
174+
} else {
175+
line = line.substring(startColumn);
176+
}
177+
} else if (isLastLine) {
178+
line = line.substring(0, endColumn);
179+
}
180+
181+
if (isLastLine) {
182+
// Don't ever annotate the last line - it could make the rest of the code on
183+
// that line get commented out.
184+
data = 0;
185+
}
186+
187+
return data ? `${line} // ${data} ms` : line;
188+
});
189+
const annotatedCode = lines.join('\n');
190+
codeWithContext = before + `<FUNCTION_START>${annotatedCode}<FUNCTION_END>` + after;
191+
} else {
192+
codeWithContext = before + `<FUNCTION_START>${code}<FUNCTION_END>` + after;
193+
}
95194

96195
return {
97196
functionBounds,
@@ -137,26 +236,16 @@ export async function getFunctionCodeFromLocation(
137236
return await getFunctionCodeFromRawLocation(rawLocation, options);
138237
}
139238

140-
const formatCache =
141-
new WeakMap<Workspace.UISourceCode.UISourceCode, Promise<Formatter.ScriptFormatter.FormattedContent|null>>();
142-
143-
async function formatAndCache(uiSourceCode: Workspace.UISourceCode.UISourceCode, content: string):
239+
async function format(uiSourceCode: Workspace.UISourceCode.UISourceCode, content: string):
144240
Promise<Formatter.ScriptFormatter.FormattedContent|null> {
145-
let cachedPromise = formatCache.get(uiSourceCode);
146-
if (cachedPromise) {
147-
return await cachedPromise;
148-
}
149-
150241
const contentType = uiSourceCode.contentType();
151242
const shouldFormat = !contentType.isFromSourceMap() && (contentType.isDocument() || contentType.isScript()) &&
152243
TextUtils.TextUtils.isMinified(content);
153244
if (!shouldFormat) {
154245
return null;
155246
}
156247

157-
cachedPromise = Formatter.ScriptFormatter.formatScriptContent(contentType.canonicalMimeType(), content, '\t');
158-
formatCache.set(uiSourceCode, cachedPromise);
159-
return await cachedPromise;
248+
return await Formatter.ScriptFormatter.formatScriptContent(contentType.canonicalMimeType(), content, '\t');
160249
}
161250

162251
/**
@@ -176,6 +265,6 @@ export async function getFunctionCodeFromRawLocation(
176265
return null;
177266
}
178267

179-
const formattedContent = await formatAndCache(functionBounds.uiSourceCode, content);
180-
return createFunctionCode(content, formattedContent, functionBounds, options);
268+
const inputData = await prepareInputAndCache(functionBounds.uiSourceCode, content);
269+
return createFunctionCode(inputData, functionBounds, options);
181270
}

0 commit comments

Comments
 (0)