@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.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
|
|
106
|
-
const
|
|
107
|
-
|
|
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
|
|
179
|
+
const nowUs = hrtimeToUs(process.hrtime());
|
|
120
180
|
for (const [, op] of this.pending) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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, ''');
|
|
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
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
|
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(
|
|
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>
|
|
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 · ' + 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(
|
|
882
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(pkgTime)) + ' · ' + escapeHtml(formatPct(pkgTime, pctTotal)) + ' · ' + 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(
|
|
892
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(fileTime)) + ' · ' + escapeHtml(formatPct(fileTime, pctTotal)) + ' · ' + 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(
|
|
902
|
+
html += ' <span class="tree-stats">' + escapeHtml(formatTime(fnTime)) + ' · ' + escapeHtml(formatPct(fnTime, pctTotal)) + ' · ' + 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>
|
|
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
|
-
|
|
894
|
-
|
|
1075
|
+
const wallFormatted = data.wallTimeUs ? escapeHtml(formatTime(data.wallTimeUs)) : null;
|
|
1076
|
+
let metaLine = `Generated ${escapeHtml(data.timestamp)}`;
|
|
1077
|
+
if (wallFormatted) metaLine += ` · Wall time: ${wallFormatted}`;
|
|
1078
|
+
metaLine += ` · CPU time: ${totalFormatted}`;
|
|
1079
|
+
if (hasAsync) metaLine += ` · 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 (
|
|
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();
|