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