devtools-tracing 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/cli.ts +29 -5
- package/dist/cli.js +158 -45
- package/dist/index.d.ts +1 -0
- package/dist/index.js +47 -0
- package/dist/src/invalidations.d.ts +6 -0
- package/package.json +1 -1
- package/commands/inp.ts +0 -27
- package/commands/selector-stats.ts +0 -145
- package/commands/sourcemap.ts +0 -114
- package/commands/stats.ts +0 -58
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ npx devtools-tracing inp trace.json.gz
|
|
|
31
31
|
npx devtools-tracing stats trace.json.gz
|
|
32
32
|
|
|
33
33
|
# Source map symbolication (writes a .symbolicated.json.gz alongside the input)
|
|
34
|
-
npx devtools-tracing sourcemap trace.json.gz
|
|
34
|
+
npx devtools-tracing sourcemap trace.json.gz -H "Cookie: $cookie" -H "User-Agent: $ua"
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
See the [`commands/`](./commands) directory for full source.
|
package/cli.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { run as cssSelectors } from './commands/selector-stats.js';
|
|
2
2
|
import { run as inp } from './commands/inp.js';
|
|
3
|
-
import { run as sourcemap } from './commands/sourcemap.js';
|
|
3
|
+
import { run as sourcemap, type SourcemapOptions } from './commands/sourcemap.js';
|
|
4
4
|
import { run as stats } from './commands/stats.js';
|
|
5
5
|
|
|
6
|
-
const commands: Record<string, (tracePath: string) => Promise<void>> = {
|
|
6
|
+
const commands: Record<string, (tracePath: string, ...args: any[]) => Promise<void>> = {
|
|
7
7
|
'selector-stats': cssSelectors,
|
|
8
8
|
inp,
|
|
9
9
|
sourcemap,
|
|
@@ -15,12 +15,30 @@ function printUsage() {
|
|
|
15
15
|
console.log('\nCommands:');
|
|
16
16
|
console.log(' selector-stats Top CSS selectors from selector stats and invalidation tracking');
|
|
17
17
|
console.log(' inp Extract INP (Interaction to Next Paint) breakdown');
|
|
18
|
-
console.log(' sourcemap Symbolicate a trace using source maps');
|
|
18
|
+
console.log(' sourcemap Symbolicate a trace using source maps [-H "Header: value"]');
|
|
19
19
|
console.log(' stats Generate timeline category statistics');
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function parseHeaders(args: string[]): Record<string, string> {
|
|
23
|
+
const headers: Record<string, string> = {};
|
|
24
|
+
for (let i = 0; i < args.length; i++) {
|
|
25
|
+
if (args[i] === '-H' && i + 1 < args.length) {
|
|
26
|
+
const value = args[++i];
|
|
27
|
+
const colonIdx = value.indexOf(':');
|
|
28
|
+
if (colonIdx === -1) {
|
|
29
|
+
console.error(`Invalid header (missing ':'): ${value}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
headers[value.slice(0, colonIdx).trim()] = value.slice(colonIdx + 1).trim();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return headers;
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
async function main() {
|
|
23
|
-
const
|
|
39
|
+
const args = process.argv.slice(2);
|
|
40
|
+
const command = args[0];
|
|
41
|
+
const tracePath = args[1];
|
|
24
42
|
|
|
25
43
|
if (!command || command === '--help' || command === '-h') {
|
|
26
44
|
printUsage();
|
|
@@ -40,7 +58,13 @@ async function main() {
|
|
|
40
58
|
process.exit(1);
|
|
41
59
|
}
|
|
42
60
|
|
|
43
|
-
|
|
61
|
+
if (command === 'sourcemap') {
|
|
62
|
+
const headers = parseHeaders(args.slice(2));
|
|
63
|
+
const options: SourcemapOptions = Object.keys(headers).length > 0 ? { headers } : {};
|
|
64
|
+
await sourcemap(tracePath, options);
|
|
65
|
+
} else {
|
|
66
|
+
await run(tracePath);
|
|
67
|
+
}
|
|
44
68
|
}
|
|
45
69
|
|
|
46
70
|
main();
|
package/dist/cli.js
CHANGED
|
@@ -27,6 +27,7 @@ var fs = __toESM(require("node:fs"));
|
|
|
27
27
|
var zlib = __toESM(require("node:zlib"));
|
|
28
28
|
var import__ = require("../");
|
|
29
29
|
var SelectorTimingsKey = import__.Trace.Types.Events.SelectorTimingsKey;
|
|
30
|
+
var microToMilli = (us) => import__.Trace.Helpers.Timing.microToMilli(import__.Trace.Types.Timing.Micro(us));
|
|
30
31
|
async function run(tracePath) {
|
|
31
32
|
(0, import__.initDevToolsTracing)();
|
|
32
33
|
const fileData = fs.readFileSync(tracePath);
|
|
@@ -66,55 +67,69 @@ async function run(tracePath) {
|
|
|
66
67
|
}
|
|
67
68
|
const allTimings = [...selectorMap.values()];
|
|
68
69
|
const TOP_N = 15;
|
|
70
|
+
const truncateSelector = (s) => s.length > 80 ? `${s.slice(0, 77)}...` : s;
|
|
69
71
|
const byElapsed = allTimings.sort(
|
|
70
72
|
(a, b) => b[SelectorTimingsKey.Elapsed] - a[SelectorTimingsKey.Elapsed]
|
|
71
73
|
).slice(0, TOP_N);
|
|
72
74
|
console.log(`
|
|
73
75
|
=== Top ${TOP_N} CSS Selectors by Elapsed Time ===
|
|
74
76
|
`);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
console.table(
|
|
78
|
+
Object.fromEntries(
|
|
79
|
+
byElapsed.map((t, i) => [
|
|
80
|
+
i + 1,
|
|
81
|
+
{
|
|
82
|
+
selector: truncateSelector(t[SelectorTimingsKey.Selector]),
|
|
83
|
+
"elapsed (ms)": microToMilli(t[SelectorTimingsKey.Elapsed]),
|
|
84
|
+
match_attempts: t[SelectorTimingsKey.MatchAttempts],
|
|
85
|
+
match_count: t[SelectorTimingsKey.MatchCount],
|
|
86
|
+
fast_reject_count: t[SelectorTimingsKey.FastRejectCount]
|
|
87
|
+
}
|
|
88
|
+
])
|
|
89
|
+
)
|
|
90
|
+
);
|
|
81
91
|
const byAttempts = [...selectorMap.values()].sort(
|
|
82
92
|
(a, b) => b[SelectorTimingsKey.MatchAttempts] - a[SelectorTimingsKey.MatchAttempts]
|
|
83
93
|
).slice(0, TOP_N);
|
|
84
94
|
console.log(`
|
|
85
95
|
=== Top ${TOP_N} CSS Selectors by Match Attempts ===
|
|
86
96
|
`);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
console.table(
|
|
98
|
+
Object.fromEntries(
|
|
99
|
+
byAttempts.map((t, i) => [
|
|
100
|
+
i + 1,
|
|
101
|
+
{
|
|
102
|
+
selector: truncateSelector(t[SelectorTimingsKey.Selector]),
|
|
103
|
+
match_attempts: t[SelectorTimingsKey.MatchAttempts],
|
|
104
|
+
"elapsed (ms)": microToMilli(t[SelectorTimingsKey.Elapsed]),
|
|
105
|
+
match_count: t[SelectorTimingsKey.MatchCount],
|
|
106
|
+
fast_reject_count: t[SelectorTimingsKey.FastRejectCount]
|
|
107
|
+
}
|
|
108
|
+
])
|
|
109
|
+
)
|
|
98
110
|
);
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
invalidationSelectorCounts.set(sel.selector, count + 1);
|
|
104
|
-
}
|
|
111
|
+
const invalidationsData = parsedTraceData.Invalidations;
|
|
112
|
+
const allInvalidations = [];
|
|
113
|
+
for (const [, invalidations] of invalidationsData.invalidationsForEvent) {
|
|
114
|
+
allInvalidations.push(...invalidations);
|
|
105
115
|
}
|
|
106
|
-
const
|
|
116
|
+
const { groupedByReason, backendNodeIds } = (0, import__.generateInvalidationsList)(allInvalidations);
|
|
117
|
+
const reasons = Object.entries(groupedByReason).sort((a, b) => b[1].length - a[1].length);
|
|
107
118
|
console.log(
|
|
108
|
-
`
|
|
119
|
+
`
|
|
120
|
+
=== Invalidation Tracking (${allInvalidations.length} invalidations, ${backendNodeIds.size} unique nodes) ===
|
|
109
121
|
`
|
|
110
122
|
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
console.table(
|
|
124
|
+
Object.fromEntries(
|
|
125
|
+
reasons.map(([reason, invalidations], i) => [
|
|
126
|
+
i + 1,
|
|
127
|
+
{
|
|
128
|
+
reason,
|
|
129
|
+
count: invalidations.length
|
|
130
|
+
}
|
|
131
|
+
])
|
|
132
|
+
)
|
|
118
133
|
);
|
|
119
134
|
let totalElapsedUs = 0;
|
|
120
135
|
let totalMatchAttempts = 0;
|
|
@@ -127,10 +142,12 @@ async function run(tracePath) {
|
|
|
127
142
|
console.log(`
|
|
128
143
|
=== Totals ===
|
|
129
144
|
`);
|
|
130
|
-
console.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
145
|
+
console.table({
|
|
146
|
+
"Unique selectors": allTimings.length,
|
|
147
|
+
"Total elapsed (ms)": microToMilli(totalElapsedUs),
|
|
148
|
+
"Total match attempts": totalMatchAttempts,
|
|
149
|
+
"Total match count": totalMatchCount
|
|
150
|
+
});
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
// commands/inp.ts
|
|
@@ -159,8 +176,9 @@ async function run2(tracePath) {
|
|
|
159
176
|
var fs3 = __toESM(require("node:fs"));
|
|
160
177
|
var zlib3 = __toESM(require("node:zlib"));
|
|
161
178
|
var import__3 = require("../");
|
|
162
|
-
async function run3(tracePath) {
|
|
179
|
+
async function run3(tracePath, options) {
|
|
163
180
|
(0, import__3.initDevToolsTracing)();
|
|
181
|
+
const extraHeaders = options?.headers;
|
|
164
182
|
const fileData = fs3.readFileSync(tracePath);
|
|
165
183
|
const decompressedData = tracePath.endsWith(".gz") ? zlib3.gunzipSync(fileData) : fileData;
|
|
166
184
|
const traceData = JSON.parse(
|
|
@@ -168,9 +186,10 @@ async function run3(tracePath) {
|
|
|
168
186
|
);
|
|
169
187
|
const traceModel = import__3.Trace.TraceModel.Model.createWithAllHandlers();
|
|
170
188
|
const resolveSourceMap = (0, import__3.createSourceMapResolver)({
|
|
171
|
-
fetch: verboseFetch
|
|
189
|
+
fetch: (url) => verboseFetch(url, extraHeaders)
|
|
172
190
|
});
|
|
173
|
-
|
|
191
|
+
const preprocessedEvents = fixupElectronTraceEvents(traceData.traceEvents);
|
|
192
|
+
await traceModel.parse(preprocessedEvents, {
|
|
174
193
|
isCPUProfile: false,
|
|
175
194
|
isFreshRecording: false,
|
|
176
195
|
metadata: traceData.metadata,
|
|
@@ -221,7 +240,7 @@ async function run3(tracePath) {
|
|
|
221
240
|
console.log(
|
|
222
241
|
`Symbolicated ${result.symbolicatedFrames} frames across ${result.symbolicatedEvents} events`
|
|
223
242
|
);
|
|
224
|
-
const outPath = tracePath.replace(/(\.(json|json\.gz))$/i, ".symbolicated$1");
|
|
243
|
+
const outPath = tracePath.replace(/(\.(json|trace|json\.gz))$/i, ".symbolicated$1");
|
|
225
244
|
if (outPath === tracePath) {
|
|
226
245
|
console.error(
|
|
227
246
|
"Could not determine output path (expected .json or .json.gz extension)"
|
|
@@ -236,9 +255,80 @@ async function run3(tracePath) {
|
|
|
236
255
|
}
|
|
237
256
|
console.log(`Wrote symbolicated trace to ${outPath}`);
|
|
238
257
|
}
|
|
239
|
-
|
|
258
|
+
var { isTracingStartedInBrowser, isFunctionCall, isRundownScript, isProcessName } = import__3.Trace.Types.Events;
|
|
259
|
+
function fixupElectronTraceEvents(events) {
|
|
260
|
+
const hasFrameData = events.some(
|
|
261
|
+
(e) => isTracingStartedInBrowser(e) && e.args.data?.frames?.length
|
|
262
|
+
);
|
|
263
|
+
if (hasFrameData) {
|
|
264
|
+
return events;
|
|
265
|
+
}
|
|
266
|
+
const rundownCats = /* @__PURE__ */ new Set([
|
|
267
|
+
"disabled-by-default-devtools.v8-source-rundown",
|
|
268
|
+
"disabled-by-default-devtools.v8-source-rundown-sources"
|
|
269
|
+
]);
|
|
270
|
+
const isolateToFrame = /* @__PURE__ */ new Map();
|
|
271
|
+
const frameToPid = /* @__PURE__ */ new Map();
|
|
272
|
+
for (const event of events) {
|
|
273
|
+
if (!isFunctionCall(event)) continue;
|
|
274
|
+
const data = event.args?.data;
|
|
275
|
+
if (!data?.isolate || !data?.frame) continue;
|
|
276
|
+
isolateToFrame.set(String(data.isolate), data.frame);
|
|
277
|
+
frameToPid.set(data.frame, event.pid);
|
|
278
|
+
}
|
|
279
|
+
const frameToUrl = /* @__PURE__ */ new Map();
|
|
280
|
+
for (const event of events) {
|
|
281
|
+
if (!isRundownScript(event)) continue;
|
|
282
|
+
const data = event.args.data;
|
|
283
|
+
const url = data.url ?? "";
|
|
284
|
+
const isolate = String(data.isolate ?? "");
|
|
285
|
+
const frame = isolateToFrame.get(isolate);
|
|
286
|
+
if (!frame || !url || !url.startsWith("http")) continue;
|
|
287
|
+
const existing = frameToUrl.get(frame);
|
|
288
|
+
const isDocument = !url.match(/\.(js|cjs|mjs|wasm)(\?|$)/i);
|
|
289
|
+
const existingIsDocument = existing && !existing.match(/\.(js|cjs|mjs|wasm)(\?|$)/i);
|
|
290
|
+
if (!existing || isDocument && !existingIsDocument) {
|
|
291
|
+
frameToUrl.set(frame, url);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const browserPid = events.find(
|
|
295
|
+
(e) => isProcessName(e) && e.args?.name === "Browser"
|
|
296
|
+
)?.pid;
|
|
297
|
+
const syntheticEvents = [];
|
|
298
|
+
for (const [frame, pid] of frameToPid) {
|
|
299
|
+
const url = frameToUrl.get(frame) ?? "";
|
|
300
|
+
syntheticEvents.push({
|
|
301
|
+
cat: "disabled-by-default-devtools.timeline",
|
|
302
|
+
name: "FrameCommittedInBrowser",
|
|
303
|
+
ph: import__3.Trace.Types.Events.Phase.INSTANT,
|
|
304
|
+
pid: browserPid ?? pid,
|
|
305
|
+
tid: 0,
|
|
306
|
+
ts: 0,
|
|
307
|
+
s: import__3.Trace.Types.Events.Scope.GLOBAL,
|
|
308
|
+
args: {
|
|
309
|
+
data: {
|
|
310
|
+
frame,
|
|
311
|
+
name: "",
|
|
312
|
+
processId: pid,
|
|
313
|
+
url
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
const regular = [];
|
|
319
|
+
const rundown = [];
|
|
320
|
+
for (const event of events) {
|
|
321
|
+
if (rundownCats.has(event.cat)) {
|
|
322
|
+
rundown.push(event);
|
|
323
|
+
} else {
|
|
324
|
+
regular.push(event);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return [...syntheticEvents, ...regular, ...rundown];
|
|
328
|
+
}
|
|
329
|
+
async function verboseFetch(url, headers) {
|
|
240
330
|
console.log(`[sourcemap] fetching ${url}`);
|
|
241
|
-
const response = await fetch(url);
|
|
331
|
+
const response = await fetch(url, headers ? { headers } : void 0);
|
|
242
332
|
if (!response.ok) {
|
|
243
333
|
console.warn(
|
|
244
334
|
`[sourcemap] ${url} -> ${response.status} ${response.statusText}`
|
|
@@ -306,11 +396,28 @@ function printUsage() {
|
|
|
306
396
|
console.log("\nCommands:");
|
|
307
397
|
console.log(" selector-stats Top CSS selectors from selector stats and invalidation tracking");
|
|
308
398
|
console.log(" inp Extract INP (Interaction to Next Paint) breakdown");
|
|
309
|
-
console.log(
|
|
399
|
+
console.log(' sourcemap Symbolicate a trace using source maps [-H "Header: value"]');
|
|
310
400
|
console.log(" stats Generate timeline category statistics");
|
|
311
401
|
}
|
|
402
|
+
function parseHeaders(args) {
|
|
403
|
+
const headers = {};
|
|
404
|
+
for (let i = 0; i < args.length; i++) {
|
|
405
|
+
if (args[i] === "-H" && i + 1 < args.length) {
|
|
406
|
+
const value = args[++i];
|
|
407
|
+
const colonIdx = value.indexOf(":");
|
|
408
|
+
if (colonIdx === -1) {
|
|
409
|
+
console.error(`Invalid header (missing ':'): ${value}`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
}
|
|
412
|
+
headers[value.slice(0, colonIdx).trim()] = value.slice(colonIdx + 1).trim();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return headers;
|
|
416
|
+
}
|
|
312
417
|
async function main() {
|
|
313
|
-
const
|
|
418
|
+
const args = process.argv.slice(2);
|
|
419
|
+
const command = args[0];
|
|
420
|
+
const tracePath = args[1];
|
|
314
421
|
if (!command || command === "--help" || command === "-h") {
|
|
315
422
|
printUsage();
|
|
316
423
|
process.exit(0);
|
|
@@ -326,6 +433,12 @@ async function main() {
|
|
|
326
433
|
console.error(`Usage: devtools-tracing ${command} <trace-file>`);
|
|
327
434
|
process.exit(1);
|
|
328
435
|
}
|
|
329
|
-
|
|
436
|
+
if (command === "sourcemap") {
|
|
437
|
+
const headers = parseHeaders(args.slice(2));
|
|
438
|
+
const options = Object.keys(headers).length > 0 ? { headers } : {};
|
|
439
|
+
await run3(tracePath, options);
|
|
440
|
+
} else {
|
|
441
|
+
await run5(tracePath);
|
|
442
|
+
}
|
|
330
443
|
}
|
|
331
444
|
main();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { initDevToolsTracing } from './src/init.js';
|
|
2
2
|
export { statsForTimeRange, entryIsVisibleInTimeline } from './src/timeline.js';
|
|
3
|
+
export { generateInvalidationsList } from './src/invalidations.js';
|
|
3
4
|
export { createSourceMapResolver, symbolicateTrace } from './src/sourcemap.js';
|
|
4
5
|
export type { SymbolicateOptions, SymbolicateResult } from './src/sourcemap.js';
|
|
5
6
|
import * as Trace from './lib/front_end/models/trace/trace.js';
|
package/dist/index.js
CHANGED
|
@@ -10998,6 +10998,7 @@ __export(index_exports, {
|
|
|
10998
10998
|
Trace: () => trace_exports,
|
|
10999
10999
|
createSourceMapResolver: () => createSourceMapResolver,
|
|
11000
11000
|
entryIsVisibleInTimeline: () => entryIsVisibleInTimeline,
|
|
11001
|
+
generateInvalidationsList: () => generateInvalidationsList,
|
|
11001
11002
|
initDevToolsTracing: () => initDevToolsTracing,
|
|
11002
11003
|
statsForTimeRange: () => statsForTimeRange,
|
|
11003
11004
|
symbolicateTrace: () => symbolicateTrace
|
|
@@ -42414,6 +42415,51 @@ function entryIsVisibleInTimeline(entry, parsedTrace) {
|
|
|
42414
42415
|
return eventStyle && !eventStyle.hidden || eventIsTiming;
|
|
42415
42416
|
}
|
|
42416
42417
|
|
|
42418
|
+
// src/invalidations.ts
|
|
42419
|
+
function generateInvalidationsList(invalidations) {
|
|
42420
|
+
const groupedByReason = {};
|
|
42421
|
+
const backendNodeIds2 = /* @__PURE__ */ new Set();
|
|
42422
|
+
for (const invalidation of invalidations) {
|
|
42423
|
+
backendNodeIds2.add(invalidation.args.data.nodeId);
|
|
42424
|
+
let reason = invalidation.args.data.reason || "unknown";
|
|
42425
|
+
if (reason === "unknown" && types_exports.Events.isScheduleStyleInvalidationTracking(invalidation) && invalidation.args.data.invalidatedSelectorId) {
|
|
42426
|
+
switch (invalidation.args.data.invalidatedSelectorId) {
|
|
42427
|
+
case "attribute":
|
|
42428
|
+
reason = "Attribute";
|
|
42429
|
+
if (invalidation.args.data.changedAttribute) {
|
|
42430
|
+
reason += ` (${invalidation.args.data.changedAttribute})`;
|
|
42431
|
+
}
|
|
42432
|
+
break;
|
|
42433
|
+
case "class":
|
|
42434
|
+
reason = "Class";
|
|
42435
|
+
if (invalidation.args.data.changedClass) {
|
|
42436
|
+
reason += ` (${invalidation.args.data.changedClass})`;
|
|
42437
|
+
}
|
|
42438
|
+
break;
|
|
42439
|
+
case "id":
|
|
42440
|
+
reason = "Id";
|
|
42441
|
+
if (invalidation.args.data.changedId) {
|
|
42442
|
+
reason += ` (${invalidation.args.data.changedId})`;
|
|
42443
|
+
}
|
|
42444
|
+
break;
|
|
42445
|
+
}
|
|
42446
|
+
}
|
|
42447
|
+
if (reason === "PseudoClass" && types_exports.Events.isStyleRecalcInvalidationTracking(invalidation) && invalidation.args.data.extraData) {
|
|
42448
|
+
reason += invalidation.args.data.extraData;
|
|
42449
|
+
}
|
|
42450
|
+
if (reason === "Attribute" && types_exports.Events.isStyleRecalcInvalidationTracking(invalidation) && invalidation.args.data.extraData) {
|
|
42451
|
+
reason += ` (${invalidation.args.data.extraData})`;
|
|
42452
|
+
}
|
|
42453
|
+
if (reason === "StyleInvalidator") {
|
|
42454
|
+
continue;
|
|
42455
|
+
}
|
|
42456
|
+
const existing = groupedByReason[reason] || [];
|
|
42457
|
+
existing.push(invalidation);
|
|
42458
|
+
groupedByReason[reason] = existing;
|
|
42459
|
+
}
|
|
42460
|
+
return { groupedByReason, backendNodeIds: backendNodeIds2 };
|
|
42461
|
+
}
|
|
42462
|
+
|
|
42417
42463
|
// lib/front_end/core/sdk/sdk.ts
|
|
42418
42464
|
var sdk_exports = {};
|
|
42419
42465
|
__export(sdk_exports, {
|
|
@@ -86844,6 +86890,7 @@ function decodeDataUrl(dataUrl) {
|
|
|
86844
86890
|
Trace,
|
|
86845
86891
|
createSourceMapResolver,
|
|
86846
86892
|
entryIsVisibleInTimeline,
|
|
86893
|
+
generateInvalidationsList,
|
|
86847
86894
|
initDevToolsTracing,
|
|
86848
86895
|
statsForTimeRange,
|
|
86849
86896
|
symbolicateTrace
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type * as Protocol from '../lib/front_end/generated/protocol.js';
|
|
2
|
+
import * as Trace from '../lib/front_end/models/trace/trace.js';
|
|
3
|
+
export declare function generateInvalidationsList(invalidations: Trace.Types.Events.InvalidationTrackingEvent[]): {
|
|
4
|
+
groupedByReason: Record<string, Trace.Types.Events.InvalidationTrackingEvent[]>;
|
|
5
|
+
backendNodeIds: Set<Protocol.DOM.BackendNodeId>;
|
|
6
|
+
};
|
package/package.json
CHANGED
package/commands/inp.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as zlib from 'node:zlib';
|
|
3
|
-
|
|
4
|
-
import { initDevToolsTracing, Trace } from '../';
|
|
5
|
-
|
|
6
|
-
export async function run(tracePath: string) {
|
|
7
|
-
initDevToolsTracing();
|
|
8
|
-
|
|
9
|
-
const fileData = fs.readFileSync(tracePath);
|
|
10
|
-
const decompressedData = tracePath.endsWith('.gz')
|
|
11
|
-
? zlib.gunzipSync(fileData)
|
|
12
|
-
: fileData;
|
|
13
|
-
const traceData = JSON.parse(
|
|
14
|
-
decompressedData.toString()
|
|
15
|
-
) as Trace.Types.File.TraceFile;
|
|
16
|
-
|
|
17
|
-
const processor = Trace.Processor.TraceProcessor.createWithAllHandlers();
|
|
18
|
-
await processor.parse(traceData.traceEvents, {});
|
|
19
|
-
const insights = processor.insights!.get('NO_NAVIGATION');
|
|
20
|
-
const longestInteractionEvent =
|
|
21
|
-
insights!.model.INPBreakdown.longestInteractionEvent!;
|
|
22
|
-
const inp = longestInteractionEvent.dur / 1000;
|
|
23
|
-
console.log({
|
|
24
|
-
insights: insights?.model.INPBreakdown,
|
|
25
|
-
inp,
|
|
26
|
-
});
|
|
27
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as zlib from 'node:zlib';
|
|
3
|
-
|
|
4
|
-
import { initDevToolsTracing, Trace } from '../';
|
|
5
|
-
|
|
6
|
-
const SelectorTimingsKey = Trace.Types.Events.SelectorTimingsKey;
|
|
7
|
-
|
|
8
|
-
export async function run(tracePath: string) {
|
|
9
|
-
initDevToolsTracing();
|
|
10
|
-
|
|
11
|
-
const fileData = fs.readFileSync(tracePath);
|
|
12
|
-
const decompressedData = tracePath.endsWith('.gz')
|
|
13
|
-
? zlib.gunzipSync(fileData)
|
|
14
|
-
: fileData;
|
|
15
|
-
const traceData = JSON.parse(
|
|
16
|
-
decompressedData.toString(),
|
|
17
|
-
) as Trace.Types.File.TraceFile;
|
|
18
|
-
|
|
19
|
-
const traceModel = Trace.TraceModel.Model.createWithAllHandlers({
|
|
20
|
-
debugMode: true,
|
|
21
|
-
enableAnimationsFrameHandler: false,
|
|
22
|
-
maxInvalidationEventsPerEvent: 20,
|
|
23
|
-
showAllEvents: false,
|
|
24
|
-
});
|
|
25
|
-
await traceModel.parse(traceData.traceEvents, {
|
|
26
|
-
isCPUProfile: false,
|
|
27
|
-
isFreshRecording: false,
|
|
28
|
-
metadata: traceData.metadata,
|
|
29
|
-
showAllEvents: false,
|
|
30
|
-
});
|
|
31
|
-
const parsedTrace = traceModel.parsedTrace(0)!;
|
|
32
|
-
const parsedTraceData = parsedTrace.data;
|
|
33
|
-
|
|
34
|
-
// --- Selector Stats ---
|
|
35
|
-
const selectorStatsData = parsedTraceData.SelectorStats;
|
|
36
|
-
const selectorMap = new Map<
|
|
37
|
-
string,
|
|
38
|
-
Trace.Types.Events.SelectorTiming
|
|
39
|
-
>();
|
|
40
|
-
|
|
41
|
-
for (const [, value] of selectorStatsData.dataForRecalcStyleEvent) {
|
|
42
|
-
for (const timing of value.timings) {
|
|
43
|
-
const key =
|
|
44
|
-
timing[SelectorTimingsKey.Selector] +
|
|
45
|
-
'_' +
|
|
46
|
-
timing[SelectorTimingsKey.StyleSheetId];
|
|
47
|
-
const existing = selectorMap.get(key);
|
|
48
|
-
if (existing) {
|
|
49
|
-
existing[SelectorTimingsKey.Elapsed] +=
|
|
50
|
-
timing[SelectorTimingsKey.Elapsed];
|
|
51
|
-
existing[SelectorTimingsKey.MatchAttempts] +=
|
|
52
|
-
timing[SelectorTimingsKey.MatchAttempts];
|
|
53
|
-
existing[SelectorTimingsKey.MatchCount] +=
|
|
54
|
-
timing[SelectorTimingsKey.MatchCount];
|
|
55
|
-
existing[SelectorTimingsKey.FastRejectCount] +=
|
|
56
|
-
timing[SelectorTimingsKey.FastRejectCount];
|
|
57
|
-
} else {
|
|
58
|
-
selectorMap.set(key, { ...timing });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const allTimings = [...selectorMap.values()];
|
|
64
|
-
const TOP_N = 15;
|
|
65
|
-
|
|
66
|
-
// Top selectors by elapsed time
|
|
67
|
-
const byElapsed = allTimings
|
|
68
|
-
.sort(
|
|
69
|
-
(a, b) =>
|
|
70
|
-
b[SelectorTimingsKey.Elapsed] - a[SelectorTimingsKey.Elapsed],
|
|
71
|
-
)
|
|
72
|
-
.slice(0, TOP_N);
|
|
73
|
-
|
|
74
|
-
console.log(`\n=== Top ${TOP_N} CSS Selectors by Elapsed Time ===\n`);
|
|
75
|
-
for (const t of byElapsed) {
|
|
76
|
-
const elapsedMs = (t[SelectorTimingsKey.Elapsed] / 1000).toFixed(2);
|
|
77
|
-
console.log(
|
|
78
|
-
` ${elapsedMs}ms | attempts: ${t[SelectorTimingsKey.MatchAttempts]} | matches: ${t[SelectorTimingsKey.MatchCount]} | ${t[SelectorTimingsKey.Selector]}`,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Top selectors by match attempts
|
|
83
|
-
const byAttempts = [...selectorMap.values()]
|
|
84
|
-
.sort(
|
|
85
|
-
(a, b) =>
|
|
86
|
-
b[SelectorTimingsKey.MatchAttempts] -
|
|
87
|
-
a[SelectorTimingsKey.MatchAttempts],
|
|
88
|
-
)
|
|
89
|
-
.slice(0, TOP_N);
|
|
90
|
-
|
|
91
|
-
console.log(`\n=== Top ${TOP_N} CSS Selectors by Match Attempts ===\n`);
|
|
92
|
-
for (const t of byAttempts) {
|
|
93
|
-
const elapsedMs = (t[SelectorTimingsKey.Elapsed] / 1000).toFixed(2);
|
|
94
|
-
console.log(
|
|
95
|
-
` ${t[SelectorTimingsKey.MatchAttempts]} attempts | ${elapsedMs}ms | matches: ${t[SelectorTimingsKey.MatchCount]} | ${t[SelectorTimingsKey.Selector]}`,
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// --- Invalidation Tracking ---
|
|
100
|
-
const invalidatedNodes = selectorStatsData.invalidatedNodeList;
|
|
101
|
-
console.log(
|
|
102
|
-
`\n=== Invalidation Tracking (${invalidatedNodes.length} invalidated nodes) ===\n`,
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
// Aggregate selectors that caused invalidations
|
|
106
|
-
const invalidationSelectorCounts = new Map<string, number>();
|
|
107
|
-
for (const node of invalidatedNodes) {
|
|
108
|
-
for (const sel of node.selectorList) {
|
|
109
|
-
const count = invalidationSelectorCounts.get(sel.selector) ?? 0;
|
|
110
|
-
invalidationSelectorCounts.set(sel.selector, count + 1);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const topInvalidationSelectors = [...invalidationSelectorCounts.entries()]
|
|
115
|
-
.sort((a, b) => b[1] - a[1])
|
|
116
|
-
.slice(0, TOP_N);
|
|
117
|
-
|
|
118
|
-
console.log(
|
|
119
|
-
` Top ${TOP_N} selectors causing invalidations:\n`,
|
|
120
|
-
);
|
|
121
|
-
for (const [selector, count] of topInvalidationSelectors) {
|
|
122
|
-
console.log(` ${count}x | ${selector}`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Subtree vs single-node invalidations
|
|
126
|
-
const subtreeCount = invalidatedNodes.filter((n) => n.subtree).length;
|
|
127
|
-
console.log(
|
|
128
|
-
`\n Subtree invalidations: ${subtreeCount} / ${invalidatedNodes.length} total`,
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
// --- Totals ---
|
|
132
|
-
let totalElapsedUs = 0;
|
|
133
|
-
let totalMatchAttempts = 0;
|
|
134
|
-
let totalMatchCount = 0;
|
|
135
|
-
for (const t of allTimings) {
|
|
136
|
-
totalElapsedUs += t[SelectorTimingsKey.Elapsed];
|
|
137
|
-
totalMatchAttempts += t[SelectorTimingsKey.MatchAttempts];
|
|
138
|
-
totalMatchCount += t[SelectorTimingsKey.MatchCount];
|
|
139
|
-
}
|
|
140
|
-
console.log(`\n=== Totals ===\n`);
|
|
141
|
-
console.log(` Unique selectors: ${allTimings.length}`);
|
|
142
|
-
console.log(` Total elapsed: ${(totalElapsedUs / 1000).toFixed(2)}ms`);
|
|
143
|
-
console.log(` Total match attempts: ${totalMatchAttempts}`);
|
|
144
|
-
console.log(` Total match count: ${totalMatchCount}`);
|
|
145
|
-
}
|
package/commands/sourcemap.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as zlib from 'node:zlib';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
initDevToolsTracing,
|
|
6
|
-
createSourceMapResolver,
|
|
7
|
-
symbolicateTrace,
|
|
8
|
-
Trace,
|
|
9
|
-
} from '../';
|
|
10
|
-
|
|
11
|
-
export async function run(tracePath: string) {
|
|
12
|
-
initDevToolsTracing();
|
|
13
|
-
|
|
14
|
-
const fileData = fs.readFileSync(tracePath);
|
|
15
|
-
const decompressedData = tracePath.endsWith('.gz')
|
|
16
|
-
? zlib.gunzipSync(fileData)
|
|
17
|
-
: fileData;
|
|
18
|
-
const traceData = JSON.parse(
|
|
19
|
-
decompressedData.toString(),
|
|
20
|
-
) as Trace.Types.File.TraceFile;
|
|
21
|
-
|
|
22
|
-
const traceModel = Trace.TraceModel.Model.createWithAllHandlers();
|
|
23
|
-
const resolveSourceMap = createSourceMapResolver({
|
|
24
|
-
fetch: verboseFetch,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
await traceModel.parse(traceData.traceEvents, {
|
|
28
|
-
isCPUProfile: false,
|
|
29
|
-
isFreshRecording: false,
|
|
30
|
-
metadata: traceData.metadata,
|
|
31
|
-
showAllEvents: false,
|
|
32
|
-
resolveSourceMap,
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const parsedTrace = traceModel.parsedTrace(0)!;
|
|
36
|
-
const parsedTraceData = parsedTrace.data;
|
|
37
|
-
const scripts = parsedTraceData.Scripts.scripts;
|
|
38
|
-
|
|
39
|
-
const metadataSourceMaps = traceData.metadata?.sourceMaps?.length ?? 0;
|
|
40
|
-
console.log(`Found ${scripts.length} scripts in trace`);
|
|
41
|
-
console.log(`Source maps in trace metadata: ${metadataSourceMaps}\n`);
|
|
42
|
-
|
|
43
|
-
// Diagnostic: show script properties to explain resolution results.
|
|
44
|
-
let withUrl = 0;
|
|
45
|
-
let withFrame = 0;
|
|
46
|
-
let withSourceMapUrl = 0;
|
|
47
|
-
let withElided = 0;
|
|
48
|
-
for (const script of scripts) {
|
|
49
|
-
if (script.url) withUrl++;
|
|
50
|
-
if (script.frame) withFrame++;
|
|
51
|
-
if (script.sourceMapUrl) withSourceMapUrl++;
|
|
52
|
-
if (script.sourceMapUrlElided) withElided++;
|
|
53
|
-
}
|
|
54
|
-
console.log('Script breakdown:');
|
|
55
|
-
console.log(` with url: ${withUrl}`);
|
|
56
|
-
console.log(` with frame: ${withFrame}`);
|
|
57
|
-
console.log(` with sourceMapUrl: ${withSourceMapUrl}`);
|
|
58
|
-
console.log(` with sourceMapUrlElided: ${withElided}`);
|
|
59
|
-
console.log(` (resolution requires url + frame + sourceMapUrl/elided)\n`);
|
|
60
|
-
|
|
61
|
-
let resolvedCount = 0;
|
|
62
|
-
for (const script of scripts) {
|
|
63
|
-
if (!script.sourceMap) {
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
resolvedCount++;
|
|
67
|
-
|
|
68
|
-
const sourceMap = script.sourceMap;
|
|
69
|
-
const sourceURLs = sourceMap.sourceURLs();
|
|
70
|
-
|
|
71
|
-
console.log(
|
|
72
|
-
` ${script.url || script.scriptId} -> ${sourceURLs.length} sources, ${sourceMap.mappings().length} mappings`,
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
console.log(
|
|
77
|
-
`Resolved source maps for ${resolvedCount} of ${scripts.length} scripts\n`,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
// Symbolicate the raw trace events in-place using resolved source maps.
|
|
81
|
-
const result = symbolicateTrace(traceData.traceEvents, scripts);
|
|
82
|
-
console.log(
|
|
83
|
-
`Symbolicated ${result.symbolicatedFrames} frames across ${result.symbolicatedEvents} events`,
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
// Write the symbolicated trace to a new file.
|
|
87
|
-
const outPath = tracePath.replace(/(\.(json|json\.gz))$/i, '.symbolicated$1');
|
|
88
|
-
if (outPath === tracePath) {
|
|
89
|
-
console.error(
|
|
90
|
-
'Could not determine output path (expected .json or .json.gz extension)',
|
|
91
|
-
);
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
94
|
-
const outputJson = JSON.stringify(traceData);
|
|
95
|
-
if (outPath.endsWith('.gz')) {
|
|
96
|
-
fs.writeFileSync(outPath, zlib.gzipSync(outputJson));
|
|
97
|
-
} else {
|
|
98
|
-
fs.writeFileSync(outPath, outputJson);
|
|
99
|
-
}
|
|
100
|
-
console.log(`Wrote symbolicated trace to ${outPath}`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function verboseFetch(url: string): Promise<Response> {
|
|
104
|
-
console.log(`[sourcemap] fetching ${url}`);
|
|
105
|
-
const response = await fetch(url);
|
|
106
|
-
if (!response.ok) {
|
|
107
|
-
console.warn(
|
|
108
|
-
`[sourcemap] ${url} -> ${response.status} ${response.statusText}`,
|
|
109
|
-
);
|
|
110
|
-
} else {
|
|
111
|
-
console.log(`[sourcemap] ${url} -> ok`);
|
|
112
|
-
}
|
|
113
|
-
return response;
|
|
114
|
-
}
|
package/commands/stats.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import * as fs from 'node:fs';
|
|
2
|
-
import * as zlib from 'node:zlib';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
initDevToolsTracing,
|
|
6
|
-
entryIsVisibleInTimeline,
|
|
7
|
-
statsForTimeRange,
|
|
8
|
-
Trace,
|
|
9
|
-
} from '../';
|
|
10
|
-
|
|
11
|
-
export async function run(tracePath: string) {
|
|
12
|
-
initDevToolsTracing();
|
|
13
|
-
|
|
14
|
-
const fileData = fs.readFileSync(tracePath);
|
|
15
|
-
const decompressedData = tracePath.endsWith('.gz')
|
|
16
|
-
? zlib.gunzipSync(fileData)
|
|
17
|
-
: fileData;
|
|
18
|
-
const traceData = JSON.parse(
|
|
19
|
-
decompressedData.toString(),
|
|
20
|
-
) as Trace.Types.File.TraceFile;
|
|
21
|
-
|
|
22
|
-
const traceModel = Trace.TraceModel.Model.createWithAllHandlers({
|
|
23
|
-
debugMode: true,
|
|
24
|
-
enableAnimationsFrameHandler: false,
|
|
25
|
-
maxInvalidationEventsPerEvent: 20,
|
|
26
|
-
showAllEvents: false,
|
|
27
|
-
});
|
|
28
|
-
await traceModel.parse(traceData.traceEvents, {
|
|
29
|
-
isCPUProfile: false,
|
|
30
|
-
isFreshRecording: false,
|
|
31
|
-
metadata: traceData.metadata,
|
|
32
|
-
showAllEvents: false,
|
|
33
|
-
});
|
|
34
|
-
const parsedTrace = traceModel.parsedTrace(0)!;
|
|
35
|
-
const parsedTraceData = parsedTrace!.data!;
|
|
36
|
-
|
|
37
|
-
const startTime = Trace.Helpers.Timing.microToMilli(
|
|
38
|
-
parsedTraceData.Meta.traceBounds.min,
|
|
39
|
-
);
|
|
40
|
-
const endTime = Trace.Helpers.Timing.microToMilli(
|
|
41
|
-
parsedTraceData.Meta.traceBounds.max,
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
const threads = Trace.Handlers.Threads.threadsInTrace(parsedTrace.data);
|
|
45
|
-
const mainThread = threads.find(
|
|
46
|
-
(t) => t.type === Trace.Handlers.Threads.ThreadType.MAIN_THREAD,
|
|
47
|
-
);
|
|
48
|
-
if (!mainThread) {
|
|
49
|
-
throw new Error('No renderer main thread found in trace file');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const rendererEvents = [...mainThread.entries].filter((e) =>
|
|
53
|
-
entryIsVisibleInTimeline(e, parsedTrace),
|
|
54
|
-
);
|
|
55
|
-
const stats = statsForTimeRange(rendererEvents, startTime, endTime);
|
|
56
|
-
|
|
57
|
-
console.log(stats);
|
|
58
|
-
}
|