devtools-tracing 1.3.0 → 1.5.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.
@@ -4,9 +4,33 @@
4
4
  {
5
5
  "type": "node",
6
6
  "request": "launch",
7
- "name": "Debug sourcemap example",
7
+ "name": "Debug selector-stats",
8
8
  "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
9
- "args": ["examples/sourcemap.ts", "${input:traceFile}"],
9
+ "args": ["cli.ts", "selector-stats", "${input:traceFile}"],
10
+ "console": "integratedTerminal"
11
+ },
12
+ {
13
+ "type": "node",
14
+ "request": "launch",
15
+ "name": "Debug inp",
16
+ "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
17
+ "args": ["cli.ts", "inp", "${input:traceFile}"],
18
+ "console": "integratedTerminal"
19
+ },
20
+ {
21
+ "type": "node",
22
+ "request": "launch",
23
+ "name": "Debug sourcemap",
24
+ "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
25
+ "args": ["cli.ts", "sourcemap", "${input:traceFile}"],
26
+ "console": "integratedTerminal"
27
+ },
28
+ {
29
+ "type": "node",
30
+ "request": "launch",
31
+ "name": "Debug stats",
32
+ "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
33
+ "args": ["cli.ts", "stats", "${input:traceFile}"],
10
34
  "console": "integratedTerminal"
11
35
  }
12
36
  ],
package/README.md CHANGED
@@ -8,7 +8,35 @@ Node.js library for programmatic analysis of Chrome DevTools performance traces.
8
8
  npm install devtools-tracing
9
9
  ```
10
10
 
11
- ## Quick start
11
+ ## CLI
12
+
13
+ Analyze traces directly from the command line:
14
+
15
+ ```sh
16
+ npx devtools-tracing <command> <trace-file>
17
+ ```
18
+
19
+ Supports both `.json` and `.json.gz` trace files.
20
+
21
+ ### Commands
22
+
23
+ ```sh
24
+ # Top CSS selectors by elapsed time and match attempts, plus invalidation tracking
25
+ npx devtools-tracing selector-stats trace.json.gz
26
+
27
+ # INP (Interaction to Next Paint) breakdown
28
+ npx devtools-tracing inp trace.json.gz
29
+
30
+ # Timeline category statistics
31
+ npx devtools-tracing stats trace.json.gz
32
+
33
+ # Source map symbolication (writes a .symbolicated.json.gz alongside the input)
34
+ npx devtools-tracing sourcemap trace.json.gz
35
+ ```
36
+
37
+ See the [`commands/`](./commands) directory for full source.
38
+
39
+ ## Library usage
12
40
 
13
41
  ```ts
14
42
  import * as fs from 'node:fs';
@@ -33,23 +61,6 @@ await model.parse(traceData.traceEvents, {
33
61
  const parsedTrace = model.parsedTrace(0);
34
62
  ```
35
63
 
36
- ## Examples
37
-
38
- Run with a trace file (`.json` or `.json.gz`):
39
-
40
- ```sh
41
- # INP breakdown
42
- npm run example:inp -- trace.json.gz
43
-
44
- # Timeline category stats
45
- npm run example:stats -- trace.json.gz
46
-
47
- # Source map symbolication
48
- npm run example:sourcemap -- trace.json.gz
49
- ```
50
-
51
- See the [`examples/`](./examples) directory for full source.
52
-
53
64
  ## How it works
54
65
 
55
66
  A [build script](./generate.ts) extracts trace-related code from the `chrome-devtools-frontend` npm package into `lib/`, resolving dependencies via AST analysis and stubbing browser APIs. The result is bundled into a single CommonJS file via esbuild.
package/cli.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { run as cssSelectors } from './commands/selector-stats.js';
2
+ import { run as inp } from './commands/inp.js';
3
+ import { run as sourcemap } from './commands/sourcemap.js';
4
+ import { run as stats } from './commands/stats.js';
5
+
6
+ const commands: Record<string, (tracePath: string) => Promise<void>> = {
7
+ 'selector-stats': cssSelectors,
8
+ inp,
9
+ sourcemap,
10
+ stats,
11
+ };
12
+
13
+ function printUsage() {
14
+ console.log('Usage: devtools-tracing <command> <trace-file>');
15
+ console.log('\nCommands:');
16
+ console.log(' selector-stats Top CSS selectors from selector stats and invalidation tracking');
17
+ console.log(' inp Extract INP (Interaction to Next Paint) breakdown');
18
+ console.log(' sourcemap Symbolicate a trace using source maps');
19
+ console.log(' stats Generate timeline category statistics');
20
+ }
21
+
22
+ async function main() {
23
+ const [command, tracePath] = process.argv.slice(2);
24
+
25
+ if (!command || command === '--help' || command === '-h') {
26
+ printUsage();
27
+ process.exit(0);
28
+ }
29
+
30
+ const run = commands[command];
31
+ if (!run) {
32
+ console.error(`Unknown command: ${command}`);
33
+ printUsage();
34
+ process.exit(1);
35
+ }
36
+
37
+ if (!tracePath) {
38
+ console.error(`Missing trace file path.`);
39
+ console.error(`Usage: devtools-tracing ${command} <trace-file>`);
40
+ process.exit(1);
41
+ }
42
+
43
+ await run(tracePath);
44
+ }
45
+
46
+ main();
package/dist/cli.js ADDED
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env node
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+
25
+ // commands/selector-stats.ts
26
+ var fs = __toESM(require("node:fs"));
27
+ var zlib = __toESM(require("node:zlib"));
28
+ var import__ = require("../");
29
+ var SelectorTimingsKey = import__.Trace.Types.Events.SelectorTimingsKey;
30
+ var microToMilli = (us) => import__.Trace.Helpers.Timing.microToMilli(import__.Trace.Types.Timing.Micro(us));
31
+ async function run(tracePath) {
32
+ (0, import__.initDevToolsTracing)();
33
+ const fileData = fs.readFileSync(tracePath);
34
+ const decompressedData = tracePath.endsWith(".gz") ? zlib.gunzipSync(fileData) : fileData;
35
+ const traceData = JSON.parse(
36
+ decompressedData.toString()
37
+ );
38
+ const traceModel = import__.Trace.TraceModel.Model.createWithAllHandlers({
39
+ debugMode: true,
40
+ enableAnimationsFrameHandler: false,
41
+ maxInvalidationEventsPerEvent: 20,
42
+ showAllEvents: false
43
+ });
44
+ await traceModel.parse(traceData.traceEvents, {
45
+ isCPUProfile: false,
46
+ isFreshRecording: false,
47
+ metadata: traceData.metadata,
48
+ showAllEvents: false
49
+ });
50
+ const parsedTrace = traceModel.parsedTrace(0);
51
+ const parsedTraceData = parsedTrace.data;
52
+ const selectorStatsData = parsedTraceData.SelectorStats;
53
+ const selectorMap = /* @__PURE__ */ new Map();
54
+ for (const [, value] of selectorStatsData.dataForRecalcStyleEvent) {
55
+ for (const timing of value.timings) {
56
+ const key = timing[SelectorTimingsKey.Selector] + "_" + timing[SelectorTimingsKey.StyleSheetId];
57
+ const existing = selectorMap.get(key);
58
+ if (existing) {
59
+ existing[SelectorTimingsKey.Elapsed] += timing[SelectorTimingsKey.Elapsed];
60
+ existing[SelectorTimingsKey.MatchAttempts] += timing[SelectorTimingsKey.MatchAttempts];
61
+ existing[SelectorTimingsKey.MatchCount] += timing[SelectorTimingsKey.MatchCount];
62
+ existing[SelectorTimingsKey.FastRejectCount] += timing[SelectorTimingsKey.FastRejectCount];
63
+ } else {
64
+ selectorMap.set(key, { ...timing });
65
+ }
66
+ }
67
+ }
68
+ const allTimings = [...selectorMap.values()];
69
+ const TOP_N = 15;
70
+ const truncateSelector = (s) => s.length > 80 ? `${s.slice(0, 77)}...` : s;
71
+ const byElapsed = allTimings.sort(
72
+ (a, b) => b[SelectorTimingsKey.Elapsed] - a[SelectorTimingsKey.Elapsed]
73
+ ).slice(0, TOP_N);
74
+ console.log(`
75
+ === Top ${TOP_N} CSS Selectors by Elapsed Time ===
76
+ `);
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
+ );
91
+ const byAttempts = [...selectorMap.values()].sort(
92
+ (a, b) => b[SelectorTimingsKey.MatchAttempts] - a[SelectorTimingsKey.MatchAttempts]
93
+ ).slice(0, TOP_N);
94
+ console.log(`
95
+ === Top ${TOP_N} CSS Selectors by Match Attempts ===
96
+ `);
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
+ )
110
+ );
111
+ const invalidationsData = parsedTraceData.Invalidations;
112
+ const allInvalidations = [];
113
+ for (const [, invalidations] of invalidationsData.invalidationsForEvent) {
114
+ allInvalidations.push(...invalidations);
115
+ }
116
+ const { groupedByReason, backendNodeIds } = (0, import__.generateInvalidationsList)(allInvalidations);
117
+ const reasons = Object.entries(groupedByReason).sort((a, b) => b[1].length - a[1].length);
118
+ console.log(
119
+ `
120
+ === Invalidation Tracking (${allInvalidations.length} invalidations, ${backendNodeIds.size} unique nodes) ===
121
+ `
122
+ );
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
+ )
133
+ );
134
+ let totalElapsedUs = 0;
135
+ let totalMatchAttempts = 0;
136
+ let totalMatchCount = 0;
137
+ for (const t of allTimings) {
138
+ totalElapsedUs += t[SelectorTimingsKey.Elapsed];
139
+ totalMatchAttempts += t[SelectorTimingsKey.MatchAttempts];
140
+ totalMatchCount += t[SelectorTimingsKey.MatchCount];
141
+ }
142
+ console.log(`
143
+ === Totals ===
144
+ `);
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
+ });
151
+ }
152
+
153
+ // commands/inp.ts
154
+ var fs2 = __toESM(require("node:fs"));
155
+ var zlib2 = __toESM(require("node:zlib"));
156
+ var import__2 = require("../");
157
+ async function run2(tracePath) {
158
+ (0, import__2.initDevToolsTracing)();
159
+ const fileData = fs2.readFileSync(tracePath);
160
+ const decompressedData = tracePath.endsWith(".gz") ? zlib2.gunzipSync(fileData) : fileData;
161
+ const traceData = JSON.parse(
162
+ decompressedData.toString()
163
+ );
164
+ const processor = import__2.Trace.Processor.TraceProcessor.createWithAllHandlers();
165
+ await processor.parse(traceData.traceEvents, {});
166
+ const insights = processor.insights.get("NO_NAVIGATION");
167
+ const longestInteractionEvent = insights.model.INPBreakdown.longestInteractionEvent;
168
+ const inp = longestInteractionEvent.dur / 1e3;
169
+ console.log({
170
+ insights: insights?.model.INPBreakdown,
171
+ inp
172
+ });
173
+ }
174
+
175
+ // commands/sourcemap.ts
176
+ var fs3 = __toESM(require("node:fs"));
177
+ var zlib3 = __toESM(require("node:zlib"));
178
+ var import__3 = require("../");
179
+ async function run3(tracePath) {
180
+ (0, import__3.initDevToolsTracing)();
181
+ const fileData = fs3.readFileSync(tracePath);
182
+ const decompressedData = tracePath.endsWith(".gz") ? zlib3.gunzipSync(fileData) : fileData;
183
+ const traceData = JSON.parse(
184
+ decompressedData.toString()
185
+ );
186
+ const traceModel = import__3.Trace.TraceModel.Model.createWithAllHandlers();
187
+ const resolveSourceMap = (0, import__3.createSourceMapResolver)({
188
+ fetch: verboseFetch
189
+ });
190
+ await traceModel.parse(traceData.traceEvents, {
191
+ isCPUProfile: false,
192
+ isFreshRecording: false,
193
+ metadata: traceData.metadata,
194
+ showAllEvents: false,
195
+ resolveSourceMap
196
+ });
197
+ const parsedTrace = traceModel.parsedTrace(0);
198
+ const parsedTraceData = parsedTrace.data;
199
+ const scripts = parsedTraceData.Scripts.scripts;
200
+ const metadataSourceMaps = traceData.metadata?.sourceMaps?.length ?? 0;
201
+ console.log(`Found ${scripts.length} scripts in trace`);
202
+ console.log(`Source maps in trace metadata: ${metadataSourceMaps}
203
+ `);
204
+ let withUrl = 0;
205
+ let withFrame = 0;
206
+ let withSourceMapUrl = 0;
207
+ let withElided = 0;
208
+ for (const script of scripts) {
209
+ if (script.url) withUrl++;
210
+ if (script.frame) withFrame++;
211
+ if (script.sourceMapUrl) withSourceMapUrl++;
212
+ if (script.sourceMapUrlElided) withElided++;
213
+ }
214
+ console.log("Script breakdown:");
215
+ console.log(` with url: ${withUrl}`);
216
+ console.log(` with frame: ${withFrame}`);
217
+ console.log(` with sourceMapUrl: ${withSourceMapUrl}`);
218
+ console.log(` with sourceMapUrlElided: ${withElided}`);
219
+ console.log(` (resolution requires url + frame + sourceMapUrl/elided)
220
+ `);
221
+ let resolvedCount = 0;
222
+ for (const script of scripts) {
223
+ if (!script.sourceMap) {
224
+ continue;
225
+ }
226
+ resolvedCount++;
227
+ const sourceMap = script.sourceMap;
228
+ const sourceURLs = sourceMap.sourceURLs();
229
+ console.log(
230
+ ` ${script.url || script.scriptId} -> ${sourceURLs.length} sources, ${sourceMap.mappings().length} mappings`
231
+ );
232
+ }
233
+ console.log(
234
+ `Resolved source maps for ${resolvedCount} of ${scripts.length} scripts
235
+ `
236
+ );
237
+ const result = (0, import__3.symbolicateTrace)(traceData.traceEvents, scripts);
238
+ console.log(
239
+ `Symbolicated ${result.symbolicatedFrames} frames across ${result.symbolicatedEvents} events`
240
+ );
241
+ const outPath = tracePath.replace(/(\.(json|json\.gz))$/i, ".symbolicated$1");
242
+ if (outPath === tracePath) {
243
+ console.error(
244
+ "Could not determine output path (expected .json or .json.gz extension)"
245
+ );
246
+ process.exit(1);
247
+ }
248
+ const outputJson = JSON.stringify(traceData);
249
+ if (outPath.endsWith(".gz")) {
250
+ fs3.writeFileSync(outPath, zlib3.gzipSync(outputJson));
251
+ } else {
252
+ fs3.writeFileSync(outPath, outputJson);
253
+ }
254
+ console.log(`Wrote symbolicated trace to ${outPath}`);
255
+ }
256
+ async function verboseFetch(url) {
257
+ console.log(`[sourcemap] fetching ${url}`);
258
+ const response = await fetch(url);
259
+ if (!response.ok) {
260
+ console.warn(
261
+ `[sourcemap] ${url} -> ${response.status} ${response.statusText}`
262
+ );
263
+ } else {
264
+ console.log(`[sourcemap] ${url} -> ok`);
265
+ }
266
+ return response;
267
+ }
268
+
269
+ // commands/stats.ts
270
+ var fs4 = __toESM(require("node:fs"));
271
+ var zlib4 = __toESM(require("node:zlib"));
272
+ var import__4 = require("../");
273
+ async function run4(tracePath) {
274
+ (0, import__4.initDevToolsTracing)();
275
+ const fileData = fs4.readFileSync(tracePath);
276
+ const decompressedData = tracePath.endsWith(".gz") ? zlib4.gunzipSync(fileData) : fileData;
277
+ const traceData = JSON.parse(
278
+ decompressedData.toString()
279
+ );
280
+ const traceModel = import__4.Trace.TraceModel.Model.createWithAllHandlers({
281
+ debugMode: true,
282
+ enableAnimationsFrameHandler: false,
283
+ maxInvalidationEventsPerEvent: 20,
284
+ showAllEvents: false
285
+ });
286
+ await traceModel.parse(traceData.traceEvents, {
287
+ isCPUProfile: false,
288
+ isFreshRecording: false,
289
+ metadata: traceData.metadata,
290
+ showAllEvents: false
291
+ });
292
+ const parsedTrace = traceModel.parsedTrace(0);
293
+ const parsedTraceData = parsedTrace.data;
294
+ const startTime = import__4.Trace.Helpers.Timing.microToMilli(
295
+ parsedTraceData.Meta.traceBounds.min
296
+ );
297
+ const endTime = import__4.Trace.Helpers.Timing.microToMilli(
298
+ parsedTraceData.Meta.traceBounds.max
299
+ );
300
+ const threads = import__4.Trace.Handlers.Threads.threadsInTrace(parsedTrace.data);
301
+ const mainThread = threads.find(
302
+ (t) => t.type === import__4.Trace.Handlers.Threads.ThreadType.MAIN_THREAD
303
+ );
304
+ if (!mainThread) {
305
+ throw new Error("No renderer main thread found in trace file");
306
+ }
307
+ const rendererEvents = [...mainThread.entries].filter(
308
+ (e) => (0, import__4.entryIsVisibleInTimeline)(e, parsedTrace)
309
+ );
310
+ const stats = (0, import__4.statsForTimeRange)(rendererEvents, startTime, endTime);
311
+ console.log(stats);
312
+ }
313
+
314
+ // cli.ts
315
+ var commands = {
316
+ "selector-stats": run,
317
+ inp: run2,
318
+ sourcemap: run3,
319
+ stats: run4
320
+ };
321
+ function printUsage() {
322
+ console.log("Usage: devtools-tracing <command> <trace-file>");
323
+ console.log("\nCommands:");
324
+ console.log(" selector-stats Top CSS selectors from selector stats and invalidation tracking");
325
+ console.log(" inp Extract INP (Interaction to Next Paint) breakdown");
326
+ console.log(" sourcemap Symbolicate a trace using source maps");
327
+ console.log(" stats Generate timeline category statistics");
328
+ }
329
+ async function main() {
330
+ const [command, tracePath] = process.argv.slice(2);
331
+ if (!command || command === "--help" || command === "-h") {
332
+ printUsage();
333
+ process.exit(0);
334
+ }
335
+ const run5 = commands[command];
336
+ if (!run5) {
337
+ console.error(`Unknown command: ${command}`);
338
+ printUsage();
339
+ process.exit(1);
340
+ }
341
+ if (!tracePath) {
342
+ console.error(`Missing trace file path.`);
343
+ console.error(`Usage: devtools-tracing ${command} <trace-file>`);
344
+ process.exit(1);
345
+ }
346
+ await run5(tracePath);
347
+ }
348
+ 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
@@ -14414,6 +14415,15 @@ function initDevToolsTracing() {
14414
14415
  lookupClosestDevToolsLocale: identity
14415
14416
  };
14416
14417
  DevToolsLocale.instance({ create: true, data: data31 });
14418
+ for (const name of Object.values(ExperimentName)) {
14419
+ if (name === "*" /* ALL */) {
14420
+ continue;
14421
+ }
14422
+ try {
14423
+ experiments.register(name, name);
14424
+ } catch {
14425
+ }
14426
+ }
14417
14427
  }
14418
14428
 
14419
14429
  // lib/front_end/models/trace/trace.ts
@@ -42405,6 +42415,51 @@ function entryIsVisibleInTimeline(entry, parsedTrace) {
42405
42415
  return eventStyle && !eventStyle.hidden || eventIsTiming;
42406
42416
  }
42407
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
+
42408
42463
  // lib/front_end/core/sdk/sdk.ts
42409
42464
  var sdk_exports = {};
42410
42465
  __export(sdk_exports, {
@@ -86835,6 +86890,7 @@ function decodeDataUrl(dataUrl) {
86835
86890
  Trace,
86836
86891
  createSourceMapResolver,
86837
86892
  entryIsVisibleInTimeline,
86893
+ generateInvalidationsList,
86838
86894
  initDevToolsTracing,
86839
86895
  statsForTimeRange,
86840
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
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "devtools-tracing",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Utilities for working with trace files",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "devtools-tracing": "dist/cli.js"
9
+ },
7
10
  "scripts": {
8
- "build": "tsc --emitDeclarationOnly || true; esbuild index.ts --bundle --platform=node --outfile=dist/index.js --format=cjs",
11
+ "build": "tsc --emitDeclarationOnly || true; esbuild index.ts --bundle --platform=node --outfile=dist/index.js --format=cjs && esbuild cli.ts --bundle --platform=node --outfile=dist/cli.js --format=cjs --banner:js='#!/usr/bin/env node' --external:../",
9
12
  "generate": "tsx generate.ts",
10
- "example:inp": "tsx examples/inp.ts",
11
- "example:sourcemap": "tsx examples/sourcemap.ts",
12
- "example:stats": "tsx examples/stats.ts",
13
13
  "size": "du -sh dist/",
14
14
  "prepublishOnly": "npm run generate && npm run build"
15
15
  },
package/examples/inp.ts DELETED
@@ -1,34 +0,0 @@
1
- import * as fs from 'node:fs';
2
- import * as zlib from 'node:zlib';
3
-
4
- import { initDevToolsTracing, Trace } from '../';
5
-
6
- async function main() {
7
- const tracePath = process.argv[2];
8
- if (!tracePath) {
9
- console.error('Usage: npm run examples:inp <path-to-trace-file>');
10
- process.exit(1);
11
- }
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 processor = Trace.Processor.TraceProcessor.createWithAllHandlers();
23
- await processor.parse(traceData.traceEvents, {});
24
- const insights = processor.insights!.get('NO_NAVIGATION');
25
- const longestInteractionEvent =
26
- insights!.model.INPBreakdown.longestInteractionEvent!;
27
- const inp = longestInteractionEvent.dur / 1000;
28
- console.log({
29
- insights: insights?.model.INPBreakdown,
30
- inp,
31
- });
32
- }
33
-
34
- main();
@@ -1,121 +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
- async function main() {
12
- const tracePath = process.argv[2];
13
- if (!tracePath) {
14
- console.error('Usage: npm run example:sourcemap <path-to-trace-file>');
15
- process.exit(1);
16
- }
17
- initDevToolsTracing();
18
-
19
- const fileData = fs.readFileSync(tracePath);
20
- const decompressedData = tracePath.endsWith('.gz')
21
- ? zlib.gunzipSync(fileData)
22
- : fileData;
23
- const traceData = JSON.parse(
24
- decompressedData.toString(),
25
- ) as Trace.Types.File.TraceFile;
26
-
27
- const traceModel = Trace.TraceModel.Model.createWithAllHandlers();
28
- const resolveSourceMap = createSourceMapResolver({
29
- fetch: verboseFetch,
30
- });
31
-
32
- await traceModel.parse(traceData.traceEvents, {
33
- isCPUProfile: false,
34
- isFreshRecording: false,
35
- metadata: traceData.metadata,
36
- showAllEvents: false,
37
- resolveSourceMap,
38
- });
39
-
40
- const parsedTrace = traceModel.parsedTrace(0)!;
41
- const parsedTraceData = parsedTrace.data;
42
- const scripts = parsedTraceData.Scripts.scripts;
43
-
44
- const metadataSourceMaps = traceData.metadata?.sourceMaps?.length ?? 0;
45
- console.log(`Found ${scripts.length} scripts in trace`);
46
- console.log(`Source maps in trace metadata: ${metadataSourceMaps}\n`);
47
-
48
- // Diagnostic: show script properties to explain resolution results.
49
- let withUrl = 0;
50
- let withFrame = 0;
51
- let withSourceMapUrl = 0;
52
- let withElided = 0;
53
- for (const script of scripts) {
54
- if (script.url) withUrl++;
55
- if (script.frame) withFrame++;
56
- if (script.sourceMapUrl) withSourceMapUrl++;
57
- if (script.sourceMapUrlElided) withElided++;
58
- }
59
- console.log('Script breakdown:');
60
- console.log(` with url: ${withUrl}`);
61
- console.log(` with frame: ${withFrame}`);
62
- console.log(` with sourceMapUrl: ${withSourceMapUrl}`);
63
- console.log(` with sourceMapUrlElided: ${withElided}`);
64
- console.log(` (resolution requires url + frame + sourceMapUrl/elided)\n`);
65
-
66
- let resolvedCount = 0;
67
- for (const script of scripts) {
68
- if (!script.sourceMap) {
69
- continue;
70
- }
71
- resolvedCount++;
72
-
73
- const sourceMap = script.sourceMap;
74
- const sourceURLs = sourceMap.sourceURLs();
75
-
76
- console.log(
77
- ` ${script.url || script.scriptId} -> ${sourceURLs.length} sources, ${sourceMap.mappings().length} mappings`,
78
- );
79
- }
80
-
81
- console.log(
82
- `Resolved source maps for ${resolvedCount} of ${scripts.length} scripts\n`,
83
- );
84
-
85
- // Symbolicate the raw trace events in-place using resolved source maps.
86
- const result = symbolicateTrace(traceData.traceEvents, scripts);
87
- console.log(
88
- `Symbolicated ${result.symbolicatedFrames} frames across ${result.symbolicatedEvents} events`,
89
- );
90
-
91
- // Write the symbolicated trace to a new file.
92
- const outPath = tracePath.replace(/(\.(json|json\.gz))$/i, '.symbolicated$1');
93
- if (outPath === tracePath) {
94
- console.error(
95
- 'Could not determine output path (expected .json or .json.gz extension)',
96
- );
97
- process.exit(1);
98
- }
99
- const outputJson = JSON.stringify(traceData);
100
- if (outPath.endsWith('.gz')) {
101
- fs.writeFileSync(outPath, zlib.gzipSync(outputJson));
102
- } else {
103
- fs.writeFileSync(outPath, outputJson);
104
- }
105
- console.log(`Wrote symbolicated trace to ${outPath}`);
106
- }
107
-
108
- async function verboseFetch(url: string): Promise<Response> {
109
- console.log(`[sourcemap] fetching ${url}`);
110
- const response = await fetch(url);
111
- if (!response.ok) {
112
- console.warn(
113
- `[sourcemap] ${url} -> ${response.status} ${response.statusText}`,
114
- );
115
- } else {
116
- console.log(`[sourcemap] ${url} -> ok`);
117
- }
118
- return response;
119
- }
120
-
121
- main();
package/examples/stats.ts DELETED
@@ -1,66 +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
- async function main() {
12
- const tracePath = process.argv[2];
13
- if (!tracePath) {
14
- console.error('Usage: npm run examples:stats <path-to-trace-file>');
15
- process.exit(1);
16
- }
17
- initDevToolsTracing();
18
-
19
- const fileData = fs.readFileSync(tracePath);
20
- const decompressedData = tracePath.endsWith('.gz')
21
- ? zlib.gunzipSync(fileData)
22
- : fileData;
23
- const traceData = JSON.parse(
24
- decompressedData.toString(),
25
- ) as Trace.Types.File.TraceFile;
26
-
27
- const traceModel = Trace.TraceModel.Model.createWithAllHandlers({
28
- debugMode: true,
29
- enableAnimationsFrameHandler: false,
30
- // includeRuntimeCallStats: false,
31
- maxInvalidationEventsPerEvent: 20,
32
- showAllEvents: false,
33
- });
34
- await traceModel.parse(traceData.traceEvents, {
35
- isCPUProfile: false,
36
- isFreshRecording: false,
37
- metadata: traceData.metadata,
38
- showAllEvents: false,
39
- });
40
- const parsedTrace = traceModel.parsedTrace(0)!;
41
- const parsedTraceData = parsedTrace!.data!;
42
-
43
- const startTime = Trace.Helpers.Timing.microToMilli(
44
- parsedTraceData.Meta.traceBounds.min,
45
- );
46
- const endTime = Trace.Helpers.Timing.microToMilli(
47
- parsedTraceData.Meta.traceBounds.max,
48
- );
49
-
50
- const threads = Trace.Handlers.Threads.threadsInTrace(parsedTrace.data);
51
- const mainThread = threads.find(
52
- (t) => t.type === Trace.Handlers.Threads.ThreadType.MAIN_THREAD,
53
- );
54
- if (!mainThread) {
55
- throw new Error('No renderer main thread found in trace file');
56
- }
57
-
58
- const rendererEvents = [...mainThread.entries].filter((e) =>
59
- entryIsVisibleInTimeline(e, parsedTrace),
60
- );
61
- const stats = statsForTimeRange(rendererEvents, startTime, endTime);
62
-
63
- console.log(stats);
64
- }
65
-
66
- main();