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