@mtharrison/pkg-profiler 1.0.1 → 2.0.0

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