@mtharrison/pkg-profiler 2.0.0 → 2.2.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/README.md +12 -12
- package/dist/index.cjs +343 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +343 -46
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/async-tracker.ts +128 -17
- package/src/dep-chain.ts +111 -0
- package/src/pkg-profile.ts +4 -0
- package/src/reporter/aggregate.ts +27 -3
- package/src/reporter/html.ts +152 -28
- package/src/sampler.ts +14 -1
- package/src/types.ts +2 -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
|
|
|
@@ -280,6 +364,10 @@ function escapeHtml(str) {
|
|
|
280
364
|
|
|
281
365
|
//#endregion
|
|
282
366
|
//#region src/reporter/html.ts
|
|
367
|
+
function formatDepChain(depChain) {
|
|
368
|
+
if (!depChain || depChain.length === 0) return "";
|
|
369
|
+
return `<span class="dep-chain">via ${depChain.map((n) => escapeHtml(n)).join(" > ")}</span>`;
|
|
370
|
+
}
|
|
283
371
|
function generateCss() {
|
|
284
372
|
return `
|
|
285
373
|
:root {
|
|
@@ -423,6 +511,7 @@ function generateCss() {
|
|
|
423
511
|
}
|
|
424
512
|
|
|
425
513
|
td.pkg-name { font-family: var(--font-mono); font-size: 0.85rem; }
|
|
514
|
+
.dep-chain { display: block; font-size: 0.7rem; color: var(--muted); font-family: var(--font-sans); }
|
|
426
515
|
td.numeric { text-align: right; font-family: var(--font-mono); font-size: 0.85rem; }
|
|
427
516
|
td.async-col { color: var(--bar-fill-async); }
|
|
428
517
|
|
|
@@ -566,9 +655,59 @@ function generateCss() {
|
|
|
566
655
|
.other-item.indent-1 { padding-left: 2rem; }
|
|
567
656
|
.other-item.indent-2 { padding-left: 3.25rem; }
|
|
568
657
|
|
|
658
|
+
/* Sort control */
|
|
659
|
+
.sort-control {
|
|
660
|
+
display: inline-flex;
|
|
661
|
+
align-items: center;
|
|
662
|
+
gap: 0.5rem;
|
|
663
|
+
margin-left: 1.5rem;
|
|
664
|
+
font-size: 0.85rem;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.sort-control label {
|
|
668
|
+
font-weight: 600;
|
|
669
|
+
color: var(--muted);
|
|
670
|
+
text-transform: uppercase;
|
|
671
|
+
letter-spacing: 0.04em;
|
|
672
|
+
font-size: 0.8rem;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.sort-toggle {
|
|
676
|
+
display: inline-flex;
|
|
677
|
+
border: 1px solid var(--border);
|
|
678
|
+
border-radius: 4px;
|
|
679
|
+
overflow: hidden;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.sort-toggle button {
|
|
683
|
+
font-family: var(--font-sans);
|
|
684
|
+
font-size: 0.8rem;
|
|
685
|
+
padding: 0.25rem 0.6rem;
|
|
686
|
+
border: none;
|
|
687
|
+
background: #fff;
|
|
688
|
+
color: var(--muted);
|
|
689
|
+
cursor: pointer;
|
|
690
|
+
transition: background 0.15s, color 0.15s;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.sort-toggle button + button {
|
|
694
|
+
border-left: 1px solid var(--border);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.sort-toggle button.active {
|
|
698
|
+
background: var(--bar-fill);
|
|
699
|
+
color: #fff;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.sort-toggle button.active-async {
|
|
703
|
+
background: var(--bar-fill-async);
|
|
704
|
+
color: #fff;
|
|
705
|
+
}
|
|
706
|
+
|
|
569
707
|
@media (max-width: 600px) {
|
|
570
708
|
body { padding: 1rem; }
|
|
571
709
|
.bar-cell { width: 25%; }
|
|
710
|
+
.sort-control { margin-left: 0; margin-top: 0.5rem; }
|
|
572
711
|
}
|
|
573
712
|
`;
|
|
574
713
|
}
|
|
@@ -601,14 +740,32 @@ function generateJs() {
|
|
|
601
740
|
.replace(/'/g, ''');
|
|
602
741
|
}
|
|
603
742
|
|
|
743
|
+
function depChainHtml(depChain) {
|
|
744
|
+
if (!depChain || depChain.length === 0) return '';
|
|
745
|
+
return '<span class="dep-chain">via ' + depChain.map(function(n) { return escapeHtml(n); }).join(' > ') + '</span>';
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
var sortBy = 'cpu';
|
|
749
|
+
|
|
750
|
+
function metricTime(entry) {
|
|
751
|
+
return sortBy === 'async' ? (entry.asyncTimeUs || 0) : entry.timeUs;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function sortDesc(arr) {
|
|
755
|
+
return arr.slice().sort(function(a, b) { return metricTime(b) - metricTime(a); });
|
|
756
|
+
}
|
|
757
|
+
|
|
604
758
|
function applyThreshold(data, pct) {
|
|
605
|
-
var
|
|
759
|
+
var totalBase = sortBy === 'async' ? (data.totalAsyncTimeUs || 0) : data.totalTimeUs;
|
|
760
|
+
var threshold = totalBase * (pct / 100);
|
|
606
761
|
var filtered = [];
|
|
607
762
|
var otherCount = 0;
|
|
608
763
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
764
|
+
var pkgs = sortDesc(data.packages);
|
|
765
|
+
|
|
766
|
+
for (var i = 0; i < pkgs.length; i++) {
|
|
767
|
+
var pkg = pkgs[i];
|
|
768
|
+
if (metricTime(pkg) < threshold) {
|
|
612
769
|
otherCount++;
|
|
613
770
|
continue;
|
|
614
771
|
}
|
|
@@ -616,9 +773,11 @@ function generateJs() {
|
|
|
616
773
|
var files = [];
|
|
617
774
|
var fileOtherCount = 0;
|
|
618
775
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
776
|
+
var sortedFiles = sortDesc(pkg.files);
|
|
777
|
+
|
|
778
|
+
for (var j = 0; j < sortedFiles.length; j++) {
|
|
779
|
+
var file = sortedFiles[j];
|
|
780
|
+
if (metricTime(file) < threshold) {
|
|
622
781
|
fileOtherCount++;
|
|
623
782
|
continue;
|
|
624
783
|
}
|
|
@@ -626,9 +785,11 @@ function generateJs() {
|
|
|
626
785
|
var functions = [];
|
|
627
786
|
var funcOtherCount = 0;
|
|
628
787
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
788
|
+
var sortedFns = sortDesc(file.functions);
|
|
789
|
+
|
|
790
|
+
for (var k = 0; k < sortedFns.length; k++) {
|
|
791
|
+
var fn = sortedFns[k];
|
|
792
|
+
if (metricTime(fn) < threshold) {
|
|
632
793
|
funcOtherCount++;
|
|
633
794
|
continue;
|
|
634
795
|
}
|
|
@@ -654,6 +815,7 @@ function generateJs() {
|
|
|
654
815
|
pct: pkg.pct,
|
|
655
816
|
isFirstParty: pkg.isFirstParty,
|
|
656
817
|
sampleCount: pkg.sampleCount,
|
|
818
|
+
depChain: pkg.depChain,
|
|
657
819
|
asyncTimeUs: pkg.asyncTimeUs,
|
|
658
820
|
asyncPct: pkg.asyncPct,
|
|
659
821
|
asyncOpCount: pkg.asyncOpCount,
|
|
@@ -665,18 +827,21 @@ function generateJs() {
|
|
|
665
827
|
return { packages: filtered, otherCount: otherCount };
|
|
666
828
|
}
|
|
667
829
|
|
|
668
|
-
function renderTable(packages, otherCount, totalTimeUs) {
|
|
830
|
+
function renderTable(packages, otherCount, totalTimeUs, totalAsyncTimeUs) {
|
|
669
831
|
var rows = '';
|
|
832
|
+
var isAsync = sortBy === 'async';
|
|
833
|
+
var barTotal = isAsync ? (totalAsyncTimeUs || 0) : totalTimeUs;
|
|
670
834
|
for (var i = 0; i < packages.length; i++) {
|
|
671
835
|
var pkg = packages[i];
|
|
672
836
|
var cls = pkg.isFirstParty ? 'first-party' : 'dependency';
|
|
673
|
-
var
|
|
837
|
+
var barVal = isAsync ? (pkg.asyncTimeUs || 0) : pkg.timeUs;
|
|
838
|
+
var pctVal = barTotal > 0 ? (barVal / barTotal) * 100 : 0;
|
|
674
839
|
rows += '<tr class="' + cls + '">' +
|
|
675
|
-
'<td class="pkg-name">' + escapeHtml(pkg.name) + '</td>' +
|
|
840
|
+
'<td class="pkg-name">' + escapeHtml(pkg.name) + depChainHtml(pkg.depChain) + '</td>' +
|
|
676
841
|
'<td class="numeric">' + escapeHtml(formatTime(pkg.timeUs)) + '</td>' +
|
|
677
842
|
'<td class="bar-cell"><div class="bar-container">' +
|
|
678
843
|
'<div class="bar-track"><div class="bar-fill" style="width:' + pctVal.toFixed(1) + '%"></div></div>' +
|
|
679
|
-
'<span class="bar-pct">' + escapeHtml(formatPct(
|
|
844
|
+
'<span class="bar-pct">' + escapeHtml(formatPct(barVal, barTotal)) + '</span>' +
|
|
680
845
|
'</div></td>' +
|
|
681
846
|
'<td class="numeric">' + pkg.sampleCount + '</td>';
|
|
682
847
|
if (HAS_ASYNC) {
|
|
@@ -698,9 +863,9 @@ function generateJs() {
|
|
|
698
863
|
rows += '</tr>';
|
|
699
864
|
}
|
|
700
865
|
|
|
701
|
-
var headers = '<th>Package</th><th>
|
|
866
|
+
var headers = '<th>Package</th><th>CPU Time</th><th>% of Total</th><th>Samples</th>';
|
|
702
867
|
if (HAS_ASYNC) {
|
|
703
|
-
headers += '<th>Async Wait</th><th>Async Ops</th>';
|
|
868
|
+
headers += '<th>Async I/O Wait</th><th>Async Ops</th>';
|
|
704
869
|
}
|
|
705
870
|
|
|
706
871
|
return '<table><thead><tr>' + headers + '</tr></thead><tbody>' + rows + '</tbody></table>';
|
|
@@ -714,34 +879,40 @@ function generateJs() {
|
|
|
714
879
|
return ' <span class="tree-async">| ' + escapeHtml(formatTime(at)) + ' async · ' + ac + ' ops</span>';
|
|
715
880
|
}
|
|
716
881
|
|
|
717
|
-
function renderTree(packages, otherCount, totalTimeUs) {
|
|
882
|
+
function renderTree(packages, otherCount, totalTimeUs, totalAsyncTimeUs) {
|
|
718
883
|
var html = '<div class="tree">';
|
|
884
|
+
var isAsync = sortBy === 'async';
|
|
885
|
+
var pctTotal = isAsync ? (totalAsyncTimeUs || 0) : totalTimeUs;
|
|
719
886
|
|
|
720
887
|
for (var i = 0; i < packages.length; i++) {
|
|
721
888
|
var pkg = packages[i];
|
|
722
889
|
var fpCls = pkg.isFirstParty ? ' fp-pkg' : '';
|
|
890
|
+
var pkgTime = isAsync ? (pkg.asyncTimeUs || 0) : pkg.timeUs;
|
|
723
891
|
html += '<details class="level-0' + fpCls + '"><summary>';
|
|
724
892
|
html += '<span class="tree-label pkg">pkg</span>';
|
|
725
893
|
html += '<span class="tree-name">' + escapeHtml(pkg.name) + '</span>';
|
|
726
|
-
html +=
|
|
894
|
+
html += depChainHtml(pkg.depChain);
|
|
895
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(pkgTime)) + ' · ' + escapeHtml(formatPct(pkgTime, pctTotal)) + ' · ' + pkg.sampleCount + ' samples</span>';
|
|
727
896
|
html += asyncStats(pkg);
|
|
728
897
|
html += '</summary>';
|
|
729
898
|
|
|
730
899
|
for (var j = 0; j < pkg.files.length; j++) {
|
|
731
900
|
var file = pkg.files[j];
|
|
901
|
+
var fileTime = isAsync ? (file.asyncTimeUs || 0) : file.timeUs;
|
|
732
902
|
html += '<details class="level-1"><summary>';
|
|
733
903
|
html += '<span class="tree-label file">file</span>';
|
|
734
904
|
html += '<span class="tree-name">' + escapeHtml(file.name) + '</span>';
|
|
735
|
-
html += '<span class="tree-stats">' + escapeHtml(formatTime(
|
|
905
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(fileTime)) + ' · ' + escapeHtml(formatPct(fileTime, pctTotal)) + ' · ' + file.sampleCount + ' samples</span>';
|
|
736
906
|
html += asyncStats(file);
|
|
737
907
|
html += '</summary>';
|
|
738
908
|
|
|
739
909
|
for (var k = 0; k < file.functions.length; k++) {
|
|
740
910
|
var fn = file.functions[k];
|
|
911
|
+
var fnTime = isAsync ? (fn.asyncTimeUs || 0) : fn.timeUs;
|
|
741
912
|
html += '<div class="level-2">';
|
|
742
913
|
html += '<span class="tree-label fn">fn</span> ';
|
|
743
914
|
html += '<span class="tree-name">' + escapeHtml(fn.name) + '</span>';
|
|
744
|
-
html += ' <span class="tree-stats">' + escapeHtml(formatTime(
|
|
915
|
+
html += ' <span class="tree-stats">' + escapeHtml(formatTime(fnTime)) + ' · ' + escapeHtml(formatPct(fnTime, pctTotal)) + ' · ' + fn.sampleCount + ' samples</span>';
|
|
745
916
|
html += asyncStats(fn);
|
|
746
917
|
html += '</div>';
|
|
747
918
|
}
|
|
@@ -768,12 +939,26 @@ function generateJs() {
|
|
|
768
939
|
return html;
|
|
769
940
|
}
|
|
770
941
|
|
|
942
|
+
var currentThreshold = 5;
|
|
943
|
+
|
|
771
944
|
function update(pct) {
|
|
945
|
+
currentThreshold = pct;
|
|
772
946
|
var result = applyThreshold(DATA, pct);
|
|
773
947
|
var summaryEl = document.getElementById('summary-container');
|
|
774
948
|
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);
|
|
949
|
+
if (summaryEl) summaryEl.innerHTML = renderTable(result.packages, result.otherCount, DATA.totalTimeUs, DATA.totalAsyncTimeUs);
|
|
950
|
+
if (treeEl) treeEl.innerHTML = renderTree(result.packages, result.otherCount, DATA.totalTimeUs, DATA.totalAsyncTimeUs);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function updateSortButtons() {
|
|
954
|
+
var btns = document.querySelectorAll('.sort-toggle button');
|
|
955
|
+
for (var i = 0; i < btns.length; i++) {
|
|
956
|
+
var btn = btns[i];
|
|
957
|
+
btn.className = '';
|
|
958
|
+
if (btn.getAttribute('data-sort') === sortBy) {
|
|
959
|
+
btn.className = sortBy === 'async' ? 'active-async' : 'active';
|
|
960
|
+
}
|
|
961
|
+
}
|
|
777
962
|
}
|
|
778
963
|
|
|
779
964
|
document.addEventListener('DOMContentLoaded', function() {
|
|
@@ -787,6 +972,15 @@ function generateJs() {
|
|
|
787
972
|
update(val);
|
|
788
973
|
});
|
|
789
974
|
}
|
|
975
|
+
|
|
976
|
+
var sortBtns = document.querySelectorAll('.sort-toggle button');
|
|
977
|
+
for (var i = 0; i < sortBtns.length; i++) {
|
|
978
|
+
sortBtns[i].addEventListener('click', function() {
|
|
979
|
+
sortBy = this.getAttribute('data-sort') || 'cpu';
|
|
980
|
+
updateSortButtons();
|
|
981
|
+
update(currentThreshold);
|
|
982
|
+
});
|
|
983
|
+
}
|
|
790
984
|
});
|
|
791
985
|
})();
|
|
792
986
|
`;
|
|
@@ -798,7 +992,7 @@ function renderSummaryTable(packages, otherCount, totalTimeUs, hasAsync) {
|
|
|
798
992
|
const pctVal = totalTimeUs > 0 ? pkg.timeUs / totalTimeUs * 100 : 0;
|
|
799
993
|
rows += `
|
|
800
994
|
<tr class="${cls}">
|
|
801
|
-
<td class="pkg-name">${escapeHtml(pkg.name)}</td>
|
|
995
|
+
<td class="pkg-name">${escapeHtml(pkg.name)}${formatDepChain(pkg.depChain)}</td>
|
|
802
996
|
<td class="numeric">${escapeHtml(formatTime(pkg.timeUs))}</td>
|
|
803
997
|
<td class="bar-cell">
|
|
804
998
|
<div class="bar-container">
|
|
@@ -825,10 +1019,10 @@ function renderSummaryTable(packages, otherCount, totalTimeUs, hasAsync) {
|
|
|
825
1019
|
<thead>
|
|
826
1020
|
<tr>
|
|
827
1021
|
<th>Package</th>
|
|
828
|
-
<th>
|
|
1022
|
+
<th>CPU Time</th>
|
|
829
1023
|
<th>% of Total</th>
|
|
830
1024
|
<th>Samples</th>${hasAsync ? `
|
|
831
|
-
<th>Async Wait</th>
|
|
1025
|
+
<th>Async I/O Wait</th>
|
|
832
1026
|
<th>Async Ops</th>` : ""}
|
|
833
1027
|
</tr>
|
|
834
1028
|
</thead>
|
|
@@ -850,6 +1044,7 @@ function renderTree(packages, otherCount, totalTimeUs, hasAsync) {
|
|
|
850
1044
|
html += `<summary>`;
|
|
851
1045
|
html += `<span class="tree-label pkg">pkg</span>`;
|
|
852
1046
|
html += `<span class="tree-name">${escapeHtml(pkg.name)}</span>`;
|
|
1047
|
+
html += formatDepChain(pkg.depChain);
|
|
853
1048
|
html += `<span class="tree-stats">${escapeHtml(formatTime(pkg.timeUs))} · ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} · ${pkg.sampleCount} samples</span>`;
|
|
854
1049
|
if (hasAsync) html += formatAsyncStats(pkg);
|
|
855
1050
|
html += `</summary>`;
|
|
@@ -891,8 +1086,11 @@ function renderHtml(data) {
|
|
|
891
1086
|
const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs, hasAsync);
|
|
892
1087
|
const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));
|
|
893
1088
|
const titleName = escapeHtml(data.projectName);
|
|
894
|
-
|
|
895
|
-
|
|
1089
|
+
const wallFormatted = data.wallTimeUs ? escapeHtml(formatTime(data.wallTimeUs)) : null;
|
|
1090
|
+
let metaLine = `Generated ${escapeHtml(data.timestamp)}`;
|
|
1091
|
+
if (wallFormatted) metaLine += ` · Wall time: ${wallFormatted}`;
|
|
1092
|
+
metaLine += ` · CPU time: ${totalFormatted}`;
|
|
1093
|
+
if (hasAsync) metaLine += ` · Async I/O wait: ${escapeHtml(formatTime(data.totalAsyncTimeUs))}`;
|
|
896
1094
|
const safeJson = JSON.stringify(data).replace(/</g, "\\u003c");
|
|
897
1095
|
return `<!DOCTYPE html>
|
|
898
1096
|
<html lang="en">
|
|
@@ -911,7 +1109,14 @@ function renderHtml(data) {
|
|
|
911
1109
|
<div class="threshold-control">
|
|
912
1110
|
<label>Threshold</label>
|
|
913
1111
|
<input type="range" id="threshold-slider" min="0" max="20" step="0.5" value="5">
|
|
914
|
-
<span id="threshold-value">5.0%</span
|
|
1112
|
+
<span id="threshold-value">5.0%</span>${hasAsync ? `
|
|
1113
|
+
<span class="sort-control">
|
|
1114
|
+
<label>Sort by</label>
|
|
1115
|
+
<span class="sort-toggle">
|
|
1116
|
+
<button data-sort="cpu" class="active">CPU Time</button>
|
|
1117
|
+
<button data-sort="async">Async I/O Wait</button>
|
|
1118
|
+
</span>
|
|
1119
|
+
</span>` : ""}
|
|
915
1120
|
</div>
|
|
916
1121
|
<div id="summary-container">${summaryTable}</div>
|
|
917
1122
|
|
|
@@ -950,6 +1155,8 @@ var PkgProfile = class {
|
|
|
950
1155
|
projectName;
|
|
951
1156
|
/** Total async wait time in microseconds (undefined when async tracking not enabled) */
|
|
952
1157
|
totalAsyncTimeUs;
|
|
1158
|
+
/** Elapsed wall time in microseconds from start() to stop() */
|
|
1159
|
+
wallTimeUs;
|
|
953
1160
|
/** @internal */
|
|
954
1161
|
constructor(data) {
|
|
955
1162
|
this.timestamp = data.timestamp;
|
|
@@ -958,6 +1165,7 @@ var PkgProfile = class {
|
|
|
958
1165
|
this.otherCount = data.otherCount;
|
|
959
1166
|
this.projectName = data.projectName;
|
|
960
1167
|
this.totalAsyncTimeUs = data.totalAsyncTimeUs;
|
|
1168
|
+
this.wallTimeUs = data.wallTimeUs;
|
|
961
1169
|
}
|
|
962
1170
|
/**
|
|
963
1171
|
* Write a self-contained HTML report to disk.
|
|
@@ -972,7 +1180,8 @@ var PkgProfile = class {
|
|
|
972
1180
|
packages: this.packages,
|
|
973
1181
|
otherCount: this.otherCount,
|
|
974
1182
|
projectName: this.projectName,
|
|
975
|
-
totalAsyncTimeUs: this.totalAsyncTimeUs
|
|
1183
|
+
totalAsyncTimeUs: this.totalAsyncTimeUs,
|
|
1184
|
+
wallTimeUs: this.wallTimeUs
|
|
976
1185
|
});
|
|
977
1186
|
let filepath;
|
|
978
1187
|
if (path) filepath = (0, node_path.resolve)(path);
|
|
@@ -985,6 +1194,73 @@ var PkgProfile = class {
|
|
|
985
1194
|
}
|
|
986
1195
|
};
|
|
987
1196
|
|
|
1197
|
+
//#endregion
|
|
1198
|
+
//#region src/dep-chain.ts
|
|
1199
|
+
/**
|
|
1200
|
+
* Resolve dependency chains for transitive npm packages.
|
|
1201
|
+
*
|
|
1202
|
+
* BFS through node_modules package.json files starting from the project's
|
|
1203
|
+
* direct dependencies to find the shortest path to each profiled package.
|
|
1204
|
+
*/
|
|
1205
|
+
function readPkgJson(dir, cache) {
|
|
1206
|
+
if (cache.has(dir)) return cache.get(dir);
|
|
1207
|
+
try {
|
|
1208
|
+
const raw = (0, node_fs.readFileSync)((0, node_path.join)(dir, "package.json"), "utf-8");
|
|
1209
|
+
const parsed = JSON.parse(raw);
|
|
1210
|
+
cache.set(dir, parsed);
|
|
1211
|
+
return parsed;
|
|
1212
|
+
} catch {
|
|
1213
|
+
cache.set(dir, null);
|
|
1214
|
+
return null;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function depsOf(pkg) {
|
|
1218
|
+
return [...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.optionalDependencies ?? {})];
|
|
1219
|
+
}
|
|
1220
|
+
/**
|
|
1221
|
+
* Resolve the shortest dependency chain from the project's direct deps
|
|
1222
|
+
* to each of the given package names.
|
|
1223
|
+
*
|
|
1224
|
+
* @param projectRoot - Absolute path to the project root (contains package.json and node_modules/)
|
|
1225
|
+
* @param packageNames - Set of package names that appeared in profiling data
|
|
1226
|
+
* @param maxDepth - Maximum BFS depth to search (default 5)
|
|
1227
|
+
* @returns Map from package name to chain array (e.g. `["express", "qs"]` means project -> express -> qs)
|
|
1228
|
+
*/
|
|
1229
|
+
function resolveDependencyChains(projectRoot, packageNames, maxDepth = 5) {
|
|
1230
|
+
const result = /* @__PURE__ */ new Map();
|
|
1231
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1232
|
+
const rootPkg = readPkgJson(projectRoot, cache);
|
|
1233
|
+
if (!rootPkg) return result;
|
|
1234
|
+
const directDeps = new Set(depsOf(rootPkg));
|
|
1235
|
+
for (const name of packageNames) if (directDeps.has(name)) {}
|
|
1236
|
+
const targets = /* @__PURE__ */ new Set();
|
|
1237
|
+
for (const name of packageNames) if (!directDeps.has(name)) targets.add(name);
|
|
1238
|
+
if (targets.size === 0) return result;
|
|
1239
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1240
|
+
const queue = [];
|
|
1241
|
+
for (const dep of directDeps) {
|
|
1242
|
+
queue.push([dep, [dep]]);
|
|
1243
|
+
visited.add(dep);
|
|
1244
|
+
}
|
|
1245
|
+
let qi = 0;
|
|
1246
|
+
while (qi < queue.length && targets.size > 0) {
|
|
1247
|
+
const [pkgName, chain] = queue[qi++];
|
|
1248
|
+
if (chain.length > maxDepth) continue;
|
|
1249
|
+
if (targets.has(pkgName)) {
|
|
1250
|
+
result.set(pkgName, chain.slice(0, -1));
|
|
1251
|
+
targets.delete(pkgName);
|
|
1252
|
+
if (targets.size === 0) break;
|
|
1253
|
+
}
|
|
1254
|
+
const pkg = readPkgJson((0, node_path.join)(projectRoot, "node_modules", pkgName), cache);
|
|
1255
|
+
if (!pkg) continue;
|
|
1256
|
+
for (const child of depsOf(pkg)) if (!visited.has(child)) {
|
|
1257
|
+
visited.add(child);
|
|
1258
|
+
queue.push([child, [...chain, child]]);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return result;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
988
1264
|
//#endregion
|
|
989
1265
|
//#region src/reporter/aggregate.ts
|
|
990
1266
|
/**
|
|
@@ -1003,9 +1279,10 @@ function sumStore(store) {
|
|
|
1003
1279
|
* @param asyncStore - Optional SampleStore with async wait time data
|
|
1004
1280
|
* @returns ReportData with all packages sorted desc by time, no threshold applied
|
|
1005
1281
|
*/
|
|
1006
|
-
function aggregate(store, projectName, asyncStore) {
|
|
1282
|
+
function aggregate(store, projectName, asyncStore, globalAsyncTimeUs, wallTimeUs, projectRoot) {
|
|
1007
1283
|
const totalTimeUs = sumStore(store);
|
|
1008
1284
|
const totalAsyncTimeUs = asyncStore ? sumStore(asyncStore) : 0;
|
|
1285
|
+
const headerAsyncTimeUs = globalAsyncTimeUs ?? totalAsyncTimeUs;
|
|
1009
1286
|
if (totalTimeUs === 0 && totalAsyncTimeUs === 0) return {
|
|
1010
1287
|
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
1011
1288
|
totalTimeUs: 0,
|
|
@@ -1102,6 +1379,16 @@ function aggregate(store, projectName, asyncStore) {
|
|
|
1102
1379
|
}
|
|
1103
1380
|
packages.push(pkgEntry);
|
|
1104
1381
|
}
|
|
1382
|
+
if (projectRoot) {
|
|
1383
|
+
const thirdPartyNames = new Set(packages.filter((p) => !p.isFirstParty).map((p) => p.name));
|
|
1384
|
+
if (thirdPartyNames.size > 0) {
|
|
1385
|
+
const chains = resolveDependencyChains(projectRoot, thirdPartyNames);
|
|
1386
|
+
for (const pkg of packages) {
|
|
1387
|
+
const chain = chains.get(pkg.name);
|
|
1388
|
+
if (chain && chain.length > 0) pkg.depChain = chain;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1105
1392
|
packages.sort((a, b) => b.timeUs - a.timeUs);
|
|
1106
1393
|
const result = {
|
|
1107
1394
|
timestamp: (/* @__PURE__ */ new Date()).toLocaleString(),
|
|
@@ -1110,7 +1397,8 @@ function aggregate(store, projectName, asyncStore) {
|
|
|
1110
1397
|
otherCount: 0,
|
|
1111
1398
|
projectName
|
|
1112
1399
|
};
|
|
1113
|
-
if (
|
|
1400
|
+
if (headerAsyncTimeUs > 0) result.totalAsyncTimeUs = headerAsyncTimeUs;
|
|
1401
|
+
if (wallTimeUs !== void 0) result.wallTimeUs = wallTimeUs;
|
|
1114
1402
|
return result;
|
|
1115
1403
|
}
|
|
1116
1404
|
|
|
@@ -1194,6 +1482,7 @@ var SampleStore = class {
|
|
|
1194
1482
|
//#region src/sampler.ts
|
|
1195
1483
|
let session = null;
|
|
1196
1484
|
let profiling = false;
|
|
1485
|
+
let startHrtime = null;
|
|
1197
1486
|
const store = new SampleStore();
|
|
1198
1487
|
const asyncStore = new SampleStore();
|
|
1199
1488
|
const resolver = new PackageResolver(process.cwd());
|
|
@@ -1250,15 +1539,21 @@ function buildEmptyProfile() {
|
|
|
1250
1539
|
*/
|
|
1251
1540
|
function stopSync() {
|
|
1252
1541
|
if (!profiling || !session) return buildEmptyProfile();
|
|
1542
|
+
const elapsed = startHrtime ? process.hrtime(startHrtime) : null;
|
|
1543
|
+
const wallTimeUs = elapsed ? elapsed[0] * 1e6 + Math.round(elapsed[1] / 1e3) : void 0;
|
|
1544
|
+
startHrtime = null;
|
|
1253
1545
|
const { profile } = postSync("Profiler.stop");
|
|
1254
1546
|
postSync("Profiler.disable");
|
|
1255
1547
|
profiling = false;
|
|
1548
|
+
let globalAsyncTimeUs;
|
|
1256
1549
|
if (asyncTracker) {
|
|
1257
1550
|
asyncTracker.disable();
|
|
1551
|
+
globalAsyncTimeUs = asyncTracker.mergedTotalUs;
|
|
1258
1552
|
asyncTracker = null;
|
|
1259
1553
|
}
|
|
1260
1554
|
processProfile(profile);
|
|
1261
|
-
const
|
|
1555
|
+
const cwd = process.cwd();
|
|
1556
|
+
const data = aggregate(store, readProjectName(cwd), asyncStore.packages.size > 0 ? asyncStore : void 0, globalAsyncTimeUs, wallTimeUs, cwd);
|
|
1262
1557
|
store.clear();
|
|
1263
1558
|
asyncStore.clear();
|
|
1264
1559
|
return new PkgProfile(data);
|
|
@@ -1280,6 +1575,7 @@ async function start(options) {
|
|
|
1280
1575
|
if (options?.interval !== void 0) await postAsync("Profiler.setSamplingInterval", { interval: options.interval });
|
|
1281
1576
|
await postAsync("Profiler.start");
|
|
1282
1577
|
profiling = true;
|
|
1578
|
+
startHrtime = process.hrtime();
|
|
1283
1579
|
if (options?.trackAsync) {
|
|
1284
1580
|
asyncTracker = new AsyncTracker(resolver, asyncStore);
|
|
1285
1581
|
asyncTracker.enable();
|
|
@@ -1303,6 +1599,7 @@ async function clear() {
|
|
|
1303
1599
|
postSync("Profiler.disable");
|
|
1304
1600
|
profiling = false;
|
|
1305
1601
|
}
|
|
1602
|
+
startHrtime = null;
|
|
1306
1603
|
store.clear();
|
|
1307
1604
|
if (asyncTracker) {
|
|
1308
1605
|
asyncTracker.disable();
|