@mtharrison/pkg-profiler 1.1.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/README.md +62 -7
- package/dist/index.cjs +981 -153
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +94 -11
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +94 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +976 -152
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/async-tracker.ts +289 -0
- package/src/frame-parser.ts +3 -0
- package/src/index.ts +10 -3
- package/src/pkg-profile.ts +77 -0
- package/src/reporter/aggregate.ts +156 -64
- package/src/reporter/format.ts +10 -0
- package/src/reporter/html.ts +441 -10
- package/src/sampler.ts +197 -38
- package/src/types.ts +23 -0
- package/src/reporter.ts +0 -42
package/src/reporter/html.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTML renderer for the profiling report.
|
|
3
3
|
*
|
|
4
|
-
* Generates a self-contained HTML file (inline CSS, no external dependencies)
|
|
5
|
-
* with a summary table
|
|
4
|
+
* Generates a self-contained HTML file (inline CSS/JS, no external dependencies)
|
|
5
|
+
* with a summary table, expandable Package > File > Function tree, and an
|
|
6
|
+
* interactive threshold slider that filters data client-side.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { ReportData, PackageEntry, FileEntry } from '../types.js';
|
|
@@ -21,6 +22,7 @@ function generateCss(): string {
|
|
|
21
22
|
--bar-track: #e8eaed;
|
|
22
23
|
--bar-fill: #5b8def;
|
|
23
24
|
--bar-fill-fp: #3b6cf5;
|
|
25
|
+
--bar-fill-async: #f5943b;
|
|
24
26
|
--other-text: #a0a4b8;
|
|
25
27
|
--table-header-bg: #f4f5f7;
|
|
26
28
|
--shadow: 0 1px 3px rgba(0,0,0,0.06);
|
|
@@ -60,6 +62,59 @@ function generateCss(): string {
|
|
|
60
62
|
margin-top: 2rem;
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
/* Threshold slider */
|
|
66
|
+
.threshold-control {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 0.75rem;
|
|
70
|
+
margin-bottom: 1rem;
|
|
71
|
+
font-size: 0.85rem;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.threshold-control label {
|
|
75
|
+
font-weight: 600;
|
|
76
|
+
color: var(--muted);
|
|
77
|
+
text-transform: uppercase;
|
|
78
|
+
letter-spacing: 0.04em;
|
|
79
|
+
font-size: 0.8rem;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.threshold-control input[type="range"] {
|
|
83
|
+
flex: 1;
|
|
84
|
+
max-width: 240px;
|
|
85
|
+
height: 8px;
|
|
86
|
+
appearance: none;
|
|
87
|
+
-webkit-appearance: none;
|
|
88
|
+
background: var(--bar-track);
|
|
89
|
+
border-radius: 4px;
|
|
90
|
+
outline: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.threshold-control input[type="range"]::-webkit-slider-thumb {
|
|
94
|
+
appearance: none;
|
|
95
|
+
-webkit-appearance: none;
|
|
96
|
+
width: 16px;
|
|
97
|
+
height: 16px;
|
|
98
|
+
border-radius: 50%;
|
|
99
|
+
background: var(--bar-fill);
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.threshold-control input[type="range"]::-moz-range-thumb {
|
|
104
|
+
width: 16px;
|
|
105
|
+
height: 16px;
|
|
106
|
+
border-radius: 50%;
|
|
107
|
+
background: var(--bar-fill);
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
border: none;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.threshold-control span {
|
|
113
|
+
font-family: var(--font-mono);
|
|
114
|
+
font-size: 0.85rem;
|
|
115
|
+
min-width: 3.5em;
|
|
116
|
+
}
|
|
117
|
+
|
|
63
118
|
/* Summary table */
|
|
64
119
|
table {
|
|
65
120
|
width: 100%;
|
|
@@ -98,6 +153,7 @@ function generateCss(): string {
|
|
|
98
153
|
|
|
99
154
|
td.pkg-name { font-family: var(--font-mono); font-size: 0.85rem; }
|
|
100
155
|
td.numeric { text-align: right; font-family: var(--font-mono); font-size: 0.85rem; }
|
|
156
|
+
td.async-col { color: var(--bar-fill-async); }
|
|
101
157
|
|
|
102
158
|
.bar-cell {
|
|
103
159
|
width: 30%;
|
|
@@ -211,6 +267,13 @@ function generateCss(): string {
|
|
|
211
267
|
flex-shrink: 0;
|
|
212
268
|
}
|
|
213
269
|
|
|
270
|
+
.tree-async {
|
|
271
|
+
font-family: var(--font-mono);
|
|
272
|
+
font-size: 0.8rem;
|
|
273
|
+
color: var(--bar-fill-async);
|
|
274
|
+
flex-shrink: 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
214
277
|
/* Level indentation */
|
|
215
278
|
.level-0 > summary { padding-left: 0.75rem; }
|
|
216
279
|
.level-1 > summary { padding-left: 2rem; }
|
|
@@ -232,17 +295,336 @@ function generateCss(): string {
|
|
|
232
295
|
.other-item.indent-1 { padding-left: 2rem; }
|
|
233
296
|
.other-item.indent-2 { padding-left: 3.25rem; }
|
|
234
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
|
+
|
|
235
347
|
@media (max-width: 600px) {
|
|
236
348
|
body { padding: 1rem; }
|
|
237
349
|
.bar-cell { width: 25%; }
|
|
350
|
+
.sort-control { margin-left: 0; margin-top: 0.5rem; }
|
|
238
351
|
}
|
|
239
352
|
`;
|
|
240
353
|
}
|
|
241
354
|
|
|
355
|
+
function generateJs(): string {
|
|
356
|
+
return `
|
|
357
|
+
(function() {
|
|
358
|
+
var DATA = window.__REPORT_DATA__;
|
|
359
|
+
if (!DATA) return;
|
|
360
|
+
var HAS_ASYNC = !!(DATA.totalAsyncTimeUs && DATA.totalAsyncTimeUs > 0);
|
|
361
|
+
|
|
362
|
+
function formatTime(us) {
|
|
363
|
+
if (us === 0) return '0ms';
|
|
364
|
+
var ms = us / 1000;
|
|
365
|
+
if (ms >= 1000) return (ms / 1000).toFixed(2) + 's';
|
|
366
|
+
var rounded = Math.round(ms);
|
|
367
|
+
return (rounded < 1 ? 1 : rounded) + 'ms';
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function formatPct(us, totalUs) {
|
|
371
|
+
if (totalUs === 0) return '0.0%';
|
|
372
|
+
return ((us / totalUs) * 100).toFixed(1) + '%';
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function escapeHtml(str) {
|
|
376
|
+
return str
|
|
377
|
+
.replace(/&/g, '&')
|
|
378
|
+
.replace(/</g, '<')
|
|
379
|
+
.replace(/>/g, '>')
|
|
380
|
+
.replace(/"/g, '"')
|
|
381
|
+
.replace(/'/g, ''');
|
|
382
|
+
}
|
|
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
|
+
|
|
394
|
+
function applyThreshold(data, pct) {
|
|
395
|
+
var totalBase = sortBy === 'async' ? (data.totalAsyncTimeUs || 0) : data.totalTimeUs;
|
|
396
|
+
var threshold = totalBase * (pct / 100);
|
|
397
|
+
var filtered = [];
|
|
398
|
+
var otherCount = 0;
|
|
399
|
+
|
|
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) {
|
|
405
|
+
otherCount++;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
var files = [];
|
|
410
|
+
var fileOtherCount = 0;
|
|
411
|
+
|
|
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) {
|
|
417
|
+
fileOtherCount++;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
var functions = [];
|
|
422
|
+
var funcOtherCount = 0;
|
|
423
|
+
|
|
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) {
|
|
429
|
+
funcOtherCount++;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
functions.push(fn);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
files.push({
|
|
436
|
+
name: file.name,
|
|
437
|
+
timeUs: file.timeUs,
|
|
438
|
+
pct: file.pct,
|
|
439
|
+
sampleCount: file.sampleCount,
|
|
440
|
+
asyncTimeUs: file.asyncTimeUs,
|
|
441
|
+
asyncPct: file.asyncPct,
|
|
442
|
+
asyncOpCount: file.asyncOpCount,
|
|
443
|
+
functions: functions,
|
|
444
|
+
otherCount: funcOtherCount
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
filtered.push({
|
|
449
|
+
name: pkg.name,
|
|
450
|
+
timeUs: pkg.timeUs,
|
|
451
|
+
pct: pkg.pct,
|
|
452
|
+
isFirstParty: pkg.isFirstParty,
|
|
453
|
+
sampleCount: pkg.sampleCount,
|
|
454
|
+
asyncTimeUs: pkg.asyncTimeUs,
|
|
455
|
+
asyncPct: pkg.asyncPct,
|
|
456
|
+
asyncOpCount: pkg.asyncOpCount,
|
|
457
|
+
files: files,
|
|
458
|
+
otherCount: fileOtherCount
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return { packages: filtered, otherCount: otherCount };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function renderTable(packages, otherCount, totalTimeUs, totalAsyncTimeUs) {
|
|
466
|
+
var rows = '';
|
|
467
|
+
var isAsync = sortBy === 'async';
|
|
468
|
+
var barTotal = isAsync ? (totalAsyncTimeUs || 0) : totalTimeUs;
|
|
469
|
+
for (var i = 0; i < packages.length; i++) {
|
|
470
|
+
var pkg = packages[i];
|
|
471
|
+
var cls = pkg.isFirstParty ? 'first-party' : 'dependency';
|
|
472
|
+
var barVal = isAsync ? (pkg.asyncTimeUs || 0) : pkg.timeUs;
|
|
473
|
+
var pctVal = barTotal > 0 ? (barVal / barTotal) * 100 : 0;
|
|
474
|
+
rows += '<tr class="' + cls + '">' +
|
|
475
|
+
'<td class="pkg-name">' + escapeHtml(pkg.name) + '</td>' +
|
|
476
|
+
'<td class="numeric">' + escapeHtml(formatTime(pkg.timeUs)) + '</td>' +
|
|
477
|
+
'<td class="bar-cell"><div class="bar-container">' +
|
|
478
|
+
'<div class="bar-track"><div class="bar-fill" style="width:' + pctVal.toFixed(1) + '%"></div></div>' +
|
|
479
|
+
'<span class="bar-pct">' + escapeHtml(formatPct(barVal, barTotal)) + '</span>' +
|
|
480
|
+
'</div></td>' +
|
|
481
|
+
'<td class="numeric">' + pkg.sampleCount + '</td>';
|
|
482
|
+
if (HAS_ASYNC) {
|
|
483
|
+
rows += '<td class="numeric async-col">' + escapeHtml(formatTime(pkg.asyncTimeUs || 0)) + '</td>' +
|
|
484
|
+
'<td class="numeric async-col">' + (pkg.asyncOpCount || 0) + '</td>';
|
|
485
|
+
}
|
|
486
|
+
rows += '</tr>';
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (otherCount > 0) {
|
|
490
|
+
rows += '<tr class="other-row">' +
|
|
491
|
+
'<td class="pkg-name">Other (' + otherCount + ' items)</td>' +
|
|
492
|
+
'<td class="numeric"></td>' +
|
|
493
|
+
'<td class="bar-cell"></td>' +
|
|
494
|
+
'<td class="numeric"></td>';
|
|
495
|
+
if (HAS_ASYNC) {
|
|
496
|
+
rows += '<td class="numeric"></td><td class="numeric"></td>';
|
|
497
|
+
}
|
|
498
|
+
rows += '</tr>';
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
var headers = '<th>Package</th><th>CPU Time</th><th>% of Total</th><th>Samples</th>';
|
|
502
|
+
if (HAS_ASYNC) {
|
|
503
|
+
headers += '<th>Async I/O Wait</th><th>Async Ops</th>';
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return '<table><thead><tr>' + headers + '</tr></thead><tbody>' + rows + '</tbody></table>';
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function asyncStats(entry) {
|
|
510
|
+
if (!HAS_ASYNC) return '';
|
|
511
|
+
var at = entry.asyncTimeUs || 0;
|
|
512
|
+
var ac = entry.asyncOpCount || 0;
|
|
513
|
+
if (at === 0 && ac === 0) return '';
|
|
514
|
+
return ' <span class="tree-async">| ' + escapeHtml(formatTime(at)) + ' async · ' + ac + ' ops</span>';
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function renderTree(packages, otherCount, totalTimeUs, totalAsyncTimeUs) {
|
|
518
|
+
var html = '<div class="tree">';
|
|
519
|
+
var isAsync = sortBy === 'async';
|
|
520
|
+
var pctTotal = isAsync ? (totalAsyncTimeUs || 0) : totalTimeUs;
|
|
521
|
+
|
|
522
|
+
for (var i = 0; i < packages.length; i++) {
|
|
523
|
+
var pkg = packages[i];
|
|
524
|
+
var fpCls = pkg.isFirstParty ? ' fp-pkg' : '';
|
|
525
|
+
var pkgTime = isAsync ? (pkg.asyncTimeUs || 0) : pkg.timeUs;
|
|
526
|
+
html += '<details class="level-0' + fpCls + '"><summary>';
|
|
527
|
+
html += '<span class="tree-label pkg">pkg</span>';
|
|
528
|
+
html += '<span class="tree-name">' + escapeHtml(pkg.name) + '</span>';
|
|
529
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(pkgTime)) + ' · ' + escapeHtml(formatPct(pkgTime, pctTotal)) + ' · ' + pkg.sampleCount + ' samples</span>';
|
|
530
|
+
html += asyncStats(pkg);
|
|
531
|
+
html += '</summary>';
|
|
532
|
+
|
|
533
|
+
for (var j = 0; j < pkg.files.length; j++) {
|
|
534
|
+
var file = pkg.files[j];
|
|
535
|
+
var fileTime = isAsync ? (file.asyncTimeUs || 0) : file.timeUs;
|
|
536
|
+
html += '<details class="level-1"><summary>';
|
|
537
|
+
html += '<span class="tree-label file">file</span>';
|
|
538
|
+
html += '<span class="tree-name">' + escapeHtml(file.name) + '</span>';
|
|
539
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(fileTime)) + ' · ' + escapeHtml(formatPct(fileTime, pctTotal)) + ' · ' + file.sampleCount + ' samples</span>';
|
|
540
|
+
html += asyncStats(file);
|
|
541
|
+
html += '</summary>';
|
|
542
|
+
|
|
543
|
+
for (var k = 0; k < file.functions.length; k++) {
|
|
544
|
+
var fn = file.functions[k];
|
|
545
|
+
var fnTime = isAsync ? (fn.asyncTimeUs || 0) : fn.timeUs;
|
|
546
|
+
html += '<div class="level-2">';
|
|
547
|
+
html += '<span class="tree-label fn">fn</span> ';
|
|
548
|
+
html += '<span class="tree-name">' + escapeHtml(fn.name) + '</span>';
|
|
549
|
+
html += ' <span class="tree-stats">' + escapeHtml(formatTime(fnTime)) + ' · ' + escapeHtml(formatPct(fnTime, pctTotal)) + ' · ' + fn.sampleCount + ' samples</span>';
|
|
550
|
+
html += asyncStats(fn);
|
|
551
|
+
html += '</div>';
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (file.otherCount > 0) {
|
|
555
|
+
html += '<div class="other-item indent-2">Other (' + file.otherCount + ' items)</div>';
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
html += '</details>';
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (pkg.otherCount > 0) {
|
|
562
|
+
html += '<div class="other-item indent-1">Other (' + pkg.otherCount + ' items)</div>';
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
html += '</details>';
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (otherCount > 0) {
|
|
569
|
+
html += '<div class="other-item">Other (' + otherCount + ' packages)</div>';
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
html += '</div>';
|
|
573
|
+
return html;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
var currentThreshold = 5;
|
|
577
|
+
|
|
578
|
+
function update(pct) {
|
|
579
|
+
currentThreshold = pct;
|
|
580
|
+
var result = applyThreshold(DATA, pct);
|
|
581
|
+
var summaryEl = document.getElementById('summary-container');
|
|
582
|
+
var treeEl = document.getElementById('tree-container');
|
|
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
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
599
|
+
update(5);
|
|
600
|
+
var slider = document.getElementById('threshold-slider');
|
|
601
|
+
var label = document.getElementById('threshold-value');
|
|
602
|
+
if (slider) {
|
|
603
|
+
slider.addEventListener('input', function() {
|
|
604
|
+
var val = parseFloat(slider.value);
|
|
605
|
+
if (label) label.textContent = val.toFixed(1) + '%';
|
|
606
|
+
update(val);
|
|
607
|
+
});
|
|
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
|
+
}
|
|
618
|
+
});
|
|
619
|
+
})();
|
|
620
|
+
`;
|
|
621
|
+
}
|
|
622
|
+
|
|
242
623
|
function renderSummaryTable(
|
|
243
624
|
packages: PackageEntry[],
|
|
244
625
|
otherCount: number,
|
|
245
626
|
totalTimeUs: number,
|
|
627
|
+
hasAsync: boolean,
|
|
246
628
|
): string {
|
|
247
629
|
let rows = '';
|
|
248
630
|
|
|
@@ -259,7 +641,9 @@ function renderSummaryTable(
|
|
|
259
641
|
<span class="bar-pct">${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))}</span>
|
|
260
642
|
</div>
|
|
261
643
|
</td>
|
|
262
|
-
<td class="numeric">${pkg.sampleCount}</td
|
|
644
|
+
<td class="numeric">${pkg.sampleCount}</td>${hasAsync ? `
|
|
645
|
+
<td class="numeric async-col">${escapeHtml(formatTime(pkg.asyncTimeUs ?? 0))}</td>
|
|
646
|
+
<td class="numeric async-col">${pkg.asyncOpCount ?? 0}</td>` : ''}
|
|
263
647
|
</tr>`;
|
|
264
648
|
}
|
|
265
649
|
|
|
@@ -269,7 +653,9 @@ function renderSummaryTable(
|
|
|
269
653
|
<td class="pkg-name">Other (${otherCount} items)</td>
|
|
270
654
|
<td class="numeric"></td>
|
|
271
655
|
<td class="bar-cell"></td>
|
|
656
|
+
<td class="numeric"></td>${hasAsync ? `
|
|
272
657
|
<td class="numeric"></td>
|
|
658
|
+
<td class="numeric"></td>` : ''}
|
|
273
659
|
</tr>`;
|
|
274
660
|
}
|
|
275
661
|
|
|
@@ -278,9 +664,11 @@ function renderSummaryTable(
|
|
|
278
664
|
<thead>
|
|
279
665
|
<tr>
|
|
280
666
|
<th>Package</th>
|
|
281
|
-
<th>
|
|
667
|
+
<th>CPU Time</th>
|
|
282
668
|
<th>% of Total</th>
|
|
283
|
-
<th>Samples</th
|
|
669
|
+
<th>Samples</th>${hasAsync ? `
|
|
670
|
+
<th>Async I/O Wait</th>
|
|
671
|
+
<th>Async Ops</th>` : ''}
|
|
284
672
|
</tr>
|
|
285
673
|
</thead>
|
|
286
674
|
<tbody>${rows}
|
|
@@ -288,10 +676,18 @@ function renderSummaryTable(
|
|
|
288
676
|
</table>`;
|
|
289
677
|
}
|
|
290
678
|
|
|
679
|
+
function formatAsyncStats(entry: { asyncTimeUs?: number; asyncOpCount?: number }): string {
|
|
680
|
+
const at = entry.asyncTimeUs ?? 0;
|
|
681
|
+
const ac = entry.asyncOpCount ?? 0;
|
|
682
|
+
if (at === 0 && ac === 0) return '';
|
|
683
|
+
return ` <span class="tree-async">| ${escapeHtml(formatTime(at))} async · ${ac} ops</span>`;
|
|
684
|
+
}
|
|
685
|
+
|
|
291
686
|
function renderTree(
|
|
292
687
|
packages: PackageEntry[],
|
|
293
688
|
otherCount: number,
|
|
294
689
|
totalTimeUs: number,
|
|
690
|
+
hasAsync: boolean,
|
|
295
691
|
): string {
|
|
296
692
|
let html = '<div class="tree">';
|
|
297
693
|
|
|
@@ -302,6 +698,7 @@ function renderTree(
|
|
|
302
698
|
html += `<span class="tree-label pkg">pkg</span>`;
|
|
303
699
|
html += `<span class="tree-name">${escapeHtml(pkg.name)}</span>`;
|
|
304
700
|
html += `<span class="tree-stats">${escapeHtml(formatTime(pkg.timeUs))} · ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} · ${pkg.sampleCount} samples</span>`;
|
|
701
|
+
if (hasAsync) html += formatAsyncStats(pkg);
|
|
305
702
|
html += `</summary>`;
|
|
306
703
|
|
|
307
704
|
for (const file of pkg.files) {
|
|
@@ -310,6 +707,7 @@ function renderTree(
|
|
|
310
707
|
html += `<span class="tree-label file">file</span>`;
|
|
311
708
|
html += `<span class="tree-name">${escapeHtml(file.name)}</span>`;
|
|
312
709
|
html += `<span class="tree-stats">${escapeHtml(formatTime(file.timeUs))} · ${escapeHtml(formatPct(file.timeUs, totalTimeUs))} · ${file.sampleCount} samples</span>`;
|
|
710
|
+
if (hasAsync) html += formatAsyncStats(file);
|
|
313
711
|
html += `</summary>`;
|
|
314
712
|
|
|
315
713
|
for (const fn of file.functions) {
|
|
@@ -317,6 +715,7 @@ function renderTree(
|
|
|
317
715
|
html += `<span class="tree-label fn">fn</span> `;
|
|
318
716
|
html += `<span class="tree-name">${escapeHtml(fn.name)}</span>`;
|
|
319
717
|
html += ` <span class="tree-stats">${escapeHtml(formatTime(fn.timeUs))} · ${escapeHtml(formatPct(fn.timeUs, totalTimeUs))} · ${fn.sampleCount} samples</span>`;
|
|
718
|
+
if (hasAsync) html += formatAsyncStats(fn);
|
|
320
719
|
html += `</div>`;
|
|
321
720
|
}
|
|
322
721
|
|
|
@@ -344,14 +743,31 @@ function renderTree(
|
|
|
344
743
|
|
|
345
744
|
/**
|
|
346
745
|
* Render a complete self-contained HTML report from aggregated profiling data.
|
|
746
|
+
*
|
|
747
|
+
* @param data - Aggregated report data (packages, timing, project name).
|
|
748
|
+
* @returns A full HTML document string with inline CSS/JS and no external dependencies.
|
|
347
749
|
*/
|
|
348
750
|
export function renderHtml(data: ReportData): string {
|
|
349
|
-
const
|
|
350
|
-
const
|
|
751
|
+
const hasAsync = !!(data.totalAsyncTimeUs && data.totalAsyncTimeUs > 0);
|
|
752
|
+
const summaryTable = renderSummaryTable(data.packages, data.otherCount, data.totalTimeUs, hasAsync);
|
|
753
|
+
const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs, hasAsync);
|
|
351
754
|
const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));
|
|
352
755
|
|
|
353
756
|
const titleName = escapeHtml(data.projectName);
|
|
354
757
|
|
|
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}`;
|
|
764
|
+
if (hasAsync) {
|
|
765
|
+
metaLine += ` · Async I/O wait: ${escapeHtml(formatTime(data.totalAsyncTimeUs!))}`;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Sanitize JSON for safe embedding in <script> — replace < to prevent </script> injection
|
|
769
|
+
const safeJson = JSON.stringify(data).replace(/</g, '\\u003c');
|
|
770
|
+
|
|
355
771
|
return `<!DOCTYPE html>
|
|
356
772
|
<html lang="en">
|
|
357
773
|
<head>
|
|
@@ -363,13 +779,28 @@ export function renderHtml(data: ReportData): string {
|
|
|
363
779
|
</head>
|
|
364
780
|
<body>
|
|
365
781
|
<h1>${titleName}</h1>
|
|
366
|
-
<div class="meta"
|
|
782
|
+
<div class="meta">${metaLine}</div>
|
|
367
783
|
|
|
368
784
|
<h2>Summary</h2>
|
|
369
|
-
|
|
785
|
+
<div class="threshold-control">
|
|
786
|
+
<label>Threshold</label>
|
|
787
|
+
<input type="range" id="threshold-slider" min="0" max="20" step="0.5" value="5">
|
|
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>` : ''}
|
|
796
|
+
</div>
|
|
797
|
+
<div id="summary-container">${summaryTable}</div>
|
|
370
798
|
|
|
371
799
|
<h2>Details</h2>
|
|
372
|
-
|
|
800
|
+
<div id="tree-container">${tree}</div>
|
|
801
|
+
|
|
802
|
+
<script>var __REPORT_DATA__ = ${safeJson};</script>
|
|
803
|
+
<script>${generateJs()}</script>
|
|
373
804
|
</body>
|
|
374
805
|
</html>`;
|
|
375
806
|
}
|