@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
|
@@ -37,10 +37,13 @@ function sumStore(store: SampleStore): number {
|
|
|
37
37
|
* @param asyncStore - Optional SampleStore with async wait time data
|
|
38
38
|
* @returns ReportData with all packages sorted desc by time, no threshold applied
|
|
39
39
|
*/
|
|
40
|
-
export function aggregate(store: SampleStore, projectName: string, asyncStore?: SampleStore): ReportData {
|
|
40
|
+
export function aggregate(store: SampleStore, projectName: string, asyncStore?: SampleStore, globalAsyncTimeUs?: number, wallTimeUs?: number): ReportData {
|
|
41
41
|
// 1. Calculate total user-attributed time
|
|
42
42
|
const totalTimeUs = sumStore(store);
|
|
43
|
+
// Per-entry percentages use the raw sum so they add up to 100%
|
|
43
44
|
const totalAsyncTimeUs = asyncStore ? sumStore(asyncStore) : 0;
|
|
45
|
+
// Header total uses the merged (de-duplicated) global value when available
|
|
46
|
+
const headerAsyncTimeUs = globalAsyncTimeUs ?? totalAsyncTimeUs;
|
|
44
47
|
|
|
45
48
|
if (totalTimeUs === 0 && totalAsyncTimeUs === 0) {
|
|
46
49
|
return {
|
|
@@ -240,8 +243,12 @@ export function aggregate(store: SampleStore, projectName: string, asyncStore?:
|
|
|
240
243
|
projectName,
|
|
241
244
|
};
|
|
242
245
|
|
|
243
|
-
if (
|
|
244
|
-
result.totalAsyncTimeUs =
|
|
246
|
+
if (headerAsyncTimeUs > 0) {
|
|
247
|
+
result.totalAsyncTimeUs = headerAsyncTimeUs;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (wallTimeUs !== undefined) {
|
|
251
|
+
result.wallTimeUs = wallTimeUs;
|
|
245
252
|
}
|
|
246
253
|
|
|
247
254
|
return result;
|
package/src/reporter/html.ts
CHANGED
|
@@ -295,9 +295,59 @@ function generateCss(): string {
|
|
|
295
295
|
.other-item.indent-1 { padding-left: 2rem; }
|
|
296
296
|
.other-item.indent-2 { padding-left: 3.25rem; }
|
|
297
297
|
|
|
298
|
+
/* Sort control */
|
|
299
|
+
.sort-control {
|
|
300
|
+
display: inline-flex;
|
|
301
|
+
align-items: center;
|
|
302
|
+
gap: 0.5rem;
|
|
303
|
+
margin-left: 1.5rem;
|
|
304
|
+
font-size: 0.85rem;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.sort-control label {
|
|
308
|
+
font-weight: 600;
|
|
309
|
+
color: var(--muted);
|
|
310
|
+
text-transform: uppercase;
|
|
311
|
+
letter-spacing: 0.04em;
|
|
312
|
+
font-size: 0.8rem;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.sort-toggle {
|
|
316
|
+
display: inline-flex;
|
|
317
|
+
border: 1px solid var(--border);
|
|
318
|
+
border-radius: 4px;
|
|
319
|
+
overflow: hidden;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.sort-toggle button {
|
|
323
|
+
font-family: var(--font-sans);
|
|
324
|
+
font-size: 0.8rem;
|
|
325
|
+
padding: 0.25rem 0.6rem;
|
|
326
|
+
border: none;
|
|
327
|
+
background: #fff;
|
|
328
|
+
color: var(--muted);
|
|
329
|
+
cursor: pointer;
|
|
330
|
+
transition: background 0.15s, color 0.15s;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.sort-toggle button + button {
|
|
334
|
+
border-left: 1px solid var(--border);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.sort-toggle button.active {
|
|
338
|
+
background: var(--bar-fill);
|
|
339
|
+
color: #fff;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.sort-toggle button.active-async {
|
|
343
|
+
background: var(--bar-fill-async);
|
|
344
|
+
color: #fff;
|
|
345
|
+
}
|
|
346
|
+
|
|
298
347
|
@media (max-width: 600px) {
|
|
299
348
|
body { padding: 1rem; }
|
|
300
349
|
.bar-cell { width: 25%; }
|
|
350
|
+
.sort-control { margin-left: 0; margin-top: 0.5rem; }
|
|
301
351
|
}
|
|
302
352
|
`;
|
|
303
353
|
}
|
|
@@ -331,14 +381,27 @@ function generateJs(): string {
|
|
|
331
381
|
.replace(/'/g, ''');
|
|
332
382
|
}
|
|
333
383
|
|
|
384
|
+
var sortBy = 'cpu';
|
|
385
|
+
|
|
386
|
+
function metricTime(entry) {
|
|
387
|
+
return sortBy === 'async' ? (entry.asyncTimeUs || 0) : entry.timeUs;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function sortDesc(arr) {
|
|
391
|
+
return arr.slice().sort(function(a, b) { return metricTime(b) - metricTime(a); });
|
|
392
|
+
}
|
|
393
|
+
|
|
334
394
|
function applyThreshold(data, pct) {
|
|
335
|
-
var
|
|
395
|
+
var totalBase = sortBy === 'async' ? (data.totalAsyncTimeUs || 0) : data.totalTimeUs;
|
|
396
|
+
var threshold = totalBase * (pct / 100);
|
|
336
397
|
var filtered = [];
|
|
337
398
|
var otherCount = 0;
|
|
338
399
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
400
|
+
var pkgs = sortDesc(data.packages);
|
|
401
|
+
|
|
402
|
+
for (var i = 0; i < pkgs.length; i++) {
|
|
403
|
+
var pkg = pkgs[i];
|
|
404
|
+
if (metricTime(pkg) < threshold) {
|
|
342
405
|
otherCount++;
|
|
343
406
|
continue;
|
|
344
407
|
}
|
|
@@ -346,9 +409,11 @@ function generateJs(): string {
|
|
|
346
409
|
var files = [];
|
|
347
410
|
var fileOtherCount = 0;
|
|
348
411
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
412
|
+
var sortedFiles = sortDesc(pkg.files);
|
|
413
|
+
|
|
414
|
+
for (var j = 0; j < sortedFiles.length; j++) {
|
|
415
|
+
var file = sortedFiles[j];
|
|
416
|
+
if (metricTime(file) < threshold) {
|
|
352
417
|
fileOtherCount++;
|
|
353
418
|
continue;
|
|
354
419
|
}
|
|
@@ -356,9 +421,11 @@ function generateJs(): string {
|
|
|
356
421
|
var functions = [];
|
|
357
422
|
var funcOtherCount = 0;
|
|
358
423
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
424
|
+
var sortedFns = sortDesc(file.functions);
|
|
425
|
+
|
|
426
|
+
for (var k = 0; k < sortedFns.length; k++) {
|
|
427
|
+
var fn = sortedFns[k];
|
|
428
|
+
if (metricTime(fn) < threshold) {
|
|
362
429
|
funcOtherCount++;
|
|
363
430
|
continue;
|
|
364
431
|
}
|
|
@@ -395,18 +462,21 @@ function generateJs(): string {
|
|
|
395
462
|
return { packages: filtered, otherCount: otherCount };
|
|
396
463
|
}
|
|
397
464
|
|
|
398
|
-
function renderTable(packages, otherCount, totalTimeUs) {
|
|
465
|
+
function renderTable(packages, otherCount, totalTimeUs, totalAsyncTimeUs) {
|
|
399
466
|
var rows = '';
|
|
467
|
+
var isAsync = sortBy === 'async';
|
|
468
|
+
var barTotal = isAsync ? (totalAsyncTimeUs || 0) : totalTimeUs;
|
|
400
469
|
for (var i = 0; i < packages.length; i++) {
|
|
401
470
|
var pkg = packages[i];
|
|
402
471
|
var cls = pkg.isFirstParty ? 'first-party' : 'dependency';
|
|
403
|
-
var
|
|
472
|
+
var barVal = isAsync ? (pkg.asyncTimeUs || 0) : pkg.timeUs;
|
|
473
|
+
var pctVal = barTotal > 0 ? (barVal / barTotal) * 100 : 0;
|
|
404
474
|
rows += '<tr class="' + cls + '">' +
|
|
405
475
|
'<td class="pkg-name">' + escapeHtml(pkg.name) + '</td>' +
|
|
406
476
|
'<td class="numeric">' + escapeHtml(formatTime(pkg.timeUs)) + '</td>' +
|
|
407
477
|
'<td class="bar-cell"><div class="bar-container">' +
|
|
408
478
|
'<div class="bar-track"><div class="bar-fill" style="width:' + pctVal.toFixed(1) + '%"></div></div>' +
|
|
409
|
-
'<span class="bar-pct">' + escapeHtml(formatPct(
|
|
479
|
+
'<span class="bar-pct">' + escapeHtml(formatPct(barVal, barTotal)) + '</span>' +
|
|
410
480
|
'</div></td>' +
|
|
411
481
|
'<td class="numeric">' + pkg.sampleCount + '</td>';
|
|
412
482
|
if (HAS_ASYNC) {
|
|
@@ -428,9 +498,9 @@ function generateJs(): string {
|
|
|
428
498
|
rows += '</tr>';
|
|
429
499
|
}
|
|
430
500
|
|
|
431
|
-
var headers = '<th>Package</th><th>
|
|
501
|
+
var headers = '<th>Package</th><th>CPU Time</th><th>% of Total</th><th>Samples</th>';
|
|
432
502
|
if (HAS_ASYNC) {
|
|
433
|
-
headers += '<th>Async Wait</th><th>Async Ops</th>';
|
|
503
|
+
headers += '<th>Async I/O Wait</th><th>Async Ops</th>';
|
|
434
504
|
}
|
|
435
505
|
|
|
436
506
|
return '<table><thead><tr>' + headers + '</tr></thead><tbody>' + rows + '</tbody></table>';
|
|
@@ -444,34 +514,39 @@ function generateJs(): string {
|
|
|
444
514
|
return ' <span class="tree-async">| ' + escapeHtml(formatTime(at)) + ' async · ' + ac + ' ops</span>';
|
|
445
515
|
}
|
|
446
516
|
|
|
447
|
-
function renderTree(packages, otherCount, totalTimeUs) {
|
|
517
|
+
function renderTree(packages, otherCount, totalTimeUs, totalAsyncTimeUs) {
|
|
448
518
|
var html = '<div class="tree">';
|
|
519
|
+
var isAsync = sortBy === 'async';
|
|
520
|
+
var pctTotal = isAsync ? (totalAsyncTimeUs || 0) : totalTimeUs;
|
|
449
521
|
|
|
450
522
|
for (var i = 0; i < packages.length; i++) {
|
|
451
523
|
var pkg = packages[i];
|
|
452
524
|
var fpCls = pkg.isFirstParty ? ' fp-pkg' : '';
|
|
525
|
+
var pkgTime = isAsync ? (pkg.asyncTimeUs || 0) : pkg.timeUs;
|
|
453
526
|
html += '<details class="level-0' + fpCls + '"><summary>';
|
|
454
527
|
html += '<span class="tree-label pkg">pkg</span>';
|
|
455
528
|
html += '<span class="tree-name">' + escapeHtml(pkg.name) + '</span>';
|
|
456
|
-
html += '<span class="tree-stats">' + escapeHtml(formatTime(
|
|
529
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(pkgTime)) + ' · ' + escapeHtml(formatPct(pkgTime, pctTotal)) + ' · ' + pkg.sampleCount + ' samples</span>';
|
|
457
530
|
html += asyncStats(pkg);
|
|
458
531
|
html += '</summary>';
|
|
459
532
|
|
|
460
533
|
for (var j = 0; j < pkg.files.length; j++) {
|
|
461
534
|
var file = pkg.files[j];
|
|
535
|
+
var fileTime = isAsync ? (file.asyncTimeUs || 0) : file.timeUs;
|
|
462
536
|
html += '<details class="level-1"><summary>';
|
|
463
537
|
html += '<span class="tree-label file">file</span>';
|
|
464
538
|
html += '<span class="tree-name">' + escapeHtml(file.name) + '</span>';
|
|
465
|
-
html += '<span class="tree-stats">' + escapeHtml(formatTime(
|
|
539
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(fileTime)) + ' · ' + escapeHtml(formatPct(fileTime, pctTotal)) + ' · ' + file.sampleCount + ' samples</span>';
|
|
466
540
|
html += asyncStats(file);
|
|
467
541
|
html += '</summary>';
|
|
468
542
|
|
|
469
543
|
for (var k = 0; k < file.functions.length; k++) {
|
|
470
544
|
var fn = file.functions[k];
|
|
545
|
+
var fnTime = isAsync ? (fn.asyncTimeUs || 0) : fn.timeUs;
|
|
471
546
|
html += '<div class="level-2">';
|
|
472
547
|
html += '<span class="tree-label fn">fn</span> ';
|
|
473
548
|
html += '<span class="tree-name">' + escapeHtml(fn.name) + '</span>';
|
|
474
|
-
html += ' <span class="tree-stats">' + escapeHtml(formatTime(
|
|
549
|
+
html += ' <span class="tree-stats">' + escapeHtml(formatTime(fnTime)) + ' · ' + escapeHtml(formatPct(fnTime, pctTotal)) + ' · ' + fn.sampleCount + ' samples</span>';
|
|
475
550
|
html += asyncStats(fn);
|
|
476
551
|
html += '</div>';
|
|
477
552
|
}
|
|
@@ -498,12 +573,26 @@ function generateJs(): string {
|
|
|
498
573
|
return html;
|
|
499
574
|
}
|
|
500
575
|
|
|
576
|
+
var currentThreshold = 5;
|
|
577
|
+
|
|
501
578
|
function update(pct) {
|
|
579
|
+
currentThreshold = pct;
|
|
502
580
|
var result = applyThreshold(DATA, pct);
|
|
503
581
|
var summaryEl = document.getElementById('summary-container');
|
|
504
582
|
var treeEl = document.getElementById('tree-container');
|
|
505
|
-
if (summaryEl) summaryEl.innerHTML = renderTable(result.packages, result.otherCount, DATA.totalTimeUs);
|
|
506
|
-
if (treeEl) treeEl.innerHTML = renderTree(result.packages, result.otherCount, DATA.totalTimeUs);
|
|
583
|
+
if (summaryEl) summaryEl.innerHTML = renderTable(result.packages, result.otherCount, DATA.totalTimeUs, DATA.totalAsyncTimeUs);
|
|
584
|
+
if (treeEl) treeEl.innerHTML = renderTree(result.packages, result.otherCount, DATA.totalTimeUs, DATA.totalAsyncTimeUs);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function updateSortButtons() {
|
|
588
|
+
var btns = document.querySelectorAll('.sort-toggle button');
|
|
589
|
+
for (var i = 0; i < btns.length; i++) {
|
|
590
|
+
var btn = btns[i];
|
|
591
|
+
btn.className = '';
|
|
592
|
+
if (btn.getAttribute('data-sort') === sortBy) {
|
|
593
|
+
btn.className = sortBy === 'async' ? 'active-async' : 'active';
|
|
594
|
+
}
|
|
595
|
+
}
|
|
507
596
|
}
|
|
508
597
|
|
|
509
598
|
document.addEventListener('DOMContentLoaded', function() {
|
|
@@ -517,6 +606,15 @@ function generateJs(): string {
|
|
|
517
606
|
update(val);
|
|
518
607
|
});
|
|
519
608
|
}
|
|
609
|
+
|
|
610
|
+
var sortBtns = document.querySelectorAll('.sort-toggle button');
|
|
611
|
+
for (var i = 0; i < sortBtns.length; i++) {
|
|
612
|
+
sortBtns[i].addEventListener('click', function() {
|
|
613
|
+
sortBy = this.getAttribute('data-sort') || 'cpu';
|
|
614
|
+
updateSortButtons();
|
|
615
|
+
update(currentThreshold);
|
|
616
|
+
});
|
|
617
|
+
}
|
|
520
618
|
});
|
|
521
619
|
})();
|
|
522
620
|
`;
|
|
@@ -566,10 +664,10 @@ function renderSummaryTable(
|
|
|
566
664
|
<thead>
|
|
567
665
|
<tr>
|
|
568
666
|
<th>Package</th>
|
|
569
|
-
<th>
|
|
667
|
+
<th>CPU Time</th>
|
|
570
668
|
<th>% of Total</th>
|
|
571
669
|
<th>Samples</th>${hasAsync ? `
|
|
572
|
-
<th>Async Wait</th>
|
|
670
|
+
<th>Async I/O Wait</th>
|
|
573
671
|
<th>Async Ops</th>` : ''}
|
|
574
672
|
</tr>
|
|
575
673
|
</thead>
|
|
@@ -657,9 +755,14 @@ export function renderHtml(data: ReportData): string {
|
|
|
657
755
|
|
|
658
756
|
const titleName = escapeHtml(data.projectName);
|
|
659
757
|
|
|
660
|
-
|
|
758
|
+
const wallFormatted = data.wallTimeUs ? escapeHtml(formatTime(data.wallTimeUs)) : null;
|
|
759
|
+
let metaLine = `Generated ${escapeHtml(data.timestamp)}`;
|
|
760
|
+
if (wallFormatted) {
|
|
761
|
+
metaLine += ` · Wall time: ${wallFormatted}`;
|
|
762
|
+
}
|
|
763
|
+
metaLine += ` · CPU time: ${totalFormatted}`;
|
|
661
764
|
if (hasAsync) {
|
|
662
|
-
metaLine += ` ·
|
|
765
|
+
metaLine += ` · Async I/O wait: ${escapeHtml(formatTime(data.totalAsyncTimeUs!))}`;
|
|
663
766
|
}
|
|
664
767
|
|
|
665
768
|
// Sanitize JSON for safe embedding in <script> — replace < to prevent </script> injection
|
|
@@ -682,7 +785,14 @@ export function renderHtml(data: ReportData): string {
|
|
|
682
785
|
<div class="threshold-control">
|
|
683
786
|
<label>Threshold</label>
|
|
684
787
|
<input type="range" id="threshold-slider" min="0" max="20" step="0.5" value="5">
|
|
685
|
-
<span id="threshold-value">5.0%</span
|
|
788
|
+
<span id="threshold-value">5.0%</span>${hasAsync ? `
|
|
789
|
+
<span class="sort-control">
|
|
790
|
+
<label>Sort by</label>
|
|
791
|
+
<span class="sort-toggle">
|
|
792
|
+
<button data-sort="cpu" class="active">CPU Time</button>
|
|
793
|
+
<button data-sort="async">Async I/O Wait</button>
|
|
794
|
+
</span>
|
|
795
|
+
</span>` : ''}
|
|
686
796
|
</div>
|
|
687
797
|
<div id="summary-container">${summaryTable}</div>
|
|
688
798
|
|
package/src/sampler.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
// Module-level state -- lazy initialization
|
|
18
18
|
let session: Session | null = null;
|
|
19
19
|
let profiling = false;
|
|
20
|
+
let startHrtime: [number, number] | null = null;
|
|
20
21
|
const store = new SampleStore();
|
|
21
22
|
const asyncStore = new SampleStore();
|
|
22
23
|
const resolver = new PackageResolver(process.cwd());
|
|
@@ -85,12 +86,18 @@ function stopSync(): PkgProfile {
|
|
|
85
86
|
return buildEmptyProfile();
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
const elapsed = startHrtime ? process.hrtime(startHrtime) : null;
|
|
90
|
+
const wallTimeUs = elapsed ? elapsed[0] * 1_000_000 + Math.round(elapsed[1] / 1000) : undefined;
|
|
91
|
+
startHrtime = null;
|
|
92
|
+
|
|
88
93
|
const { profile } = postSync("Profiler.stop") as Profiler.StopReturnType;
|
|
89
94
|
postSync("Profiler.disable");
|
|
90
95
|
profiling = false;
|
|
91
96
|
|
|
97
|
+
let globalAsyncTimeUs: number | undefined;
|
|
92
98
|
if (asyncTracker) {
|
|
93
99
|
asyncTracker.disable();
|
|
100
|
+
globalAsyncTimeUs = asyncTracker.mergedTotalUs;
|
|
94
101
|
asyncTracker = null;
|
|
95
102
|
}
|
|
96
103
|
|
|
@@ -101,6 +108,8 @@ function stopSync(): PkgProfile {
|
|
|
101
108
|
store,
|
|
102
109
|
projectName,
|
|
103
110
|
asyncStore.packages.size > 0 ? asyncStore : undefined,
|
|
111
|
+
globalAsyncTimeUs,
|
|
112
|
+
wallTimeUs,
|
|
104
113
|
);
|
|
105
114
|
store.clear();
|
|
106
115
|
asyncStore.clear();
|
|
@@ -133,6 +142,7 @@ export async function start(options?: StartOptions): Promise<void> {
|
|
|
133
142
|
|
|
134
143
|
await postAsync("Profiler.start");
|
|
135
144
|
profiling = true;
|
|
145
|
+
startHrtime = process.hrtime();
|
|
136
146
|
|
|
137
147
|
if (options?.trackAsync) {
|
|
138
148
|
asyncTracker = new AsyncTracker(resolver, asyncStore);
|
|
@@ -159,6 +169,7 @@ export async function clear(): Promise<void> {
|
|
|
159
169
|
postSync("Profiler.disable");
|
|
160
170
|
profiling = false;
|
|
161
171
|
}
|
|
172
|
+
startHrtime = null;
|
|
162
173
|
store.clear();
|
|
163
174
|
if (asyncTracker) {
|
|
164
175
|
asyncTracker.disable();
|