Skip to content

Commit 75529f1

Browse files
Connor ClarkDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Add LH Treemap button to Duplicated JavaScript insight
This lifts a bunch of code from Lighthouse[1] needed for opening the Lighthouse Treemap webapp[2], a treemap visualization tool for JS bundles. It shows all the scripts found on the page, augmented with source map and any other data we send it (like what large modules are duplicated on the page). A couple refactors were necessary: - The DuplicatedJavaScript model has a few new properties needed to assemble the data for the treemap - computeGeneratedFileSizes moved from ScriptDuplication to ScriptsHandler, so it can be cached - Add `inline` property to Script trace model [1] https://github.com/GoogleChrome/lighthouse/blob/04f43865487b77384d5bfb02f774397ead5e3db2/core/audits/script-treemap-data.js [1] https://github.com/GoogleChrome/lighthouse/blob/04f43865487b77384d5bfb02f774397ead5e3db2/report/renderer/open-tab.js#L102C10-L102C18 [2] https://googlechrome.github.io/lighthouse/treemap/ Bug: 394373632 Change-Id: I1c873e298d8ff0611f325a1752110d7682182f5e Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6398204 Commit-Queue: Connor Clark <[email protected]> Reviewed-by: Paul Irish <[email protected]>
1 parent 92e3000 commit 75529f1

File tree

12 files changed

+722
-129
lines changed

12 files changed

+722
-129
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1983,6 +1983,7 @@ grd_files_debug_sources = [
19831983
"front_end/panels/timeline/utils/ImageCache.js",
19841984
"front_end/panels/timeline/utils/InsightAIContext.js",
19851985
"front_end/panels/timeline/utils/SourceMapsResolver.js",
1986+
"front_end/panels/timeline/utils/Treemap.js",
19861987
"front_end/panels/web_audio/AudioContextContentBuilder.js",
19871988
"front_end/panels/web_audio/AudioContextSelector.js",
19881989
"front_end/panels/web_audio/WebAudioModel.js",

front_end/models/trace/extras/ScriptDuplication.test.ts

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ async function loadScriptFixture(
2929
scriptId: `1.${name}` as Protocol.Runtime.ScriptId,
3030
frame: 'abcdef',
3131
ts: 0 as Trace.Types.Timing.Micro,
32+
inline: false,
3233
content: fixture.content,
3334
sourceMap: new SDK.SourceMap.SourceMap(compiledUrl, mapUrl, fixture.sourceMapJson),
3435
};
@@ -38,8 +39,8 @@ describeWithEnvironment('ScriptDuplication', function() {
3839
describe('computeGeneratedFileSizes', () => {
3940
it('works (simple map)', async function() {
4041
const script = await loadScriptFixture('foo.min');
41-
const results = Trace.Extras.ScriptDuplication.computeGeneratedFileSizes(script);
42-
assert.deepEqual(results, {
42+
const sizes = Trace.Handlers.ModelHandlers.Scripts.getScriptGeneratedSizes(script);
43+
assert.deepEqual(sizes, {
4344
files: {
4445
'node_modules/browser-pack/_prelude.js': 480,
4546
'src/bar.js': 104,
@@ -52,8 +53,8 @@ describeWithEnvironment('ScriptDuplication', function() {
5253

5354
it('works (complex map)', async function() {
5455
const script = await loadScriptFixture('squoosh');
55-
const results = Trace.Extras.ScriptDuplication.computeGeneratedFileSizes(script);
56-
assert.deepEqual(results, {
56+
const sizes = Trace.Handlers.ModelHandlers.Scripts.getScriptGeneratedSizes(script);
57+
assert.deepEqual(sizes, {
5758
files: {
5859
'webpack:///node_modules/comlink/comlink.js': 4117,
5960
'webpack:///node_modules/linkstate/dist/linkstate.es.js': 412,
@@ -140,8 +141,8 @@ describeWithEnvironment('ScriptDuplication', function() {
140141
// @ts-expect-error
141142
map.sources[1] = null;
142143
});
143-
const results = Trace.Extras.ScriptDuplication.computeGeneratedFileSizes(script);
144-
assert.deepEqual(results, {
144+
const sizes = Trace.Handlers.ModelHandlers.Scripts.getScriptGeneratedSizes(script);
145+
assert.deepEqual(sizes, {
145146
files: {
146147
'node_modules/browser-pack/_prelude.js': 480,
147148
null: 104,
@@ -156,8 +157,8 @@ describeWithEnvironment('ScriptDuplication', function() {
156157
const script = await loadScriptFixture('foo.min', fixture => {
157158
fixture.sourceMapJson.mappings = 'blahblah blah';
158159
});
159-
const results = Trace.Extras.ScriptDuplication.computeGeneratedFileSizes(script);
160-
assert.deepEqual(results, {
160+
const sizes = Trace.Handlers.ModelHandlers.Scripts.getScriptGeneratedSizes(script);
161+
assert.deepEqual(sizes, {
161162
files: {},
162163
totalBytes: 718,
163164
unmappedBytes: 718,
@@ -168,8 +169,8 @@ describeWithEnvironment('ScriptDuplication', function() {
168169
const script = await loadScriptFixture('foo.min', fixture => {
169170
fixture.content = 'blahblah blah';
170171
});
171-
const results = Trace.Extras.ScriptDuplication.computeGeneratedFileSizes(script);
172-
assert.deepEqual(results, {
172+
const sizes = Trace.Handlers.ModelHandlers.Scripts.getScriptGeneratedSizes(script);
173+
assert.deepEqual(sizes, {
173174
errorMessage: 'foo.min.js.map mapping for last column out of bounds: 1:14',
174175
});
175176
});
@@ -178,8 +179,8 @@ describeWithEnvironment('ScriptDuplication', function() {
178179
const script = await loadScriptFixture('foo.min', fixture => {
179180
fixture.content = '';
180181
});
181-
const results = Trace.Extras.ScriptDuplication.computeGeneratedFileSizes(script);
182-
assert.deepEqual(results, {
182+
const sizes = Trace.Handlers.ModelHandlers.Scripts.getScriptGeneratedSizes(script);
183+
assert.deepEqual(sizes, {
183184
errorMessage: 'foo.min.js.map mapping for column out of bounds: 1:1',
184185
});
185186
});
@@ -194,8 +195,8 @@ describeWithEnvironment('ScriptDuplication', function() {
194195
'AAAA';
195196
fixture.sourceMapJson.mappings = newMappings.join(',');
196197
});
197-
const results = Trace.Extras.ScriptDuplication.computeGeneratedFileSizes(script);
198-
assert.deepEqual(results, {
198+
const sizes = Trace.Handlers.ModelHandlers.Scripts.getScriptGeneratedSizes(script);
199+
assert.deepEqual(sizes, {
199200
errorMessage: 'foo.min.js.map mapping for last column out of bounds: 1:685',
200201
});
201202
});
@@ -208,8 +209,8 @@ describeWithEnvironment('ScriptDuplication', function() {
208209
// See https://sourcemaps.info/spec.html#:~:text=broken%20down%20as%20follows
209210
fixture.sourceMapJson.mappings = ';'.repeat(10) + fixture.sourceMapJson.mappings;
210211
});
211-
const results = Trace.Extras.ScriptDuplication.computeGeneratedFileSizes(script);
212-
assert.deepEqual(results, {
212+
const sizes = Trace.Handlers.ModelHandlers.Scripts.getScriptGeneratedSizes(script);
213+
assert.deepEqual(sizes, {
213214
errorMessage: 'foo.min.js.map mapping for line out of bounds: 11',
214215
});
215216
});
@@ -218,8 +219,8 @@ describeWithEnvironment('ScriptDuplication', function() {
218219
const script = await loadScriptFixture('foo.min', fixture => {
219220
fixture.sourceMapJson.names = ['blah'];
220221
});
221-
const results = Trace.Extras.ScriptDuplication.computeGeneratedFileSizes(script);
222-
assert.deepEqual(results, {
222+
const sizes = Trace.Handlers.ModelHandlers.Scripts.getScriptGeneratedSizes(script);
223+
assert.deepEqual(sizes, {
223224
files: {
224225
'node_modules/browser-pack/_prelude.js': 480,
225226
'src/bar.js': 104,
@@ -232,12 +233,17 @@ describeWithEnvironment('ScriptDuplication', function() {
232233
});
233234

234235
describe('computeScriptDuplication', () => {
236+
function getDuplication(scriptsData: Trace.Handlers.ModelHandlers.Scripts.ScriptsData):
237+
Trace.Extras.ScriptDuplication.ScriptDuplication {
238+
return Trace.Extras.ScriptDuplication.computeScriptDuplication(scriptsData).duplicationGroupedByNodeModules;
239+
}
240+
235241
it('works (simple, no duplication)', async () => {
236242
const scriptsData: Trace.Handlers.ModelHandlers.Scripts.ScriptsData = {
237243
scripts: [await loadScriptFixture('foo.min')],
238244
};
239245

240-
const results = Object.fromEntries(Trace.Extras.ScriptDuplication.computeScriptDuplication(scriptsData));
246+
const results = Object.fromEntries(getDuplication(scriptsData));
241247
assert.deepEqual(results, {});
242248
});
243249

@@ -246,8 +252,8 @@ describeWithEnvironment('ScriptDuplication', function() {
246252
scripts: [await loadScriptFixture('coursehero-bundle-1'), await loadScriptFixture('coursehero-bundle-2')],
247253
};
248254

249-
const results = Object.fromEntries(
250-
[...Trace.Extras.ScriptDuplication.computeScriptDuplication(scriptsData).entries()].map(([key, data]) => {
255+
const results =
256+
Object.fromEntries([...getDuplication(scriptsData).entries()].map(([key, data]) => {
251257
return [
252258
key, data.duplicates.map(v => ({scriptId: v.script.scriptId as string, resourceSize: v.attributedSize}))
253259
];

front_end/models/trace/extras/ScriptDuplication.ts

Lines changed: 29 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -2,83 +2,13 @@
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 SDK from '../../../core/sdk/sdk.js';
6-
import type * as Handlers from '../handlers/handlers.js';
5+
import * as Handlers from '../handlers/handlers.js';
76

87
// Ignore modules smaller than an absolute threshold.
98
const ABSOLUTE_SIZE_THRESHOLD_BYTES = 1024 * 0.5;
109
// Ignore modules smaller than a % size of largest copy of the module.
1110
const RELATIVE_SIZE_THRESHOLD = 0.1;
1211

13-
type GeneratedFileSizes = {
14-
errorMessage: string,
15-
}|{files: Record<string, number>, unmappedBytes: number, totalBytes: number};
16-
17-
/**
18-
* Using a script's contents and source map, attribute every generated byte to an authored source file.
19-
*/
20-
export function computeGeneratedFileSizes(script: Handlers.ModelHandlers.Scripts.Script): GeneratedFileSizes {
21-
if (!script.sourceMap) {
22-
throw new Error('expected source map');
23-
}
24-
25-
const map = script.sourceMap;
26-
const content = script.content ?? '';
27-
const contentLength = content.length;
28-
const lines = content.split('\n');
29-
const files: Record<string, number> = {};
30-
const totalBytes = contentLength;
31-
let unmappedBytes = totalBytes;
32-
33-
const lastGeneratedColumnMap = computeLastGeneratedColumnMap(script.sourceMap);
34-
35-
for (const mapping of map.mappings()) {
36-
const source = mapping.sourceURL;
37-
const lineNum = mapping.lineNumber;
38-
const colNum = mapping.columnNumber;
39-
const lastColNum = lastGeneratedColumnMap.get(mapping);
40-
41-
// Webpack sometimes emits null mappings.
42-
// https://github.com/mozilla/source-map/pull/303
43-
if (!source) {
44-
continue;
45-
}
46-
47-
// Lines and columns are zero-based indices. Visually, lines are shown as a 1-based index.
48-
49-
const line = lines[lineNum];
50-
if (line === null || line === undefined) {
51-
const errorMessage = `${map.url()} mapping for line out of bounds: ${lineNum + 1}`;
52-
return {errorMessage};
53-
}
54-
55-
if (colNum > line.length) {
56-
const errorMessage = `${map.url()} mapping for column out of bounds: ${lineNum + 1}:${colNum}`;
57-
return {errorMessage};
58-
}
59-
60-
let mappingLength = 0;
61-
if (lastColNum !== undefined) {
62-
if (lastColNum > line.length) {
63-
const errorMessage = `${map.url()} mapping for last column out of bounds: ${lineNum + 1}:${lastColNum}`;
64-
return {errorMessage};
65-
}
66-
mappingLength = lastColNum - colNum;
67-
} else {
68-
// Add +1 to account for the newline.
69-
mappingLength = line.length - colNum + 1;
70-
}
71-
files[source] = (files[source] || 0) + mappingLength;
72-
unmappedBytes -= mappingLength;
73-
}
74-
75-
return {
76-
files,
77-
unmappedBytes,
78-
totalBytes,
79-
};
80-
}
81-
8212
interface SourceData {
8313
source: string;
8414
resourceSize: number;
@@ -195,7 +125,7 @@ export function getNodeModuleName(source: string): string {
195125

196126
function groupByNodeModules(duplication: ScriptDuplication): ScriptDuplication {
197127
const groupedDuplication: ScriptDuplication = new Map();
198-
for (const [source, data] of duplication.entries()) {
128+
for (const [source, data] of duplication) {
199129
if (!source.includes('node_modules')) {
200130
groupedDuplication.set(source, data);
201131
continue;
@@ -222,38 +152,32 @@ function groupByNodeModules(duplication: ScriptDuplication): ScriptDuplication {
222152
return groupedDuplication;
223153
}
224154

225-
function computeLastGeneratedColumnMap(map: SDK.SourceMap.SourceMap): Map<SDK.SourceMap.SourceMapEntry, number> {
226-
const result = new Map<SDK.SourceMap.SourceMapEntry, number>();
227-
228-
const mappings = map.mappings();
229-
for (let i = 0; i < mappings.length - 1; i++) {
230-
const mapping = mappings[i];
231-
const nextMapping = mappings[i + 1];
232-
if (mapping.lineNumber === nextMapping.lineNumber) {
233-
result.set(mapping, nextMapping.columnNumber);
234-
}
235-
}
236-
237-
// Now, all but the last mapping on each line will have 'lastColumnNumber' set to a number.
238-
return result;
155+
/**
156+
* Sort by estimated savings.
157+
*/
158+
function sorted(duplication: ScriptDuplication): ScriptDuplication {
159+
return new Map([...duplication].sort((a, b) => b[1].estimatedDuplicateBytes - a[1].estimatedDuplicateBytes));
239160
}
240161

241162
/**
242-
* Returns a @see ScriptDuplication for the given collection of script contents + source maps.
163+
* Returns 2 @see ScriptDuplication for the given collection of script contents + source maps:
164+
*
165+
* 1. `duplication` keys correspond to authored files
166+
* 2. `duplication` keys correspond to authored files, except all files within the same
167+
* node_module package are aggregated under the same entry.
243168
*/
244-
export function computeScriptDuplication(scriptsData: Handlers.ModelHandlers.Scripts.ScriptsData): ScriptDuplication {
245-
const sizesMap = new Map<Handlers.ModelHandlers.Scripts.Script, GeneratedFileSizes>();
246-
for (const script of scriptsData.scripts) {
247-
if (script.content && script.sourceMap) {
248-
sizesMap.set(script, computeGeneratedFileSizes(script));
249-
}
250-
}
251-
169+
export function computeScriptDuplication(scriptsData: Handlers.ModelHandlers.Scripts.ScriptsData):
170+
{duplication: ScriptDuplication, duplicationGroupedByNodeModules: ScriptDuplication} {
252171
const sourceDatasMap = new Map<Handlers.ModelHandlers.Scripts.Script, SourceData[]>();
253172

254173
// Determine size of each `sources` entry.
255-
for (const [script, sizes] of sizesMap) {
256-
if (!script.sourceMap) {
174+
for (const script of scriptsData.scripts) {
175+
if (!script.content || !script.sourceMap) {
176+
continue;
177+
}
178+
179+
const sizes = Handlers.ModelHandlers.Scripts.getScriptGeneratedSizes(script);
180+
if (!sizes) {
257181
continue;
258182
}
259183

@@ -279,7 +203,7 @@ export function computeScriptDuplication(scriptsData: Handlers.ModelHandlers.Scr
279203
}
280204
}
281205

282-
let duplication: ScriptDuplication = new Map();
206+
const duplication: ScriptDuplication = new Map();
283207
for (const [script, sourceDataArray] of sourceDatasMap) {
284208
for (const sourceData of sourceDataArray) {
285209
let data = duplication.get(sourceData.source);
@@ -294,9 +218,13 @@ export function computeScriptDuplication(scriptsData: Handlers.ModelHandlers.Scr
294218
}
295219
}
296220

297-
duplication = groupByNodeModules(duplication);
221+
const duplicationGroupedByNodeModules = groupByNodeModules(duplication);
222+
298223
normalizeDuplication(duplication);
224+
normalizeDuplication(duplicationGroupedByNodeModules);
299225

300-
// Sort by estimated savings.
301-
return new Map([...duplication].sort((a, b) => b[1].estimatedDuplicateBytes - a[1].estimatedDuplicateBytes));
226+
return {
227+
duplication: sorted(duplication),
228+
duplicationGroupedByNodeModules: sorted(duplicationGroupedByNodeModules),
229+
};
302230
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ describe('ScriptsHandler', () => {
4242
frame: '21D58E83A5C17916277166140F6A464B',
4343
request: undefined,
4444
ts: 50442438976,
45+
inline: false,
4546
url: 'http://localhost:8080/index.html',
4647
content: 'source text 1',
4748
sourceMapUrl: 'http://localhost:8080/source.map.json',
@@ -53,6 +54,7 @@ describe('ScriptsHandler', () => {
5354
frame: '21D58E83A5C17916277166140F6A464B',
5455
request: undefined,
5556
ts: 50442438976,
57+
inline: false,
5658
url: 'http://localhost:8080/index.html',
5759
content: 'source text 2'
5860
},
@@ -63,6 +65,7 @@ describe('ScriptsHandler', () => {
6365
content: ' text ',
6466
request: undefined,
6567
ts: 50442438976,
68+
inline: false,
6669
url: 'http://localhost:8080/index.html',
6770
}
6871
]);

0 commit comments

Comments
 (0)