@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 +250 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +250 -44
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/async-tracker.ts +128 -17
- package/src/pkg-profile.ts +4 -0
- package/src/reporter/aggregate.ts +10 -3
- package/src/reporter/html.ts +136 -26
- package/src/sampler.ts +11 -0
- package/src/types.ts +1 -0
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
|
|
107
|
-
const
|
|
108
|
-
|
|
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
|
|
180
|
+
const nowUs = hrtimeToUs(process.hrtime());
|
|
121
181
|
for (const [, op] of this.pending) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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, ''');
|
|
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
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
|
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(
|
|
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>
|
|
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 · ' + 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(
|
|
883
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(pkgTime)) + ' · ' + escapeHtml(formatPct(pkgTime, pctTotal)) + ' · ' + 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(
|
|
893
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(fileTime)) + ' · ' + escapeHtml(formatPct(fileTime, pctTotal)) + ' · ' + 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(
|
|
903
|
+
html += ' <span class="tree-stats">' + escapeHtml(formatTime(fnTime)) + ' · ' + escapeHtml(formatPct(fnTime, pctTotal)) + ' · ' + 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>
|
|
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
|
-
|
|
895
|
-
|
|
1076
|
+
const wallFormatted = data.wallTimeUs ? escapeHtml(formatTime(data.wallTimeUs)) : null;
|
|
1077
|
+
let metaLine = `Generated ${escapeHtml(data.timestamp)}`;
|
|
1078
|
+
if (wallFormatted) metaLine += ` · Wall time: ${wallFormatted}`;
|
|
1079
|
+
metaLine += ` · CPU time: ${totalFormatted}`;
|
|
1080
|
+
if (hasAsync) metaLine += ` · Async I/O wait: ${escapeHtml(formatTime(data.totalAsyncTimeUs))}`;
|
|
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 (
|
|
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();
|