Skip to content

Commit c049d05

Browse files
Connor ClarkDevtools-frontend LUCI CQ
authored andcommitted
Prepare Scripts handler for elided data source map urls
The `ScriptCatchup` v8 events have a sourceMapUrl field, which today can contain data urls. These can be very large, especially for developer builds. This can fill up the buffer quickly. I'll be changing this trace event to elide sourceMapUrl if it is a data url. To prepare for that, I created a test that will mimic the expected trace format. Instead of the map coming from a trace event, the Performance panel will grab it from the Script model and place it in the metadata. This also may be the source of some trace recordings missing data. For example, this page[1] frequently drops trace events, perhaps because a single trace event is too large. This page[2] has a slightly smaller source map data url, and has no issue with recording. [1] https://dupe-modules-lh-inline-data.surge.sh/ [2] https://dupe-modules-lh-inline-data.surge.sh/smaller Bug: 409086863 Change-Id: If4d70d541d0e193543249aae8d3f4424868a5024 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6521671 Reviewed-by: Paul Irish <paulirish@chromium.org> Commit-Queue: Connor Clark <cjamcl@chromium.org>
1 parent 4cc0688 commit c049d05

File tree

7 files changed

+138
-41
lines changed

7 files changed

+138
-41
lines changed

front_end/models/trace/handlers/ScriptsHandler.ts

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ export interface Script {
2727
url?: string;
2828
sourceUrl?: string;
2929
content?: string;
30-
/** Note: this is the literal text given as the sourceMappingURL value. It has not been resolved relative to the script url. */
30+
/** Note: this is the literal text given as the sourceMappingURL value. It has not been resolved relative to the script url.
31+
* Since M138, data urls are never set here.
32+
*/
3133
sourceMapUrl?: string;
34+
/** If true, the source map url was a data URL, so it got removed from the trace event. */
35+
sourceMapUrlElided?: boolean;
3236
sourceMap?: SDK.SourceMap.SourceMap;
3337
request?: Types.Events.SyntheticNetworkRequest;
3438
/** Lazily generated - use getScriptGeneratedSizes to access. */
@@ -67,13 +71,21 @@ export function handleEvent(event: Types.Events.Event): void {
6771
}
6872

6973
if (Types.Events.isV8SourceRundownEvent(event)) {
70-
const {isolate, scriptId, url, sourceUrl, sourceMapUrl} = event.args.data;
74+
const {isolate, scriptId, url, sourceUrl, sourceMapUrl, sourceMapUrlElided} = event.args.data;
7175
const script = getOrMakeScript(isolate, scriptId);
7276
script.url = url;
7377
if (sourceUrl) {
7478
script.sourceUrl = sourceUrl;
7579
}
76-
if (sourceMapUrl) {
80+
81+
// Older traces may have data source map urls. Those can be very large, so a change
82+
// was made to elide them from the trace.
83+
// If elided, a fresh trace will fetch the source map from the Script model
84+
// (see TimelinePanel getExistingSourceMap). If not fresh, the source map is resolved
85+
// instead in this handler via `findCachedRawSourceMap`.
86+
if (sourceMapUrlElided) {
87+
script.sourceMapUrlElided = true;
88+
} else if (sourceMapUrl) {
7789
script.sourceMapUrl = sourceMapUrl;
7890
}
7991
return;
@@ -199,17 +211,37 @@ export function getScriptGeneratedSizes(script: Script): GeneratedFileSizes|null
199211
return script.sizes ?? null;
200212
}
201213

202-
function findCachedRawSourceMap(
203-
sourceMapUrl: string, options: Types.Configuration.ParseOptions): SDK.SourceMap.SourceMapV3|undefined {
204-
if (!sourceMapUrl) {
214+
function findCachedRawSourceMap(script: Script, options: Types.Configuration.ParseOptions): SDK.SourceMap.SourceMapV3|
215+
undefined {
216+
if (options.isFreshRecording || !options.metadata?.sourceMaps) {
217+
// Exit if this is not a loaded trace w/ source maps in the metadata.
218+
return;
219+
}
220+
221+
// For elided data url source maps, search the metadata source maps by script url.
222+
if (script.sourceMapUrlElided) {
223+
if (!script.url) {
224+
return;
225+
}
226+
227+
const cachedSourceMap = options.metadata.sourceMaps.find(m => m.url === script.url);
228+
if (cachedSourceMap) {
229+
return cachedSourceMap.sourceMap;
230+
}
231+
205232
return;
206233
}
207234

208-
// If loading from disk, check the metadata for source maps.
209-
// The metadata doesn't store data url source maps.
210-
const isDataUrl = sourceMapUrl.startsWith('data:');
211-
if (!options.isFreshRecording && options.metadata?.sourceMaps && !isDataUrl) {
212-
const cachedSourceMap = options.metadata.sourceMaps.find(m => m.sourceMapUrl === sourceMapUrl);
235+
if (!script.sourceMapUrl) {
236+
return;
237+
}
238+
239+
// Otherwise, search by source map url.
240+
// Note: early enhanced traces may have this field set for data urls. Ignore those,
241+
// as they were never stored in metadata sourcemap.
242+
const isDataUrl = script.sourceMapUrl.startsWith('data:');
243+
if (!isDataUrl) {
244+
const cachedSourceMap = options.metadata.sourceMaps.find(m => m.sourceMapUrl === script.sourceMapUrl);
213245
if (cachedSourceMap) {
214246
return cachedSourceMap.sourceMap;
215247
}
@@ -243,7 +275,7 @@ export async function finalize(options: Types.Configuration.ParseOptions): Promi
243275
// No frame or url means the script came from somewhere we don't care about.
244276
// Note: scripts from inline <SCRIPT> elements use the url of the HTML document,
245277
// so aren't ignored.
246-
if (!script.frame || !script.url || !script.sourceMapUrl) {
278+
if (!script.frame || !script.url || (!script.sourceMapUrl && !script.sourceMapUrlElided)) {
247279
continue;
248280
}
249281

@@ -259,22 +291,26 @@ export async function finalize(options: Types.Configuration.ParseOptions): Promi
259291
sourceUrl = Common.ParsedURL.ParsedURL.completeURL(frameUrl, script.sourceUrl) ?? script.sourceUrl;
260292
}
261293

262-
// Resolve the source map url. The value given by v8 may be relative, so resolve it here.
263-
// This process should match the one in `SourceMapManager.attachSourceMap`.
264-
const sourceMapUrl =
265-
Common.ParsedURL.ParsedURL.completeURL(sourceUrl as Platform.DevToolsPath.UrlString, script.sourceMapUrl);
266-
if (!sourceMapUrl) {
267-
continue;
268-
}
294+
let sourceMapUrl;
295+
if (script.sourceMapUrl) {
296+
// Resolve the source map url. The value given by v8 may be relative, so resolve it here.
297+
// This process should match the one in `SourceMapManager.attachSourceMap`.
298+
sourceMapUrl =
299+
Common.ParsedURL.ParsedURL.completeURL(sourceUrl as Platform.DevToolsPath.UrlString, script.sourceMapUrl);
300+
if (!sourceMapUrl) {
301+
continue;
302+
}
269303

270-
script.sourceMapUrl = sourceMapUrl;
304+
script.sourceMapUrl = sourceMapUrl;
305+
}
271306

272307
const params: Types.Configuration.ResolveSourceMapParams = {
273308
scriptId: script.scriptId,
274-
scriptUrl: sourceUrl as Platform.DevToolsPath.UrlString,
275-
sourceMapUrl: sourceMapUrl as Platform.DevToolsPath.UrlString,
309+
scriptUrl: script.url as Platform.DevToolsPath.UrlString,
310+
sourceUrl: sourceUrl as Platform.DevToolsPath.UrlString,
311+
sourceMapUrl: sourceMapUrl ?? '' as Platform.DevToolsPath.UrlString,
276312
frame: script.frame as Protocol.Page.FrameId,
277-
cachedRawSourceMap: findCachedRawSourceMap(sourceMapUrl, options),
313+
cachedRawSourceMap: findCachedRawSourceMap(script, options),
278314
};
279315
const promise = options.resolveSourceMap(params).then(sourceMap => {
280316
if (sourceMap) {

front_end/models/trace/insights/DuplicatedJavaScript.test.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
66
import {getFirstOrError, getInsightOrError, processTrace} from '../../../testing/InsightHelpers.js';
7-
import type * as Trace from '../trace.js';
7+
import {fetchFixture, TraceLoader} from '../../../testing/TraceLoader.js';
8+
import * as Trace from '../trace.js';
89

910
describeWithEnvironment('DuplicatedJavaScript', function() {
1011
it('works (external source maps)', async () => {
@@ -47,12 +48,8 @@ describeWithEnvironment('DuplicatedJavaScript', function() {
4748
assert.deepEqual(insight.metricSavings, {FCP: 100, LCP: 100} as Trace.Insights.Types.MetricSavings);
4849
});
4950

50-
it('works (inline source maps)', async () => {
51-
const {data, insights} = await processTrace(this, 'dupe-js-inline-maps.json.gz');
52-
assert.strictEqual(insights.size, 1);
53-
const insight = getInsightOrError(
54-
'DuplicatedJavaScript', insights, getFirstOrError(data.Meta.navigationsByNavigationId.values()));
55-
51+
function assertInlineMapsTestCase(
52+
insight: Trace.Insights.Models.DuplicatedJavaScript.DuplicatedJavaScriptInsightModel): void {
5653
const duplication = insight.duplicationGroupedByNodeModules;
5754
const results = Object.fromEntries(
5855
[...duplication.entries()].filter(v => v[1].estimatedDuplicateBytes > 1000 * 25).map(([key, data]) => {
@@ -85,5 +82,48 @@ describeWithEnvironment('DuplicatedJavaScript', function() {
8582
});
8683

8784
assert.deepEqual(insight.metricSavings, {FCP: 50, LCP: 50} as Trace.Insights.Types.MetricSavings);
85+
}
86+
87+
it('works (inline source maps in trace events)', async function() {
88+
const {data, insights} = await processTrace(this, 'dupe-js-inline-maps.json.gz');
89+
assert.strictEqual(insights.size, 1);
90+
const insight = getInsightOrError(
91+
'DuplicatedJavaScript', insights, getFirstOrError(data.Meta.navigationsByNavigationId.values()));
92+
93+
assertInlineMapsTestCase(insight);
94+
});
95+
96+
it('works (inline source maps in metadata)', async function() {
97+
// Load this trace in a way that mutating it is safe.
98+
const traceText = await fetchFixture(
99+
new URL('../../../panels/timeline/fixtures/traces/dupe-js-inline-maps.json.gz', import.meta.url));
100+
const fileContents = JSON.parse(traceText) as Trace.Types.File.TraceFile;
101+
102+
// Remove the source map data urls from the trace, and move to metadata.
103+
// This reflects how Chromium will elide data source map urls.
104+
// The original trace here was recorded at a time where sourceMapUrl could be a
105+
// large data url.
106+
for (const event of fileContents.traceEvents) {
107+
if (Trace.Types.Events.isV8SourceRundownEvent(event)) {
108+
const {sourceMapUrl, url} = event.args.data;
109+
if (sourceMapUrl?.startsWith('data:') && url) {
110+
const sourceMap = await (await fetch(sourceMapUrl)).json();
111+
fileContents.metadata.sourceMaps?.push({url, sourceMap});
112+
event.args.data.sourceMapUrl = undefined;
113+
event.args.data.sourceMapUrlElided = true;
114+
}
115+
}
116+
}
117+
118+
const parsedTraceData = await TraceLoader.executeTraceEngineOnFileContents(fileContents);
119+
const {parsedTrace: data, insights} = parsedTraceData;
120+
if (!insights) {
121+
throw new Error('invalid insights');
122+
}
123+
124+
assert.strictEqual(insights.size, 1);
125+
const insight = getInsightOrError(
126+
'DuplicatedJavaScript', insights, getFirstOrError(data.Meta.navigationsByNavigationId.values()));
127+
assertInlineMapsTestCase(insight);
88128
});
89129
});

front_end/models/trace/types/Configuration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ export interface ParseOptions {
8080
export interface ResolveSourceMapParams {
8181
scriptId: string;
8282
scriptUrl: Platform.DevToolsPath.UrlString;
83+
/** The url as resolved by any sourceUrl comment. */
84+
sourceUrl: Platform.DevToolsPath.UrlString;
8385
sourceMapUrl: Platform.DevToolsPath.UrlString;
8486
frame: Protocol.Page.FrameId;
8587
/** Set only if the raw source map was found on the provided metadata. Never set for source maps from data urls. */

front_end/models/trace/types/File.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,8 @@ export interface MetaData {
198198

199199
interface MetadataSourceMap {
200200
url: string;
201-
sourceMapUrl: string;
201+
/** If not defined, then this was a data url. */
202+
sourceMapUrl?: string;
202203
sourceMap: SDK.SourceMap.SourceMapV3;
203204
}
204205

front_end/models/trace/types/TraceEvents.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3167,6 +3167,7 @@ export interface V8SourceRundownEvent extends Event {
31673167
url?: string,
31683168
sourceUrl?: string,
31693169
sourceMapUrl?: string,
3170+
sourceMapUrlElided?: boolean,
31703171
},
31713172
};
31723173
}

front_end/panels/timeline/TimelinePanel.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2422,6 +2422,18 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
24222422
async #retainSourceMapsForEnhancedTrace(
24232423
parsedTrace: Trace.Handlers.Types.ParsedTrace, metadata: Trace.Types.File.MetaData): Promise<void> {
24242424
const handleScript = async(script: Trace.Handlers.ModelHandlers.Scripts.Script): Promise<void> => {
2425+
if (script.sourceMapUrlElided) {
2426+
if (metadata.sourceMaps?.find(m => m.url === script.url)) {
2427+
return;
2428+
}
2429+
2430+
const rawSourceMap = script.sourceMap?.json();
2431+
if (rawSourceMap && script.url) {
2432+
metadata.sourceMaps?.push({url: script.url, sourceMap: rawSourceMap});
2433+
}
2434+
return;
2435+
}
2436+
24252437
if (!script.sourceMapUrl || script.sourceMapUrl.startsWith('data:')) {
24262438
return;
24272439
}
@@ -2435,7 +2447,7 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
24352447
let rawSourceMap = script.sourceMap?.json();
24362448

24372449
// If the raw map is not present for some reason, fetch it again.
2438-
if (!rawSourceMap) {
2450+
if (!rawSourceMap && !script.sourceMapUrlElided) {
24392451
const initiator = {
24402452
target: null,
24412453
frameId: script.frame as Protocol.Page.FrameId,
@@ -2491,10 +2503,11 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
24912503
}
24922504

24932505
return async function resolveSourceMap(params: Trace.Types.Configuration.ResolveSourceMapParams) {
2494-
const {scriptId, scriptUrl, sourceMapUrl, frame, cachedRawSourceMap} = params;
2506+
const {scriptId, scriptUrl, sourceUrl, sourceMapUrl, frame, cachedRawSourceMap} = params;
24952507

24962508
if (cachedRawSourceMap) {
2497-
return new SDK.SourceMap.SourceMap(scriptUrl, sourceMapUrl, cachedRawSourceMap);
2509+
return new SDK.SourceMap.SourceMap(
2510+
sourceUrl, sourceMapUrl ?? '' as Platform.DevToolsPath.UrlString, cachedRawSourceMap);
24982511
}
24992512

25002513
// For still-active frames, the source map is likely already fetched or at least in-flight.
@@ -2505,13 +2518,17 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
25052518
}
25062519
}
25072520

2521+
if (!sourceMapUrl) {
2522+
return null;
2523+
}
2524+
25082525
// If loading from disk, check the metadata for source maps.
25092526
// The metadata doesn't store data url source maps.
25102527
const isDataUrl = sourceMapUrl.startsWith('data:');
25112528
if (!isFreshRecording && metadata?.sourceMaps && !isDataUrl) {
25122529
const cachedSourceMap = metadata.sourceMaps.find(m => m.sourceMapUrl === sourceMapUrl);
25132530
if (cachedSourceMap) {
2514-
return new SDK.SourceMap.SourceMap(scriptUrl, sourceMapUrl, cachedSourceMap.sourceMap);
2531+
return new SDK.SourceMap.SourceMap(sourceUrl, sourceMapUrl, cachedSourceMap.sourceMap);
25152532
}
25162533
}
25172534

@@ -2521,7 +2538,7 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
25212538
return null;
25222539
}
25232540

2524-
if (!scriptUrl) {
2541+
if (!sourceUrl) {
25252542
return null;
25262543
}
25272544

@@ -2534,9 +2551,9 @@ export class TimelinePanel extends UI.Panel.Panel implements Client, TimelineMod
25342551
// non-final navigations during the trace will never have their source maps
25352552
// fetched by the debugger model. That's only ever done here.
25362553

2537-
const initiator = {target: null, frameId: frame, initiatorUrl: scriptUrl};
2554+
const initiator = {target: null, frameId: frame, initiatorUrl: sourceUrl};
25382555
const payload = await SDK.SourceMapManager.tryLoadSourceMap(sourceMapUrl, initiator);
2539-
return payload ? new SDK.SourceMap.SourceMap(scriptUrl, sourceMapUrl, payload) : null;
2556+
return payload ? new SDK.SourceMap.SourceMap(sourceUrl, sourceMapUrl, payload) : null;
25402557
};
25412558
}
25422559

front_end/testing/TraceLoader.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -245,15 +245,15 @@ export class TraceLoader {
245245
metadata,
246246
isFreshRecording: emulateFreshRecording,
247247
async resolveSourceMap(params) {
248-
const {scriptUrl, sourceMapUrl, cachedRawSourceMap} = params;
248+
const {sourceUrl, sourceMapUrl, cachedRawSourceMap} = params;
249249

250250
if (cachedRawSourceMap) {
251-
return new SDK.SourceMap.SourceMap(scriptUrl, sourceMapUrl, cachedRawSourceMap);
251+
return new SDK.SourceMap.SourceMap(sourceUrl, sourceMapUrl, cachedRawSourceMap);
252252
}
253253

254254
if (sourceMapUrl.startsWith('data:')) {
255255
const rawSourceMap = await (await fetch(sourceMapUrl)).json();
256-
return new SDK.SourceMap.SourceMap(scriptUrl, sourceMapUrl, rawSourceMap);
256+
return new SDK.SourceMap.SourceMap(sourceUrl, sourceMapUrl, rawSourceMap);
257257
}
258258

259259
return null;

0 commit comments

Comments
 (0)