@mtharrison/pkg-profiler 1.0.1 → 2.0.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 +75 -48
- package/dist/index.cjs +777 -152
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -11
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +91 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +772 -151
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/async-tracker.ts +178 -0
- package/src/frame-parser.ts +3 -0
- package/src/index.ts +10 -3
- package/src/pkg-profile.ts +73 -0
- package/src/reporter/aggregate.ts +151 -64
- package/src/reporter/format.ts +10 -0
- package/src/reporter/html.ts +334 -11
- package/src/sampler.ts +186 -38
- package/src/types.ts +23 -0
- package/src/reporter.ts +0 -42
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,139 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
let node_inspector_promises = require("node:inspector/promises");
|
|
3
|
-
let node_url = require("node:url");
|
|
4
2
|
let node_fs = require("node:fs");
|
|
3
|
+
let node_inspector = require("node:inspector");
|
|
5
4
|
let node_path = require("node:path");
|
|
5
|
+
let node_async_hooks = require("node:async_hooks");
|
|
6
|
+
let node_url = require("node:url");
|
|
7
|
+
|
|
8
|
+
//#region src/async-tracker.ts
|
|
9
|
+
/**
|
|
10
|
+
* Opt-in async I/O wait time tracker using node:async_hooks.
|
|
11
|
+
*
|
|
12
|
+
* Tracks the time between async resource init (when the I/O op is started)
|
|
13
|
+
* and the first before callback (when the callback fires), attributing
|
|
14
|
+
* that wait time to the package/file/function that initiated the operation.
|
|
15
|
+
*/
|
|
16
|
+
/** Async resource types worth tracking — I/O and timers, not promises. */
|
|
17
|
+
const TRACKED_TYPES = new Set([
|
|
18
|
+
"TCPCONNECTWRAP",
|
|
19
|
+
"TCPWRAP",
|
|
20
|
+
"PIPEWRAP",
|
|
21
|
+
"PIPECONNECTWRAP",
|
|
22
|
+
"TLSWRAP",
|
|
23
|
+
"FSREQCALLBACK",
|
|
24
|
+
"FSREQPROMISE",
|
|
25
|
+
"GETADDRINFOREQWRAP",
|
|
26
|
+
"GETNAMEINFOREQWRAP",
|
|
27
|
+
"HTTPCLIENTREQUEST",
|
|
28
|
+
"HTTPINCOMINGMESSAGE",
|
|
29
|
+
"SHUTDOWNWRAP",
|
|
30
|
+
"WRITEWRAP",
|
|
31
|
+
"ZLIB",
|
|
32
|
+
"Timeout"
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* Parse a single line from an Error().stack trace into file path and function id.
|
|
36
|
+
* Returns null for lines that don't match V8's stack frame format or are node internals.
|
|
37
|
+
*
|
|
38
|
+
* Handles these V8 formats:
|
|
39
|
+
* " at functionName (/absolute/path:line:col)"
|
|
40
|
+
* " at /absolute/path:line:col"
|
|
41
|
+
* " at Object.functionName (/absolute/path:line:col)"
|
|
42
|
+
*/
|
|
43
|
+
function parseStackLine(line) {
|
|
44
|
+
const match = line.match(/^\s+at\s+(?:(.+?)\s+\()?(.+?):(\d+):\d+\)?$/);
|
|
45
|
+
if (!match) return null;
|
|
46
|
+
const rawFn = match[1] ?? "";
|
|
47
|
+
const filePath = match[2];
|
|
48
|
+
const lineNum = match[3];
|
|
49
|
+
if (filePath.startsWith("node:") || filePath.startsWith("<")) return null;
|
|
50
|
+
const fnParts = rawFn.split(".");
|
|
51
|
+
return {
|
|
52
|
+
filePath,
|
|
53
|
+
functionId: `${fnParts[fnParts.length - 1] || "<anonymous>"}:${lineNum}`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
var AsyncTracker = class {
|
|
57
|
+
resolver;
|
|
58
|
+
store;
|
|
59
|
+
thresholdUs;
|
|
60
|
+
hook = null;
|
|
61
|
+
pending = /* @__PURE__ */ new Map();
|
|
62
|
+
/**
|
|
63
|
+
* @param resolver - PackageResolver for mapping file paths to packages
|
|
64
|
+
* @param store - SampleStore to record async wait times into
|
|
65
|
+
* @param thresholdUs - Minimum wait duration in microseconds to record (default 1000 = 1ms)
|
|
66
|
+
*/
|
|
67
|
+
constructor(resolver, store, thresholdUs = 1e3) {
|
|
68
|
+
this.resolver = resolver;
|
|
69
|
+
this.store = store;
|
|
70
|
+
this.thresholdUs = thresholdUs;
|
|
71
|
+
}
|
|
72
|
+
enable() {
|
|
73
|
+
if (this.hook) return;
|
|
74
|
+
this.hook = (0, node_async_hooks.createHook)({
|
|
75
|
+
init: (asyncId, type) => {
|
|
76
|
+
if (!TRACKED_TYPES.has(type)) return;
|
|
77
|
+
const holder = {};
|
|
78
|
+
const origLimit = Error.stackTraceLimit;
|
|
79
|
+
Error.stackTraceLimit = 8;
|
|
80
|
+
Error.captureStackTrace(holder);
|
|
81
|
+
Error.stackTraceLimit = origLimit;
|
|
82
|
+
const stack = holder.stack;
|
|
83
|
+
if (!stack) return;
|
|
84
|
+
const lines = stack.split("\n");
|
|
85
|
+
let parsed = null;
|
|
86
|
+
for (let i = 1; i < lines.length; i++) {
|
|
87
|
+
const result = parseStackLine(lines[i]);
|
|
88
|
+
if (result) {
|
|
89
|
+
if (result.filePath.includes("async-tracker")) continue;
|
|
90
|
+
parsed = result;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (!parsed) return;
|
|
95
|
+
const { packageName, relativePath } = this.resolver.resolve(parsed.filePath);
|
|
96
|
+
this.pending.set(asyncId, {
|
|
97
|
+
startHrtime: process.hrtime(),
|
|
98
|
+
pkg: packageName,
|
|
99
|
+
file: relativePath,
|
|
100
|
+
fn: parsed.functionId
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
before: (asyncId) => {
|
|
104
|
+
const op = this.pending.get(asyncId);
|
|
105
|
+
if (!op) return;
|
|
106
|
+
const elapsed = process.hrtime(op.startHrtime);
|
|
107
|
+
const durationUs = elapsed[0] * 1e6 + Math.round(elapsed[1] / 1e3);
|
|
108
|
+
if (durationUs >= this.thresholdUs) this.store.record(op.pkg, op.file, op.fn, durationUs);
|
|
109
|
+
this.pending.delete(asyncId);
|
|
110
|
+
},
|
|
111
|
+
destroy: (asyncId) => {
|
|
112
|
+
this.pending.delete(asyncId);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
this.hook.enable();
|
|
116
|
+
}
|
|
117
|
+
disable() {
|
|
118
|
+
if (!this.hook) return;
|
|
119
|
+
this.hook.disable();
|
|
120
|
+
const now = process.hrtime();
|
|
121
|
+
for (const [, op] of this.pending) {
|
|
122
|
+
let secs = now[0] - op.startHrtime[0];
|
|
123
|
+
let nanos = now[1] - op.startHrtime[1];
|
|
124
|
+
if (nanos < 0) {
|
|
125
|
+
secs -= 1;
|
|
126
|
+
nanos += 1e9;
|
|
127
|
+
}
|
|
128
|
+
const durationUs = secs * 1e6 + Math.round(nanos / 1e3);
|
|
129
|
+
if (durationUs >= this.thresholdUs) this.store.record(op.pkg, op.file, op.fn, durationUs);
|
|
130
|
+
}
|
|
131
|
+
this.pending.clear();
|
|
132
|
+
this.hook = null;
|
|
133
|
+
}
|
|
134
|
+
};
|
|
6
135
|
|
|
136
|
+
//#endregion
|
|
7
137
|
//#region src/frame-parser.ts
|
|
8
138
|
/**
|
|
9
139
|
* Classify a V8 CPU profiler call frame and convert its URL to a filesystem path.
|
|
@@ -12,6 +142,9 @@ let node_path = require("node:path");
|
|
|
12
142
|
* It determines the frame kind (user code, internal, eval, wasm) and for user
|
|
13
143
|
* frames converts the URL to a filesystem path and builds a human-readable
|
|
14
144
|
* function identifier.
|
|
145
|
+
*
|
|
146
|
+
* @param frame - Raw call frame from the V8 CPU profiler.
|
|
147
|
+
* @returns A classified frame: `'user'` with file path and function id, or a non-user kind.
|
|
15
148
|
*/
|
|
16
149
|
function parseFrame(frame) {
|
|
17
150
|
const { url, functionName, lineNumber } = frame;
|
|
@@ -97,95 +230,6 @@ var PackageResolver = class {
|
|
|
97
230
|
}
|
|
98
231
|
};
|
|
99
232
|
|
|
100
|
-
//#endregion
|
|
101
|
-
//#region src/reporter/aggregate.ts
|
|
102
|
-
const THRESHOLD_PCT = .05;
|
|
103
|
-
/**
|
|
104
|
-
* Aggregate SampleStore data into a ReportData structure.
|
|
105
|
-
*
|
|
106
|
-
* @param store - SampleStore with accumulated microseconds and sample counts
|
|
107
|
-
* @param projectName - Name of the first-party project (for isFirstParty flag)
|
|
108
|
-
* @returns ReportData with packages sorted desc by time, thresholded at 5%
|
|
109
|
-
*/
|
|
110
|
-
function aggregate(store, projectName) {
|
|
111
|
-
let totalTimeUs = 0;
|
|
112
|
-
for (const fileMap of store.packages.values()) for (const funcMap of fileMap.values()) for (const us of funcMap.values()) totalTimeUs += us;
|
|
113
|
-
if (totalTimeUs === 0) return {
|
|
114
|
-
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
115
|
-
totalTimeUs: 0,
|
|
116
|
-
packages: [],
|
|
117
|
-
otherCount: 0
|
|
118
|
-
};
|
|
119
|
-
const threshold = totalTimeUs * THRESHOLD_PCT;
|
|
120
|
-
const packages = [];
|
|
121
|
-
let topLevelOtherCount = 0;
|
|
122
|
-
for (const [packageName, fileMap] of store.packages) {
|
|
123
|
-
let packageTimeUs = 0;
|
|
124
|
-
for (const funcMap of fileMap.values()) for (const us of funcMap.values()) packageTimeUs += us;
|
|
125
|
-
if (packageTimeUs < threshold) {
|
|
126
|
-
topLevelOtherCount++;
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
let packageSampleCount = 0;
|
|
130
|
-
const countFileMap = store.sampleCountsByPackage.get(packageName);
|
|
131
|
-
if (countFileMap) for (const countFuncMap of countFileMap.values()) for (const count of countFuncMap.values()) packageSampleCount += count;
|
|
132
|
-
const files = [];
|
|
133
|
-
let fileOtherCount = 0;
|
|
134
|
-
for (const [fileName, funcMap] of fileMap) {
|
|
135
|
-
let fileTimeUs = 0;
|
|
136
|
-
for (const us of funcMap.values()) fileTimeUs += us;
|
|
137
|
-
if (fileTimeUs < threshold) {
|
|
138
|
-
fileOtherCount++;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
let fileSampleCount = 0;
|
|
142
|
-
const countFuncMap = countFileMap?.get(fileName);
|
|
143
|
-
if (countFuncMap) for (const count of countFuncMap.values()) fileSampleCount += count;
|
|
144
|
-
const functions = [];
|
|
145
|
-
let funcOtherCount = 0;
|
|
146
|
-
for (const [funcName, funcTimeUs] of funcMap) {
|
|
147
|
-
if (funcTimeUs < threshold) {
|
|
148
|
-
funcOtherCount++;
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
const funcSampleCount = countFuncMap?.get(funcName) ?? 0;
|
|
152
|
-
functions.push({
|
|
153
|
-
name: funcName,
|
|
154
|
-
timeUs: funcTimeUs,
|
|
155
|
-
pct: funcTimeUs / totalTimeUs * 100,
|
|
156
|
-
sampleCount: funcSampleCount
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
functions.sort((a, b) => b.timeUs - a.timeUs);
|
|
160
|
-
files.push({
|
|
161
|
-
name: fileName,
|
|
162
|
-
timeUs: fileTimeUs,
|
|
163
|
-
pct: fileTimeUs / totalTimeUs * 100,
|
|
164
|
-
sampleCount: fileSampleCount,
|
|
165
|
-
functions,
|
|
166
|
-
otherCount: funcOtherCount
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
files.sort((a, b) => b.timeUs - a.timeUs);
|
|
170
|
-
packages.push({
|
|
171
|
-
name: packageName,
|
|
172
|
-
timeUs: packageTimeUs,
|
|
173
|
-
pct: packageTimeUs / totalTimeUs * 100,
|
|
174
|
-
isFirstParty: packageName === projectName,
|
|
175
|
-
sampleCount: packageSampleCount,
|
|
176
|
-
files,
|
|
177
|
-
otherCount: fileOtherCount
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
packages.sort((a, b) => b.timeUs - a.timeUs);
|
|
181
|
-
return {
|
|
182
|
-
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
183
|
-
totalTimeUs,
|
|
184
|
-
packages,
|
|
185
|
-
otherCount: topLevelOtherCount
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
233
|
//#endregion
|
|
190
234
|
//#region src/reporter/format.ts
|
|
191
235
|
/**
|
|
@@ -199,6 +243,9 @@ function aggregate(store, projectName) {
|
|
|
199
243
|
* - < 1s: shows rounded milliseconds (e.g. "432ms")
|
|
200
244
|
* - Sub-millisecond values round up to 1ms (never shows "0ms" for nonzero input)
|
|
201
245
|
* - Zero returns "0ms"
|
|
246
|
+
*
|
|
247
|
+
* @param us - Time value in microseconds.
|
|
248
|
+
* @returns Human-readable time string.
|
|
202
249
|
*/
|
|
203
250
|
function formatTime(us) {
|
|
204
251
|
if (us === 0) return "0ms";
|
|
@@ -210,6 +257,10 @@ function formatTime(us) {
|
|
|
210
257
|
/**
|
|
211
258
|
* Convert microseconds to percentage of total with one decimal place.
|
|
212
259
|
* Returns "0.0%" when totalUs is zero (avoids division by zero).
|
|
260
|
+
*
|
|
261
|
+
* @param us - Time value in microseconds.
|
|
262
|
+
* @param totalUs - Total time in microseconds (denominator).
|
|
263
|
+
* @returns Percentage string like `"12.3%"`.
|
|
213
264
|
*/
|
|
214
265
|
function formatPct(us, totalUs) {
|
|
215
266
|
if (totalUs === 0) return "0.0%";
|
|
@@ -219,6 +270,9 @@ function formatPct(us, totalUs) {
|
|
|
219
270
|
* Escape HTML-special characters to prevent broken markup.
|
|
220
271
|
* Handles: & < > " '
|
|
221
272
|
* Ampersand is replaced first to avoid double-escaping.
|
|
273
|
+
*
|
|
274
|
+
* @param str - Raw string to escape.
|
|
275
|
+
* @returns HTML-safe string.
|
|
222
276
|
*/
|
|
223
277
|
function escapeHtml(str) {
|
|
224
278
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -239,6 +293,7 @@ function generateCss() {
|
|
|
239
293
|
--bar-track: #e8eaed;
|
|
240
294
|
--bar-fill: #5b8def;
|
|
241
295
|
--bar-fill-fp: #3b6cf5;
|
|
296
|
+
--bar-fill-async: #f5943b;
|
|
242
297
|
--other-text: #a0a4b8;
|
|
243
298
|
--table-header-bg: #f4f5f7;
|
|
244
299
|
--shadow: 0 1px 3px rgba(0,0,0,0.06);
|
|
@@ -278,6 +333,59 @@ function generateCss() {
|
|
|
278
333
|
margin-top: 2rem;
|
|
279
334
|
}
|
|
280
335
|
|
|
336
|
+
/* Threshold slider */
|
|
337
|
+
.threshold-control {
|
|
338
|
+
display: flex;
|
|
339
|
+
align-items: center;
|
|
340
|
+
gap: 0.75rem;
|
|
341
|
+
margin-bottom: 1rem;
|
|
342
|
+
font-size: 0.85rem;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.threshold-control label {
|
|
346
|
+
font-weight: 600;
|
|
347
|
+
color: var(--muted);
|
|
348
|
+
text-transform: uppercase;
|
|
349
|
+
letter-spacing: 0.04em;
|
|
350
|
+
font-size: 0.8rem;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.threshold-control input[type="range"] {
|
|
354
|
+
flex: 1;
|
|
355
|
+
max-width: 240px;
|
|
356
|
+
height: 8px;
|
|
357
|
+
appearance: none;
|
|
358
|
+
-webkit-appearance: none;
|
|
359
|
+
background: var(--bar-track);
|
|
360
|
+
border-radius: 4px;
|
|
361
|
+
outline: none;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.threshold-control input[type="range"]::-webkit-slider-thumb {
|
|
365
|
+
appearance: none;
|
|
366
|
+
-webkit-appearance: none;
|
|
367
|
+
width: 16px;
|
|
368
|
+
height: 16px;
|
|
369
|
+
border-radius: 50%;
|
|
370
|
+
background: var(--bar-fill);
|
|
371
|
+
cursor: pointer;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.threshold-control input[type="range"]::-moz-range-thumb {
|
|
375
|
+
width: 16px;
|
|
376
|
+
height: 16px;
|
|
377
|
+
border-radius: 50%;
|
|
378
|
+
background: var(--bar-fill);
|
|
379
|
+
cursor: pointer;
|
|
380
|
+
border: none;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.threshold-control span {
|
|
384
|
+
font-family: var(--font-mono);
|
|
385
|
+
font-size: 0.85rem;
|
|
386
|
+
min-width: 3.5em;
|
|
387
|
+
}
|
|
388
|
+
|
|
281
389
|
/* Summary table */
|
|
282
390
|
table {
|
|
283
391
|
width: 100%;
|
|
@@ -316,6 +424,7 @@ function generateCss() {
|
|
|
316
424
|
|
|
317
425
|
td.pkg-name { font-family: var(--font-mono); font-size: 0.85rem; }
|
|
318
426
|
td.numeric { text-align: right; font-family: var(--font-mono); font-size: 0.85rem; }
|
|
427
|
+
td.async-col { color: var(--bar-fill-async); }
|
|
319
428
|
|
|
320
429
|
.bar-cell {
|
|
321
430
|
width: 30%;
|
|
@@ -429,6 +538,13 @@ function generateCss() {
|
|
|
429
538
|
flex-shrink: 0;
|
|
430
539
|
}
|
|
431
540
|
|
|
541
|
+
.tree-async {
|
|
542
|
+
font-family: var(--font-mono);
|
|
543
|
+
font-size: 0.8rem;
|
|
544
|
+
color: var(--bar-fill-async);
|
|
545
|
+
flex-shrink: 0;
|
|
546
|
+
}
|
|
547
|
+
|
|
432
548
|
/* Level indentation */
|
|
433
549
|
.level-0 > summary { padding-left: 0.75rem; }
|
|
434
550
|
.level-1 > summary { padding-left: 2rem; }
|
|
@@ -456,7 +572,226 @@ function generateCss() {
|
|
|
456
572
|
}
|
|
457
573
|
`;
|
|
458
574
|
}
|
|
459
|
-
function
|
|
575
|
+
function generateJs() {
|
|
576
|
+
return `
|
|
577
|
+
(function() {
|
|
578
|
+
var DATA = window.__REPORT_DATA__;
|
|
579
|
+
if (!DATA) return;
|
|
580
|
+
var HAS_ASYNC = !!(DATA.totalAsyncTimeUs && DATA.totalAsyncTimeUs > 0);
|
|
581
|
+
|
|
582
|
+
function formatTime(us) {
|
|
583
|
+
if (us === 0) return '0ms';
|
|
584
|
+
var ms = us / 1000;
|
|
585
|
+
if (ms >= 1000) return (ms / 1000).toFixed(2) + 's';
|
|
586
|
+
var rounded = Math.round(ms);
|
|
587
|
+
return (rounded < 1 ? 1 : rounded) + 'ms';
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function formatPct(us, totalUs) {
|
|
591
|
+
if (totalUs === 0) return '0.0%';
|
|
592
|
+
return ((us / totalUs) * 100).toFixed(1) + '%';
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function escapeHtml(str) {
|
|
596
|
+
return str
|
|
597
|
+
.replace(/&/g, '&')
|
|
598
|
+
.replace(/</g, '<')
|
|
599
|
+
.replace(/>/g, '>')
|
|
600
|
+
.replace(/"/g, '"')
|
|
601
|
+
.replace(/'/g, ''');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function applyThreshold(data, pct) {
|
|
605
|
+
var threshold = data.totalTimeUs * (pct / 100);
|
|
606
|
+
var filtered = [];
|
|
607
|
+
var otherCount = 0;
|
|
608
|
+
|
|
609
|
+
for (var i = 0; i < data.packages.length; i++) {
|
|
610
|
+
var pkg = data.packages[i];
|
|
611
|
+
if (pkg.timeUs < threshold) {
|
|
612
|
+
otherCount++;
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
var files = [];
|
|
617
|
+
var fileOtherCount = 0;
|
|
618
|
+
|
|
619
|
+
for (var j = 0; j < pkg.files.length; j++) {
|
|
620
|
+
var file = pkg.files[j];
|
|
621
|
+
if (file.timeUs < threshold) {
|
|
622
|
+
fileOtherCount++;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
var functions = [];
|
|
627
|
+
var funcOtherCount = 0;
|
|
628
|
+
|
|
629
|
+
for (var k = 0; k < file.functions.length; k++) {
|
|
630
|
+
var fn = file.functions[k];
|
|
631
|
+
if (fn.timeUs < threshold) {
|
|
632
|
+
funcOtherCount++;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
functions.push(fn);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
files.push({
|
|
639
|
+
name: file.name,
|
|
640
|
+
timeUs: file.timeUs,
|
|
641
|
+
pct: file.pct,
|
|
642
|
+
sampleCount: file.sampleCount,
|
|
643
|
+
asyncTimeUs: file.asyncTimeUs,
|
|
644
|
+
asyncPct: file.asyncPct,
|
|
645
|
+
asyncOpCount: file.asyncOpCount,
|
|
646
|
+
functions: functions,
|
|
647
|
+
otherCount: funcOtherCount
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
filtered.push({
|
|
652
|
+
name: pkg.name,
|
|
653
|
+
timeUs: pkg.timeUs,
|
|
654
|
+
pct: pkg.pct,
|
|
655
|
+
isFirstParty: pkg.isFirstParty,
|
|
656
|
+
sampleCount: pkg.sampleCount,
|
|
657
|
+
asyncTimeUs: pkg.asyncTimeUs,
|
|
658
|
+
asyncPct: pkg.asyncPct,
|
|
659
|
+
asyncOpCount: pkg.asyncOpCount,
|
|
660
|
+
files: files,
|
|
661
|
+
otherCount: fileOtherCount
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return { packages: filtered, otherCount: otherCount };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function renderTable(packages, otherCount, totalTimeUs) {
|
|
669
|
+
var rows = '';
|
|
670
|
+
for (var i = 0; i < packages.length; i++) {
|
|
671
|
+
var pkg = packages[i];
|
|
672
|
+
var cls = pkg.isFirstParty ? 'first-party' : 'dependency';
|
|
673
|
+
var pctVal = totalTimeUs > 0 ? (pkg.timeUs / totalTimeUs) * 100 : 0;
|
|
674
|
+
rows += '<tr class="' + cls + '">' +
|
|
675
|
+
'<td class="pkg-name">' + escapeHtml(pkg.name) + '</td>' +
|
|
676
|
+
'<td class="numeric">' + escapeHtml(formatTime(pkg.timeUs)) + '</td>' +
|
|
677
|
+
'<td class="bar-cell"><div class="bar-container">' +
|
|
678
|
+
'<div class="bar-track"><div class="bar-fill" style="width:' + pctVal.toFixed(1) + '%"></div></div>' +
|
|
679
|
+
'<span class="bar-pct">' + escapeHtml(formatPct(pkg.timeUs, totalTimeUs)) + '</span>' +
|
|
680
|
+
'</div></td>' +
|
|
681
|
+
'<td class="numeric">' + pkg.sampleCount + '</td>';
|
|
682
|
+
if (HAS_ASYNC) {
|
|
683
|
+
rows += '<td class="numeric async-col">' + escapeHtml(formatTime(pkg.asyncTimeUs || 0)) + '</td>' +
|
|
684
|
+
'<td class="numeric async-col">' + (pkg.asyncOpCount || 0) + '</td>';
|
|
685
|
+
}
|
|
686
|
+
rows += '</tr>';
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (otherCount > 0) {
|
|
690
|
+
rows += '<tr class="other-row">' +
|
|
691
|
+
'<td class="pkg-name">Other (' + otherCount + ' items)</td>' +
|
|
692
|
+
'<td class="numeric"></td>' +
|
|
693
|
+
'<td class="bar-cell"></td>' +
|
|
694
|
+
'<td class="numeric"></td>';
|
|
695
|
+
if (HAS_ASYNC) {
|
|
696
|
+
rows += '<td class="numeric"></td><td class="numeric"></td>';
|
|
697
|
+
}
|
|
698
|
+
rows += '</tr>';
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
var headers = '<th>Package</th><th>Wall Time</th><th>% of Total</th><th>Samples</th>';
|
|
702
|
+
if (HAS_ASYNC) {
|
|
703
|
+
headers += '<th>Async Wait</th><th>Async Ops</th>';
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return '<table><thead><tr>' + headers + '</tr></thead><tbody>' + rows + '</tbody></table>';
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function asyncStats(entry) {
|
|
710
|
+
if (!HAS_ASYNC) return '';
|
|
711
|
+
var at = entry.asyncTimeUs || 0;
|
|
712
|
+
var ac = entry.asyncOpCount || 0;
|
|
713
|
+
if (at === 0 && ac === 0) return '';
|
|
714
|
+
return ' <span class="tree-async">| ' + escapeHtml(formatTime(at)) + ' async · ' + ac + ' ops</span>';
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function renderTree(packages, otherCount, totalTimeUs) {
|
|
718
|
+
var html = '<div class="tree">';
|
|
719
|
+
|
|
720
|
+
for (var i = 0; i < packages.length; i++) {
|
|
721
|
+
var pkg = packages[i];
|
|
722
|
+
var fpCls = pkg.isFirstParty ? ' fp-pkg' : '';
|
|
723
|
+
html += '<details class="level-0' + fpCls + '"><summary>';
|
|
724
|
+
html += '<span class="tree-label pkg">pkg</span>';
|
|
725
|
+
html += '<span class="tree-name">' + escapeHtml(pkg.name) + '</span>';
|
|
726
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(pkg.timeUs)) + ' · ' + escapeHtml(formatPct(pkg.timeUs, totalTimeUs)) + ' · ' + pkg.sampleCount + ' samples</span>';
|
|
727
|
+
html += asyncStats(pkg);
|
|
728
|
+
html += '</summary>';
|
|
729
|
+
|
|
730
|
+
for (var j = 0; j < pkg.files.length; j++) {
|
|
731
|
+
var file = pkg.files[j];
|
|
732
|
+
html += '<details class="level-1"><summary>';
|
|
733
|
+
html += '<span class="tree-label file">file</span>';
|
|
734
|
+
html += '<span class="tree-name">' + escapeHtml(file.name) + '</span>';
|
|
735
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(file.timeUs)) + ' · ' + escapeHtml(formatPct(file.timeUs, totalTimeUs)) + ' · ' + file.sampleCount + ' samples</span>';
|
|
736
|
+
html += asyncStats(file);
|
|
737
|
+
html += '</summary>';
|
|
738
|
+
|
|
739
|
+
for (var k = 0; k < file.functions.length; k++) {
|
|
740
|
+
var fn = file.functions[k];
|
|
741
|
+
html += '<div class="level-2">';
|
|
742
|
+
html += '<span class="tree-label fn">fn</span> ';
|
|
743
|
+
html += '<span class="tree-name">' + escapeHtml(fn.name) + '</span>';
|
|
744
|
+
html += ' <span class="tree-stats">' + escapeHtml(formatTime(fn.timeUs)) + ' · ' + escapeHtml(formatPct(fn.timeUs, totalTimeUs)) + ' · ' + fn.sampleCount + ' samples</span>';
|
|
745
|
+
html += asyncStats(fn);
|
|
746
|
+
html += '</div>';
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if (file.otherCount > 0) {
|
|
750
|
+
html += '<div class="other-item indent-2">Other (' + file.otherCount + ' items)</div>';
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
html += '</details>';
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
if (pkg.otherCount > 0) {
|
|
757
|
+
html += '<div class="other-item indent-1">Other (' + pkg.otherCount + ' items)</div>';
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
html += '</details>';
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (otherCount > 0) {
|
|
764
|
+
html += '<div class="other-item">Other (' + otherCount + ' packages)</div>';
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
html += '</div>';
|
|
768
|
+
return html;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function update(pct) {
|
|
772
|
+
var result = applyThreshold(DATA, pct);
|
|
773
|
+
var summaryEl = document.getElementById('summary-container');
|
|
774
|
+
var treeEl = document.getElementById('tree-container');
|
|
775
|
+
if (summaryEl) summaryEl.innerHTML = renderTable(result.packages, result.otherCount, DATA.totalTimeUs);
|
|
776
|
+
if (treeEl) treeEl.innerHTML = renderTree(result.packages, result.otherCount, DATA.totalTimeUs);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
780
|
+
update(5);
|
|
781
|
+
var slider = document.getElementById('threshold-slider');
|
|
782
|
+
var label = document.getElementById('threshold-value');
|
|
783
|
+
if (slider) {
|
|
784
|
+
slider.addEventListener('input', function() {
|
|
785
|
+
var val = parseFloat(slider.value);
|
|
786
|
+
if (label) label.textContent = val.toFixed(1) + '%';
|
|
787
|
+
update(val);
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
})();
|
|
792
|
+
`;
|
|
793
|
+
}
|
|
794
|
+
function renderSummaryTable(packages, otherCount, totalTimeUs, hasAsync) {
|
|
460
795
|
let rows = "";
|
|
461
796
|
for (const pkg of packages) {
|
|
462
797
|
const cls = pkg.isFirstParty ? "first-party" : "dependency";
|
|
@@ -471,7 +806,9 @@ function renderSummaryTable(packages, otherCount, totalTimeUs) {
|
|
|
471
806
|
<span class="bar-pct">${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))}</span>
|
|
472
807
|
</div>
|
|
473
808
|
</td>
|
|
474
|
-
<td class="numeric">${pkg.sampleCount}</td
|
|
809
|
+
<td class="numeric">${pkg.sampleCount}</td>${hasAsync ? `
|
|
810
|
+
<td class="numeric async-col">${escapeHtml(formatTime(pkg.asyncTimeUs ?? 0))}</td>
|
|
811
|
+
<td class="numeric async-col">${pkg.asyncOpCount ?? 0}</td>` : ""}
|
|
475
812
|
</tr>`;
|
|
476
813
|
}
|
|
477
814
|
if (otherCount > 0) rows += `
|
|
@@ -479,7 +816,9 @@ function renderSummaryTable(packages, otherCount, totalTimeUs) {
|
|
|
479
816
|
<td class="pkg-name">Other (${otherCount} items)</td>
|
|
480
817
|
<td class="numeric"></td>
|
|
481
818
|
<td class="bar-cell"></td>
|
|
819
|
+
<td class="numeric"></td>${hasAsync ? `
|
|
482
820
|
<td class="numeric"></td>
|
|
821
|
+
<td class="numeric"></td>` : ""}
|
|
483
822
|
</tr>`;
|
|
484
823
|
return `
|
|
485
824
|
<table>
|
|
@@ -488,14 +827,22 @@ function renderSummaryTable(packages, otherCount, totalTimeUs) {
|
|
|
488
827
|
<th>Package</th>
|
|
489
828
|
<th>Wall Time</th>
|
|
490
829
|
<th>% of Total</th>
|
|
491
|
-
<th>Samples</th
|
|
830
|
+
<th>Samples</th>${hasAsync ? `
|
|
831
|
+
<th>Async Wait</th>
|
|
832
|
+
<th>Async Ops</th>` : ""}
|
|
492
833
|
</tr>
|
|
493
834
|
</thead>
|
|
494
835
|
<tbody>${rows}
|
|
495
836
|
</tbody>
|
|
496
837
|
</table>`;
|
|
497
838
|
}
|
|
498
|
-
function
|
|
839
|
+
function formatAsyncStats(entry) {
|
|
840
|
+
const at = entry.asyncTimeUs ?? 0;
|
|
841
|
+
const ac = entry.asyncOpCount ?? 0;
|
|
842
|
+
if (at === 0 && ac === 0) return "";
|
|
843
|
+
return ` <span class="tree-async">| ${escapeHtml(formatTime(at))} async · ${ac} ops</span>`;
|
|
844
|
+
}
|
|
845
|
+
function renderTree(packages, otherCount, totalTimeUs, hasAsync) {
|
|
499
846
|
let html = "<div class=\"tree\">";
|
|
500
847
|
for (const pkg of packages) {
|
|
501
848
|
const fpCls = pkg.isFirstParty ? " fp-pkg" : "";
|
|
@@ -504,6 +851,7 @@ function renderTree(packages, otherCount, totalTimeUs) {
|
|
|
504
851
|
html += `<span class="tree-label pkg">pkg</span>`;
|
|
505
852
|
html += `<span class="tree-name">${escapeHtml(pkg.name)}</span>`;
|
|
506
853
|
html += `<span class="tree-stats">${escapeHtml(formatTime(pkg.timeUs))} · ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} · ${pkg.sampleCount} samples</span>`;
|
|
854
|
+
if (hasAsync) html += formatAsyncStats(pkg);
|
|
507
855
|
html += `</summary>`;
|
|
508
856
|
for (const file of pkg.files) {
|
|
509
857
|
html += `<details class="level-1">`;
|
|
@@ -511,12 +859,14 @@ function renderTree(packages, otherCount, totalTimeUs) {
|
|
|
511
859
|
html += `<span class="tree-label file">file</span>`;
|
|
512
860
|
html += `<span class="tree-name">${escapeHtml(file.name)}</span>`;
|
|
513
861
|
html += `<span class="tree-stats">${escapeHtml(formatTime(file.timeUs))} · ${escapeHtml(formatPct(file.timeUs, totalTimeUs))} · ${file.sampleCount} samples</span>`;
|
|
862
|
+
if (hasAsync) html += formatAsyncStats(file);
|
|
514
863
|
html += `</summary>`;
|
|
515
864
|
for (const fn of file.functions) {
|
|
516
865
|
html += `<div class="level-2">`;
|
|
517
866
|
html += `<span class="tree-label fn">fn</span> `;
|
|
518
867
|
html += `<span class="tree-name">${escapeHtml(fn.name)}</span>`;
|
|
519
868
|
html += ` <span class="tree-stats">${escapeHtml(formatTime(fn.timeUs))} · ${escapeHtml(formatPct(fn.timeUs, totalTimeUs))} · ${fn.sampleCount} samples</span>`;
|
|
869
|
+
if (hasAsync) html += formatAsyncStats(fn);
|
|
520
870
|
html += `</div>`;
|
|
521
871
|
}
|
|
522
872
|
if (file.otherCount > 0) html += `<div class="other-item indent-2">Other (${file.otherCount} items)</div>`;
|
|
@@ -531,62 +881,237 @@ function renderTree(packages, otherCount, totalTimeUs) {
|
|
|
531
881
|
}
|
|
532
882
|
/**
|
|
533
883
|
* Render a complete self-contained HTML report from aggregated profiling data.
|
|
884
|
+
*
|
|
885
|
+
* @param data - Aggregated report data (packages, timing, project name).
|
|
886
|
+
* @returns A full HTML document string with inline CSS/JS and no external dependencies.
|
|
534
887
|
*/
|
|
535
888
|
function renderHtml(data) {
|
|
536
|
-
const
|
|
537
|
-
const
|
|
889
|
+
const hasAsync = !!(data.totalAsyncTimeUs && data.totalAsyncTimeUs > 0);
|
|
890
|
+
const summaryTable = renderSummaryTable(data.packages, data.otherCount, data.totalTimeUs, hasAsync);
|
|
891
|
+
const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs, hasAsync);
|
|
538
892
|
const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));
|
|
893
|
+
const titleName = escapeHtml(data.projectName);
|
|
894
|
+
let metaLine = `Generated ${escapeHtml(data.timestamp)} · Total wall time: ${totalFormatted}`;
|
|
895
|
+
if (hasAsync) metaLine += ` · Total async wait: ${escapeHtml(formatTime(data.totalAsyncTimeUs))}`;
|
|
896
|
+
const safeJson = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
539
897
|
return `<!DOCTYPE html>
|
|
540
898
|
<html lang="en">
|
|
541
899
|
<head>
|
|
542
900
|
<meta charset="utf-8">
|
|
543
901
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
544
|
-
<title
|
|
902
|
+
<title>${titleName} · where-you-at report</title>
|
|
545
903
|
<style>${generateCss()}
|
|
546
904
|
</style>
|
|
547
905
|
</head>
|
|
548
906
|
<body>
|
|
549
|
-
<h1
|
|
550
|
-
<div class="meta"
|
|
907
|
+
<h1>${titleName}</h1>
|
|
908
|
+
<div class="meta">${metaLine}</div>
|
|
551
909
|
|
|
552
910
|
<h2>Summary</h2>
|
|
553
|
-
|
|
911
|
+
<div class="threshold-control">
|
|
912
|
+
<label>Threshold</label>
|
|
913
|
+
<input type="range" id="threshold-slider" min="0" max="20" step="0.5" value="5">
|
|
914
|
+
<span id="threshold-value">5.0%</span>
|
|
915
|
+
</div>
|
|
916
|
+
<div id="summary-container">${summaryTable}</div>
|
|
554
917
|
|
|
555
918
|
<h2>Details</h2>
|
|
556
|
-
|
|
919
|
+
<div id="tree-container">${tree}</div>
|
|
920
|
+
|
|
921
|
+
<script>var __REPORT_DATA__ = ${safeJson};<\/script>
|
|
922
|
+
<script>${generateJs()}<\/script>
|
|
557
923
|
</body>
|
|
558
924
|
</html>`;
|
|
559
925
|
}
|
|
560
926
|
|
|
561
927
|
//#endregion
|
|
562
|
-
//#region src/
|
|
928
|
+
//#region src/pkg-profile.ts
|
|
563
929
|
/**
|
|
564
|
-
*
|
|
930
|
+
* Immutable profiling result returned by `stop()` and `profile()`.
|
|
565
931
|
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
932
|
+
* Contains aggregated per-package timing data and a convenience method
|
|
933
|
+
* to write a self-contained HTML report to disk.
|
|
568
934
|
*/
|
|
569
|
-
function generateFilename() {
|
|
935
|
+
function generateFilename(timestamp) {
|
|
570
936
|
const now = /* @__PURE__ */ new Date();
|
|
571
937
|
const pad = (n) => String(n).padStart(2, "0");
|
|
572
938
|
return `where-you-at-${`${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`}-${`${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`}.html`;
|
|
573
939
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
940
|
+
var PkgProfile = class {
|
|
941
|
+
/** When the profile was captured */
|
|
942
|
+
timestamp;
|
|
943
|
+
/** Total sampled wall time in microseconds */
|
|
944
|
+
totalTimeUs;
|
|
945
|
+
/** Package breakdown sorted by time descending (all packages, no threshold applied) */
|
|
946
|
+
packages;
|
|
947
|
+
/** Always 0 — threshold filtering is now applied client-side in the HTML report */
|
|
948
|
+
otherCount;
|
|
949
|
+
/** Project name (from package.json) */
|
|
950
|
+
projectName;
|
|
951
|
+
/** Total async wait time in microseconds (undefined when async tracking not enabled) */
|
|
952
|
+
totalAsyncTimeUs;
|
|
953
|
+
/** @internal */
|
|
954
|
+
constructor(data) {
|
|
955
|
+
this.timestamp = data.timestamp;
|
|
956
|
+
this.totalTimeUs = data.totalTimeUs;
|
|
957
|
+
this.packages = data.packages;
|
|
958
|
+
this.otherCount = data.otherCount;
|
|
959
|
+
this.projectName = data.projectName;
|
|
960
|
+
this.totalAsyncTimeUs = data.totalAsyncTimeUs;
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Write a self-contained HTML report to disk.
|
|
964
|
+
*
|
|
965
|
+
* @param path - Output file path. Defaults to `./where-you-at-{timestamp}.html` in cwd.
|
|
966
|
+
* @returns Absolute path to the written file.
|
|
967
|
+
*/
|
|
968
|
+
writeHtml(path) {
|
|
969
|
+
const html = renderHtml({
|
|
970
|
+
timestamp: this.timestamp,
|
|
971
|
+
totalTimeUs: this.totalTimeUs,
|
|
972
|
+
packages: this.packages,
|
|
973
|
+
otherCount: this.otherCount,
|
|
974
|
+
projectName: this.projectName,
|
|
975
|
+
totalAsyncTimeUs: this.totalAsyncTimeUs
|
|
976
|
+
});
|
|
977
|
+
let filepath;
|
|
978
|
+
if (path) filepath = (0, node_path.resolve)(path);
|
|
979
|
+
else {
|
|
980
|
+
const filename = generateFilename(this.timestamp);
|
|
981
|
+
filepath = (0, node_path.join)(process.cwd(), filename);
|
|
982
|
+
}
|
|
983
|
+
(0, node_fs.writeFileSync)(filepath, html, "utf-8");
|
|
984
|
+
return filepath;
|
|
580
985
|
}
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
//#endregion
|
|
989
|
+
//#region src/reporter/aggregate.ts
|
|
990
|
+
/**
|
|
991
|
+
* Sum all microseconds in a SampleStore.
|
|
992
|
+
*/
|
|
993
|
+
function sumStore(store) {
|
|
994
|
+
let total = 0;
|
|
995
|
+
for (const fileMap of store.packages.values()) for (const funcMap of fileMap.values()) for (const us of funcMap.values()) total += us;
|
|
996
|
+
return total;
|
|
581
997
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
998
|
+
/**
|
|
999
|
+
* Aggregate SampleStore data into a ReportData structure.
|
|
1000
|
+
*
|
|
1001
|
+
* @param store - SampleStore with accumulated microseconds and sample counts
|
|
1002
|
+
* @param projectName - Name of the first-party project (for isFirstParty flag)
|
|
1003
|
+
* @param asyncStore - Optional SampleStore with async wait time data
|
|
1004
|
+
* @returns ReportData with all packages sorted desc by time, no threshold applied
|
|
1005
|
+
*/
|
|
1006
|
+
function aggregate(store, projectName, asyncStore) {
|
|
1007
|
+
const totalTimeUs = sumStore(store);
|
|
1008
|
+
const totalAsyncTimeUs = asyncStore ? sumStore(asyncStore) : 0;
|
|
1009
|
+
if (totalTimeUs === 0 && totalAsyncTimeUs === 0) return {
|
|
1010
|
+
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
1011
|
+
totalTimeUs: 0,
|
|
1012
|
+
packages: [],
|
|
1013
|
+
otherCount: 0,
|
|
1014
|
+
projectName
|
|
1015
|
+
};
|
|
1016
|
+
const allPackageNames = /* @__PURE__ */ new Set();
|
|
1017
|
+
for (const name of store.packages.keys()) allPackageNames.add(name);
|
|
1018
|
+
if (asyncStore) for (const name of asyncStore.packages.keys()) allPackageNames.add(name);
|
|
1019
|
+
const packages = [];
|
|
1020
|
+
for (const packageName of allPackageNames) {
|
|
1021
|
+
const fileMap = store.packages.get(packageName);
|
|
1022
|
+
let packageTimeUs = 0;
|
|
1023
|
+
if (fileMap) for (const funcMap of fileMap.values()) for (const us of funcMap.values()) packageTimeUs += us;
|
|
1024
|
+
let packageSampleCount = 0;
|
|
1025
|
+
const countFileMap = store.sampleCountsByPackage.get(packageName);
|
|
1026
|
+
if (countFileMap) for (const countFuncMap of countFileMap.values()) for (const count of countFuncMap.values()) packageSampleCount += count;
|
|
1027
|
+
let packageAsyncTimeUs = 0;
|
|
1028
|
+
let packageAsyncOpCount = 0;
|
|
1029
|
+
const asyncFileMap = asyncStore?.packages.get(packageName);
|
|
1030
|
+
const asyncCountFileMap = asyncStore?.sampleCountsByPackage.get(packageName);
|
|
1031
|
+
if (asyncFileMap) for (const funcMap of asyncFileMap.values()) for (const us of funcMap.values()) packageAsyncTimeUs += us;
|
|
1032
|
+
if (asyncCountFileMap) for (const countFuncMap of asyncCountFileMap.values()) for (const count of countFuncMap.values()) packageAsyncOpCount += count;
|
|
1033
|
+
const allFileNames = /* @__PURE__ */ new Set();
|
|
1034
|
+
if (fileMap) for (const name of fileMap.keys()) allFileNames.add(name);
|
|
1035
|
+
if (asyncFileMap) for (const name of asyncFileMap.keys()) allFileNames.add(name);
|
|
1036
|
+
const files = [];
|
|
1037
|
+
for (const fileName of allFileNames) {
|
|
1038
|
+
const funcMap = fileMap?.get(fileName);
|
|
1039
|
+
let fileTimeUs = 0;
|
|
1040
|
+
if (funcMap) for (const us of funcMap.values()) fileTimeUs += us;
|
|
1041
|
+
let fileSampleCount = 0;
|
|
1042
|
+
const countFuncMap = countFileMap?.get(fileName);
|
|
1043
|
+
if (countFuncMap) for (const count of countFuncMap.values()) fileSampleCount += count;
|
|
1044
|
+
let fileAsyncTimeUs = 0;
|
|
1045
|
+
let fileAsyncOpCount = 0;
|
|
1046
|
+
const asyncFuncMap = asyncFileMap?.get(fileName);
|
|
1047
|
+
const asyncCountFuncMap = asyncCountFileMap?.get(fileName);
|
|
1048
|
+
if (asyncFuncMap) for (const us of asyncFuncMap.values()) fileAsyncTimeUs += us;
|
|
1049
|
+
if (asyncCountFuncMap) for (const count of asyncCountFuncMap.values()) fileAsyncOpCount += count;
|
|
1050
|
+
const allFuncNames = /* @__PURE__ */ new Set();
|
|
1051
|
+
if (funcMap) for (const name of funcMap.keys()) allFuncNames.add(name);
|
|
1052
|
+
if (asyncFuncMap) for (const name of asyncFuncMap.keys()) allFuncNames.add(name);
|
|
1053
|
+
const functions = [];
|
|
1054
|
+
for (const funcName of allFuncNames) {
|
|
1055
|
+
const funcTimeUs = funcMap?.get(funcName) ?? 0;
|
|
1056
|
+
const funcSampleCount = countFuncMap?.get(funcName) ?? 0;
|
|
1057
|
+
const funcAsyncTimeUs = asyncFuncMap?.get(funcName) ?? 0;
|
|
1058
|
+
const funcAsyncOpCount = asyncCountFuncMap?.get(funcName) ?? 0;
|
|
1059
|
+
const entry = {
|
|
1060
|
+
name: funcName,
|
|
1061
|
+
timeUs: funcTimeUs,
|
|
1062
|
+
pct: totalTimeUs > 0 ? funcTimeUs / totalTimeUs * 100 : 0,
|
|
1063
|
+
sampleCount: funcSampleCount
|
|
1064
|
+
};
|
|
1065
|
+
if (totalAsyncTimeUs > 0) {
|
|
1066
|
+
entry.asyncTimeUs = funcAsyncTimeUs;
|
|
1067
|
+
entry.asyncPct = funcAsyncTimeUs / totalAsyncTimeUs * 100;
|
|
1068
|
+
entry.asyncOpCount = funcAsyncOpCount;
|
|
1069
|
+
}
|
|
1070
|
+
functions.push(entry);
|
|
1071
|
+
}
|
|
1072
|
+
functions.sort((a, b) => b.timeUs - a.timeUs);
|
|
1073
|
+
const fileEntry = {
|
|
1074
|
+
name: fileName,
|
|
1075
|
+
timeUs: fileTimeUs,
|
|
1076
|
+
pct: totalTimeUs > 0 ? fileTimeUs / totalTimeUs * 100 : 0,
|
|
1077
|
+
sampleCount: fileSampleCount,
|
|
1078
|
+
functions,
|
|
1079
|
+
otherCount: 0
|
|
1080
|
+
};
|
|
1081
|
+
if (totalAsyncTimeUs > 0) {
|
|
1082
|
+
fileEntry.asyncTimeUs = fileAsyncTimeUs;
|
|
1083
|
+
fileEntry.asyncPct = fileAsyncTimeUs / totalAsyncTimeUs * 100;
|
|
1084
|
+
fileEntry.asyncOpCount = fileAsyncOpCount;
|
|
1085
|
+
}
|
|
1086
|
+
files.push(fileEntry);
|
|
1087
|
+
}
|
|
1088
|
+
files.sort((a, b) => b.timeUs - a.timeUs);
|
|
1089
|
+
const pkgEntry = {
|
|
1090
|
+
name: packageName,
|
|
1091
|
+
timeUs: packageTimeUs,
|
|
1092
|
+
pct: totalTimeUs > 0 ? packageTimeUs / totalTimeUs * 100 : 0,
|
|
1093
|
+
isFirstParty: packageName === projectName,
|
|
1094
|
+
sampleCount: packageSampleCount,
|
|
1095
|
+
files,
|
|
1096
|
+
otherCount: 0
|
|
1097
|
+
};
|
|
1098
|
+
if (totalAsyncTimeUs > 0) {
|
|
1099
|
+
pkgEntry.asyncTimeUs = packageAsyncTimeUs;
|
|
1100
|
+
pkgEntry.asyncPct = packageAsyncTimeUs / totalAsyncTimeUs * 100;
|
|
1101
|
+
pkgEntry.asyncOpCount = packageAsyncOpCount;
|
|
1102
|
+
}
|
|
1103
|
+
packages.push(pkgEntry);
|
|
1104
|
+
}
|
|
1105
|
+
packages.sort((a, b) => b.timeUs - a.timeUs);
|
|
1106
|
+
const result = {
|
|
1107
|
+
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
1108
|
+
totalTimeUs,
|
|
1109
|
+
packages,
|
|
1110
|
+
otherCount: 0,
|
|
1111
|
+
projectName
|
|
1112
|
+
};
|
|
1113
|
+
if (totalAsyncTimeUs > 0) result.totalAsyncTimeUs = totalAsyncTimeUs;
|
|
1114
|
+
return result;
|
|
590
1115
|
}
|
|
591
1116
|
|
|
592
1117
|
//#endregion
|
|
@@ -670,55 +1195,151 @@ var SampleStore = class {
|
|
|
670
1195
|
let session = null;
|
|
671
1196
|
let profiling = false;
|
|
672
1197
|
const store = new SampleStore();
|
|
1198
|
+
const asyncStore = new SampleStore();
|
|
673
1199
|
const resolver = new PackageResolver(process.cwd());
|
|
1200
|
+
let asyncTracker = null;
|
|
1201
|
+
/**
|
|
1202
|
+
* Promisify session.post for the normal async API path.
|
|
1203
|
+
*/
|
|
1204
|
+
function postAsync(method, params) {
|
|
1205
|
+
return new Promise((resolve, reject) => {
|
|
1206
|
+
const cb = (err, result) => {
|
|
1207
|
+
if (err) reject(err);
|
|
1208
|
+
else resolve(result);
|
|
1209
|
+
};
|
|
1210
|
+
if (params !== void 0) session.post(method, params, cb);
|
|
1211
|
+
else session.post(method, cb);
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Synchronous session.post — works because the V8 inspector executes
|
|
1216
|
+
* callbacks synchronously for in-process sessions.
|
|
1217
|
+
*/
|
|
1218
|
+
function postSync(method) {
|
|
1219
|
+
let result;
|
|
1220
|
+
let error = null;
|
|
1221
|
+
const cb = (err, params) => {
|
|
1222
|
+
error = err;
|
|
1223
|
+
result = params;
|
|
1224
|
+
};
|
|
1225
|
+
session.post(method, cb);
|
|
1226
|
+
if (error) throw error;
|
|
1227
|
+
return result;
|
|
1228
|
+
}
|
|
1229
|
+
function readProjectName(cwd) {
|
|
1230
|
+
try {
|
|
1231
|
+
const raw = (0, node_fs.readFileSync)((0, node_path.join)(cwd, "package.json"), "utf-8");
|
|
1232
|
+
return JSON.parse(raw).name ?? "app";
|
|
1233
|
+
} catch {
|
|
1234
|
+
return "app";
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
function buildEmptyProfile() {
|
|
1238
|
+
const projectName = readProjectName(process.cwd());
|
|
1239
|
+
return new PkgProfile({
|
|
1240
|
+
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
1241
|
+
totalTimeUs: 0,
|
|
1242
|
+
packages: [],
|
|
1243
|
+
otherCount: 0,
|
|
1244
|
+
projectName
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Shared logic for stopping the profiler and building a PkgProfile.
|
|
1249
|
+
* Synchronous — safe to call from process `exit` handlers.
|
|
1250
|
+
*/
|
|
1251
|
+
function stopSync() {
|
|
1252
|
+
if (!profiling || !session) return buildEmptyProfile();
|
|
1253
|
+
const { profile } = postSync("Profiler.stop");
|
|
1254
|
+
postSync("Profiler.disable");
|
|
1255
|
+
profiling = false;
|
|
1256
|
+
if (asyncTracker) {
|
|
1257
|
+
asyncTracker.disable();
|
|
1258
|
+
asyncTracker = null;
|
|
1259
|
+
}
|
|
1260
|
+
processProfile(profile);
|
|
1261
|
+
const data = aggregate(store, readProjectName(process.cwd()), asyncStore.packages.size > 0 ? asyncStore : void 0);
|
|
1262
|
+
store.clear();
|
|
1263
|
+
asyncStore.clear();
|
|
1264
|
+
return new PkgProfile(data);
|
|
1265
|
+
}
|
|
674
1266
|
/**
|
|
675
1267
|
* Start the V8 CPU profiler. If already profiling, this is a safe no-op.
|
|
1268
|
+
*
|
|
1269
|
+
* @param options - Optional configuration.
|
|
1270
|
+
* @param options.interval - Sampling interval in microseconds passed to V8 (defaults to 1000µs). Lower values = higher fidelity but more overhead.
|
|
1271
|
+
* @returns Resolves when the profiler is successfully started
|
|
676
1272
|
*/
|
|
677
|
-
async function
|
|
1273
|
+
async function start(options) {
|
|
678
1274
|
if (profiling) return;
|
|
679
1275
|
if (session === null) {
|
|
680
|
-
session = new
|
|
1276
|
+
session = new node_inspector.Session();
|
|
681
1277
|
session.connect();
|
|
682
1278
|
}
|
|
683
|
-
await
|
|
684
|
-
if (options?.interval !== void 0) await
|
|
685
|
-
await
|
|
1279
|
+
await postAsync("Profiler.enable");
|
|
1280
|
+
if (options?.interval !== void 0) await postAsync("Profiler.setSamplingInterval", { interval: options.interval });
|
|
1281
|
+
await postAsync("Profiler.start");
|
|
686
1282
|
profiling = true;
|
|
1283
|
+
if (options?.trackAsync) {
|
|
1284
|
+
asyncTracker = new AsyncTracker(resolver, asyncStore);
|
|
1285
|
+
asyncTracker.enable();
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Stop the profiler, process collected samples, and return a PkgProfile
|
|
1290
|
+
* containing the aggregated data. Resets the store afterward.
|
|
1291
|
+
*
|
|
1292
|
+
* @returns A PkgProfile with the profiling results, or a PkgProfile with empty data if no samples were collected.
|
|
1293
|
+
*/
|
|
1294
|
+
async function stop() {
|
|
1295
|
+
return stopSync();
|
|
687
1296
|
}
|
|
688
1297
|
/**
|
|
689
1298
|
* Stop the profiler (if running) and reset all accumulated sample data.
|
|
690
1299
|
*/
|
|
691
1300
|
async function clear() {
|
|
692
1301
|
if (profiling && session) {
|
|
693
|
-
|
|
694
|
-
|
|
1302
|
+
postSync("Profiler.stop");
|
|
1303
|
+
postSync("Profiler.disable");
|
|
695
1304
|
profiling = false;
|
|
696
1305
|
}
|
|
697
1306
|
store.clear();
|
|
1307
|
+
if (asyncTracker) {
|
|
1308
|
+
asyncTracker.disable();
|
|
1309
|
+
asyncTracker = null;
|
|
1310
|
+
}
|
|
1311
|
+
asyncStore.clear();
|
|
698
1312
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
*/
|
|
708
|
-
async function report() {
|
|
709
|
-
if (!profiling || !session) {
|
|
710
|
-
console.log("no samples collected");
|
|
711
|
-
return "";
|
|
1313
|
+
async function profile(fnOrOptions) {
|
|
1314
|
+
if (typeof fnOrOptions === "function") {
|
|
1315
|
+
await start();
|
|
1316
|
+
try {
|
|
1317
|
+
await fnOrOptions();
|
|
1318
|
+
} finally {
|
|
1319
|
+
return stop();
|
|
1320
|
+
}
|
|
712
1321
|
}
|
|
713
|
-
const {
|
|
714
|
-
await
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
1322
|
+
const { onExit, ...startOpts } = fnOrOptions;
|
|
1323
|
+
await start(startOpts);
|
|
1324
|
+
let handled = false;
|
|
1325
|
+
const handler = (signal) => {
|
|
1326
|
+
if (handled) return;
|
|
1327
|
+
handled = true;
|
|
1328
|
+
process.removeListener("SIGINT", onSignal);
|
|
1329
|
+
process.removeListener("SIGTERM", onSignal);
|
|
1330
|
+
process.removeListener("exit", onProcessExit);
|
|
1331
|
+
onExit(stopSync());
|
|
1332
|
+
if (signal) process.kill(process.pid, signal);
|
|
1333
|
+
};
|
|
1334
|
+
const onSignal = (signal) => {
|
|
1335
|
+
handler(signal);
|
|
1336
|
+
};
|
|
1337
|
+
const onProcessExit = () => {
|
|
1338
|
+
handler();
|
|
1339
|
+
};
|
|
1340
|
+
process.once("SIGINT", onSignal);
|
|
1341
|
+
process.once("SIGTERM", onSignal);
|
|
1342
|
+
process.once("exit", onProcessExit);
|
|
722
1343
|
}
|
|
723
1344
|
/**
|
|
724
1345
|
* Process a V8 CPUProfile: walk each sample, parse the frame, resolve
|
|
@@ -746,7 +1367,11 @@ function processProfile(profile) {
|
|
|
746
1367
|
}
|
|
747
1368
|
|
|
748
1369
|
//#endregion
|
|
1370
|
+
exports.PkgProfile = PkgProfile;
|
|
749
1371
|
exports.clear = clear;
|
|
750
|
-
exports.
|
|
751
|
-
exports.
|
|
1372
|
+
exports.profile = profile;
|
|
1373
|
+
exports.report = stop;
|
|
1374
|
+
exports.start = start;
|
|
1375
|
+
exports.stop = stop;
|
|
1376
|
+
exports.track = start;
|
|
752
1377
|
//# sourceMappingURL=index.cjs.map
|