@mtharrison/pkg-profiler 2.0.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/dist/index.cjs CHANGED
@@ -12,6 +12,9 @@ let node_url = require("node:url");
12
12
  * Tracks the time between async resource init (when the I/O op is started)
13
13
  * and the first before callback (when the callback fires), attributing
14
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.
15
18
  */
16
19
  /** Async resource types worth tracking — I/O and timers, not promises. */
17
20
  const TRACKED_TYPES = new Set([
@@ -32,6 +35,37 @@ const TRACKED_TYPES = new Set([
32
35
  "Timeout"
33
36
  ]);
34
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
+ /**
35
69
  * Parse a single line from an Error().stack trace into file path and function id.
36
70
  * Returns null for lines that don't match V8's stack frame format or are node internals.
37
71
  *
@@ -55,22 +89,34 @@ function parseStackLine(line) {
55
89
  }
56
90
  var AsyncTracker = class {
57
91
  resolver;
58
- store;
59
92
  thresholdUs;
60
93
  hook = null;
61
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;
62
103
  /**
63
104
  * @param resolver - PackageResolver for mapping file paths to packages
64
- * @param store - SampleStore to record async wait times into
105
+ * @param store - SampleStore to record async wait times into (used at flush time)
65
106
  * @param thresholdUs - Minimum wait duration in microseconds to record (default 1000 = 1ms)
66
107
  */
67
108
  constructor(resolver, store, thresholdUs = 1e3) {
68
- this.resolver = resolver;
69
109
  this.store = store;
110
+ this.resolver = resolver;
70
111
  this.thresholdUs = thresholdUs;
71
112
  }
113
+ /** Merged global async total in microseconds, available after disable(). */
114
+ get mergedTotalUs() {
115
+ return this._mergedTotalUs;
116
+ }
72
117
  enable() {
73
118
  if (this.hook) return;
119
+ this.originUs = hrtimeToUs(process.hrtime());
74
120
  this.hook = (0, node_async_hooks.createHook)({
75
121
  init: (asyncId, type) => {
76
122
  if (!TRACKED_TYPES.has(type)) return;
@@ -103,9 +149,23 @@ var AsyncTracker = class {
103
149
  before: (asyncId) => {
104
150
  const op = this.pending.get(asyncId);
105
151
  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);
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
+ }
109
169
  this.pending.delete(asyncId);
110
170
  },
111
171
  destroy: (asyncId) => {
@@ -117,19 +177,43 @@ var AsyncTracker = class {
117
177
  disable() {
118
178
  if (!this.hook) return;
119
179
  this.hook.disable();
120
- const now = process.hrtime();
180
+ const nowUs = hrtimeToUs(process.hrtime());
121
181
  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;
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);
127
196
  }
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
197
  }
131
198
  this.pending.clear();
132
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 = [];
133
217
  }
134
218
  };
135
219
 
@@ -566,9 +650,59 @@ function generateCss() {
566
650
  .other-item.indent-1 { padding-left: 2rem; }
567
651
  .other-item.indent-2 { padding-left: 3.25rem; }
568
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
+
569
702
  @media (max-width: 600px) {
570
703
  body { padding: 1rem; }
571
704
  .bar-cell { width: 25%; }
705
+ .sort-control { margin-left: 0; margin-top: 0.5rem; }
572
706
  }
573
707
  `;
574
708
  }
@@ -601,14 +735,27 @@ function generateJs() {
601
735
  .replace(/'/g, '&#39;');
602
736
  }
603
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
+
604
748
  function applyThreshold(data, pct) {
605
- var threshold = data.totalTimeUs * (pct / 100);
749
+ var totalBase = sortBy === 'async' ? (data.totalAsyncTimeUs || 0) : data.totalTimeUs;
750
+ var threshold = totalBase * (pct / 100);
606
751
  var filtered = [];
607
752
  var otherCount = 0;
608
753
 
609
- for (var i = 0; i < data.packages.length; i++) {
610
- var pkg = data.packages[i];
611
- if (pkg.timeUs < threshold) {
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) {
612
759
  otherCount++;
613
760
  continue;
614
761
  }
@@ -616,9 +763,11 @@ function generateJs() {
616
763
  var files = [];
617
764
  var fileOtherCount = 0;
618
765
 
619
- for (var j = 0; j < pkg.files.length; j++) {
620
- var file = pkg.files[j];
621
- if (file.timeUs < threshold) {
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) {
622
771
  fileOtherCount++;
623
772
  continue;
624
773
  }
@@ -626,9 +775,11 @@ function generateJs() {
626
775
  var functions = [];
627
776
  var funcOtherCount = 0;
628
777
 
629
- for (var k = 0; k < file.functions.length; k++) {
630
- var fn = file.functions[k];
631
- if (fn.timeUs < threshold) {
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) {
632
783
  funcOtherCount++;
633
784
  continue;
634
785
  }
@@ -665,18 +816,21 @@ function generateJs() {
665
816
  return { packages: filtered, otherCount: otherCount };
666
817
  }
667
818
 
668
- function renderTable(packages, otherCount, totalTimeUs) {
819
+ function renderTable(packages, otherCount, totalTimeUs, totalAsyncTimeUs) {
669
820
  var rows = '';
821
+ var isAsync = sortBy === 'async';
822
+ var barTotal = isAsync ? (totalAsyncTimeUs || 0) : totalTimeUs;
670
823
  for (var i = 0; i < packages.length; i++) {
671
824
  var pkg = packages[i];
672
825
  var cls = pkg.isFirstParty ? 'first-party' : 'dependency';
673
- var pctVal = totalTimeUs > 0 ? (pkg.timeUs / totalTimeUs) * 100 : 0;
826
+ var barVal = isAsync ? (pkg.asyncTimeUs || 0) : pkg.timeUs;
827
+ var pctVal = barTotal > 0 ? (barVal / barTotal) * 100 : 0;
674
828
  rows += '<tr class="' + cls + '">' +
675
829
  '<td class="pkg-name">' + escapeHtml(pkg.name) + '</td>' +
676
830
  '<td class="numeric">' + escapeHtml(formatTime(pkg.timeUs)) + '</td>' +
677
831
  '<td class="bar-cell"><div class="bar-container">' +
678
832
  '<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>' +
833
+ '<span class="bar-pct">' + escapeHtml(formatPct(barVal, barTotal)) + '</span>' +
680
834
  '</div></td>' +
681
835
  '<td class="numeric">' + pkg.sampleCount + '</td>';
682
836
  if (HAS_ASYNC) {
@@ -698,9 +852,9 @@ function generateJs() {
698
852
  rows += '</tr>';
699
853
  }
700
854
 
701
- var headers = '<th>Package</th><th>Wall Time</th><th>% of Total</th><th>Samples</th>';
855
+ var headers = '<th>Package</th><th>CPU Time</th><th>% of Total</th><th>Samples</th>';
702
856
  if (HAS_ASYNC) {
703
- headers += '<th>Async Wait</th><th>Async Ops</th>';
857
+ headers += '<th>Async I/O Wait</th><th>Async Ops</th>';
704
858
  }
705
859
 
706
860
  return '<table><thead><tr>' + headers + '</tr></thead><tbody>' + rows + '</tbody></table>';
@@ -714,34 +868,39 @@ function generateJs() {
714
868
  return ' <span class="tree-async">| ' + escapeHtml(formatTime(at)) + ' async &middot; ' + ac + ' ops</span>';
715
869
  }
716
870
 
717
- function renderTree(packages, otherCount, totalTimeUs) {
871
+ function renderTree(packages, otherCount, totalTimeUs, totalAsyncTimeUs) {
718
872
  var html = '<div class="tree">';
873
+ var isAsync = sortBy === 'async';
874
+ var pctTotal = isAsync ? (totalAsyncTimeUs || 0) : totalTimeUs;
719
875
 
720
876
  for (var i = 0; i < packages.length; i++) {
721
877
  var pkg = packages[i];
722
878
  var fpCls = pkg.isFirstParty ? ' fp-pkg' : '';
879
+ var pkgTime = isAsync ? (pkg.asyncTimeUs || 0) : pkg.timeUs;
723
880
  html += '<details class="level-0' + fpCls + '"><summary>';
724
881
  html += '<span class="tree-label pkg">pkg</span>';
725
882
  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>';
883
+ html += '<span class="tree-stats">' + escapeHtml(formatTime(pkgTime)) + ' &middot; ' + escapeHtml(formatPct(pkgTime, pctTotal)) + ' &middot; ' + pkg.sampleCount + ' samples</span>';
727
884
  html += asyncStats(pkg);
728
885
  html += '</summary>';
729
886
 
730
887
  for (var j = 0; j < pkg.files.length; j++) {
731
888
  var file = pkg.files[j];
889
+ var fileTime = isAsync ? (file.asyncTimeUs || 0) : file.timeUs;
732
890
  html += '<details class="level-1"><summary>';
733
891
  html += '<span class="tree-label file">file</span>';
734
892
  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>';
893
+ html += '<span class="tree-stats">' + escapeHtml(formatTime(fileTime)) + ' &middot; ' + escapeHtml(formatPct(fileTime, pctTotal)) + ' &middot; ' + file.sampleCount + ' samples</span>';
736
894
  html += asyncStats(file);
737
895
  html += '</summary>';
738
896
 
739
897
  for (var k = 0; k < file.functions.length; k++) {
740
898
  var fn = file.functions[k];
899
+ var fnTime = isAsync ? (fn.asyncTimeUs || 0) : fn.timeUs;
741
900
  html += '<div class="level-2">';
742
901
  html += '<span class="tree-label fn">fn</span> ';
743
902
  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>';
903
+ html += ' <span class="tree-stats">' + escapeHtml(formatTime(fnTime)) + ' &middot; ' + escapeHtml(formatPct(fnTime, pctTotal)) + ' &middot; ' + fn.sampleCount + ' samples</span>';
745
904
  html += asyncStats(fn);
746
905
  html += '</div>';
747
906
  }
@@ -768,12 +927,26 @@ function generateJs() {
768
927
  return html;
769
928
  }
770
929
 
930
+ var currentThreshold = 5;
931
+
771
932
  function update(pct) {
933
+ currentThreshold = pct;
772
934
  var result = applyThreshold(DATA, pct);
773
935
  var summaryEl = document.getElementById('summary-container');
774
936
  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);
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
+ }
777
950
  }
778
951
 
779
952
  document.addEventListener('DOMContentLoaded', function() {
@@ -787,6 +960,15 @@ function generateJs() {
787
960
  update(val);
788
961
  });
789
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
+ }
790
972
  });
791
973
  })();
792
974
  `;
@@ -825,10 +1007,10 @@ function renderSummaryTable(packages, otherCount, totalTimeUs, hasAsync) {
825
1007
  <thead>
826
1008
  <tr>
827
1009
  <th>Package</th>
828
- <th>Wall Time</th>
1010
+ <th>CPU Time</th>
829
1011
  <th>% of Total</th>
830
1012
  <th>Samples</th>${hasAsync ? `
831
- <th>Async Wait</th>
1013
+ <th>Async I/O Wait</th>
832
1014
  <th>Async Ops</th>` : ""}
833
1015
  </tr>
834
1016
  </thead>
@@ -891,8 +1073,11 @@ function renderHtml(data) {
891
1073
  const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs, hasAsync);
892
1074
  const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));
893
1075
  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))}`;
1076
+ const wallFormatted = data.wallTimeUs ? escapeHtml(formatTime(data.wallTimeUs)) : null;
1077
+ let metaLine = `Generated ${escapeHtml(data.timestamp)}`;
1078
+ if (wallFormatted) metaLine += ` &middot; Wall time: ${wallFormatted}`;
1079
+ metaLine += ` &middot; CPU time: ${totalFormatted}`;
1080
+ if (hasAsync) metaLine += ` &middot; Async I/O wait: ${escapeHtml(formatTime(data.totalAsyncTimeUs))}`;
896
1081
  const safeJson = JSON.stringify(data).replace(/</g, "\\u003c");
897
1082
  return `<!DOCTYPE html>
898
1083
  <html lang="en">
@@ -911,7 +1096,14 @@ function renderHtml(data) {
911
1096
  <div class="threshold-control">
912
1097
  <label>Threshold</label>
913
1098
  <input type="range" id="threshold-slider" min="0" max="20" step="0.5" value="5">
914
- <span id="threshold-value">5.0%</span>
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>` : ""}
915
1107
  </div>
916
1108
  <div id="summary-container">${summaryTable}</div>
917
1109
 
@@ -950,6 +1142,8 @@ var PkgProfile = class {
950
1142
  projectName;
951
1143
  /** Total async wait time in microseconds (undefined when async tracking not enabled) */
952
1144
  totalAsyncTimeUs;
1145
+ /** Elapsed wall time in microseconds from start() to stop() */
1146
+ wallTimeUs;
953
1147
  /** @internal */
954
1148
  constructor(data) {
955
1149
  this.timestamp = data.timestamp;
@@ -958,6 +1152,7 @@ var PkgProfile = class {
958
1152
  this.otherCount = data.otherCount;
959
1153
  this.projectName = data.projectName;
960
1154
  this.totalAsyncTimeUs = data.totalAsyncTimeUs;
1155
+ this.wallTimeUs = data.wallTimeUs;
961
1156
  }
962
1157
  /**
963
1158
  * Write a self-contained HTML report to disk.
@@ -972,7 +1167,8 @@ var PkgProfile = class {
972
1167
  packages: this.packages,
973
1168
  otherCount: this.otherCount,
974
1169
  projectName: this.projectName,
975
- totalAsyncTimeUs: this.totalAsyncTimeUs
1170
+ totalAsyncTimeUs: this.totalAsyncTimeUs,
1171
+ wallTimeUs: this.wallTimeUs
976
1172
  });
977
1173
  let filepath;
978
1174
  if (path) filepath = (0, node_path.resolve)(path);
@@ -1003,9 +1199,10 @@ function sumStore(store) {
1003
1199
  * @param asyncStore - Optional SampleStore with async wait time data
1004
1200
  * @returns ReportData with all packages sorted desc by time, no threshold applied
1005
1201
  */
1006
- function aggregate(store, projectName, asyncStore) {
1202
+ function aggregate(store, projectName, asyncStore, globalAsyncTimeUs, wallTimeUs) {
1007
1203
  const totalTimeUs = sumStore(store);
1008
1204
  const totalAsyncTimeUs = asyncStore ? sumStore(asyncStore) : 0;
1205
+ const headerAsyncTimeUs = globalAsyncTimeUs ?? totalAsyncTimeUs;
1009
1206
  if (totalTimeUs === 0 && totalAsyncTimeUs === 0) return {
1010
1207
  timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
1011
1208
  totalTimeUs: 0,
@@ -1110,7 +1307,8 @@ function aggregate(store, projectName, asyncStore) {
1110
1307
  otherCount: 0,
1111
1308
  projectName
1112
1309
  };
1113
- if (totalAsyncTimeUs > 0) result.totalAsyncTimeUs = totalAsyncTimeUs;
1310
+ if (headerAsyncTimeUs > 0) result.totalAsyncTimeUs = headerAsyncTimeUs;
1311
+ if (wallTimeUs !== void 0) result.wallTimeUs = wallTimeUs;
1114
1312
  return result;
1115
1313
  }
1116
1314
 
@@ -1194,6 +1392,7 @@ var SampleStore = class {
1194
1392
  //#region src/sampler.ts
1195
1393
  let session = null;
1196
1394
  let profiling = false;
1395
+ let startHrtime = null;
1197
1396
  const store = new SampleStore();
1198
1397
  const asyncStore = new SampleStore();
1199
1398
  const resolver = new PackageResolver(process.cwd());
@@ -1250,15 +1449,20 @@ function buildEmptyProfile() {
1250
1449
  */
1251
1450
  function stopSync() {
1252
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;
1253
1455
  const { profile } = postSync("Profiler.stop");
1254
1456
  postSync("Profiler.disable");
1255
1457
  profiling = false;
1458
+ let globalAsyncTimeUs;
1256
1459
  if (asyncTracker) {
1257
1460
  asyncTracker.disable();
1461
+ globalAsyncTimeUs = asyncTracker.mergedTotalUs;
1258
1462
  asyncTracker = null;
1259
1463
  }
1260
1464
  processProfile(profile);
1261
- const data = aggregate(store, readProjectName(process.cwd()), asyncStore.packages.size > 0 ? asyncStore : void 0);
1465
+ const data = aggregate(store, readProjectName(process.cwd()), asyncStore.packages.size > 0 ? asyncStore : void 0, globalAsyncTimeUs, wallTimeUs);
1262
1466
  store.clear();
1263
1467
  asyncStore.clear();
1264
1468
  return new PkgProfile(data);
@@ -1280,6 +1484,7 @@ async function start(options) {
1280
1484
  if (options?.interval !== void 0) await postAsync("Profiler.setSamplingInterval", { interval: options.interval });
1281
1485
  await postAsync("Profiler.start");
1282
1486
  profiling = true;
1487
+ startHrtime = process.hrtime();
1283
1488
  if (options?.trackAsync) {
1284
1489
  asyncTracker = new AsyncTracker(resolver, asyncStore);
1285
1490
  asyncTracker.enable();
@@ -1303,6 +1508,7 @@ async function clear() {
1303
1508
  postSync("Profiler.disable");
1304
1509
  profiling = false;
1305
1510
  }
1511
+ startHrtime = null;
1306
1512
  store.clear();
1307
1513
  if (asyncTracker) {
1308
1514
  asyncTracker.disable();