Skip to content

Commit 7f2052f

Browse files
Connor ClarkDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Resolve source maps when running trace processor
For source maps that happen to already exist due to being associated with the current primary target, no additional work is done. Otherwise, for no-longer present frames or for non-fresh traces, the required maps are fetched in parallel. For now, only runs if experimental insights are enabled. To give the debugger model ample time to have loaded the source maps by the time the trace processor will request them, `TimelineController.stopRecording` now resumes suspended targets as early as possible. Bug: 394373632 Change-Id: Idce8f7ec7a019243228bd1ae46fb2af2610d60e9 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6266475 Reviewed-by: Paul Irish <[email protected]> Commit-Queue: Connor Clark <[email protected]>
1 parent cef3f7f commit 7f2052f

File tree

9 files changed

+146
-22
lines changed

9 files changed

+146
-22
lines changed

front_end/core/sdk/SourceMapManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export class SourceMapManager<T extends FrameAssociated> extends Common.ObjectWr
174174
}
175175
}
176176

177-
async function loadSourceMap(
177+
export async function loadSourceMap(
178178
url: Platform.DevToolsPath.UrlString, initiator: PageResourceLoadInitiator): Promise<SourceMapV3> {
179179
try {
180180
const {content} = await PageResourceLoader.instance().loadResource(url, initiator);

front_end/models/trace/ModelImpl.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import * as Types from './types/types.js';
1717
export interface ParseConfig {
1818
metadata?: Types.File.MetaData;
1919
isFreshRecording?: boolean;
20+
resolveSourceMap?: Types.Configuration.ParseOptions['resolveSourceMap'];
2021
}
2122

2223
/**
@@ -117,6 +118,7 @@ export class Model extends EventTarget {
117118
isFreshRecording,
118119
isCPUProfile,
119120
metadata,
121+
resolveSourceMap: config?.resolveSourceMap,
120122
});
121123
this.#storeParsedFileData(file, this.#processor.parsedTrace, this.#processor.insights);
122124
// We only push the file onto this.#traces here once we know it's valid

front_end/models/trace/handlers/ScriptsHandler.test.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,20 @@ import * as Trace from '../trace.js';
99

1010
describe('ScriptsHandler', () => {
1111
beforeEach(async function() {
12+
Trace.Handlers.ModelHandlers.Meta.reset();
1213
Trace.Handlers.ModelHandlers.Scripts.reset();
1314
const events = await TraceLoader.rawEvents(this, 'enhanced-traces.json.gz');
1415
for (const event of events) {
16+
Trace.Handlers.ModelHandlers.Meta.handleEvent(event);
1517
Trace.Handlers.ModelHandlers.Scripts.handleEvent(event);
1618
}
19+
await Trace.Handlers.ModelHandlers.Meta.finalize();
1720
await Trace.Handlers.ModelHandlers.Scripts.finalize({
18-
async resolveSourceMap(url: string): Promise<SDK.SourceMap.SourceMap> {
19-
// Don't need to actually make a source map.
20-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
21-
return {test: url} as any;
22-
}
21+
async resolveSourceMap(params: Trace.Types.Configuration.ResolveSourceMapParams):
22+
Promise<SDK.SourceMap.SourceMap> {
23+
// Don't need to actually make a source map.
24+
return {test: params.sourceMapUrl} as unknown as SDK.SourceMap.SourceMap;
25+
}
2326
});
2427
});
2528

front_end/models/trace/handlers/ScriptsHandler.ts

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import * as Common from '../../../core/common/common.js';
56
import * as Platform from '../../../core/platform/platform.js';
67
// eslint-disable-next-line rulesdir/no-imports-in-directory
78
import type * as SDK from '../../../core/sdk/sdk.js';
89
import type * as Protocol from '../../../generated/protocol.js';
910
import * as Types from '../types/types.js';
1011

12+
import {data as metaHandlerData, type MetaHandlerData} from './MetaHandler.js';
13+
1114
export interface ScriptsData {
1215
/** Note: this is only populated when the "Enhanced Traces" feature is enabled. */
1316
scripts: Map<Protocol.Runtime.ScriptId, Script>;
@@ -18,7 +21,9 @@ export interface Script {
1821
frame: string;
1922
ts: Types.Timing.Micro;
2023
url?: string;
24+
sourceUrl?: string;
2125
content?: string;
26+
/** Note: this is the literal text given as the sourceMappingURL value. It has not been resolved relative to the script url. */
2227
sourceMapUrl?: string;
2328
sourceMap?: SDK.SourceMap.SourceMap;
2429
}
@@ -30,8 +35,10 @@ export function reset(): void {
3035
}
3136

3237
export function handleEvent(event: Types.Events.Event): void {
33-
const getOrMakeScript = (scriptId: Protocol.Runtime.ScriptId): Script =>
34-
Platform.MapUtilities.getWithDefault(scriptById, scriptId, () => ({scriptId, frame: '', ts: 0} as Script));
38+
const getOrMakeScript = (scriptIdAsNumber: number): Script => {
39+
const scriptId = String(scriptIdAsNumber) as Protocol.Runtime.ScriptId;
40+
return Platform.MapUtilities.getWithDefault(scriptById, scriptId, () => ({scriptId, frame: '', ts: 0} as Script));
41+
};
3542

3643
if (Types.Events.isTargetRundownEvent(event) && event.args.data) {
3744
const {scriptId, frame} = event.args.data;
@@ -43,9 +50,12 @@ export function handleEvent(event: Types.Events.Event): void {
4350
}
4451

4552
if (Types.Events.isV8SourceRundownEvent(event)) {
46-
const {scriptId, url, sourceMapUrl} = event.args.data;
53+
const {scriptId, url, sourceUrl, sourceMapUrl} = event.args.data;
4754
const script = getOrMakeScript(scriptId);
4855
script.url = url;
56+
if (sourceUrl) {
57+
script.sourceUrl = sourceUrl;
58+
}
4959
if (sourceMapUrl) {
5060
script.sourceMapUrl = sourceMapUrl;
5161
}
@@ -67,18 +77,65 @@ export function handleEvent(event: Types.Events.Event): void {
6777
}
6878
}
6979

80+
function findFrame(meta: MetaHandlerData, frameId: string): Types.Events.TraceFrame|null {
81+
for (const frames of meta.frameByProcessId?.values()) {
82+
const frame = frames.get(frameId);
83+
if (frame) {
84+
return frame;
85+
}
86+
}
87+
88+
return null;
89+
}
90+
7091
export async function finalize(options: Types.Configuration.ParseOptions): Promise<void> {
7192
if (!options.resolveSourceMap) {
7293
return;
7394
}
7495

96+
const meta = metaHandlerData();
97+
7598
const promises = [];
7699
for (const script of scriptById.values()) {
77-
if (script.sourceMapUrl) {
78-
promises.push(options.resolveSourceMap(script.sourceMapUrl).then(sourceMap => {
79-
script.sourceMap = sourceMap;
80-
}));
100+
// No frame or url means the script came from somewhere we don't care about.
101+
// Note: scripts from inline <SCRIPT> elements use the url of the HTML document,
102+
// so aren't ignored.
103+
if (!script.frame || !script.url || !script.sourceMapUrl) {
104+
continue;
105+
}
106+
107+
const frameUrl = findFrame(meta, script.frame)?.url as Platform.DevToolsPath.UrlString | undefined;
108+
if (!frameUrl) {
109+
continue;
110+
}
111+
112+
// If there is a `sourceURL` magic comment, resolve the compiledUrl against the frame url.
113+
// example: `// #sourceURL=foo.js` for target frame https://www.example.com/home -> https://www.example.com/home/foo.js
114+
let sourceUrl = script.url;
115+
if (script.sourceUrl) {
116+
sourceUrl = Common.ParsedURL.ParsedURL.completeURL(frameUrl, script.sourceUrl) ?? script.sourceUrl;
81117
}
118+
119+
// Resolve the source map url. The value given by v8 may be relative, so resolve it here.
120+
// This process should match the one in `SourceMapManager.attachSourceMap`.
121+
const sourceMapUrl =
122+
Common.ParsedURL.ParsedURL.completeURL(sourceUrl as Platform.DevToolsPath.UrlString, script.sourceMapUrl);
123+
if (!sourceMapUrl) {
124+
continue;
125+
}
126+
127+
const params: Types.Configuration.ResolveSourceMapParams = {
128+
scriptId: script.scriptId,
129+
scriptUrl: sourceUrl as Platform.DevToolsPath.UrlString,
130+
sourceMapUrl: sourceMapUrl as Platform.DevToolsPath.UrlString,
131+
frame: script.frame as Protocol.Page.FrameId,
132+
};
133+
const promise = options.resolveSourceMap(params).then(sourceMap => {
134+
if (sourceMap) {
135+
script.sourceMap = sourceMap;
136+
}
137+
});
138+
promises.push(promise);
82139
}
83140
await Promise.all(promises);
84141
}

front_end/models/trace/types/Configuration.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import type * as Platform from '../../../core/platform/platform.js';
56
import type * as SDK from '../../../core/sdk/sdk.js';
7+
import type * as Protocol from '../../../generated/protocol.js';
68

79
import type * as File from './File.js';
810

@@ -66,5 +68,12 @@ export interface ParseOptions {
6668
*/
6769
isCPUProfile?: boolean;
6870
metadata?: File.MetaData;
69-
resolveSourceMap?: (url: string) => Promise<SDK.SourceMap.SourceMap>;
71+
resolveSourceMap?: (params: ResolveSourceMapParams) => Promise<SDK.SourceMap.SourceMap|null>;
72+
}
73+
74+
export interface ResolveSourceMapParams {
75+
scriptId: string;
76+
scriptUrl: Platform.DevToolsPath.UrlString;
77+
sourceMapUrl: Platform.DevToolsPath.UrlString;
78+
frame: Protocol.Page.FrameId;
7079
}

front_end/models/trace/types/TraceEvents.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3083,7 +3083,7 @@ export interface TargetRundownEvent extends Event {
30833083
isolate: string,
30843084
v8context: string,
30853085
origin: string,
3086-
scriptId: Protocol.Runtime.ScriptId,
3086+
scriptId: number,
30873087
isDefault?: boolean,
30883088
contextType?: string,
30893089
},
@@ -3101,7 +3101,7 @@ export interface V8SourceRundownEvent extends Event {
31013101
data: {
31023102
isolate: string,
31033103
executionContextId: Protocol.Runtime.ExecutionContextId,
3104-
scriptId: Protocol.Runtime.ScriptId,
3104+
scriptId: number,
31053105
startLine: number,
31063106
startColumn: number,
31073107
endLine: number,
@@ -3126,7 +3126,7 @@ export interface V8SourceRundownSourcesScriptCatchupEvent extends Event {
31263126
args: Args&{
31273127
data: {
31283128
isolate: string,
3129-
scriptId: Protocol.Runtime.ScriptId,
3129+
scriptId: number,
31303130
length: number,
31313131
sourceText: string,
31323132
},
@@ -3144,7 +3144,7 @@ export interface V8SourceRundownSourcesLargeScriptCatchupEvent extends Event {
31443144
args: Args&{
31453145
data: {
31463146
isolate: string,
3147-
scriptId: Protocol.Runtime.ScriptId,
3147+
scriptId: number,
31483148
splitIndex: number,
31493149
splitCount: number,
31503150
sourceText: string,

front_end/panels/timeline/TimelineController.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,14 @@ export class TimelineController implements Trace.TracingManager.TracingManagerCl
177177
throttlingManager.setCPUThrottlingOption(SDK.CPUThrottlingManager.NoThrottlingOption);
178178

179179
this.client.loadingStarted();
180-
this.#fieldData = await this.fetchFieldData();
181-
await this.waitForTracingToStop();
180+
181+
const [fieldData] = await Promise.all([
182+
this.fetchFieldData(),
183+
// TODO(crbug.com/366072294): Report the progress of this resumption, as it can be lengthy on heavy pages.
184+
SDK.TargetManager.TargetManager.instance().resumeAllTargets(),
185+
this.waitForTracingToStop(),
186+
]);
187+
this.#fieldData = fieldData;
182188

183189
// Now we re-enable throttling again to maintain the setting being persistent.
184190
throttlingManager.setCPUThrottlingOption(optionDuringRecording);
@@ -259,8 +265,6 @@ export class TimelineController implements Trace.TracingManager.TracingManagerCl
259265
}
260266

261267
private async allSourcesFinished(): Promise<void> {
262-
// TODO(crbug.com/366072294): Report the progress of this resumption, as it can be lengthy on heavy pages.
263-
await SDK.TargetManager.TargetManager.instance().resumeAllTargets();
264268
Extensions.ExtensionServer.ExtensionServer.instance().profilingStopped();
265269

266270
this.client.processingStarted();

front_end/panels/timeline/TimelinePanel.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2418,6 +2418,54 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
24182418
});
24192419
}
24202420

2421+
#createSourceMapResolver(isFreshRecording: boolean): Trace.TraceModel.ParseConfig['resolveSourceMap'] {
2422+
// Currently, only experimental insights need source maps.
2423+
if (!Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.TIMELINE_EXPERIMENTAL_INSIGHTS)) {
2424+
return;
2425+
}
2426+
2427+
const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget();
2428+
const debuggerModel = target?.model(SDK.DebuggerModel.DebuggerModel);
2429+
const resourceModel = target?.model(SDK.ResourceTreeModel.ResourceTreeModel);
2430+
const activeFrameIds = (resourceModel?.frames() ?? []).map(frame => frame.id);
2431+
2432+
return async function resolveSourceMap(params: Trace.Types.Configuration.ResolveSourceMapParams) {
2433+
const {scriptId, scriptUrl, sourceMapUrl, frame} = params;
2434+
2435+
// For the still-active frame, the source map is likely already fetched.
2436+
// TODO(cjamcl): hook into in-flight requests for source maps (await on active SourceMapManager resolution, if present).
2437+
if (isFreshRecording && activeFrameIds.includes(frame)) {
2438+
const map = debuggerModel?.scriptForId(scriptId)?.sourceMap();
2439+
if (map && (!scriptUrl || map.compiledURL() === scriptUrl)) {
2440+
return map;
2441+
}
2442+
2443+
// Even for the still-active frame, it could be that the source map has not been fetched yet.
2444+
// There is no mechanism for waiting for an in-flight source map, so instead we fallback to
2445+
// fetching it again here.
2446+
}
2447+
2448+
// Else... fetch it!
2449+
if (!scriptUrl) {
2450+
return null;
2451+
}
2452+
2453+
// In all other cases, we must fetch the source map.
2454+
// For example, since the debugger model is disable during recording, any non-final navigations during
2455+
// the trace will never have their source maps fetched by the debugger model. That's only ever done here.
2456+
2457+
try {
2458+
const initiator = {target: null, frameId: frame, initiatorUrl: scriptUrl};
2459+
const payload = await SDK.SourceMapManager.loadSourceMap(sourceMapUrl, initiator);
2460+
return new SDK.SourceMap.SourceMap(scriptUrl, sourceMapUrl, payload);
2461+
} catch (cause) {
2462+
console.error(`Could not load content for ${sourceMapUrl}: ${cause.message}`, {cause});
2463+
}
2464+
2465+
return null;
2466+
};
2467+
}
2468+
24212469
async #executeNewTrace(
24222470
collectedEvents: Trace.Types.Events.Event[], isFreshRecording: boolean,
24232471
metadata: Trace.Types.File.MetaData|null): Promise<void> {
@@ -2426,6 +2474,7 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
24262474
{
24272475
metadata: metadata ?? undefined,
24282476
isFreshRecording,
2477+
resolveSourceMap: this.#createSourceMapResolver(isFreshRecording),
24292478
},
24302479
);
24312480
}
Binary file not shown.

0 commit comments

Comments
 (0)