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