@mtharrison/pkg-profiler 1.1.0 → 2.1.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 +981 -153
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +94 -11
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +94 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +976 -152
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/async-tracker.ts +289 -0
- package/src/frame-parser.ts +3 -0
- package/src/index.ts +10 -3
- package/src/pkg-profile.ts +77 -0
- package/src/reporter/aggregate.ts +156 -64
- package/src/reporter/format.ts +10 -0
- package/src/reporter/html.ts +441 -10
- package/src/sampler.ts +197 -38
- package/src/types.ts +23 -0
- package/src/reporter.ts +0 -42
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,223 @@
|
|
|
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
|
+
* Intervals are buffered and merged at disable() time so that overlapping
|
|
17
|
+
* concurrent I/O is not double-counted.
|
|
18
|
+
*/
|
|
19
|
+
/** Async resource types worth tracking — I/O and timers, not promises. */
|
|
20
|
+
const TRACKED_TYPES = new Set([
|
|
21
|
+
"TCPCONNECTWRAP",
|
|
22
|
+
"TCPWRAP",
|
|
23
|
+
"PIPEWRAP",
|
|
24
|
+
"PIPECONNECTWRAP",
|
|
25
|
+
"TLSWRAP",
|
|
26
|
+
"FSREQCALLBACK",
|
|
27
|
+
"FSREQPROMISE",
|
|
28
|
+
"GETADDRINFOREQWRAP",
|
|
29
|
+
"GETNAMEINFOREQWRAP",
|
|
30
|
+
"HTTPCLIENTREQUEST",
|
|
31
|
+
"HTTPINCOMINGMESSAGE",
|
|
32
|
+
"SHUTDOWNWRAP",
|
|
33
|
+
"WRITEWRAP",
|
|
34
|
+
"ZLIB",
|
|
35
|
+
"Timeout"
|
|
36
|
+
]);
|
|
37
|
+
/**
|
|
38
|
+
* Merge overlapping or adjacent intervals. Returns a new sorted array
|
|
39
|
+
* of non-overlapping intervals.
|
|
40
|
+
*/
|
|
41
|
+
function mergeIntervals(intervals) {
|
|
42
|
+
if (intervals.length <= 1) return intervals.slice();
|
|
43
|
+
const sorted = intervals.slice().sort((a, b) => a.startUs - b.startUs);
|
|
44
|
+
const merged = [{ ...sorted[0] }];
|
|
45
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
46
|
+
const current = sorted[i];
|
|
47
|
+
const last = merged[merged.length - 1];
|
|
48
|
+
if (current.startUs <= last.endUs) {
|
|
49
|
+
if (current.endUs > last.endUs) last.endUs = current.endUs;
|
|
50
|
+
} else merged.push({ ...current });
|
|
51
|
+
}
|
|
52
|
+
return merged;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Sum the durations of a list of (presumably non-overlapping) intervals.
|
|
56
|
+
*/
|
|
57
|
+
function sumIntervals(intervals) {
|
|
58
|
+
let total = 0;
|
|
59
|
+
for (const iv of intervals) total += iv.endUs - iv.startUs;
|
|
60
|
+
return total;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Convert an hrtime tuple to absolute microseconds.
|
|
64
|
+
*/
|
|
65
|
+
function hrtimeToUs(hr) {
|
|
66
|
+
return hr[0] * 1e6 + Math.round(hr[1] / 1e3);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parse a single line from an Error().stack trace into file path and function id.
|
|
70
|
+
* Returns null for lines that don't match V8's stack frame format or are node internals.
|
|
71
|
+
*
|
|
72
|
+
* Handles these V8 formats:
|
|
73
|
+
* " at functionName (/absolute/path:line:col)"
|
|
74
|
+
* " at /absolute/path:line:col"
|
|
75
|
+
* " at Object.functionName (/absolute/path:line:col)"
|
|
76
|
+
*/
|
|
77
|
+
function parseStackLine(line) {
|
|
78
|
+
const match = line.match(/^\s+at\s+(?:(.+?)\s+\()?(.+?):(\d+):\d+\)?$/);
|
|
79
|
+
if (!match) return null;
|
|
80
|
+
const rawFn = match[1] ?? "";
|
|
81
|
+
const filePath = match[2];
|
|
82
|
+
const lineNum = match[3];
|
|
83
|
+
if (filePath.startsWith("node:") || filePath.startsWith("<")) return null;
|
|
84
|
+
const fnParts = rawFn.split(".");
|
|
85
|
+
return {
|
|
86
|
+
filePath,
|
|
87
|
+
functionId: `${fnParts[fnParts.length - 1] || "<anonymous>"}:${lineNum}`
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
var AsyncTracker = class {
|
|
91
|
+
resolver;
|
|
92
|
+
thresholdUs;
|
|
93
|
+
hook = null;
|
|
94
|
+
pending = /* @__PURE__ */ new Map();
|
|
95
|
+
/** Buffered intervals keyed by "pkg\0file\0fn" */
|
|
96
|
+
keyedIntervals = /* @__PURE__ */ new Map();
|
|
97
|
+
/** Flat list of all intervals for global merging */
|
|
98
|
+
globalIntervals = [];
|
|
99
|
+
/** Origin time in absolute microseconds, set when enable() is called */
|
|
100
|
+
originUs = 0;
|
|
101
|
+
/** Merged global total set after flush() */
|
|
102
|
+
_mergedTotalUs = 0;
|
|
103
|
+
/**
|
|
104
|
+
* @param resolver - PackageResolver for mapping file paths to packages
|
|
105
|
+
* @param store - SampleStore to record async wait times into (used at flush time)
|
|
106
|
+
* @param thresholdUs - Minimum wait duration in microseconds to record (default 1000 = 1ms)
|
|
107
|
+
*/
|
|
108
|
+
constructor(resolver, store, thresholdUs = 1e3) {
|
|
109
|
+
this.store = store;
|
|
110
|
+
this.resolver = resolver;
|
|
111
|
+
this.thresholdUs = thresholdUs;
|
|
112
|
+
}
|
|
113
|
+
/** Merged global async total in microseconds, available after disable(). */
|
|
114
|
+
get mergedTotalUs() {
|
|
115
|
+
return this._mergedTotalUs;
|
|
116
|
+
}
|
|
117
|
+
enable() {
|
|
118
|
+
if (this.hook) return;
|
|
119
|
+
this.originUs = hrtimeToUs(process.hrtime());
|
|
120
|
+
this.hook = (0, node_async_hooks.createHook)({
|
|
121
|
+
init: (asyncId, type) => {
|
|
122
|
+
if (!TRACKED_TYPES.has(type)) return;
|
|
123
|
+
const holder = {};
|
|
124
|
+
const origLimit = Error.stackTraceLimit;
|
|
125
|
+
Error.stackTraceLimit = 8;
|
|
126
|
+
Error.captureStackTrace(holder);
|
|
127
|
+
Error.stackTraceLimit = origLimit;
|
|
128
|
+
const stack = holder.stack;
|
|
129
|
+
if (!stack) return;
|
|
130
|
+
const lines = stack.split("\n");
|
|
131
|
+
let parsed = null;
|
|
132
|
+
for (let i = 1; i < lines.length; i++) {
|
|
133
|
+
const result = parseStackLine(lines[i]);
|
|
134
|
+
if (result) {
|
|
135
|
+
if (result.filePath.includes("async-tracker")) continue;
|
|
136
|
+
parsed = result;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!parsed) return;
|
|
141
|
+
const { packageName, relativePath } = this.resolver.resolve(parsed.filePath);
|
|
142
|
+
this.pending.set(asyncId, {
|
|
143
|
+
startHrtime: process.hrtime(),
|
|
144
|
+
pkg: packageName,
|
|
145
|
+
file: relativePath,
|
|
146
|
+
fn: parsed.functionId
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
before: (asyncId) => {
|
|
150
|
+
const op = this.pending.get(asyncId);
|
|
151
|
+
if (!op) return;
|
|
152
|
+
const endHr = process.hrtime();
|
|
153
|
+
const startUs = hrtimeToUs(op.startHrtime);
|
|
154
|
+
const endUs = hrtimeToUs(endHr);
|
|
155
|
+
if (endUs - startUs >= this.thresholdUs) {
|
|
156
|
+
const interval = {
|
|
157
|
+
startUs,
|
|
158
|
+
endUs
|
|
159
|
+
};
|
|
160
|
+
const key = `${op.pkg}\0${op.file}\0${op.fn}`;
|
|
161
|
+
let arr = this.keyedIntervals.get(key);
|
|
162
|
+
if (!arr) {
|
|
163
|
+
arr = [];
|
|
164
|
+
this.keyedIntervals.set(key, arr);
|
|
165
|
+
}
|
|
166
|
+
arr.push(interval);
|
|
167
|
+
this.globalIntervals.push(interval);
|
|
168
|
+
}
|
|
169
|
+
this.pending.delete(asyncId);
|
|
170
|
+
},
|
|
171
|
+
destroy: (asyncId) => {
|
|
172
|
+
this.pending.delete(asyncId);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
this.hook.enable();
|
|
176
|
+
}
|
|
177
|
+
disable() {
|
|
178
|
+
if (!this.hook) return;
|
|
179
|
+
this.hook.disable();
|
|
180
|
+
const nowUs = hrtimeToUs(process.hrtime());
|
|
181
|
+
for (const [, op] of this.pending) {
|
|
182
|
+
const startUs = hrtimeToUs(op.startHrtime);
|
|
183
|
+
if (nowUs - startUs >= this.thresholdUs) {
|
|
184
|
+
const interval = {
|
|
185
|
+
startUs,
|
|
186
|
+
endUs: nowUs
|
|
187
|
+
};
|
|
188
|
+
const key = `${op.pkg}\0${op.file}\0${op.fn}`;
|
|
189
|
+
let arr = this.keyedIntervals.get(key);
|
|
190
|
+
if (!arr) {
|
|
191
|
+
arr = [];
|
|
192
|
+
this.keyedIntervals.set(key, arr);
|
|
193
|
+
}
|
|
194
|
+
arr.push(interval);
|
|
195
|
+
this.globalIntervals.push(interval);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
this.pending.clear();
|
|
199
|
+
this.hook = null;
|
|
200
|
+
this.flush();
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Merge buffered intervals and record to the store.
|
|
204
|
+
* Sets mergedTotalUs to the global merged duration.
|
|
205
|
+
*/
|
|
206
|
+
flush() {
|
|
207
|
+
for (const [key, intervals] of this.keyedIntervals) {
|
|
208
|
+
const totalUs = sumIntervals(mergeIntervals(intervals));
|
|
209
|
+
if (totalUs > 0) {
|
|
210
|
+
const parts = key.split("\0");
|
|
211
|
+
this.store.record(parts[0], parts[1], parts[2], totalUs);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
this._mergedTotalUs = sumIntervals(mergeIntervals(this.globalIntervals));
|
|
215
|
+
this.keyedIntervals.clear();
|
|
216
|
+
this.globalIntervals = [];
|
|
217
|
+
}
|
|
218
|
+
};
|
|
6
219
|
|
|
220
|
+
//#endregion
|
|
7
221
|
//#region src/frame-parser.ts
|
|
8
222
|
/**
|
|
9
223
|
* Classify a V8 CPU profiler call frame and convert its URL to a filesystem path.
|
|
@@ -12,6 +226,9 @@ let node_path = require("node:path");
|
|
|
12
226
|
* It determines the frame kind (user code, internal, eval, wasm) and for user
|
|
13
227
|
* frames converts the URL to a filesystem path and builds a human-readable
|
|
14
228
|
* function identifier.
|
|
229
|
+
*
|
|
230
|
+
* @param frame - Raw call frame from the V8 CPU profiler.
|
|
231
|
+
* @returns A classified frame: `'user'` with file path and function id, or a non-user kind.
|
|
15
232
|
*/
|
|
16
233
|
function parseFrame(frame) {
|
|
17
234
|
const { url, functionName, lineNumber } = frame;
|
|
@@ -97,97 +314,6 @@ var PackageResolver = class {
|
|
|
97
314
|
}
|
|
98
315
|
};
|
|
99
316
|
|
|
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
|
-
projectName
|
|
119
|
-
};
|
|
120
|
-
const threshold = totalTimeUs * THRESHOLD_PCT;
|
|
121
|
-
const packages = [];
|
|
122
|
-
let topLevelOtherCount = 0;
|
|
123
|
-
for (const [packageName, fileMap] of store.packages) {
|
|
124
|
-
let packageTimeUs = 0;
|
|
125
|
-
for (const funcMap of fileMap.values()) for (const us of funcMap.values()) packageTimeUs += us;
|
|
126
|
-
if (packageTimeUs < threshold) {
|
|
127
|
-
topLevelOtherCount++;
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
let packageSampleCount = 0;
|
|
131
|
-
const countFileMap = store.sampleCountsByPackage.get(packageName);
|
|
132
|
-
if (countFileMap) for (const countFuncMap of countFileMap.values()) for (const count of countFuncMap.values()) packageSampleCount += count;
|
|
133
|
-
const files = [];
|
|
134
|
-
let fileOtherCount = 0;
|
|
135
|
-
for (const [fileName, funcMap] of fileMap) {
|
|
136
|
-
let fileTimeUs = 0;
|
|
137
|
-
for (const us of funcMap.values()) fileTimeUs += us;
|
|
138
|
-
if (fileTimeUs < threshold) {
|
|
139
|
-
fileOtherCount++;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
let fileSampleCount = 0;
|
|
143
|
-
const countFuncMap = countFileMap?.get(fileName);
|
|
144
|
-
if (countFuncMap) for (const count of countFuncMap.values()) fileSampleCount += count;
|
|
145
|
-
const functions = [];
|
|
146
|
-
let funcOtherCount = 0;
|
|
147
|
-
for (const [funcName, funcTimeUs] of funcMap) {
|
|
148
|
-
if (funcTimeUs < threshold) {
|
|
149
|
-
funcOtherCount++;
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
const funcSampleCount = countFuncMap?.get(funcName) ?? 0;
|
|
153
|
-
functions.push({
|
|
154
|
-
name: funcName,
|
|
155
|
-
timeUs: funcTimeUs,
|
|
156
|
-
pct: funcTimeUs / totalTimeUs * 100,
|
|
157
|
-
sampleCount: funcSampleCount
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
functions.sort((a, b) => b.timeUs - a.timeUs);
|
|
161
|
-
files.push({
|
|
162
|
-
name: fileName,
|
|
163
|
-
timeUs: fileTimeUs,
|
|
164
|
-
pct: fileTimeUs / totalTimeUs * 100,
|
|
165
|
-
sampleCount: fileSampleCount,
|
|
166
|
-
functions,
|
|
167
|
-
otherCount: funcOtherCount
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
files.sort((a, b) => b.timeUs - a.timeUs);
|
|
171
|
-
packages.push({
|
|
172
|
-
name: packageName,
|
|
173
|
-
timeUs: packageTimeUs,
|
|
174
|
-
pct: packageTimeUs / totalTimeUs * 100,
|
|
175
|
-
isFirstParty: packageName === projectName,
|
|
176
|
-
sampleCount: packageSampleCount,
|
|
177
|
-
files,
|
|
178
|
-
otherCount: fileOtherCount
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
packages.sort((a, b) => b.timeUs - a.timeUs);
|
|
182
|
-
return {
|
|
183
|
-
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
184
|
-
totalTimeUs,
|
|
185
|
-
packages,
|
|
186
|
-
otherCount: topLevelOtherCount,
|
|
187
|
-
projectName
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
317
|
//#endregion
|
|
192
318
|
//#region src/reporter/format.ts
|
|
193
319
|
/**
|
|
@@ -201,6 +327,9 @@ function aggregate(store, projectName) {
|
|
|
201
327
|
* - < 1s: shows rounded milliseconds (e.g. "432ms")
|
|
202
328
|
* - Sub-millisecond values round up to 1ms (never shows "0ms" for nonzero input)
|
|
203
329
|
* - Zero returns "0ms"
|
|
330
|
+
*
|
|
331
|
+
* @param us - Time value in microseconds.
|
|
332
|
+
* @returns Human-readable time string.
|
|
204
333
|
*/
|
|
205
334
|
function formatTime(us) {
|
|
206
335
|
if (us === 0) return "0ms";
|
|
@@ -212,6 +341,10 @@ function formatTime(us) {
|
|
|
212
341
|
/**
|
|
213
342
|
* Convert microseconds to percentage of total with one decimal place.
|
|
214
343
|
* Returns "0.0%" when totalUs is zero (avoids division by zero).
|
|
344
|
+
*
|
|
345
|
+
* @param us - Time value in microseconds.
|
|
346
|
+
* @param totalUs - Total time in microseconds (denominator).
|
|
347
|
+
* @returns Percentage string like `"12.3%"`.
|
|
215
348
|
*/
|
|
216
349
|
function formatPct(us, totalUs) {
|
|
217
350
|
if (totalUs === 0) return "0.0%";
|
|
@@ -221,6 +354,9 @@ function formatPct(us, totalUs) {
|
|
|
221
354
|
* Escape HTML-special characters to prevent broken markup.
|
|
222
355
|
* Handles: & < > " '
|
|
223
356
|
* Ampersand is replaced first to avoid double-escaping.
|
|
357
|
+
*
|
|
358
|
+
* @param str - Raw string to escape.
|
|
359
|
+
* @returns HTML-safe string.
|
|
224
360
|
*/
|
|
225
361
|
function escapeHtml(str) {
|
|
226
362
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -241,6 +377,7 @@ function generateCss() {
|
|
|
241
377
|
--bar-track: #e8eaed;
|
|
242
378
|
--bar-fill: #5b8def;
|
|
243
379
|
--bar-fill-fp: #3b6cf5;
|
|
380
|
+
--bar-fill-async: #f5943b;
|
|
244
381
|
--other-text: #a0a4b8;
|
|
245
382
|
--table-header-bg: #f4f5f7;
|
|
246
383
|
--shadow: 0 1px 3px rgba(0,0,0,0.06);
|
|
@@ -280,6 +417,59 @@ function generateCss() {
|
|
|
280
417
|
margin-top: 2rem;
|
|
281
418
|
}
|
|
282
419
|
|
|
420
|
+
/* Threshold slider */
|
|
421
|
+
.threshold-control {
|
|
422
|
+
display: flex;
|
|
423
|
+
align-items: center;
|
|
424
|
+
gap: 0.75rem;
|
|
425
|
+
margin-bottom: 1rem;
|
|
426
|
+
font-size: 0.85rem;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.threshold-control label {
|
|
430
|
+
font-weight: 600;
|
|
431
|
+
color: var(--muted);
|
|
432
|
+
text-transform: uppercase;
|
|
433
|
+
letter-spacing: 0.04em;
|
|
434
|
+
font-size: 0.8rem;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.threshold-control input[type="range"] {
|
|
438
|
+
flex: 1;
|
|
439
|
+
max-width: 240px;
|
|
440
|
+
height: 8px;
|
|
441
|
+
appearance: none;
|
|
442
|
+
-webkit-appearance: none;
|
|
443
|
+
background: var(--bar-track);
|
|
444
|
+
border-radius: 4px;
|
|
445
|
+
outline: none;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.threshold-control input[type="range"]::-webkit-slider-thumb {
|
|
449
|
+
appearance: none;
|
|
450
|
+
-webkit-appearance: none;
|
|
451
|
+
width: 16px;
|
|
452
|
+
height: 16px;
|
|
453
|
+
border-radius: 50%;
|
|
454
|
+
background: var(--bar-fill);
|
|
455
|
+
cursor: pointer;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.threshold-control input[type="range"]::-moz-range-thumb {
|
|
459
|
+
width: 16px;
|
|
460
|
+
height: 16px;
|
|
461
|
+
border-radius: 50%;
|
|
462
|
+
background: var(--bar-fill);
|
|
463
|
+
cursor: pointer;
|
|
464
|
+
border: none;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.threshold-control span {
|
|
468
|
+
font-family: var(--font-mono);
|
|
469
|
+
font-size: 0.85rem;
|
|
470
|
+
min-width: 3.5em;
|
|
471
|
+
}
|
|
472
|
+
|
|
283
473
|
/* Summary table */
|
|
284
474
|
table {
|
|
285
475
|
width: 100%;
|
|
@@ -318,6 +508,7 @@ function generateCss() {
|
|
|
318
508
|
|
|
319
509
|
td.pkg-name { font-family: var(--font-mono); font-size: 0.85rem; }
|
|
320
510
|
td.numeric { text-align: right; font-family: var(--font-mono); font-size: 0.85rem; }
|
|
511
|
+
td.async-col { color: var(--bar-fill-async); }
|
|
321
512
|
|
|
322
513
|
.bar-cell {
|
|
323
514
|
width: 30%;
|
|
@@ -431,6 +622,13 @@ function generateCss() {
|
|
|
431
622
|
flex-shrink: 0;
|
|
432
623
|
}
|
|
433
624
|
|
|
625
|
+
.tree-async {
|
|
626
|
+
font-family: var(--font-mono);
|
|
627
|
+
font-size: 0.8rem;
|
|
628
|
+
color: var(--bar-fill-async);
|
|
629
|
+
flex-shrink: 0;
|
|
630
|
+
}
|
|
631
|
+
|
|
434
632
|
/* Level indentation */
|
|
435
633
|
.level-0 > summary { padding-left: 0.75rem; }
|
|
436
634
|
.level-1 > summary { padding-left: 2rem; }
|
|
@@ -452,13 +650,330 @@ function generateCss() {
|
|
|
452
650
|
.other-item.indent-1 { padding-left: 2rem; }
|
|
453
651
|
.other-item.indent-2 { padding-left: 3.25rem; }
|
|
454
652
|
|
|
653
|
+
/* Sort control */
|
|
654
|
+
.sort-control {
|
|
655
|
+
display: inline-flex;
|
|
656
|
+
align-items: center;
|
|
657
|
+
gap: 0.5rem;
|
|
658
|
+
margin-left: 1.5rem;
|
|
659
|
+
font-size: 0.85rem;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.sort-control label {
|
|
663
|
+
font-weight: 600;
|
|
664
|
+
color: var(--muted);
|
|
665
|
+
text-transform: uppercase;
|
|
666
|
+
letter-spacing: 0.04em;
|
|
667
|
+
font-size: 0.8rem;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
.sort-toggle {
|
|
671
|
+
display: inline-flex;
|
|
672
|
+
border: 1px solid var(--border);
|
|
673
|
+
border-radius: 4px;
|
|
674
|
+
overflow: hidden;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.sort-toggle button {
|
|
678
|
+
font-family: var(--font-sans);
|
|
679
|
+
font-size: 0.8rem;
|
|
680
|
+
padding: 0.25rem 0.6rem;
|
|
681
|
+
border: none;
|
|
682
|
+
background: #fff;
|
|
683
|
+
color: var(--muted);
|
|
684
|
+
cursor: pointer;
|
|
685
|
+
transition: background 0.15s, color 0.15s;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
.sort-toggle button + button {
|
|
689
|
+
border-left: 1px solid var(--border);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.sort-toggle button.active {
|
|
693
|
+
background: var(--bar-fill);
|
|
694
|
+
color: #fff;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.sort-toggle button.active-async {
|
|
698
|
+
background: var(--bar-fill-async);
|
|
699
|
+
color: #fff;
|
|
700
|
+
}
|
|
701
|
+
|
|
455
702
|
@media (max-width: 600px) {
|
|
456
703
|
body { padding: 1rem; }
|
|
457
704
|
.bar-cell { width: 25%; }
|
|
705
|
+
.sort-control { margin-left: 0; margin-top: 0.5rem; }
|
|
458
706
|
}
|
|
459
707
|
`;
|
|
460
708
|
}
|
|
461
|
-
function
|
|
709
|
+
function generateJs() {
|
|
710
|
+
return `
|
|
711
|
+
(function() {
|
|
712
|
+
var DATA = window.__REPORT_DATA__;
|
|
713
|
+
if (!DATA) return;
|
|
714
|
+
var HAS_ASYNC = !!(DATA.totalAsyncTimeUs && DATA.totalAsyncTimeUs > 0);
|
|
715
|
+
|
|
716
|
+
function formatTime(us) {
|
|
717
|
+
if (us === 0) return '0ms';
|
|
718
|
+
var ms = us / 1000;
|
|
719
|
+
if (ms >= 1000) return (ms / 1000).toFixed(2) + 's';
|
|
720
|
+
var rounded = Math.round(ms);
|
|
721
|
+
return (rounded < 1 ? 1 : rounded) + 'ms';
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function formatPct(us, totalUs) {
|
|
725
|
+
if (totalUs === 0) return '0.0%';
|
|
726
|
+
return ((us / totalUs) * 100).toFixed(1) + '%';
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function escapeHtml(str) {
|
|
730
|
+
return str
|
|
731
|
+
.replace(/&/g, '&')
|
|
732
|
+
.replace(/</g, '<')
|
|
733
|
+
.replace(/>/g, '>')
|
|
734
|
+
.replace(/"/g, '"')
|
|
735
|
+
.replace(/'/g, ''');
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
var sortBy = 'cpu';
|
|
739
|
+
|
|
740
|
+
function metricTime(entry) {
|
|
741
|
+
return sortBy === 'async' ? (entry.asyncTimeUs || 0) : entry.timeUs;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function sortDesc(arr) {
|
|
745
|
+
return arr.slice().sort(function(a, b) { return metricTime(b) - metricTime(a); });
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function applyThreshold(data, pct) {
|
|
749
|
+
var totalBase = sortBy === 'async' ? (data.totalAsyncTimeUs || 0) : data.totalTimeUs;
|
|
750
|
+
var threshold = totalBase * (pct / 100);
|
|
751
|
+
var filtered = [];
|
|
752
|
+
var otherCount = 0;
|
|
753
|
+
|
|
754
|
+
var pkgs = sortDesc(data.packages);
|
|
755
|
+
|
|
756
|
+
for (var i = 0; i < pkgs.length; i++) {
|
|
757
|
+
var pkg = pkgs[i];
|
|
758
|
+
if (metricTime(pkg) < threshold) {
|
|
759
|
+
otherCount++;
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
var files = [];
|
|
764
|
+
var fileOtherCount = 0;
|
|
765
|
+
|
|
766
|
+
var sortedFiles = sortDesc(pkg.files);
|
|
767
|
+
|
|
768
|
+
for (var j = 0; j < sortedFiles.length; j++) {
|
|
769
|
+
var file = sortedFiles[j];
|
|
770
|
+
if (metricTime(file) < threshold) {
|
|
771
|
+
fileOtherCount++;
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
var functions = [];
|
|
776
|
+
var funcOtherCount = 0;
|
|
777
|
+
|
|
778
|
+
var sortedFns = sortDesc(file.functions);
|
|
779
|
+
|
|
780
|
+
for (var k = 0; k < sortedFns.length; k++) {
|
|
781
|
+
var fn = sortedFns[k];
|
|
782
|
+
if (metricTime(fn) < threshold) {
|
|
783
|
+
funcOtherCount++;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
functions.push(fn);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
files.push({
|
|
790
|
+
name: file.name,
|
|
791
|
+
timeUs: file.timeUs,
|
|
792
|
+
pct: file.pct,
|
|
793
|
+
sampleCount: file.sampleCount,
|
|
794
|
+
asyncTimeUs: file.asyncTimeUs,
|
|
795
|
+
asyncPct: file.asyncPct,
|
|
796
|
+
asyncOpCount: file.asyncOpCount,
|
|
797
|
+
functions: functions,
|
|
798
|
+
otherCount: funcOtherCount
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
filtered.push({
|
|
803
|
+
name: pkg.name,
|
|
804
|
+
timeUs: pkg.timeUs,
|
|
805
|
+
pct: pkg.pct,
|
|
806
|
+
isFirstParty: pkg.isFirstParty,
|
|
807
|
+
sampleCount: pkg.sampleCount,
|
|
808
|
+
asyncTimeUs: pkg.asyncTimeUs,
|
|
809
|
+
asyncPct: pkg.asyncPct,
|
|
810
|
+
asyncOpCount: pkg.asyncOpCount,
|
|
811
|
+
files: files,
|
|
812
|
+
otherCount: fileOtherCount
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return { packages: filtered, otherCount: otherCount };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function renderTable(packages, otherCount, totalTimeUs, totalAsyncTimeUs) {
|
|
820
|
+
var rows = '';
|
|
821
|
+
var isAsync = sortBy === 'async';
|
|
822
|
+
var barTotal = isAsync ? (totalAsyncTimeUs || 0) : totalTimeUs;
|
|
823
|
+
for (var i = 0; i < packages.length; i++) {
|
|
824
|
+
var pkg = packages[i];
|
|
825
|
+
var cls = pkg.isFirstParty ? 'first-party' : 'dependency';
|
|
826
|
+
var barVal = isAsync ? (pkg.asyncTimeUs || 0) : pkg.timeUs;
|
|
827
|
+
var pctVal = barTotal > 0 ? (barVal / barTotal) * 100 : 0;
|
|
828
|
+
rows += '<tr class="' + cls + '">' +
|
|
829
|
+
'<td class="pkg-name">' + escapeHtml(pkg.name) + '</td>' +
|
|
830
|
+
'<td class="numeric">' + escapeHtml(formatTime(pkg.timeUs)) + '</td>' +
|
|
831
|
+
'<td class="bar-cell"><div class="bar-container">' +
|
|
832
|
+
'<div class="bar-track"><div class="bar-fill" style="width:' + pctVal.toFixed(1) + '%"></div></div>' +
|
|
833
|
+
'<span class="bar-pct">' + escapeHtml(formatPct(barVal, barTotal)) + '</span>' +
|
|
834
|
+
'</div></td>' +
|
|
835
|
+
'<td class="numeric">' + pkg.sampleCount + '</td>';
|
|
836
|
+
if (HAS_ASYNC) {
|
|
837
|
+
rows += '<td class="numeric async-col">' + escapeHtml(formatTime(pkg.asyncTimeUs || 0)) + '</td>' +
|
|
838
|
+
'<td class="numeric async-col">' + (pkg.asyncOpCount || 0) + '</td>';
|
|
839
|
+
}
|
|
840
|
+
rows += '</tr>';
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (otherCount > 0) {
|
|
844
|
+
rows += '<tr class="other-row">' +
|
|
845
|
+
'<td class="pkg-name">Other (' + otherCount + ' items)</td>' +
|
|
846
|
+
'<td class="numeric"></td>' +
|
|
847
|
+
'<td class="bar-cell"></td>' +
|
|
848
|
+
'<td class="numeric"></td>';
|
|
849
|
+
if (HAS_ASYNC) {
|
|
850
|
+
rows += '<td class="numeric"></td><td class="numeric"></td>';
|
|
851
|
+
}
|
|
852
|
+
rows += '</tr>';
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
var headers = '<th>Package</th><th>CPU Time</th><th>% of Total</th><th>Samples</th>';
|
|
856
|
+
if (HAS_ASYNC) {
|
|
857
|
+
headers += '<th>Async I/O Wait</th><th>Async Ops</th>';
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return '<table><thead><tr>' + headers + '</tr></thead><tbody>' + rows + '</tbody></table>';
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function asyncStats(entry) {
|
|
864
|
+
if (!HAS_ASYNC) return '';
|
|
865
|
+
var at = entry.asyncTimeUs || 0;
|
|
866
|
+
var ac = entry.asyncOpCount || 0;
|
|
867
|
+
if (at === 0 && ac === 0) return '';
|
|
868
|
+
return ' <span class="tree-async">| ' + escapeHtml(formatTime(at)) + ' async · ' + ac + ' ops</span>';
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function renderTree(packages, otherCount, totalTimeUs, totalAsyncTimeUs) {
|
|
872
|
+
var html = '<div class="tree">';
|
|
873
|
+
var isAsync = sortBy === 'async';
|
|
874
|
+
var pctTotal = isAsync ? (totalAsyncTimeUs || 0) : totalTimeUs;
|
|
875
|
+
|
|
876
|
+
for (var i = 0; i < packages.length; i++) {
|
|
877
|
+
var pkg = packages[i];
|
|
878
|
+
var fpCls = pkg.isFirstParty ? ' fp-pkg' : '';
|
|
879
|
+
var pkgTime = isAsync ? (pkg.asyncTimeUs || 0) : pkg.timeUs;
|
|
880
|
+
html += '<details class="level-0' + fpCls + '"><summary>';
|
|
881
|
+
html += '<span class="tree-label pkg">pkg</span>';
|
|
882
|
+
html += '<span class="tree-name">' + escapeHtml(pkg.name) + '</span>';
|
|
883
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(pkgTime)) + ' · ' + escapeHtml(formatPct(pkgTime, pctTotal)) + ' · ' + pkg.sampleCount + ' samples</span>';
|
|
884
|
+
html += asyncStats(pkg);
|
|
885
|
+
html += '</summary>';
|
|
886
|
+
|
|
887
|
+
for (var j = 0; j < pkg.files.length; j++) {
|
|
888
|
+
var file = pkg.files[j];
|
|
889
|
+
var fileTime = isAsync ? (file.asyncTimeUs || 0) : file.timeUs;
|
|
890
|
+
html += '<details class="level-1"><summary>';
|
|
891
|
+
html += '<span class="tree-label file">file</span>';
|
|
892
|
+
html += '<span class="tree-name">' + escapeHtml(file.name) + '</span>';
|
|
893
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(fileTime)) + ' · ' + escapeHtml(formatPct(fileTime, pctTotal)) + ' · ' + file.sampleCount + ' samples</span>';
|
|
894
|
+
html += asyncStats(file);
|
|
895
|
+
html += '</summary>';
|
|
896
|
+
|
|
897
|
+
for (var k = 0; k < file.functions.length; k++) {
|
|
898
|
+
var fn = file.functions[k];
|
|
899
|
+
var fnTime = isAsync ? (fn.asyncTimeUs || 0) : fn.timeUs;
|
|
900
|
+
html += '<div class="level-2">';
|
|
901
|
+
html += '<span class="tree-label fn">fn</span> ';
|
|
902
|
+
html += '<span class="tree-name">' + escapeHtml(fn.name) + '</span>';
|
|
903
|
+
html += ' <span class="tree-stats">' + escapeHtml(formatTime(fnTime)) + ' · ' + escapeHtml(formatPct(fnTime, pctTotal)) + ' · ' + fn.sampleCount + ' samples</span>';
|
|
904
|
+
html += asyncStats(fn);
|
|
905
|
+
html += '</div>';
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (file.otherCount > 0) {
|
|
909
|
+
html += '<div class="other-item indent-2">Other (' + file.otherCount + ' items)</div>';
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
html += '</details>';
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (pkg.otherCount > 0) {
|
|
916
|
+
html += '<div class="other-item indent-1">Other (' + pkg.otherCount + ' items)</div>';
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
html += '</details>';
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (otherCount > 0) {
|
|
923
|
+
html += '<div class="other-item">Other (' + otherCount + ' packages)</div>';
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
html += '</div>';
|
|
927
|
+
return html;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
var currentThreshold = 5;
|
|
931
|
+
|
|
932
|
+
function update(pct) {
|
|
933
|
+
currentThreshold = pct;
|
|
934
|
+
var result = applyThreshold(DATA, pct);
|
|
935
|
+
var summaryEl = document.getElementById('summary-container');
|
|
936
|
+
var treeEl = document.getElementById('tree-container');
|
|
937
|
+
if (summaryEl) summaryEl.innerHTML = renderTable(result.packages, result.otherCount, DATA.totalTimeUs, DATA.totalAsyncTimeUs);
|
|
938
|
+
if (treeEl) treeEl.innerHTML = renderTree(result.packages, result.otherCount, DATA.totalTimeUs, DATA.totalAsyncTimeUs);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function updateSortButtons() {
|
|
942
|
+
var btns = document.querySelectorAll('.sort-toggle button');
|
|
943
|
+
for (var i = 0; i < btns.length; i++) {
|
|
944
|
+
var btn = btns[i];
|
|
945
|
+
btn.className = '';
|
|
946
|
+
if (btn.getAttribute('data-sort') === sortBy) {
|
|
947
|
+
btn.className = sortBy === 'async' ? 'active-async' : 'active';
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
953
|
+
update(5);
|
|
954
|
+
var slider = document.getElementById('threshold-slider');
|
|
955
|
+
var label = document.getElementById('threshold-value');
|
|
956
|
+
if (slider) {
|
|
957
|
+
slider.addEventListener('input', function() {
|
|
958
|
+
var val = parseFloat(slider.value);
|
|
959
|
+
if (label) label.textContent = val.toFixed(1) + '%';
|
|
960
|
+
update(val);
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
var sortBtns = document.querySelectorAll('.sort-toggle button');
|
|
965
|
+
for (var i = 0; i < sortBtns.length; i++) {
|
|
966
|
+
sortBtns[i].addEventListener('click', function() {
|
|
967
|
+
sortBy = this.getAttribute('data-sort') || 'cpu';
|
|
968
|
+
updateSortButtons();
|
|
969
|
+
update(currentThreshold);
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
});
|
|
973
|
+
})();
|
|
974
|
+
`;
|
|
975
|
+
}
|
|
976
|
+
function renderSummaryTable(packages, otherCount, totalTimeUs, hasAsync) {
|
|
462
977
|
let rows = "";
|
|
463
978
|
for (const pkg of packages) {
|
|
464
979
|
const cls = pkg.isFirstParty ? "first-party" : "dependency";
|
|
@@ -473,7 +988,9 @@ function renderSummaryTable(packages, otherCount, totalTimeUs) {
|
|
|
473
988
|
<span class="bar-pct">${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))}</span>
|
|
474
989
|
</div>
|
|
475
990
|
</td>
|
|
476
|
-
<td class="numeric">${pkg.sampleCount}</td
|
|
991
|
+
<td class="numeric">${pkg.sampleCount}</td>${hasAsync ? `
|
|
992
|
+
<td class="numeric async-col">${escapeHtml(formatTime(pkg.asyncTimeUs ?? 0))}</td>
|
|
993
|
+
<td class="numeric async-col">${pkg.asyncOpCount ?? 0}</td>` : ""}
|
|
477
994
|
</tr>`;
|
|
478
995
|
}
|
|
479
996
|
if (otherCount > 0) rows += `
|
|
@@ -481,23 +998,33 @@ function renderSummaryTable(packages, otherCount, totalTimeUs) {
|
|
|
481
998
|
<td class="pkg-name">Other (${otherCount} items)</td>
|
|
482
999
|
<td class="numeric"></td>
|
|
483
1000
|
<td class="bar-cell"></td>
|
|
1001
|
+
<td class="numeric"></td>${hasAsync ? `
|
|
484
1002
|
<td class="numeric"></td>
|
|
1003
|
+
<td class="numeric"></td>` : ""}
|
|
485
1004
|
</tr>`;
|
|
486
1005
|
return `
|
|
487
1006
|
<table>
|
|
488
1007
|
<thead>
|
|
489
1008
|
<tr>
|
|
490
1009
|
<th>Package</th>
|
|
491
|
-
<th>
|
|
1010
|
+
<th>CPU Time</th>
|
|
492
1011
|
<th>% of Total</th>
|
|
493
|
-
<th>Samples</th
|
|
1012
|
+
<th>Samples</th>${hasAsync ? `
|
|
1013
|
+
<th>Async I/O Wait</th>
|
|
1014
|
+
<th>Async Ops</th>` : ""}
|
|
494
1015
|
</tr>
|
|
495
1016
|
</thead>
|
|
496
1017
|
<tbody>${rows}
|
|
497
1018
|
</tbody>
|
|
498
1019
|
</table>`;
|
|
499
1020
|
}
|
|
500
|
-
function
|
|
1021
|
+
function formatAsyncStats(entry) {
|
|
1022
|
+
const at = entry.asyncTimeUs ?? 0;
|
|
1023
|
+
const ac = entry.asyncOpCount ?? 0;
|
|
1024
|
+
if (at === 0 && ac === 0) return "";
|
|
1025
|
+
return ` <span class="tree-async">| ${escapeHtml(formatTime(at))} async · ${ac} ops</span>`;
|
|
1026
|
+
}
|
|
1027
|
+
function renderTree(packages, otherCount, totalTimeUs, hasAsync) {
|
|
501
1028
|
let html = "<div class=\"tree\">";
|
|
502
1029
|
for (const pkg of packages) {
|
|
503
1030
|
const fpCls = pkg.isFirstParty ? " fp-pkg" : "";
|
|
@@ -506,6 +1033,7 @@ function renderTree(packages, otherCount, totalTimeUs) {
|
|
|
506
1033
|
html += `<span class="tree-label pkg">pkg</span>`;
|
|
507
1034
|
html += `<span class="tree-name">${escapeHtml(pkg.name)}</span>`;
|
|
508
1035
|
html += `<span class="tree-stats">${escapeHtml(formatTime(pkg.timeUs))} · ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} · ${pkg.sampleCount} samples</span>`;
|
|
1036
|
+
if (hasAsync) html += formatAsyncStats(pkg);
|
|
509
1037
|
html += `</summary>`;
|
|
510
1038
|
for (const file of pkg.files) {
|
|
511
1039
|
html += `<details class="level-1">`;
|
|
@@ -513,12 +1041,14 @@ function renderTree(packages, otherCount, totalTimeUs) {
|
|
|
513
1041
|
html += `<span class="tree-label file">file</span>`;
|
|
514
1042
|
html += `<span class="tree-name">${escapeHtml(file.name)}</span>`;
|
|
515
1043
|
html += `<span class="tree-stats">${escapeHtml(formatTime(file.timeUs))} · ${escapeHtml(formatPct(file.timeUs, totalTimeUs))} · ${file.sampleCount} samples</span>`;
|
|
1044
|
+
if (hasAsync) html += formatAsyncStats(file);
|
|
516
1045
|
html += `</summary>`;
|
|
517
1046
|
for (const fn of file.functions) {
|
|
518
1047
|
html += `<div class="level-2">`;
|
|
519
1048
|
html += `<span class="tree-label fn">fn</span> `;
|
|
520
1049
|
html += `<span class="tree-name">${escapeHtml(fn.name)}</span>`;
|
|
521
1050
|
html += ` <span class="tree-stats">${escapeHtml(formatTime(fn.timeUs))} · ${escapeHtml(formatPct(fn.timeUs, totalTimeUs))} · ${fn.sampleCount} samples</span>`;
|
|
1051
|
+
if (hasAsync) html += formatAsyncStats(fn);
|
|
522
1052
|
html += `</div>`;
|
|
523
1053
|
}
|
|
524
1054
|
if (file.otherCount > 0) html += `<div class="other-item indent-2">Other (${file.otherCount} items)</div>`;
|
|
@@ -533,12 +1063,22 @@ function renderTree(packages, otherCount, totalTimeUs) {
|
|
|
533
1063
|
}
|
|
534
1064
|
/**
|
|
535
1065
|
* Render a complete self-contained HTML report from aggregated profiling data.
|
|
1066
|
+
*
|
|
1067
|
+
* @param data - Aggregated report data (packages, timing, project name).
|
|
1068
|
+
* @returns A full HTML document string with inline CSS/JS and no external dependencies.
|
|
536
1069
|
*/
|
|
537
1070
|
function renderHtml(data) {
|
|
538
|
-
const
|
|
539
|
-
const
|
|
1071
|
+
const hasAsync = !!(data.totalAsyncTimeUs && data.totalAsyncTimeUs > 0);
|
|
1072
|
+
const summaryTable = renderSummaryTable(data.packages, data.otherCount, data.totalTimeUs, hasAsync);
|
|
1073
|
+
const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs, hasAsync);
|
|
540
1074
|
const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));
|
|
541
1075
|
const titleName = escapeHtml(data.projectName);
|
|
1076
|
+
const wallFormatted = data.wallTimeUs ? escapeHtml(formatTime(data.wallTimeUs)) : null;
|
|
1077
|
+
let metaLine = `Generated ${escapeHtml(data.timestamp)}`;
|
|
1078
|
+
if (wallFormatted) metaLine += ` · Wall time: ${wallFormatted}`;
|
|
1079
|
+
metaLine += ` · CPU time: ${totalFormatted}`;
|
|
1080
|
+
if (hasAsync) metaLine += ` · Async I/O wait: ${escapeHtml(formatTime(data.totalAsyncTimeUs))}`;
|
|
1081
|
+
const safeJson = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
542
1082
|
return `<!DOCTYPE html>
|
|
543
1083
|
<html lang="en">
|
|
544
1084
|
<head>
|
|
@@ -550,46 +1090,226 @@ function renderHtml(data) {
|
|
|
550
1090
|
</head>
|
|
551
1091
|
<body>
|
|
552
1092
|
<h1>${titleName}</h1>
|
|
553
|
-
<div class="meta"
|
|
1093
|
+
<div class="meta">${metaLine}</div>
|
|
554
1094
|
|
|
555
1095
|
<h2>Summary</h2>
|
|
556
|
-
|
|
1096
|
+
<div class="threshold-control">
|
|
1097
|
+
<label>Threshold</label>
|
|
1098
|
+
<input type="range" id="threshold-slider" min="0" max="20" step="0.5" value="5">
|
|
1099
|
+
<span id="threshold-value">5.0%</span>${hasAsync ? `
|
|
1100
|
+
<span class="sort-control">
|
|
1101
|
+
<label>Sort by</label>
|
|
1102
|
+
<span class="sort-toggle">
|
|
1103
|
+
<button data-sort="cpu" class="active">CPU Time</button>
|
|
1104
|
+
<button data-sort="async">Async I/O Wait</button>
|
|
1105
|
+
</span>
|
|
1106
|
+
</span>` : ""}
|
|
1107
|
+
</div>
|
|
1108
|
+
<div id="summary-container">${summaryTable}</div>
|
|
557
1109
|
|
|
558
1110
|
<h2>Details</h2>
|
|
559
|
-
|
|
1111
|
+
<div id="tree-container">${tree}</div>
|
|
1112
|
+
|
|
1113
|
+
<script>var __REPORT_DATA__ = ${safeJson};<\/script>
|
|
1114
|
+
<script>${generateJs()}<\/script>
|
|
560
1115
|
</body>
|
|
561
1116
|
</html>`;
|
|
562
1117
|
}
|
|
563
1118
|
|
|
564
1119
|
//#endregion
|
|
565
|
-
//#region src/
|
|
1120
|
+
//#region src/pkg-profile.ts
|
|
566
1121
|
/**
|
|
567
|
-
*
|
|
1122
|
+
* Immutable profiling result returned by `stop()` and `profile()`.
|
|
568
1123
|
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
1124
|
+
* Contains aggregated per-package timing data and a convenience method
|
|
1125
|
+
* to write a self-contained HTML report to disk.
|
|
571
1126
|
*/
|
|
572
|
-
function generateFilename() {
|
|
1127
|
+
function generateFilename(timestamp) {
|
|
573
1128
|
const now = /* @__PURE__ */ new Date();
|
|
574
1129
|
const pad = (n) => String(n).padStart(2, "0");
|
|
575
1130
|
return `where-you-at-${`${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`}-${`${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`}.html`;
|
|
576
1131
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
1132
|
+
var PkgProfile = class {
|
|
1133
|
+
/** When the profile was captured */
|
|
1134
|
+
timestamp;
|
|
1135
|
+
/** Total sampled wall time in microseconds */
|
|
1136
|
+
totalTimeUs;
|
|
1137
|
+
/** Package breakdown sorted by time descending (all packages, no threshold applied) */
|
|
1138
|
+
packages;
|
|
1139
|
+
/** Always 0 — threshold filtering is now applied client-side in the HTML report */
|
|
1140
|
+
otherCount;
|
|
1141
|
+
/** Project name (from package.json) */
|
|
1142
|
+
projectName;
|
|
1143
|
+
/** Total async wait time in microseconds (undefined when async tracking not enabled) */
|
|
1144
|
+
totalAsyncTimeUs;
|
|
1145
|
+
/** Elapsed wall time in microseconds from start() to stop() */
|
|
1146
|
+
wallTimeUs;
|
|
1147
|
+
/** @internal */
|
|
1148
|
+
constructor(data) {
|
|
1149
|
+
this.timestamp = data.timestamp;
|
|
1150
|
+
this.totalTimeUs = data.totalTimeUs;
|
|
1151
|
+
this.packages = data.packages;
|
|
1152
|
+
this.otherCount = data.otherCount;
|
|
1153
|
+
this.projectName = data.projectName;
|
|
1154
|
+
this.totalAsyncTimeUs = data.totalAsyncTimeUs;
|
|
1155
|
+
this.wallTimeUs = data.wallTimeUs;
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* Write a self-contained HTML report to disk.
|
|
1159
|
+
*
|
|
1160
|
+
* @param path - Output file path. Defaults to `./where-you-at-{timestamp}.html` in cwd.
|
|
1161
|
+
* @returns Absolute path to the written file.
|
|
1162
|
+
*/
|
|
1163
|
+
writeHtml(path) {
|
|
1164
|
+
const html = renderHtml({
|
|
1165
|
+
timestamp: this.timestamp,
|
|
1166
|
+
totalTimeUs: this.totalTimeUs,
|
|
1167
|
+
packages: this.packages,
|
|
1168
|
+
otherCount: this.otherCount,
|
|
1169
|
+
projectName: this.projectName,
|
|
1170
|
+
totalAsyncTimeUs: this.totalAsyncTimeUs,
|
|
1171
|
+
wallTimeUs: this.wallTimeUs
|
|
1172
|
+
});
|
|
1173
|
+
let filepath;
|
|
1174
|
+
if (path) filepath = (0, node_path.resolve)(path);
|
|
1175
|
+
else {
|
|
1176
|
+
const filename = generateFilename(this.timestamp);
|
|
1177
|
+
filepath = (0, node_path.join)(process.cwd(), filename);
|
|
1178
|
+
}
|
|
1179
|
+
(0, node_fs.writeFileSync)(filepath, html, "utf-8");
|
|
1180
|
+
return filepath;
|
|
583
1181
|
}
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
//#endregion
|
|
1185
|
+
//#region src/reporter/aggregate.ts
|
|
1186
|
+
/**
|
|
1187
|
+
* Sum all microseconds in a SampleStore.
|
|
1188
|
+
*/
|
|
1189
|
+
function sumStore(store) {
|
|
1190
|
+
let total = 0;
|
|
1191
|
+
for (const fileMap of store.packages.values()) for (const funcMap of fileMap.values()) for (const us of funcMap.values()) total += us;
|
|
1192
|
+
return total;
|
|
584
1193
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
1194
|
+
/**
|
|
1195
|
+
* Aggregate SampleStore data into a ReportData structure.
|
|
1196
|
+
*
|
|
1197
|
+
* @param store - SampleStore with accumulated microseconds and sample counts
|
|
1198
|
+
* @param projectName - Name of the first-party project (for isFirstParty flag)
|
|
1199
|
+
* @param asyncStore - Optional SampleStore with async wait time data
|
|
1200
|
+
* @returns ReportData with all packages sorted desc by time, no threshold applied
|
|
1201
|
+
*/
|
|
1202
|
+
function aggregate(store, projectName, asyncStore, globalAsyncTimeUs, wallTimeUs) {
|
|
1203
|
+
const totalTimeUs = sumStore(store);
|
|
1204
|
+
const totalAsyncTimeUs = asyncStore ? sumStore(asyncStore) : 0;
|
|
1205
|
+
const headerAsyncTimeUs = globalAsyncTimeUs ?? totalAsyncTimeUs;
|
|
1206
|
+
if (totalTimeUs === 0 && totalAsyncTimeUs === 0) return {
|
|
1207
|
+
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
1208
|
+
totalTimeUs: 0,
|
|
1209
|
+
packages: [],
|
|
1210
|
+
otherCount: 0,
|
|
1211
|
+
projectName
|
|
1212
|
+
};
|
|
1213
|
+
const allPackageNames = /* @__PURE__ */ new Set();
|
|
1214
|
+
for (const name of store.packages.keys()) allPackageNames.add(name);
|
|
1215
|
+
if (asyncStore) for (const name of asyncStore.packages.keys()) allPackageNames.add(name);
|
|
1216
|
+
const packages = [];
|
|
1217
|
+
for (const packageName of allPackageNames) {
|
|
1218
|
+
const fileMap = store.packages.get(packageName);
|
|
1219
|
+
let packageTimeUs = 0;
|
|
1220
|
+
if (fileMap) for (const funcMap of fileMap.values()) for (const us of funcMap.values()) packageTimeUs += us;
|
|
1221
|
+
let packageSampleCount = 0;
|
|
1222
|
+
const countFileMap = store.sampleCountsByPackage.get(packageName);
|
|
1223
|
+
if (countFileMap) for (const countFuncMap of countFileMap.values()) for (const count of countFuncMap.values()) packageSampleCount += count;
|
|
1224
|
+
let packageAsyncTimeUs = 0;
|
|
1225
|
+
let packageAsyncOpCount = 0;
|
|
1226
|
+
const asyncFileMap = asyncStore?.packages.get(packageName);
|
|
1227
|
+
const asyncCountFileMap = asyncStore?.sampleCountsByPackage.get(packageName);
|
|
1228
|
+
if (asyncFileMap) for (const funcMap of asyncFileMap.values()) for (const us of funcMap.values()) packageAsyncTimeUs += us;
|
|
1229
|
+
if (asyncCountFileMap) for (const countFuncMap of asyncCountFileMap.values()) for (const count of countFuncMap.values()) packageAsyncOpCount += count;
|
|
1230
|
+
const allFileNames = /* @__PURE__ */ new Set();
|
|
1231
|
+
if (fileMap) for (const name of fileMap.keys()) allFileNames.add(name);
|
|
1232
|
+
if (asyncFileMap) for (const name of asyncFileMap.keys()) allFileNames.add(name);
|
|
1233
|
+
const files = [];
|
|
1234
|
+
for (const fileName of allFileNames) {
|
|
1235
|
+
const funcMap = fileMap?.get(fileName);
|
|
1236
|
+
let fileTimeUs = 0;
|
|
1237
|
+
if (funcMap) for (const us of funcMap.values()) fileTimeUs += us;
|
|
1238
|
+
let fileSampleCount = 0;
|
|
1239
|
+
const countFuncMap = countFileMap?.get(fileName);
|
|
1240
|
+
if (countFuncMap) for (const count of countFuncMap.values()) fileSampleCount += count;
|
|
1241
|
+
let fileAsyncTimeUs = 0;
|
|
1242
|
+
let fileAsyncOpCount = 0;
|
|
1243
|
+
const asyncFuncMap = asyncFileMap?.get(fileName);
|
|
1244
|
+
const asyncCountFuncMap = asyncCountFileMap?.get(fileName);
|
|
1245
|
+
if (asyncFuncMap) for (const us of asyncFuncMap.values()) fileAsyncTimeUs += us;
|
|
1246
|
+
if (asyncCountFuncMap) for (const count of asyncCountFuncMap.values()) fileAsyncOpCount += count;
|
|
1247
|
+
const allFuncNames = /* @__PURE__ */ new Set();
|
|
1248
|
+
if (funcMap) for (const name of funcMap.keys()) allFuncNames.add(name);
|
|
1249
|
+
if (asyncFuncMap) for (const name of asyncFuncMap.keys()) allFuncNames.add(name);
|
|
1250
|
+
const functions = [];
|
|
1251
|
+
for (const funcName of allFuncNames) {
|
|
1252
|
+
const funcTimeUs = funcMap?.get(funcName) ?? 0;
|
|
1253
|
+
const funcSampleCount = countFuncMap?.get(funcName) ?? 0;
|
|
1254
|
+
const funcAsyncTimeUs = asyncFuncMap?.get(funcName) ?? 0;
|
|
1255
|
+
const funcAsyncOpCount = asyncCountFuncMap?.get(funcName) ?? 0;
|
|
1256
|
+
const entry = {
|
|
1257
|
+
name: funcName,
|
|
1258
|
+
timeUs: funcTimeUs,
|
|
1259
|
+
pct: totalTimeUs > 0 ? funcTimeUs / totalTimeUs * 100 : 0,
|
|
1260
|
+
sampleCount: funcSampleCount
|
|
1261
|
+
};
|
|
1262
|
+
if (totalAsyncTimeUs > 0) {
|
|
1263
|
+
entry.asyncTimeUs = funcAsyncTimeUs;
|
|
1264
|
+
entry.asyncPct = funcAsyncTimeUs / totalAsyncTimeUs * 100;
|
|
1265
|
+
entry.asyncOpCount = funcAsyncOpCount;
|
|
1266
|
+
}
|
|
1267
|
+
functions.push(entry);
|
|
1268
|
+
}
|
|
1269
|
+
functions.sort((a, b) => b.timeUs - a.timeUs);
|
|
1270
|
+
const fileEntry = {
|
|
1271
|
+
name: fileName,
|
|
1272
|
+
timeUs: fileTimeUs,
|
|
1273
|
+
pct: totalTimeUs > 0 ? fileTimeUs / totalTimeUs * 100 : 0,
|
|
1274
|
+
sampleCount: fileSampleCount,
|
|
1275
|
+
functions,
|
|
1276
|
+
otherCount: 0
|
|
1277
|
+
};
|
|
1278
|
+
if (totalAsyncTimeUs > 0) {
|
|
1279
|
+
fileEntry.asyncTimeUs = fileAsyncTimeUs;
|
|
1280
|
+
fileEntry.asyncPct = fileAsyncTimeUs / totalAsyncTimeUs * 100;
|
|
1281
|
+
fileEntry.asyncOpCount = fileAsyncOpCount;
|
|
1282
|
+
}
|
|
1283
|
+
files.push(fileEntry);
|
|
1284
|
+
}
|
|
1285
|
+
files.sort((a, b) => b.timeUs - a.timeUs);
|
|
1286
|
+
const pkgEntry = {
|
|
1287
|
+
name: packageName,
|
|
1288
|
+
timeUs: packageTimeUs,
|
|
1289
|
+
pct: totalTimeUs > 0 ? packageTimeUs / totalTimeUs * 100 : 0,
|
|
1290
|
+
isFirstParty: packageName === projectName,
|
|
1291
|
+
sampleCount: packageSampleCount,
|
|
1292
|
+
files,
|
|
1293
|
+
otherCount: 0
|
|
1294
|
+
};
|
|
1295
|
+
if (totalAsyncTimeUs > 0) {
|
|
1296
|
+
pkgEntry.asyncTimeUs = packageAsyncTimeUs;
|
|
1297
|
+
pkgEntry.asyncPct = packageAsyncTimeUs / totalAsyncTimeUs * 100;
|
|
1298
|
+
pkgEntry.asyncOpCount = packageAsyncOpCount;
|
|
1299
|
+
}
|
|
1300
|
+
packages.push(pkgEntry);
|
|
1301
|
+
}
|
|
1302
|
+
packages.sort((a, b) => b.timeUs - a.timeUs);
|
|
1303
|
+
const result = {
|
|
1304
|
+
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
1305
|
+
totalTimeUs,
|
|
1306
|
+
packages,
|
|
1307
|
+
otherCount: 0,
|
|
1308
|
+
projectName
|
|
1309
|
+
};
|
|
1310
|
+
if (headerAsyncTimeUs > 0) result.totalAsyncTimeUs = headerAsyncTimeUs;
|
|
1311
|
+
if (wallTimeUs !== void 0) result.wallTimeUs = wallTimeUs;
|
|
1312
|
+
return result;
|
|
593
1313
|
}
|
|
594
1314
|
|
|
595
1315
|
//#endregion
|
|
@@ -672,56 +1392,160 @@ var SampleStore = class {
|
|
|
672
1392
|
//#region src/sampler.ts
|
|
673
1393
|
let session = null;
|
|
674
1394
|
let profiling = false;
|
|
1395
|
+
let startHrtime = null;
|
|
675
1396
|
const store = new SampleStore();
|
|
1397
|
+
const asyncStore = new SampleStore();
|
|
676
1398
|
const resolver = new PackageResolver(process.cwd());
|
|
1399
|
+
let asyncTracker = null;
|
|
1400
|
+
/**
|
|
1401
|
+
* Promisify session.post for the normal async API path.
|
|
1402
|
+
*/
|
|
1403
|
+
function postAsync(method, params) {
|
|
1404
|
+
return new Promise((resolve, reject) => {
|
|
1405
|
+
const cb = (err, result) => {
|
|
1406
|
+
if (err) reject(err);
|
|
1407
|
+
else resolve(result);
|
|
1408
|
+
};
|
|
1409
|
+
if (params !== void 0) session.post(method, params, cb);
|
|
1410
|
+
else session.post(method, cb);
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Synchronous session.post — works because the V8 inspector executes
|
|
1415
|
+
* callbacks synchronously for in-process sessions.
|
|
1416
|
+
*/
|
|
1417
|
+
function postSync(method) {
|
|
1418
|
+
let result;
|
|
1419
|
+
let error = null;
|
|
1420
|
+
const cb = (err, params) => {
|
|
1421
|
+
error = err;
|
|
1422
|
+
result = params;
|
|
1423
|
+
};
|
|
1424
|
+
session.post(method, cb);
|
|
1425
|
+
if (error) throw error;
|
|
1426
|
+
return result;
|
|
1427
|
+
}
|
|
1428
|
+
function readProjectName(cwd) {
|
|
1429
|
+
try {
|
|
1430
|
+
const raw = (0, node_fs.readFileSync)((0, node_path.join)(cwd, "package.json"), "utf-8");
|
|
1431
|
+
return JSON.parse(raw).name ?? "app";
|
|
1432
|
+
} catch {
|
|
1433
|
+
return "app";
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
function buildEmptyProfile() {
|
|
1437
|
+
const projectName = readProjectName(process.cwd());
|
|
1438
|
+
return new PkgProfile({
|
|
1439
|
+
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
1440
|
+
totalTimeUs: 0,
|
|
1441
|
+
packages: [],
|
|
1442
|
+
otherCount: 0,
|
|
1443
|
+
projectName
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Shared logic for stopping the profiler and building a PkgProfile.
|
|
1448
|
+
* Synchronous — safe to call from process `exit` handlers.
|
|
1449
|
+
*/
|
|
1450
|
+
function stopSync() {
|
|
1451
|
+
if (!profiling || !session) return buildEmptyProfile();
|
|
1452
|
+
const elapsed = startHrtime ? process.hrtime(startHrtime) : null;
|
|
1453
|
+
const wallTimeUs = elapsed ? elapsed[0] * 1e6 + Math.round(elapsed[1] / 1e3) : void 0;
|
|
1454
|
+
startHrtime = null;
|
|
1455
|
+
const { profile } = postSync("Profiler.stop");
|
|
1456
|
+
postSync("Profiler.disable");
|
|
1457
|
+
profiling = false;
|
|
1458
|
+
let globalAsyncTimeUs;
|
|
1459
|
+
if (asyncTracker) {
|
|
1460
|
+
asyncTracker.disable();
|
|
1461
|
+
globalAsyncTimeUs = asyncTracker.mergedTotalUs;
|
|
1462
|
+
asyncTracker = null;
|
|
1463
|
+
}
|
|
1464
|
+
processProfile(profile);
|
|
1465
|
+
const data = aggregate(store, readProjectName(process.cwd()), asyncStore.packages.size > 0 ? asyncStore : void 0, globalAsyncTimeUs, wallTimeUs);
|
|
1466
|
+
store.clear();
|
|
1467
|
+
asyncStore.clear();
|
|
1468
|
+
return new PkgProfile(data);
|
|
1469
|
+
}
|
|
677
1470
|
/**
|
|
678
1471
|
* Start the V8 CPU profiler. If already profiling, this is a safe no-op.
|
|
1472
|
+
*
|
|
1473
|
+
* @param options - Optional configuration.
|
|
1474
|
+
* @param options.interval - Sampling interval in microseconds passed to V8 (defaults to 1000µs). Lower values = higher fidelity but more overhead.
|
|
1475
|
+
* @returns Resolves when the profiler is successfully started
|
|
679
1476
|
*/
|
|
680
|
-
async function
|
|
1477
|
+
async function start(options) {
|
|
681
1478
|
if (profiling) return;
|
|
682
1479
|
if (session === null) {
|
|
683
|
-
session = new
|
|
1480
|
+
session = new node_inspector.Session();
|
|
684
1481
|
session.connect();
|
|
685
1482
|
}
|
|
686
|
-
await
|
|
687
|
-
if (options?.interval !== void 0) await
|
|
688
|
-
await
|
|
1483
|
+
await postAsync("Profiler.enable");
|
|
1484
|
+
if (options?.interval !== void 0) await postAsync("Profiler.setSamplingInterval", { interval: options.interval });
|
|
1485
|
+
await postAsync("Profiler.start");
|
|
689
1486
|
profiling = true;
|
|
1487
|
+
startHrtime = process.hrtime();
|
|
1488
|
+
if (options?.trackAsync) {
|
|
1489
|
+
asyncTracker = new AsyncTracker(resolver, asyncStore);
|
|
1490
|
+
asyncTracker.enable();
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Stop the profiler, process collected samples, and return a PkgProfile
|
|
1495
|
+
* containing the aggregated data. Resets the store afterward.
|
|
1496
|
+
*
|
|
1497
|
+
* @returns A PkgProfile with the profiling results, or a PkgProfile with empty data if no samples were collected.
|
|
1498
|
+
*/
|
|
1499
|
+
async function stop() {
|
|
1500
|
+
return stopSync();
|
|
690
1501
|
}
|
|
691
1502
|
/**
|
|
692
1503
|
* Stop the profiler (if running) and reset all accumulated sample data.
|
|
693
1504
|
*/
|
|
694
1505
|
async function clear() {
|
|
695
1506
|
if (profiling && session) {
|
|
696
|
-
|
|
697
|
-
|
|
1507
|
+
postSync("Profiler.stop");
|
|
1508
|
+
postSync("Profiler.disable");
|
|
698
1509
|
profiling = false;
|
|
699
1510
|
}
|
|
1511
|
+
startHrtime = null;
|
|
700
1512
|
store.clear();
|
|
1513
|
+
if (asyncTracker) {
|
|
1514
|
+
asyncTracker.disable();
|
|
1515
|
+
asyncTracker = null;
|
|
1516
|
+
}
|
|
1517
|
+
asyncStore.clear();
|
|
701
1518
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
*/
|
|
711
|
-
async function report() {
|
|
712
|
-
if (!profiling || !session) {
|
|
713
|
-
console.log("no samples collected");
|
|
714
|
-
return "";
|
|
1519
|
+
async function profile(fnOrOptions) {
|
|
1520
|
+
if (typeof fnOrOptions === "function") {
|
|
1521
|
+
await start();
|
|
1522
|
+
try {
|
|
1523
|
+
await fnOrOptions();
|
|
1524
|
+
} finally {
|
|
1525
|
+
return stop();
|
|
1526
|
+
}
|
|
715
1527
|
}
|
|
716
|
-
const {
|
|
717
|
-
await
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1528
|
+
const { onExit, ...startOpts } = fnOrOptions;
|
|
1529
|
+
await start(startOpts);
|
|
1530
|
+
let handled = false;
|
|
1531
|
+
const handler = (signal) => {
|
|
1532
|
+
if (handled) return;
|
|
1533
|
+
handled = true;
|
|
1534
|
+
process.removeListener("SIGINT", onSignal);
|
|
1535
|
+
process.removeListener("SIGTERM", onSignal);
|
|
1536
|
+
process.removeListener("exit", onProcessExit);
|
|
1537
|
+
onExit(stopSync());
|
|
1538
|
+
if (signal) process.kill(process.pid, signal);
|
|
1539
|
+
};
|
|
1540
|
+
const onSignal = (signal) => {
|
|
1541
|
+
handler(signal);
|
|
1542
|
+
};
|
|
1543
|
+
const onProcessExit = () => {
|
|
1544
|
+
handler();
|
|
1545
|
+
};
|
|
1546
|
+
process.once("SIGINT", onSignal);
|
|
1547
|
+
process.once("SIGTERM", onSignal);
|
|
1548
|
+
process.once("exit", onProcessExit);
|
|
725
1549
|
}
|
|
726
1550
|
/**
|
|
727
1551
|
* Process a V8 CPUProfile: walk each sample, parse the frame, resolve
|
|
@@ -749,7 +1573,11 @@ function processProfile(profile) {
|
|
|
749
1573
|
}
|
|
750
1574
|
|
|
751
1575
|
//#endregion
|
|
1576
|
+
exports.PkgProfile = PkgProfile;
|
|
752
1577
|
exports.clear = clear;
|
|
753
|
-
exports.
|
|
754
|
-
exports.
|
|
1578
|
+
exports.profile = profile;
|
|
1579
|
+
exports.report = stop;
|
|
1580
|
+
exports.start = start;
|
|
1581
|
+
exports.stop = stop;
|
|
1582
|
+
exports.track = start;
|
|
755
1583
|
//# sourceMappingURL=index.cjs.map
|