Skip to content

Commit 6ca4ad9

Browse files
Connor ClarkDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Save source maps to trace metadata for enhanced traces
Also modify source map fetching in RPP to 1) never do it for non-fresh traces and 2) ensure that the raw source map is always available for purposes of saving to trace metadata (required a hack in SourceMap to not unconditionally drop the raw payload). Bug: 394373632 Change-Id: Icff6d0da8d66111e19ba9fd966c6ab5ab014262e Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6287830 Reviewed-by: Paul Irish <[email protected]> Commit-Queue: Connor Clark <[email protected]> Auto-Submit: Connor Clark <[email protected]>
1 parent 317b92f commit 6ca4ad9

File tree

6 files changed

+144
-23
lines changed

6 files changed

+144
-23
lines changed

front_end/core/sdk/SourceMap.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ interface SourceInfo {
148148
}
149149

150150
export class SourceMap {
151+
static retainRawSourceMaps = false;
152+
151153
#json: SourceMapV3|null;
152154
readonly #compiledURLInternal: Platform.DevToolsPath.UrlString;
153155
readonly #sourceMappingURL: Platform.DevToolsPath.UrlString;
@@ -181,6 +183,10 @@ export class SourceMap {
181183
this.eachSection(this.parseSources.bind(this));
182184
}
183185

186+
json(): SourceMapV3|null {
187+
return this.#json;
188+
}
189+
184190
augmentWithScopes(scriptUrl: Platform.DevToolsPath.UrlString, ranges: NamedFunctionRange[]): void {
185191
this.#ensureMappingsProcessed();
186192
if (this.#json && this.#json.version > 3) {
@@ -411,6 +417,9 @@ export class SourceMap {
411417
this.mappings().sort(SourceMapEntry.compare);
412418

413419
this.#computeReverseMappings(this.#mappingsInternal);
420+
}
421+
422+
if (!SourceMap.retainRawSourceMaps) {
414423
this.#json = null;
415424
}
416425
}

front_end/core/sdk/SourceMapManager.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,17 @@ export async function loadSourceMap(
182182
}
183183
}
184184

185+
export async function tryLoadSourceMap(
186+
url: Platform.DevToolsPath.UrlString, initiator: PageResourceLoadInitiator): Promise<SourceMapV3|null> {
187+
try {
188+
const {content} = await PageResourceLoader.instance().loadResource(url, initiator);
189+
return parseSourceMap(content);
190+
} catch (cause) {
191+
console.error(`Could not load content for ${url}: ${cause.message}`, {cause});
192+
return null;
193+
}
194+
}
195+
185196
interface ClientData {
186197
relativeSourceURL: Platform.DevToolsPath.UrlString;
187198
// Stores the raw sourceMappingURL as provided by V8. These are not guaranteed to

front_end/models/trace/handlers/ScriptsHandler.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ export async function finalize(options: Types.Configuration.ParseOptions): Promi
124124
continue;
125125
}
126126

127+
script.sourceMapUrl = sourceMapUrl;
128+
127129
const params: Types.Configuration.ResolveSourceMapParams = {
128130
scriptId: script.scriptId,
129131
scriptUrl: sourceUrl as Platform.DevToolsPath.UrlString,

front_end/models/trace/types/File.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,14 @@ export interface MetaData {
192192
enhancedTraceVersion?: number;
193193
modifications?: Modifications;
194194
cruxFieldData?: CrUXManager.PageResult[];
195+
/** Currently only stores JS maps, not CSS. This never stores data url source maps. */
196+
sourceMaps?: MetadataSourceMap[];
197+
}
198+
199+
interface MetadataSourceMap {
200+
url: string;
201+
sourceMapUrl: string;
202+
sourceMap: SDK.SourceMap.SourceMapV3;
195203
}
196204

197205
export type Contents = TraceFile|Event[];

front_end/panels/timeline/TimelineController.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,22 @@ export class TimelineController implements Trace.TracingManager.TracingManagerCl
178178

179179
this.client.loadingStarted();
180180

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-
]);
181+
// Give `TimelinePanel.#executeNewTrace` a chance to retain source maps from SDK.SourceMap.SourceMapManager.
182+
SDK.SourceMap.SourceMap.retainRawSourceMaps = true;
183+
184+
const [fieldData] =
185+
await Promise
186+
.all([
187+
this.fetchFieldData(),
188+
// TODO(crbug.com/366072294): Report the progress of this resumption, as it can be lengthy on heavy pages.
189+
SDK.TargetManager.TargetManager.instance().resumeAllTargets(),
190+
this.waitForTracingToStop(),
191+
])
192+
.catch(e => {
193+
// Normally set false in allSourcesFinished, but just in case something fails, catch it here.
194+
SDK.SourceMap.SourceMap.retainRawSourceMaps = false;
195+
throw e;
196+
});
187197
this.#fieldData = fieldData;
188198

189199
// Now we re-enable throttling again to maintain the setting being persistent.
@@ -271,6 +281,7 @@ export class TimelineController implements Trace.TracingManager.TracingManagerCl
271281
const metadata = await this.createMetadata();
272282
await this.client.loadingComplete(this.#collectedEvents, /* exclusiveFilter= */ null, metadata);
273283
this.client.loadingCompleteForTest();
284+
SDK.SourceMap.SourceMap.retainRawSourceMaps = false;
274285
}
275286

276287
tracingBufferUsage(usage: number): void {

front_end/panels/timeline/TimelinePanel.ts

Lines changed: 97 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,7 +1400,10 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
14001400
traceAsString = cpuprofileJsonGenerator(profile);
14011401
}
14021402
} else {
1403-
const formattedTraceIter = traceJsonGenerator(traceEvents, metadata);
1403+
const formattedTraceIter = traceJsonGenerator(traceEvents, {
1404+
...metadata,
1405+
sourceMaps: isEnhancedTraces ? metadata?.sourceMaps : undefined,
1406+
});
14041407
traceAsString = Array.from(formattedTraceIter).join('');
14051408
}
14061409
if (!traceAsString) {
@@ -2416,7 +2419,59 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
24162419
});
24172420
}
24182421

2419-
#createSourceMapResolver(isFreshRecording: boolean): Trace.TraceModel.ParseConfig['resolveSourceMap'] {
2422+
/**
2423+
* Store source maps on trace metadata (but just the non-data url ones).
2424+
*
2425+
* Many raw source maps are already in memory, but there are some cases where they may
2426+
* not be and have to be fetched here:
2427+
*
2428+
* 1. If the trace processor (via `#createSourceMapResolver`) never fetched it,
2429+
* due to `ScriptHandler` skipping the script if it could not find an associated frame.
2430+
* 2. If the initial fetch failed (perhaps the failure was intermittent and a
2431+
* subsequent attempt will work).
2432+
*/
2433+
async #retainSourceMapsForEnhancedTrace(
2434+
parsedTrace: Trace.Handlers.Types.ParsedTrace, metadata: Trace.Types.File.MetaData): Promise<void> {
2435+
const handleScript = async(script: Trace.Handlers.ModelHandlers.Scripts.Script): Promise<void> => {
2436+
if (!script.sourceMapUrl || script.sourceMapUrl.startsWith('data:')) {
2437+
return;
2438+
}
2439+
2440+
if (metadata.sourceMaps?.find(m => m.sourceMapUrl === script.sourceMapUrl)) {
2441+
return;
2442+
}
2443+
2444+
// TimelineController sets `SDK.SourceMap.SourceMap.retainRawSourceMaps` to true,
2445+
// which means the raw source map is present (assuming `script.sourceMap` is too).
2446+
let rawSourceMap = script.sourceMap?.json();
2447+
2448+
// If the raw map is not present for some reason, fetch it again.
2449+
if (!rawSourceMap) {
2450+
const initiator = {
2451+
target: null,
2452+
frameId: script.frame as Protocol.Page.FrameId,
2453+
initiatorUrl: script.url as Platform.DevToolsPath.UrlString
2454+
};
2455+
rawSourceMap = await SDK.SourceMapManager.tryLoadSourceMap(
2456+
script.sourceMapUrl as Platform.DevToolsPath.UrlString, initiator);
2457+
}
2458+
2459+
if (script.url && rawSourceMap) {
2460+
metadata.sourceMaps?.push({url: script.url, sourceMapUrl: script.sourceMapUrl, sourceMap: rawSourceMap});
2461+
}
2462+
};
2463+
2464+
metadata.sourceMaps = [];
2465+
2466+
const promises = [];
2467+
for (const script of parsedTrace?.Scripts.scripts.values() ?? []) {
2468+
promises.push(handleScript(script));
2469+
}
2470+
await Promise.all(promises);
2471+
}
2472+
2473+
#createSourceMapResolver(isFreshRecording: boolean, metadata: Trace.Types.File.MetaData|null):
2474+
Trace.TraceModel.ParseConfig['resolveSourceMap'] {
24202475
// Currently, only experimental insights need source maps.
24212476
if (!Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.TIMELINE_EXPERIMENTAL_INSIGHTS)) {
24222477
return;
@@ -2462,38 +2517,63 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
24622517
}
24632518
}
24642519

2465-
// Else... fetch it!
2466-
if (!scriptUrl) {
2467-
return null;
2520+
// If loading from disk, check the metadata for source maps.
2521+
// The metadata doesn't store data url source maps.
2522+
const isDataUrl = sourceMapUrl.startsWith('data:');
2523+
if (!isFreshRecording && metadata?.sourceMaps && !isDataUrl) {
2524+
const cachedSourceMap = metadata.sourceMaps.find(m => m.sourceMapUrl === sourceMapUrl);
2525+
if (cachedSourceMap) {
2526+
return new SDK.SourceMap.SourceMap(scriptUrl, sourceMapUrl, cachedSourceMap.sourceMap);
2527+
}
24682528
}
24692529

2470-
// In all other cases, we must fetch the source map.
2471-
// For example, since the debugger model is disable during recording, any non-final navigations during
2472-
// the trace will never have their source maps fetched by the debugger model. That's only ever done here.
2530+
// Never fetch source maps if the trace is not fresh - the source maps may not
2531+
// reflect what was actually loaded by the page for this trace on disk.
2532+
if (!isFreshRecording && !isDataUrl) {
2533+
return null;
2534+
}
24732535

2474-
try {
2475-
const initiator = {target: null, frameId: frame, initiatorUrl: scriptUrl};
2476-
const payload = await SDK.SourceMapManager.loadSourceMap(sourceMapUrl, initiator);
2477-
return new SDK.SourceMap.SourceMap(scriptUrl, sourceMapUrl, payload);
2478-
} catch (cause) {
2479-
console.error(`Could not load content for ${sourceMapUrl}: ${cause.message}`, {cause});
2536+
if (!scriptUrl) {
2537+
return null;
24802538
}
24812539

2482-
return null;
2540+
// In all other cases, fetch the source map.
2541+
//
2542+
// 1) data urls
2543+
// 2) fresh recording + source map not for active frame
2544+
//
2545+
// For example, since the debugger model is disable during recording, any
2546+
// non-final navigations during the trace will never have their source maps
2547+
// fetched by the debugger model. That's only ever done here.
2548+
2549+
const initiator = {target: null, frameId: frame, initiatorUrl: scriptUrl};
2550+
const payload = await SDK.SourceMapManager.tryLoadSourceMap(sourceMapUrl, initiator);
2551+
return payload ? new SDK.SourceMap.SourceMap(scriptUrl, sourceMapUrl, payload) : null;
24832552
};
24842553
}
24852554

24862555
async #executeNewTrace(
24872556
collectedEvents: Trace.Types.Events.Event[], isFreshRecording: boolean,
24882557
metadata: Trace.Types.File.MetaData|null): Promise<void> {
2489-
return await this.#traceEngineModel.parse(
2558+
await this.#traceEngineModel.parse(
24902559
collectedEvents,
24912560
{
24922561
metadata: metadata ?? undefined,
24932562
isFreshRecording,
2494-
resolveSourceMap: this.#createSourceMapResolver(isFreshRecording),
2563+
resolveSourceMap: this.#createSourceMapResolver(isFreshRecording, metadata),
24952564
},
24962565
);
2566+
2567+
// Store all source maps on the trace metadata.
2568+
// If not fresh, we can't validate the maps are still accurate.
2569+
if (isFreshRecording && metadata &&
2570+
Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.TIMELINE_ENHANCED_TRACES)) {
2571+
const traceIndex = this.#traceEngineModel.lastTraceIndex();
2572+
const parsedTrace = this.#traceEngineModel.parsedTrace(traceIndex);
2573+
if (parsedTrace) {
2574+
await this.#retainSourceMapsForEnhancedTrace(parsedTrace, metadata);
2575+
}
2576+
}
24972577
}
24982578

24992579
loadingCompleteForTest(): void {

0 commit comments

Comments
 (0)