@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.
@@ -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 (totalAsyncTimeUs > 0) {
244
- result.totalAsyncTimeUs = 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;
@@ -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 threshold = data.totalTimeUs * (pct / 100);
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
- for (var i = 0; i < data.packages.length; i++) {
340
- var pkg = data.packages[i];
341
- if (pkg.timeUs < threshold) {
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
- for (var j = 0; j < pkg.files.length; j++) {
350
- var file = pkg.files[j];
351
- if (file.timeUs < threshold) {
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
- for (var k = 0; k < file.functions.length; k++) {
360
- var fn = file.functions[k];
361
- if (fn.timeUs < threshold) {
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 pctVal = totalTimeUs > 0 ? (pkg.timeUs / totalTimeUs) * 100 : 0;
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(pkg.timeUs, totalTimeUs)) + '</span>' +
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>Wall Time</th><th>% of Total</th><th>Samples</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 &middot; ' + 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(pkg.timeUs)) + ' &middot; ' + escapeHtml(formatPct(pkg.timeUs, totalTimeUs)) + ' &middot; ' + pkg.sampleCount + ' samples</span>';
529
+ html += '<span class="tree-stats">' + escapeHtml(formatTime(pkgTime)) + ' &middot; ' + escapeHtml(formatPct(pkgTime, pctTotal)) + ' &middot; ' + 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(file.timeUs)) + ' &middot; ' + escapeHtml(formatPct(file.timeUs, totalTimeUs)) + ' &middot; ' + file.sampleCount + ' samples</span>';
539
+ html += '<span class="tree-stats">' + escapeHtml(formatTime(fileTime)) + ' &middot; ' + escapeHtml(formatPct(fileTime, pctTotal)) + ' &middot; ' + 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(fn.timeUs)) + ' &middot; ' + escapeHtml(formatPct(fn.timeUs, totalTimeUs)) + ' &middot; ' + fn.sampleCount + ' samples</span>';
549
+ html += ' <span class="tree-stats">' + escapeHtml(formatTime(fnTime)) + ' &middot; ' + escapeHtml(formatPct(fnTime, pctTotal)) + ' &middot; ' + 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>Wall Time</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
- let metaLine = `Generated ${escapeHtml(data.timestamp)} &middot; Total wall time: ${totalFormatted}`;
758
+ const wallFormatted = data.wallTimeUs ? escapeHtml(formatTime(data.wallTimeUs)) : null;
759
+ let metaLine = `Generated ${escapeHtml(data.timestamp)}`;
760
+ if (wallFormatted) {
761
+ metaLine += ` &middot; Wall time: ${wallFormatted}`;
762
+ }
763
+ metaLine += ` &middot; CPU time: ${totalFormatted}`;
661
764
  if (hasAsync) {
662
- metaLine += ` &middot; Total async wait: ${escapeHtml(formatTime(data.totalAsyncTimeUs!))}`;
765
+ metaLine += ` &middot; 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();
package/src/types.ts CHANGED
@@ -52,6 +52,7 @@ export interface ReportData {
52
52
  otherCount: number;
53
53
  projectName: string;
54
54
  totalAsyncTimeUs?: number;
55
+ wallTimeUs?: number;
55
56
  }
56
57
 
57
58
  /**