devtools-tracing 1.3.0 → 1.4.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/.vscode/launch.json +26 -2
- package/README.md +29 -18
- package/cli.ts +46 -0
- package/{examples → commands}/inp.ts +1 -8
- package/commands/selector-stats.ts +145 -0
- package/{examples → commands}/sourcemap.ts +1 -8
- package/{examples → commands}/stats.ts +1 -9
- package/dist/cli.js +331 -0
- package/dist/index.js +9 -0
- package/package.json +5 -5
package/.vscode/launch.json
CHANGED
|
@@ -4,9 +4,33 @@
|
|
|
4
4
|
{
|
|
5
5
|
"type": "node",
|
|
6
6
|
"request": "launch",
|
|
7
|
-
"name": "Debug
|
|
7
|
+
"name": "Debug selector-stats",
|
|
8
8
|
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/tsx",
|
|
9
|
-
"args": ["
|
|
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
|
-
##
|
|
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();
|
|
@@ -3,12 +3,7 @@ import * as zlib from 'node:zlib';
|
|
|
3
3
|
|
|
4
4
|
import { initDevToolsTracing, Trace } from '../';
|
|
5
5
|
|
|
6
|
-
async function
|
|
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
|
-
}
|
|
6
|
+
export async function run(tracePath: string) {
|
|
12
7
|
initDevToolsTracing();
|
|
13
8
|
|
|
14
9
|
const fileData = fs.readFileSync(tracePath);
|
|
@@ -30,5 +25,3 @@ async function main() {
|
|
|
30
25
|
inp,
|
|
31
26
|
});
|
|
32
27
|
}
|
|
33
|
-
|
|
34
|
-
main();
|
|
@@ -0,0 +1,145 @@
|
|
|
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
|
+
}
|
|
@@ -8,12 +8,7 @@ import {
|
|
|
8
8
|
Trace,
|
|
9
9
|
} from '../';
|
|
10
10
|
|
|
11
|
-
async function
|
|
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
|
-
}
|
|
11
|
+
export async function run(tracePath: string) {
|
|
17
12
|
initDevToolsTracing();
|
|
18
13
|
|
|
19
14
|
const fileData = fs.readFileSync(tracePath);
|
|
@@ -117,5 +112,3 @@ async function verboseFetch(url: string): Promise<Response> {
|
|
|
117
112
|
}
|
|
118
113
|
return response;
|
|
119
114
|
}
|
|
120
|
-
|
|
121
|
-
main();
|
|
@@ -8,12 +8,7 @@ import {
|
|
|
8
8
|
Trace,
|
|
9
9
|
} from '../';
|
|
10
10
|
|
|
11
|
-
async function
|
|
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
|
-
}
|
|
11
|
+
export async function run(tracePath: string) {
|
|
17
12
|
initDevToolsTracing();
|
|
18
13
|
|
|
19
14
|
const fileData = fs.readFileSync(tracePath);
|
|
@@ -27,7 +22,6 @@ async function main() {
|
|
|
27
22
|
const traceModel = Trace.TraceModel.Model.createWithAllHandlers({
|
|
28
23
|
debugMode: true,
|
|
29
24
|
enableAnimationsFrameHandler: false,
|
|
30
|
-
// includeRuntimeCallStats: false,
|
|
31
25
|
maxInvalidationEventsPerEvent: 20,
|
|
32
26
|
showAllEvents: false,
|
|
33
27
|
});
|
|
@@ -62,5 +56,3 @@ async function main() {
|
|
|
62
56
|
|
|
63
57
|
console.log(stats);
|
|
64
58
|
}
|
|
65
|
-
|
|
66
|
-
main();
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
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
|
+
async function run(tracePath) {
|
|
31
|
+
(0, import__.initDevToolsTracing)();
|
|
32
|
+
const fileData = fs.readFileSync(tracePath);
|
|
33
|
+
const decompressedData = tracePath.endsWith(".gz") ? zlib.gunzipSync(fileData) : fileData;
|
|
34
|
+
const traceData = JSON.parse(
|
|
35
|
+
decompressedData.toString()
|
|
36
|
+
);
|
|
37
|
+
const traceModel = import__.Trace.TraceModel.Model.createWithAllHandlers({
|
|
38
|
+
debugMode: true,
|
|
39
|
+
enableAnimationsFrameHandler: false,
|
|
40
|
+
maxInvalidationEventsPerEvent: 20,
|
|
41
|
+
showAllEvents: false
|
|
42
|
+
});
|
|
43
|
+
await traceModel.parse(traceData.traceEvents, {
|
|
44
|
+
isCPUProfile: false,
|
|
45
|
+
isFreshRecording: false,
|
|
46
|
+
metadata: traceData.metadata,
|
|
47
|
+
showAllEvents: false
|
|
48
|
+
});
|
|
49
|
+
const parsedTrace = traceModel.parsedTrace(0);
|
|
50
|
+
const parsedTraceData = parsedTrace.data;
|
|
51
|
+
const selectorStatsData = parsedTraceData.SelectorStats;
|
|
52
|
+
const selectorMap = /* @__PURE__ */ new Map();
|
|
53
|
+
for (const [, value] of selectorStatsData.dataForRecalcStyleEvent) {
|
|
54
|
+
for (const timing of value.timings) {
|
|
55
|
+
const key = timing[SelectorTimingsKey.Selector] + "_" + timing[SelectorTimingsKey.StyleSheetId];
|
|
56
|
+
const existing = selectorMap.get(key);
|
|
57
|
+
if (existing) {
|
|
58
|
+
existing[SelectorTimingsKey.Elapsed] += timing[SelectorTimingsKey.Elapsed];
|
|
59
|
+
existing[SelectorTimingsKey.MatchAttempts] += timing[SelectorTimingsKey.MatchAttempts];
|
|
60
|
+
existing[SelectorTimingsKey.MatchCount] += timing[SelectorTimingsKey.MatchCount];
|
|
61
|
+
existing[SelectorTimingsKey.FastRejectCount] += timing[SelectorTimingsKey.FastRejectCount];
|
|
62
|
+
} else {
|
|
63
|
+
selectorMap.set(key, { ...timing });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const allTimings = [...selectorMap.values()];
|
|
68
|
+
const TOP_N = 15;
|
|
69
|
+
const byElapsed = allTimings.sort(
|
|
70
|
+
(a, b) => b[SelectorTimingsKey.Elapsed] - a[SelectorTimingsKey.Elapsed]
|
|
71
|
+
).slice(0, TOP_N);
|
|
72
|
+
console.log(`
|
|
73
|
+
=== Top ${TOP_N} CSS Selectors by Elapsed Time ===
|
|
74
|
+
`);
|
|
75
|
+
for (const t of byElapsed) {
|
|
76
|
+
const elapsedMs = (t[SelectorTimingsKey.Elapsed] / 1e3).toFixed(2);
|
|
77
|
+
console.log(
|
|
78
|
+
` ${elapsedMs}ms | attempts: ${t[SelectorTimingsKey.MatchAttempts]} | matches: ${t[SelectorTimingsKey.MatchCount]} | ${t[SelectorTimingsKey.Selector]}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
const byAttempts = [...selectorMap.values()].sort(
|
|
82
|
+
(a, b) => b[SelectorTimingsKey.MatchAttempts] - a[SelectorTimingsKey.MatchAttempts]
|
|
83
|
+
).slice(0, TOP_N);
|
|
84
|
+
console.log(`
|
|
85
|
+
=== Top ${TOP_N} CSS Selectors by Match Attempts ===
|
|
86
|
+
`);
|
|
87
|
+
for (const t of byAttempts) {
|
|
88
|
+
const elapsedMs = (t[SelectorTimingsKey.Elapsed] / 1e3).toFixed(2);
|
|
89
|
+
console.log(
|
|
90
|
+
` ${t[SelectorTimingsKey.MatchAttempts]} attempts | ${elapsedMs}ms | matches: ${t[SelectorTimingsKey.MatchCount]} | ${t[SelectorTimingsKey.Selector]}`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const invalidatedNodes = selectorStatsData.invalidatedNodeList;
|
|
94
|
+
console.log(
|
|
95
|
+
`
|
|
96
|
+
=== Invalidation Tracking (${invalidatedNodes.length} invalidated nodes) ===
|
|
97
|
+
`
|
|
98
|
+
);
|
|
99
|
+
const invalidationSelectorCounts = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const node of invalidatedNodes) {
|
|
101
|
+
for (const sel of node.selectorList) {
|
|
102
|
+
const count = invalidationSelectorCounts.get(sel.selector) ?? 0;
|
|
103
|
+
invalidationSelectorCounts.set(sel.selector, count + 1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const topInvalidationSelectors = [...invalidationSelectorCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, TOP_N);
|
|
107
|
+
console.log(
|
|
108
|
+
` Top ${TOP_N} selectors causing invalidations:
|
|
109
|
+
`
|
|
110
|
+
);
|
|
111
|
+
for (const [selector, count] of topInvalidationSelectors) {
|
|
112
|
+
console.log(` ${count}x | ${selector}`);
|
|
113
|
+
}
|
|
114
|
+
const subtreeCount = invalidatedNodes.filter((n) => n.subtree).length;
|
|
115
|
+
console.log(
|
|
116
|
+
`
|
|
117
|
+
Subtree invalidations: ${subtreeCount} / ${invalidatedNodes.length} total`
|
|
118
|
+
);
|
|
119
|
+
let totalElapsedUs = 0;
|
|
120
|
+
let totalMatchAttempts = 0;
|
|
121
|
+
let totalMatchCount = 0;
|
|
122
|
+
for (const t of allTimings) {
|
|
123
|
+
totalElapsedUs += t[SelectorTimingsKey.Elapsed];
|
|
124
|
+
totalMatchAttempts += t[SelectorTimingsKey.MatchAttempts];
|
|
125
|
+
totalMatchCount += t[SelectorTimingsKey.MatchCount];
|
|
126
|
+
}
|
|
127
|
+
console.log(`
|
|
128
|
+
=== Totals ===
|
|
129
|
+
`);
|
|
130
|
+
console.log(` Unique selectors: ${allTimings.length}`);
|
|
131
|
+
console.log(` Total elapsed: ${(totalElapsedUs / 1e3).toFixed(2)}ms`);
|
|
132
|
+
console.log(` Total match attempts: ${totalMatchAttempts}`);
|
|
133
|
+
console.log(` Total match count: ${totalMatchCount}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// commands/inp.ts
|
|
137
|
+
var fs2 = __toESM(require("node:fs"));
|
|
138
|
+
var zlib2 = __toESM(require("node:zlib"));
|
|
139
|
+
var import__2 = require("../");
|
|
140
|
+
async function run2(tracePath) {
|
|
141
|
+
(0, import__2.initDevToolsTracing)();
|
|
142
|
+
const fileData = fs2.readFileSync(tracePath);
|
|
143
|
+
const decompressedData = tracePath.endsWith(".gz") ? zlib2.gunzipSync(fileData) : fileData;
|
|
144
|
+
const traceData = JSON.parse(
|
|
145
|
+
decompressedData.toString()
|
|
146
|
+
);
|
|
147
|
+
const processor = import__2.Trace.Processor.TraceProcessor.createWithAllHandlers();
|
|
148
|
+
await processor.parse(traceData.traceEvents, {});
|
|
149
|
+
const insights = processor.insights.get("NO_NAVIGATION");
|
|
150
|
+
const longestInteractionEvent = insights.model.INPBreakdown.longestInteractionEvent;
|
|
151
|
+
const inp = longestInteractionEvent.dur / 1e3;
|
|
152
|
+
console.log({
|
|
153
|
+
insights: insights?.model.INPBreakdown,
|
|
154
|
+
inp
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// commands/sourcemap.ts
|
|
159
|
+
var fs3 = __toESM(require("node:fs"));
|
|
160
|
+
var zlib3 = __toESM(require("node:zlib"));
|
|
161
|
+
var import__3 = require("../");
|
|
162
|
+
async function run3(tracePath) {
|
|
163
|
+
(0, import__3.initDevToolsTracing)();
|
|
164
|
+
const fileData = fs3.readFileSync(tracePath);
|
|
165
|
+
const decompressedData = tracePath.endsWith(".gz") ? zlib3.gunzipSync(fileData) : fileData;
|
|
166
|
+
const traceData = JSON.parse(
|
|
167
|
+
decompressedData.toString()
|
|
168
|
+
);
|
|
169
|
+
const traceModel = import__3.Trace.TraceModel.Model.createWithAllHandlers();
|
|
170
|
+
const resolveSourceMap = (0, import__3.createSourceMapResolver)({
|
|
171
|
+
fetch: verboseFetch
|
|
172
|
+
});
|
|
173
|
+
await traceModel.parse(traceData.traceEvents, {
|
|
174
|
+
isCPUProfile: false,
|
|
175
|
+
isFreshRecording: false,
|
|
176
|
+
metadata: traceData.metadata,
|
|
177
|
+
showAllEvents: false,
|
|
178
|
+
resolveSourceMap
|
|
179
|
+
});
|
|
180
|
+
const parsedTrace = traceModel.parsedTrace(0);
|
|
181
|
+
const parsedTraceData = parsedTrace.data;
|
|
182
|
+
const scripts = parsedTraceData.Scripts.scripts;
|
|
183
|
+
const metadataSourceMaps = traceData.metadata?.sourceMaps?.length ?? 0;
|
|
184
|
+
console.log(`Found ${scripts.length} scripts in trace`);
|
|
185
|
+
console.log(`Source maps in trace metadata: ${metadataSourceMaps}
|
|
186
|
+
`);
|
|
187
|
+
let withUrl = 0;
|
|
188
|
+
let withFrame = 0;
|
|
189
|
+
let withSourceMapUrl = 0;
|
|
190
|
+
let withElided = 0;
|
|
191
|
+
for (const script of scripts) {
|
|
192
|
+
if (script.url) withUrl++;
|
|
193
|
+
if (script.frame) withFrame++;
|
|
194
|
+
if (script.sourceMapUrl) withSourceMapUrl++;
|
|
195
|
+
if (script.sourceMapUrlElided) withElided++;
|
|
196
|
+
}
|
|
197
|
+
console.log("Script breakdown:");
|
|
198
|
+
console.log(` with url: ${withUrl}`);
|
|
199
|
+
console.log(` with frame: ${withFrame}`);
|
|
200
|
+
console.log(` with sourceMapUrl: ${withSourceMapUrl}`);
|
|
201
|
+
console.log(` with sourceMapUrlElided: ${withElided}`);
|
|
202
|
+
console.log(` (resolution requires url + frame + sourceMapUrl/elided)
|
|
203
|
+
`);
|
|
204
|
+
let resolvedCount = 0;
|
|
205
|
+
for (const script of scripts) {
|
|
206
|
+
if (!script.sourceMap) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
resolvedCount++;
|
|
210
|
+
const sourceMap = script.sourceMap;
|
|
211
|
+
const sourceURLs = sourceMap.sourceURLs();
|
|
212
|
+
console.log(
|
|
213
|
+
` ${script.url || script.scriptId} -> ${sourceURLs.length} sources, ${sourceMap.mappings().length} mappings`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
console.log(
|
|
217
|
+
`Resolved source maps for ${resolvedCount} of ${scripts.length} scripts
|
|
218
|
+
`
|
|
219
|
+
);
|
|
220
|
+
const result = (0, import__3.symbolicateTrace)(traceData.traceEvents, scripts);
|
|
221
|
+
console.log(
|
|
222
|
+
`Symbolicated ${result.symbolicatedFrames} frames across ${result.symbolicatedEvents} events`
|
|
223
|
+
);
|
|
224
|
+
const outPath = tracePath.replace(/(\.(json|json\.gz))$/i, ".symbolicated$1");
|
|
225
|
+
if (outPath === tracePath) {
|
|
226
|
+
console.error(
|
|
227
|
+
"Could not determine output path (expected .json or .json.gz extension)"
|
|
228
|
+
);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
const outputJson = JSON.stringify(traceData);
|
|
232
|
+
if (outPath.endsWith(".gz")) {
|
|
233
|
+
fs3.writeFileSync(outPath, zlib3.gzipSync(outputJson));
|
|
234
|
+
} else {
|
|
235
|
+
fs3.writeFileSync(outPath, outputJson);
|
|
236
|
+
}
|
|
237
|
+
console.log(`Wrote symbolicated trace to ${outPath}`);
|
|
238
|
+
}
|
|
239
|
+
async function verboseFetch(url) {
|
|
240
|
+
console.log(`[sourcemap] fetching ${url}`);
|
|
241
|
+
const response = await fetch(url);
|
|
242
|
+
if (!response.ok) {
|
|
243
|
+
console.warn(
|
|
244
|
+
`[sourcemap] ${url} -> ${response.status} ${response.statusText}`
|
|
245
|
+
);
|
|
246
|
+
} else {
|
|
247
|
+
console.log(`[sourcemap] ${url} -> ok`);
|
|
248
|
+
}
|
|
249
|
+
return response;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// commands/stats.ts
|
|
253
|
+
var fs4 = __toESM(require("node:fs"));
|
|
254
|
+
var zlib4 = __toESM(require("node:zlib"));
|
|
255
|
+
var import__4 = require("../");
|
|
256
|
+
async function run4(tracePath) {
|
|
257
|
+
(0, import__4.initDevToolsTracing)();
|
|
258
|
+
const fileData = fs4.readFileSync(tracePath);
|
|
259
|
+
const decompressedData = tracePath.endsWith(".gz") ? zlib4.gunzipSync(fileData) : fileData;
|
|
260
|
+
const traceData = JSON.parse(
|
|
261
|
+
decompressedData.toString()
|
|
262
|
+
);
|
|
263
|
+
const traceModel = import__4.Trace.TraceModel.Model.createWithAllHandlers({
|
|
264
|
+
debugMode: true,
|
|
265
|
+
enableAnimationsFrameHandler: false,
|
|
266
|
+
maxInvalidationEventsPerEvent: 20,
|
|
267
|
+
showAllEvents: false
|
|
268
|
+
});
|
|
269
|
+
await traceModel.parse(traceData.traceEvents, {
|
|
270
|
+
isCPUProfile: false,
|
|
271
|
+
isFreshRecording: false,
|
|
272
|
+
metadata: traceData.metadata,
|
|
273
|
+
showAllEvents: false
|
|
274
|
+
});
|
|
275
|
+
const parsedTrace = traceModel.parsedTrace(0);
|
|
276
|
+
const parsedTraceData = parsedTrace.data;
|
|
277
|
+
const startTime = import__4.Trace.Helpers.Timing.microToMilli(
|
|
278
|
+
parsedTraceData.Meta.traceBounds.min
|
|
279
|
+
);
|
|
280
|
+
const endTime = import__4.Trace.Helpers.Timing.microToMilli(
|
|
281
|
+
parsedTraceData.Meta.traceBounds.max
|
|
282
|
+
);
|
|
283
|
+
const threads = import__4.Trace.Handlers.Threads.threadsInTrace(parsedTrace.data);
|
|
284
|
+
const mainThread = threads.find(
|
|
285
|
+
(t) => t.type === import__4.Trace.Handlers.Threads.ThreadType.MAIN_THREAD
|
|
286
|
+
);
|
|
287
|
+
if (!mainThread) {
|
|
288
|
+
throw new Error("No renderer main thread found in trace file");
|
|
289
|
+
}
|
|
290
|
+
const rendererEvents = [...mainThread.entries].filter(
|
|
291
|
+
(e) => (0, import__4.entryIsVisibleInTimeline)(e, parsedTrace)
|
|
292
|
+
);
|
|
293
|
+
const stats = (0, import__4.statsForTimeRange)(rendererEvents, startTime, endTime);
|
|
294
|
+
console.log(stats);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// cli.ts
|
|
298
|
+
var commands = {
|
|
299
|
+
"selector-stats": run,
|
|
300
|
+
inp: run2,
|
|
301
|
+
sourcemap: run3,
|
|
302
|
+
stats: run4
|
|
303
|
+
};
|
|
304
|
+
function printUsage() {
|
|
305
|
+
console.log("Usage: devtools-tracing <command> <trace-file>");
|
|
306
|
+
console.log("\nCommands:");
|
|
307
|
+
console.log(" selector-stats Top CSS selectors from selector stats and invalidation tracking");
|
|
308
|
+
console.log(" inp Extract INP (Interaction to Next Paint) breakdown");
|
|
309
|
+
console.log(" sourcemap Symbolicate a trace using source maps");
|
|
310
|
+
console.log(" stats Generate timeline category statistics");
|
|
311
|
+
}
|
|
312
|
+
async function main() {
|
|
313
|
+
const [command, tracePath] = process.argv.slice(2);
|
|
314
|
+
if (!command || command === "--help" || command === "-h") {
|
|
315
|
+
printUsage();
|
|
316
|
+
process.exit(0);
|
|
317
|
+
}
|
|
318
|
+
const run5 = commands[command];
|
|
319
|
+
if (!run5) {
|
|
320
|
+
console.error(`Unknown command: ${command}`);
|
|
321
|
+
printUsage();
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
if (!tracePath) {
|
|
325
|
+
console.error(`Missing trace file path.`);
|
|
326
|
+
console.error(`Usage: devtools-tracing ${command} <trace-file>`);
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
await run5(tracePath);
|
|
330
|
+
}
|
|
331
|
+
main();
|
package/dist/index.js
CHANGED
|
@@ -14414,6 +14414,15 @@ function initDevToolsTracing() {
|
|
|
14414
14414
|
lookupClosestDevToolsLocale: identity
|
|
14415
14415
|
};
|
|
14416
14416
|
DevToolsLocale.instance({ create: true, data: data31 });
|
|
14417
|
+
for (const name of Object.values(ExperimentName)) {
|
|
14418
|
+
if (name === "*" /* ALL */) {
|
|
14419
|
+
continue;
|
|
14420
|
+
}
|
|
14421
|
+
try {
|
|
14422
|
+
experiments.register(name, name);
|
|
14423
|
+
} catch {
|
|
14424
|
+
}
|
|
14425
|
+
}
|
|
14417
14426
|
}
|
|
14418
14427
|
|
|
14419
14428
|
// lib/front_end/models/trace/trace.ts
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devtools-tracing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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
|
},
|