@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.js CHANGED
@@ -11,6 +11,9 @@ import { fileURLToPath } from "node:url";
11
11
  * Tracks the time between async resource init (when the I/O op is started)
12
12
  * and the first before callback (when the callback fires), attributing
13
13
  * that wait time to the package/file/function that initiated the operation.
14
+ *
15
+ * Intervals are buffered and merged at disable() time so that overlapping
16
+ * concurrent I/O is not double-counted.
14
17
  */
15
18
  /** Async resource types worth tracking — I/O and timers, not promises. */
16
19
  const TRACKED_TYPES = new Set([
@@ -31,6 +34,37 @@ const TRACKED_TYPES = new Set([
31
34
  "Timeout"
32
35
  ]);
33
36
  /**
37
+ * Merge overlapping or adjacent intervals. Returns a new sorted array
38
+ * of non-overlapping intervals.
39
+ */
40
+ function mergeIntervals(intervals) {
41
+ if (intervals.length <= 1) return intervals.slice();
42
+ const sorted = intervals.slice().sort((a, b) => a.startUs - b.startUs);
43
+ const merged = [{ ...sorted[0] }];
44
+ for (let i = 1; i < sorted.length; i++) {
45
+ const current = sorted[i];
46
+ const last = merged[merged.length - 1];
47
+ if (current.startUs <= last.endUs) {
48
+ if (current.endUs > last.endUs) last.endUs = current.endUs;
49
+ } else merged.push({ ...current });
50
+ }
51
+ return merged;
52
+ }
53
+ /**
54
+ * Sum the durations of a list of (presumably non-overlapping) intervals.
55
+ */
56
+ function sumIntervals(intervals) {
57
+ let total = 0;
58
+ for (const iv of intervals) total += iv.endUs - iv.startUs;
59
+ return total;
60
+ }
61
+ /**
62
+ * Convert an hrtime tuple to absolute microseconds.
63
+ */
64
+ function hrtimeToUs(hr) {
65
+ return hr[0] * 1e6 + Math.round(hr[1] / 1e3);
66
+ }
67
+ /**
34
68
  * Parse a single line from an Error().stack trace into file path and function id.
35
69
  * Returns null for lines that don't match V8's stack frame format or are node internals.
36
70
  *
@@ -54,22 +88,34 @@ function parseStackLine(line) {
54
88
  }
55
89
  var AsyncTracker = class {
56
90
  resolver;
57
- store;
58
91
  thresholdUs;
59
92
  hook = null;
60
93
  pending = /* @__PURE__ */ new Map();
94
+ /** Buffered intervals keyed by "pkg\0file\0fn" */
95
+ keyedIntervals = /* @__PURE__ */ new Map();
96
+ /** Flat list of all intervals for global merging */
97
+ globalIntervals = [];
98
+ /** Origin time in absolute microseconds, set when enable() is called */
99
+ originUs = 0;
100
+ /** Merged global total set after flush() */
101
+ _mergedTotalUs = 0;
61
102
  /**
62
103
  * @param resolver - PackageResolver for mapping file paths to packages
63
- * @param store - SampleStore to record async wait times into
104
+ * @param store - SampleStore to record async wait times into (used at flush time)
64
105
  * @param thresholdUs - Minimum wait duration in microseconds to record (default 1000 = 1ms)
65
106
  */
66
107
  constructor(resolver, store, thresholdUs = 1e3) {
67
- this.resolver = resolver;
68
108
  this.store = store;
109
+ this.resolver = resolver;
69
110
  this.thresholdUs = thresholdUs;
70
111
  }
112
+ /** Merged global async total in microseconds, available after disable(). */
113
+ get mergedTotalUs() {
114
+ return this._mergedTotalUs;
115
+ }
71
116
  enable() {
72
117
  if (this.hook) return;
118
+ this.originUs = hrtimeToUs(process.hrtime());
73
119
  this.hook = createHook({
74
120
  init: (asyncId, type) => {
75
121
  if (!TRACKED_TYPES.has(type)) return;
@@ -102,9 +148,23 @@ var AsyncTracker = class {
102
148
  before: (asyncId) => {
103
149
  const op = this.pending.get(asyncId);
104
150
  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);
151
+ const endHr = process.hrtime();
152
+ const startUs = hrtimeToUs(op.startHrtime);
153
+ const endUs = hrtimeToUs(endHr);
154
+ if (endUs - startUs >= this.thresholdUs) {
155
+ const interval = {
156
+ startUs,
157
+ endUs
158
+ };
159
+ const key = `${op.pkg}\0${op.file}\0${op.fn}`;
160
+ let arr = this.keyedIntervals.get(key);
161
+ if (!arr) {
162
+ arr = [];
163
+ this.keyedIntervals.set(key, arr);
164
+ }
165
+ arr.push(interval);
166
+ this.globalIntervals.push(interval);
167
+ }
108
168
  this.pending.delete(asyncId);
109
169
  },
110
170
  destroy: (asyncId) => {
@@ -116,19 +176,43 @@ var AsyncTracker = class {
116
176
  disable() {
117
177
  if (!this.hook) return;
118
178
  this.hook.disable();
119
- const now = process.hrtime();
179
+ const nowUs = hrtimeToUs(process.hrtime());
120
180
  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;
181
+ const startUs = hrtimeToUs(op.startHrtime);
182
+ if (nowUs - startUs >= this.thresholdUs) {
183
+ const interval = {
184
+ startUs,
185
+ endUs: nowUs
186
+ };
187
+ const key = `${op.pkg}\0${op.file}\0${op.fn}`;
188
+ let arr = this.keyedIntervals.get(key);
189
+ if (!arr) {
190
+ arr = [];
191
+ this.keyedIntervals.set(key, arr);
192
+ }
193
+ arr.push(interval);
194
+ this.globalIntervals.push(interval);
126
195
  }
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
196
  }
130
197
  this.pending.clear();
131
198
  this.hook = null;
199
+ this.flush();
200
+ }
201
+ /**
202
+ * Merge buffered intervals and record to the store.
203
+ * Sets mergedTotalUs to the global merged duration.
204
+ */
205
+ flush() {
206
+ for (const [key, intervals] of this.keyedIntervals) {
207
+ const totalUs = sumIntervals(mergeIntervals(intervals));
208
+ if (totalUs > 0) {
209
+ const parts = key.split("\0");
210
+ this.store.record(parts[0], parts[1], parts[2], totalUs);
211
+ }
212
+ }
213
+ this._mergedTotalUs = sumIntervals(mergeIntervals(this.globalIntervals));
214
+ this.keyedIntervals.clear();
215
+ this.globalIntervals = [];
132
216
  }
133
217
  };
134
218
 
@@ -565,9 +649,59 @@ function generateCss() {
565
649
  .other-item.indent-1 { padding-left: 2rem; }
566
650
  .other-item.indent-2 { padding-left: 3.25rem; }
567
651
 
652
+ /* Sort control */
653
+ .sort-control {
654
+ display: inline-flex;
655
+ align-items: center;
656
+ gap: 0.5rem;
657
+ margin-left: 1.5rem;
658
+ font-size: 0.85rem;
659
+ }
660
+
661
+ .sort-control label {
662
+ font-weight: 600;
663
+ color: var(--muted);
664
+ text-transform: uppercase;
665
+ letter-spacing: 0.04em;
666
+ font-size: 0.8rem;
667
+ }
668
+
669
+ .sort-toggle {
670
+ display: inline-flex;
671
+ border: 1px solid var(--border);
672
+ border-radius: 4px;
673
+ overflow: hidden;
674
+ }
675
+
676
+ .sort-toggle button {
677
+ font-family: var(--font-sans);
678
+ font-size: 0.8rem;
679
+ padding: 0.25rem 0.6rem;
680
+ border: none;
681
+ background: #fff;
682
+ color: var(--muted);
683
+ cursor: pointer;
684
+ transition: background 0.15s, color 0.15s;
685
+ }
686
+
687
+ .sort-toggle button + button {
688
+ border-left: 1px solid var(--border);
689
+ }
690
+
691
+ .sort-toggle button.active {
692
+ background: var(--bar-fill);
693
+ color: #fff;
694
+ }
695
+
696
+ .sort-toggle button.active-async {
697
+ background: var(--bar-fill-async);
698
+ color: #fff;
699
+ }
700
+
568
701
  @media (max-width: 600px) {
569
702
  body { padding: 1rem; }
570
703
  .bar-cell { width: 25%; }
704
+ .sort-control { margin-left: 0; margin-top: 0.5rem; }
571
705
  }
572
706
  `;
573
707
  }
@@ -600,14 +734,27 @@ function generateJs() {
600
734
  .replace(/'/g, '&#39;');
601
735
  }
602
736
 
737
+ var sortBy = 'cpu';
738
+
739
+ function metricTime(entry) {
740
+ return sortBy === 'async' ? (entry.asyncTimeUs || 0) : entry.timeUs;
741
+ }
742
+
743
+ function sortDesc(arr) {
744
+ return arr.slice().sort(function(a, b) { return metricTime(b) - metricTime(a); });
745
+ }
746
+
603
747
  function applyThreshold(data, pct) {
604
- var threshold = data.totalTimeUs * (pct / 100);
748
+ var totalBase = sortBy === 'async' ? (data.totalAsyncTimeUs || 0) : data.totalTimeUs;
749
+ var threshold = totalBase * (pct / 100);
605
750
  var filtered = [];
606
751
  var otherCount = 0;
607
752
 
608
- for (var i = 0; i < data.packages.length; i++) {
609
- var pkg = data.packages[i];
610
- if (pkg.timeUs < threshold) {
753
+ var pkgs = sortDesc(data.packages);
754
+
755
+ for (var i = 0; i < pkgs.length; i++) {
756
+ var pkg = pkgs[i];
757
+ if (metricTime(pkg) < threshold) {
611
758
  otherCount++;
612
759
  continue;
613
760
  }
@@ -615,9 +762,11 @@ function generateJs() {
615
762
  var files = [];
616
763
  var fileOtherCount = 0;
617
764
 
618
- for (var j = 0; j < pkg.files.length; j++) {
619
- var file = pkg.files[j];
620
- if (file.timeUs < threshold) {
765
+ var sortedFiles = sortDesc(pkg.files);
766
+
767
+ for (var j = 0; j < sortedFiles.length; j++) {
768
+ var file = sortedFiles[j];
769
+ if (metricTime(file) < threshold) {
621
770
  fileOtherCount++;
622
771
  continue;
623
772
  }
@@ -625,9 +774,11 @@ function generateJs() {
625
774
  var functions = [];
626
775
  var funcOtherCount = 0;
627
776
 
628
- for (var k = 0; k < file.functions.length; k++) {
629
- var fn = file.functions[k];
630
- if (fn.timeUs < threshold) {
777
+ var sortedFns = sortDesc(file.functions);
778
+
779
+ for (var k = 0; k < sortedFns.length; k++) {
780
+ var fn = sortedFns[k];
781
+ if (metricTime(fn) < threshold) {
631
782
  funcOtherCount++;
632
783
  continue;
633
784
  }
@@ -664,18 +815,21 @@ function generateJs() {
664
815
  return { packages: filtered, otherCount: otherCount };
665
816
  }
666
817
 
667
- function renderTable(packages, otherCount, totalTimeUs) {
818
+ function renderTable(packages, otherCount, totalTimeUs, totalAsyncTimeUs) {
668
819
  var rows = '';
820
+ var isAsync = sortBy === 'async';
821
+ var barTotal = isAsync ? (totalAsyncTimeUs || 0) : totalTimeUs;
669
822
  for (var i = 0; i < packages.length; i++) {
670
823
  var pkg = packages[i];
671
824
  var cls = pkg.isFirstParty ? 'first-party' : 'dependency';
672
- var pctVal = totalTimeUs > 0 ? (pkg.timeUs / totalTimeUs) * 100 : 0;
825
+ var barVal = isAsync ? (pkg.asyncTimeUs || 0) : pkg.timeUs;
826
+ var pctVal = barTotal > 0 ? (barVal / barTotal) * 100 : 0;
673
827
  rows += '<tr class="' + cls + '">' +
674
828
  '<td class="pkg-name">' + escapeHtml(pkg.name) + '</td>' +
675
829
  '<td class="numeric">' + escapeHtml(formatTime(pkg.timeUs)) + '</td>' +
676
830
  '<td class="bar-cell"><div class="bar-container">' +
677
831
  '<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>' +
832
+ '<span class="bar-pct">' + escapeHtml(formatPct(barVal, barTotal)) + '</span>' +
679
833
  '</div></td>' +
680
834
  '<td class="numeric">' + pkg.sampleCount + '</td>';
681
835
  if (HAS_ASYNC) {
@@ -697,9 +851,9 @@ function generateJs() {
697
851
  rows += '</tr>';
698
852
  }
699
853
 
700
- var headers = '<th>Package</th><th>Wall Time</th><th>% of Total</th><th>Samples</th>';
854
+ var headers = '<th>Package</th><th>CPU Time</th><th>% of Total</th><th>Samples</th>';
701
855
  if (HAS_ASYNC) {
702
- headers += '<th>Async Wait</th><th>Async Ops</th>';
856
+ headers += '<th>Async I/O Wait</th><th>Async Ops</th>';
703
857
  }
704
858
 
705
859
  return '<table><thead><tr>' + headers + '</tr></thead><tbody>' + rows + '</tbody></table>';
@@ -713,34 +867,39 @@ function generateJs() {
713
867
  return ' <span class="tree-async">| ' + escapeHtml(formatTime(at)) + ' async &middot; ' + ac + ' ops</span>';
714
868
  }
715
869
 
716
- function renderTree(packages, otherCount, totalTimeUs) {
870
+ function renderTree(packages, otherCount, totalTimeUs, totalAsyncTimeUs) {
717
871
  var html = '<div class="tree">';
872
+ var isAsync = sortBy === 'async';
873
+ var pctTotal = isAsync ? (totalAsyncTimeUs || 0) : totalTimeUs;
718
874
 
719
875
  for (var i = 0; i < packages.length; i++) {
720
876
  var pkg = packages[i];
721
877
  var fpCls = pkg.isFirstParty ? ' fp-pkg' : '';
878
+ var pkgTime = isAsync ? (pkg.asyncTimeUs || 0) : pkg.timeUs;
722
879
  html += '<details class="level-0' + fpCls + '"><summary>';
723
880
  html += '<span class="tree-label pkg">pkg</span>';
724
881
  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>';
882
+ html += '<span class="tree-stats">' + escapeHtml(formatTime(pkgTime)) + ' &middot; ' + escapeHtml(formatPct(pkgTime, pctTotal)) + ' &middot; ' + pkg.sampleCount + ' samples</span>';
726
883
  html += asyncStats(pkg);
727
884
  html += '</summary>';
728
885
 
729
886
  for (var j = 0; j < pkg.files.length; j++) {
730
887
  var file = pkg.files[j];
888
+ var fileTime = isAsync ? (file.asyncTimeUs || 0) : file.timeUs;
731
889
  html += '<details class="level-1"><summary>';
732
890
  html += '<span class="tree-label file">file</span>';
733
891
  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>';
892
+ html += '<span class="tree-stats">' + escapeHtml(formatTime(fileTime)) + ' &middot; ' + escapeHtml(formatPct(fileTime, pctTotal)) + ' &middot; ' + file.sampleCount + ' samples</span>';
735
893
  html += asyncStats(file);
736
894
  html += '</summary>';
737
895
 
738
896
  for (var k = 0; k < file.functions.length; k++) {
739
897
  var fn = file.functions[k];
898
+ var fnTime = isAsync ? (fn.asyncTimeUs || 0) : fn.timeUs;
740
899
  html += '<div class="level-2">';
741
900
  html += '<span class="tree-label fn">fn</span> ';
742
901
  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>';
902
+ html += ' <span class="tree-stats">' + escapeHtml(formatTime(fnTime)) + ' &middot; ' + escapeHtml(formatPct(fnTime, pctTotal)) + ' &middot; ' + fn.sampleCount + ' samples</span>';
744
903
  html += asyncStats(fn);
745
904
  html += '</div>';
746
905
  }
@@ -767,12 +926,26 @@ function generateJs() {
767
926
  return html;
768
927
  }
769
928
 
929
+ var currentThreshold = 5;
930
+
770
931
  function update(pct) {
932
+ currentThreshold = pct;
771
933
  var result = applyThreshold(DATA, pct);
772
934
  var summaryEl = document.getElementById('summary-container');
773
935
  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);
936
+ if (summaryEl) summaryEl.innerHTML = renderTable(result.packages, result.otherCount, DATA.totalTimeUs, DATA.totalAsyncTimeUs);
937
+ if (treeEl) treeEl.innerHTML = renderTree(result.packages, result.otherCount, DATA.totalTimeUs, DATA.totalAsyncTimeUs);
938
+ }
939
+
940
+ function updateSortButtons() {
941
+ var btns = document.querySelectorAll('.sort-toggle button');
942
+ for (var i = 0; i < btns.length; i++) {
943
+ var btn = btns[i];
944
+ btn.className = '';
945
+ if (btn.getAttribute('data-sort') === sortBy) {
946
+ btn.className = sortBy === 'async' ? 'active-async' : 'active';
947
+ }
948
+ }
776
949
  }
777
950
 
778
951
  document.addEventListener('DOMContentLoaded', function() {
@@ -786,6 +959,15 @@ function generateJs() {
786
959
  update(val);
787
960
  });
788
961
  }
962
+
963
+ var sortBtns = document.querySelectorAll('.sort-toggle button');
964
+ for (var i = 0; i < sortBtns.length; i++) {
965
+ sortBtns[i].addEventListener('click', function() {
966
+ sortBy = this.getAttribute('data-sort') || 'cpu';
967
+ updateSortButtons();
968
+ update(currentThreshold);
969
+ });
970
+ }
789
971
  });
790
972
  })();
791
973
  `;
@@ -824,10 +1006,10 @@ function renderSummaryTable(packages, otherCount, totalTimeUs, hasAsync) {
824
1006
  <thead>
825
1007
  <tr>
826
1008
  <th>Package</th>
827
- <th>Wall Time</th>
1009
+ <th>CPU Time</th>
828
1010
  <th>% of Total</th>
829
1011
  <th>Samples</th>${hasAsync ? `
830
- <th>Async Wait</th>
1012
+ <th>Async I/O Wait</th>
831
1013
  <th>Async Ops</th>` : ""}
832
1014
  </tr>
833
1015
  </thead>
@@ -890,8 +1072,11 @@ function renderHtml(data) {
890
1072
  const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs, hasAsync);
891
1073
  const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));
892
1074
  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))}`;
1075
+ const wallFormatted = data.wallTimeUs ? escapeHtml(formatTime(data.wallTimeUs)) : null;
1076
+ let metaLine = `Generated ${escapeHtml(data.timestamp)}`;
1077
+ if (wallFormatted) metaLine += ` &middot; Wall time: ${wallFormatted}`;
1078
+ metaLine += ` &middot; CPU time: ${totalFormatted}`;
1079
+ if (hasAsync) metaLine += ` &middot; Async I/O wait: ${escapeHtml(formatTime(data.totalAsyncTimeUs))}`;
895
1080
  const safeJson = JSON.stringify(data).replace(/</g, "\\u003c");
896
1081
  return `<!DOCTYPE html>
897
1082
  <html lang="en">
@@ -910,7 +1095,14 @@ function renderHtml(data) {
910
1095
  <div class="threshold-control">
911
1096
  <label>Threshold</label>
912
1097
  <input type="range" id="threshold-slider" min="0" max="20" step="0.5" value="5">
913
- <span id="threshold-value">5.0%</span>
1098
+ <span id="threshold-value">5.0%</span>${hasAsync ? `
1099
+ <span class="sort-control">
1100
+ <label>Sort by</label>
1101
+ <span class="sort-toggle">
1102
+ <button data-sort="cpu" class="active">CPU Time</button>
1103
+ <button data-sort="async">Async I/O Wait</button>
1104
+ </span>
1105
+ </span>` : ""}
914
1106
  </div>
915
1107
  <div id="summary-container">${summaryTable}</div>
916
1108
 
@@ -949,6 +1141,8 @@ var PkgProfile = class {
949
1141
  projectName;
950
1142
  /** Total async wait time in microseconds (undefined when async tracking not enabled) */
951
1143
  totalAsyncTimeUs;
1144
+ /** Elapsed wall time in microseconds from start() to stop() */
1145
+ wallTimeUs;
952
1146
  /** @internal */
953
1147
  constructor(data) {
954
1148
  this.timestamp = data.timestamp;
@@ -957,6 +1151,7 @@ var PkgProfile = class {
957
1151
  this.otherCount = data.otherCount;
958
1152
  this.projectName = data.projectName;
959
1153
  this.totalAsyncTimeUs = data.totalAsyncTimeUs;
1154
+ this.wallTimeUs = data.wallTimeUs;
960
1155
  }
961
1156
  /**
962
1157
  * Write a self-contained HTML report to disk.
@@ -971,7 +1166,8 @@ var PkgProfile = class {
971
1166
  packages: this.packages,
972
1167
  otherCount: this.otherCount,
973
1168
  projectName: this.projectName,
974
- totalAsyncTimeUs: this.totalAsyncTimeUs
1169
+ totalAsyncTimeUs: this.totalAsyncTimeUs,
1170
+ wallTimeUs: this.wallTimeUs
975
1171
  });
976
1172
  let filepath;
977
1173
  if (path) filepath = resolve(path);
@@ -1002,9 +1198,10 @@ function sumStore(store) {
1002
1198
  * @param asyncStore - Optional SampleStore with async wait time data
1003
1199
  * @returns ReportData with all packages sorted desc by time, no threshold applied
1004
1200
  */
1005
- function aggregate(store, projectName, asyncStore) {
1201
+ function aggregate(store, projectName, asyncStore, globalAsyncTimeUs, wallTimeUs) {
1006
1202
  const totalTimeUs = sumStore(store);
1007
1203
  const totalAsyncTimeUs = asyncStore ? sumStore(asyncStore) : 0;
1204
+ const headerAsyncTimeUs = globalAsyncTimeUs ?? totalAsyncTimeUs;
1008
1205
  if (totalTimeUs === 0 && totalAsyncTimeUs === 0) return {
1009
1206
  timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
1010
1207
  totalTimeUs: 0,
@@ -1109,7 +1306,8 @@ function aggregate(store, projectName, asyncStore) {
1109
1306
  otherCount: 0,
1110
1307
  projectName
1111
1308
  };
1112
- if (totalAsyncTimeUs > 0) result.totalAsyncTimeUs = totalAsyncTimeUs;
1309
+ if (headerAsyncTimeUs > 0) result.totalAsyncTimeUs = headerAsyncTimeUs;
1310
+ if (wallTimeUs !== void 0) result.wallTimeUs = wallTimeUs;
1113
1311
  return result;
1114
1312
  }
1115
1313
 
@@ -1193,6 +1391,7 @@ var SampleStore = class {
1193
1391
  //#region src/sampler.ts
1194
1392
  let session = null;
1195
1393
  let profiling = false;
1394
+ let startHrtime = null;
1196
1395
  const store = new SampleStore();
1197
1396
  const asyncStore = new SampleStore();
1198
1397
  const resolver = new PackageResolver(process.cwd());
@@ -1249,15 +1448,20 @@ function buildEmptyProfile() {
1249
1448
  */
1250
1449
  function stopSync() {
1251
1450
  if (!profiling || !session) return buildEmptyProfile();
1451
+ const elapsed = startHrtime ? process.hrtime(startHrtime) : null;
1452
+ const wallTimeUs = elapsed ? elapsed[0] * 1e6 + Math.round(elapsed[1] / 1e3) : void 0;
1453
+ startHrtime = null;
1252
1454
  const { profile } = postSync("Profiler.stop");
1253
1455
  postSync("Profiler.disable");
1254
1456
  profiling = false;
1457
+ let globalAsyncTimeUs;
1255
1458
  if (asyncTracker) {
1256
1459
  asyncTracker.disable();
1460
+ globalAsyncTimeUs = asyncTracker.mergedTotalUs;
1257
1461
  asyncTracker = null;
1258
1462
  }
1259
1463
  processProfile(profile);
1260
- const data = aggregate(store, readProjectName(process.cwd()), asyncStore.packages.size > 0 ? asyncStore : void 0);
1464
+ const data = aggregate(store, readProjectName(process.cwd()), asyncStore.packages.size > 0 ? asyncStore : void 0, globalAsyncTimeUs, wallTimeUs);
1261
1465
  store.clear();
1262
1466
  asyncStore.clear();
1263
1467
  return new PkgProfile(data);
@@ -1279,6 +1483,7 @@ async function start(options) {
1279
1483
  if (options?.interval !== void 0) await postAsync("Profiler.setSamplingInterval", { interval: options.interval });
1280
1484
  await postAsync("Profiler.start");
1281
1485
  profiling = true;
1486
+ startHrtime = process.hrtime();
1282
1487
  if (options?.trackAsync) {
1283
1488
  asyncTracker = new AsyncTracker(resolver, asyncStore);
1284
1489
  asyncTracker.enable();
@@ -1302,6 +1507,7 @@ async function clear() {
1302
1507
  postSync("Profiler.disable");
1303
1508
  profiling = false;
1304
1509
  }
1510
+ startHrtime = null;
1305
1511
  store.clear();
1306
1512
  if (asyncTracker) {
1307
1513
  asyncTracker.disable();