@mtharrison/pkg-profiler 1.0.1 → 2.0.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 +75 -48
- package/dist/index.cjs +777 -152
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -11
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +91 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +772 -151
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/async-tracker.ts +178 -0
- package/src/frame-parser.ts +3 -0
- package/src/index.ts +10 -3
- package/src/pkg-profile.ts +73 -0
- package/src/reporter/aggregate.ts +151 -64
- package/src/reporter/format.ts +10 -0
- package/src/reporter/html.ts +334 -11
- package/src/sampler.ts +186 -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; }
|
|
@@ -239,10 +302,231 @@ function generateCss(): string {
|
|
|
239
302
|
`;
|
|
240
303
|
}
|
|
241
304
|
|
|
305
|
+
function generateJs(): string {
|
|
306
|
+
return `
|
|
307
|
+
(function() {
|
|
308
|
+
var DATA = window.__REPORT_DATA__;
|
|
309
|
+
if (!DATA) return;
|
|
310
|
+
var HAS_ASYNC = !!(DATA.totalAsyncTimeUs && DATA.totalAsyncTimeUs > 0);
|
|
311
|
+
|
|
312
|
+
function formatTime(us) {
|
|
313
|
+
if (us === 0) return '0ms';
|
|
314
|
+
var ms = us / 1000;
|
|
315
|
+
if (ms >= 1000) return (ms / 1000).toFixed(2) + 's';
|
|
316
|
+
var rounded = Math.round(ms);
|
|
317
|
+
return (rounded < 1 ? 1 : rounded) + 'ms';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function formatPct(us, totalUs) {
|
|
321
|
+
if (totalUs === 0) return '0.0%';
|
|
322
|
+
return ((us / totalUs) * 100).toFixed(1) + '%';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function escapeHtml(str) {
|
|
326
|
+
return str
|
|
327
|
+
.replace(/&/g, '&')
|
|
328
|
+
.replace(/</g, '<')
|
|
329
|
+
.replace(/>/g, '>')
|
|
330
|
+
.replace(/"/g, '"')
|
|
331
|
+
.replace(/'/g, ''');
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function applyThreshold(data, pct) {
|
|
335
|
+
var threshold = data.totalTimeUs * (pct / 100);
|
|
336
|
+
var filtered = [];
|
|
337
|
+
var otherCount = 0;
|
|
338
|
+
|
|
339
|
+
for (var i = 0; i < data.packages.length; i++) {
|
|
340
|
+
var pkg = data.packages[i];
|
|
341
|
+
if (pkg.timeUs < threshold) {
|
|
342
|
+
otherCount++;
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
var files = [];
|
|
347
|
+
var fileOtherCount = 0;
|
|
348
|
+
|
|
349
|
+
for (var j = 0; j < pkg.files.length; j++) {
|
|
350
|
+
var file = pkg.files[j];
|
|
351
|
+
if (file.timeUs < threshold) {
|
|
352
|
+
fileOtherCount++;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
var functions = [];
|
|
357
|
+
var funcOtherCount = 0;
|
|
358
|
+
|
|
359
|
+
for (var k = 0; k < file.functions.length; k++) {
|
|
360
|
+
var fn = file.functions[k];
|
|
361
|
+
if (fn.timeUs < threshold) {
|
|
362
|
+
funcOtherCount++;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
functions.push(fn);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
files.push({
|
|
369
|
+
name: file.name,
|
|
370
|
+
timeUs: file.timeUs,
|
|
371
|
+
pct: file.pct,
|
|
372
|
+
sampleCount: file.sampleCount,
|
|
373
|
+
asyncTimeUs: file.asyncTimeUs,
|
|
374
|
+
asyncPct: file.asyncPct,
|
|
375
|
+
asyncOpCount: file.asyncOpCount,
|
|
376
|
+
functions: functions,
|
|
377
|
+
otherCount: funcOtherCount
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
filtered.push({
|
|
382
|
+
name: pkg.name,
|
|
383
|
+
timeUs: pkg.timeUs,
|
|
384
|
+
pct: pkg.pct,
|
|
385
|
+
isFirstParty: pkg.isFirstParty,
|
|
386
|
+
sampleCount: pkg.sampleCount,
|
|
387
|
+
asyncTimeUs: pkg.asyncTimeUs,
|
|
388
|
+
asyncPct: pkg.asyncPct,
|
|
389
|
+
asyncOpCount: pkg.asyncOpCount,
|
|
390
|
+
files: files,
|
|
391
|
+
otherCount: fileOtherCount
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return { packages: filtered, otherCount: otherCount };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function renderTable(packages, otherCount, totalTimeUs) {
|
|
399
|
+
var rows = '';
|
|
400
|
+
for (var i = 0; i < packages.length; i++) {
|
|
401
|
+
var pkg = packages[i];
|
|
402
|
+
var cls = pkg.isFirstParty ? 'first-party' : 'dependency';
|
|
403
|
+
var pctVal = totalTimeUs > 0 ? (pkg.timeUs / totalTimeUs) * 100 : 0;
|
|
404
|
+
rows += '<tr class="' + cls + '">' +
|
|
405
|
+
'<td class="pkg-name">' + escapeHtml(pkg.name) + '</td>' +
|
|
406
|
+
'<td class="numeric">' + escapeHtml(formatTime(pkg.timeUs)) + '</td>' +
|
|
407
|
+
'<td class="bar-cell"><div class="bar-container">' +
|
|
408
|
+
'<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>' +
|
|
410
|
+
'</div></td>' +
|
|
411
|
+
'<td class="numeric">' + pkg.sampleCount + '</td>';
|
|
412
|
+
if (HAS_ASYNC) {
|
|
413
|
+
rows += '<td class="numeric async-col">' + escapeHtml(formatTime(pkg.asyncTimeUs || 0)) + '</td>' +
|
|
414
|
+
'<td class="numeric async-col">' + (pkg.asyncOpCount || 0) + '</td>';
|
|
415
|
+
}
|
|
416
|
+
rows += '</tr>';
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (otherCount > 0) {
|
|
420
|
+
rows += '<tr class="other-row">' +
|
|
421
|
+
'<td class="pkg-name">Other (' + otherCount + ' items)</td>' +
|
|
422
|
+
'<td class="numeric"></td>' +
|
|
423
|
+
'<td class="bar-cell"></td>' +
|
|
424
|
+
'<td class="numeric"></td>';
|
|
425
|
+
if (HAS_ASYNC) {
|
|
426
|
+
rows += '<td class="numeric"></td><td class="numeric"></td>';
|
|
427
|
+
}
|
|
428
|
+
rows += '</tr>';
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
var headers = '<th>Package</th><th>Wall Time</th><th>% of Total</th><th>Samples</th>';
|
|
432
|
+
if (HAS_ASYNC) {
|
|
433
|
+
headers += '<th>Async Wait</th><th>Async Ops</th>';
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return '<table><thead><tr>' + headers + '</tr></thead><tbody>' + rows + '</tbody></table>';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function asyncStats(entry) {
|
|
440
|
+
if (!HAS_ASYNC) return '';
|
|
441
|
+
var at = entry.asyncTimeUs || 0;
|
|
442
|
+
var ac = entry.asyncOpCount || 0;
|
|
443
|
+
if (at === 0 && ac === 0) return '';
|
|
444
|
+
return ' <span class="tree-async">| ' + escapeHtml(formatTime(at)) + ' async · ' + ac + ' ops</span>';
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function renderTree(packages, otherCount, totalTimeUs) {
|
|
448
|
+
var html = '<div class="tree">';
|
|
449
|
+
|
|
450
|
+
for (var i = 0; i < packages.length; i++) {
|
|
451
|
+
var pkg = packages[i];
|
|
452
|
+
var fpCls = pkg.isFirstParty ? ' fp-pkg' : '';
|
|
453
|
+
html += '<details class="level-0' + fpCls + '"><summary>';
|
|
454
|
+
html += '<span class="tree-label pkg">pkg</span>';
|
|
455
|
+
html += '<span class="tree-name">' + escapeHtml(pkg.name) + '</span>';
|
|
456
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(pkg.timeUs)) + ' · ' + escapeHtml(formatPct(pkg.timeUs, totalTimeUs)) + ' · ' + pkg.sampleCount + ' samples</span>';
|
|
457
|
+
html += asyncStats(pkg);
|
|
458
|
+
html += '</summary>';
|
|
459
|
+
|
|
460
|
+
for (var j = 0; j < pkg.files.length; j++) {
|
|
461
|
+
var file = pkg.files[j];
|
|
462
|
+
html += '<details class="level-1"><summary>';
|
|
463
|
+
html += '<span class="tree-label file">file</span>';
|
|
464
|
+
html += '<span class="tree-name">' + escapeHtml(file.name) + '</span>';
|
|
465
|
+
html += '<span class="tree-stats">' + escapeHtml(formatTime(file.timeUs)) + ' · ' + escapeHtml(formatPct(file.timeUs, totalTimeUs)) + ' · ' + file.sampleCount + ' samples</span>';
|
|
466
|
+
html += asyncStats(file);
|
|
467
|
+
html += '</summary>';
|
|
468
|
+
|
|
469
|
+
for (var k = 0; k < file.functions.length; k++) {
|
|
470
|
+
var fn = file.functions[k];
|
|
471
|
+
html += '<div class="level-2">';
|
|
472
|
+
html += '<span class="tree-label fn">fn</span> ';
|
|
473
|
+
html += '<span class="tree-name">' + escapeHtml(fn.name) + '</span>';
|
|
474
|
+
html += ' <span class="tree-stats">' + escapeHtml(formatTime(fn.timeUs)) + ' · ' + escapeHtml(formatPct(fn.timeUs, totalTimeUs)) + ' · ' + fn.sampleCount + ' samples</span>';
|
|
475
|
+
html += asyncStats(fn);
|
|
476
|
+
html += '</div>';
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (file.otherCount > 0) {
|
|
480
|
+
html += '<div class="other-item indent-2">Other (' + file.otherCount + ' items)</div>';
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
html += '</details>';
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (pkg.otherCount > 0) {
|
|
487
|
+
html += '<div class="other-item indent-1">Other (' + pkg.otherCount + ' items)</div>';
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
html += '</details>';
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (otherCount > 0) {
|
|
494
|
+
html += '<div class="other-item">Other (' + otherCount + ' packages)</div>';
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
html += '</div>';
|
|
498
|
+
return html;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function update(pct) {
|
|
502
|
+
var result = applyThreshold(DATA, pct);
|
|
503
|
+
var summaryEl = document.getElementById('summary-container');
|
|
504
|
+
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);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
510
|
+
update(5);
|
|
511
|
+
var slider = document.getElementById('threshold-slider');
|
|
512
|
+
var label = document.getElementById('threshold-value');
|
|
513
|
+
if (slider) {
|
|
514
|
+
slider.addEventListener('input', function() {
|
|
515
|
+
var val = parseFloat(slider.value);
|
|
516
|
+
if (label) label.textContent = val.toFixed(1) + '%';
|
|
517
|
+
update(val);
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
})();
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
524
|
+
|
|
242
525
|
function renderSummaryTable(
|
|
243
526
|
packages: PackageEntry[],
|
|
244
527
|
otherCount: number,
|
|
245
528
|
totalTimeUs: number,
|
|
529
|
+
hasAsync: boolean,
|
|
246
530
|
): string {
|
|
247
531
|
let rows = '';
|
|
248
532
|
|
|
@@ -259,7 +543,9 @@ function renderSummaryTable(
|
|
|
259
543
|
<span class="bar-pct">${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))}</span>
|
|
260
544
|
</div>
|
|
261
545
|
</td>
|
|
262
|
-
<td class="numeric">${pkg.sampleCount}</td
|
|
546
|
+
<td class="numeric">${pkg.sampleCount}</td>${hasAsync ? `
|
|
547
|
+
<td class="numeric async-col">${escapeHtml(formatTime(pkg.asyncTimeUs ?? 0))}</td>
|
|
548
|
+
<td class="numeric async-col">${pkg.asyncOpCount ?? 0}</td>` : ''}
|
|
263
549
|
</tr>`;
|
|
264
550
|
}
|
|
265
551
|
|
|
@@ -269,7 +555,9 @@ function renderSummaryTable(
|
|
|
269
555
|
<td class="pkg-name">Other (${otherCount} items)</td>
|
|
270
556
|
<td class="numeric"></td>
|
|
271
557
|
<td class="bar-cell"></td>
|
|
558
|
+
<td class="numeric"></td>${hasAsync ? `
|
|
272
559
|
<td class="numeric"></td>
|
|
560
|
+
<td class="numeric"></td>` : ''}
|
|
273
561
|
</tr>`;
|
|
274
562
|
}
|
|
275
563
|
|
|
@@ -280,7 +568,9 @@ function renderSummaryTable(
|
|
|
280
568
|
<th>Package</th>
|
|
281
569
|
<th>Wall Time</th>
|
|
282
570
|
<th>% of Total</th>
|
|
283
|
-
<th>Samples</th
|
|
571
|
+
<th>Samples</th>${hasAsync ? `
|
|
572
|
+
<th>Async Wait</th>
|
|
573
|
+
<th>Async Ops</th>` : ''}
|
|
284
574
|
</tr>
|
|
285
575
|
</thead>
|
|
286
576
|
<tbody>${rows}
|
|
@@ -288,10 +578,18 @@ function renderSummaryTable(
|
|
|
288
578
|
</table>`;
|
|
289
579
|
}
|
|
290
580
|
|
|
581
|
+
function formatAsyncStats(entry: { asyncTimeUs?: number; asyncOpCount?: number }): string {
|
|
582
|
+
const at = entry.asyncTimeUs ?? 0;
|
|
583
|
+
const ac = entry.asyncOpCount ?? 0;
|
|
584
|
+
if (at === 0 && ac === 0) return '';
|
|
585
|
+
return ` <span class="tree-async">| ${escapeHtml(formatTime(at))} async · ${ac} ops</span>`;
|
|
586
|
+
}
|
|
587
|
+
|
|
291
588
|
function renderTree(
|
|
292
589
|
packages: PackageEntry[],
|
|
293
590
|
otherCount: number,
|
|
294
591
|
totalTimeUs: number,
|
|
592
|
+
hasAsync: boolean,
|
|
295
593
|
): string {
|
|
296
594
|
let html = '<div class="tree">';
|
|
297
595
|
|
|
@@ -302,6 +600,7 @@ function renderTree(
|
|
|
302
600
|
html += `<span class="tree-label pkg">pkg</span>`;
|
|
303
601
|
html += `<span class="tree-name">${escapeHtml(pkg.name)}</span>`;
|
|
304
602
|
html += `<span class="tree-stats">${escapeHtml(formatTime(pkg.timeUs))} · ${escapeHtml(formatPct(pkg.timeUs, totalTimeUs))} · ${pkg.sampleCount} samples</span>`;
|
|
603
|
+
if (hasAsync) html += formatAsyncStats(pkg);
|
|
305
604
|
html += `</summary>`;
|
|
306
605
|
|
|
307
606
|
for (const file of pkg.files) {
|
|
@@ -310,6 +609,7 @@ function renderTree(
|
|
|
310
609
|
html += `<span class="tree-label file">file</span>`;
|
|
311
610
|
html += `<span class="tree-name">${escapeHtml(file.name)}</span>`;
|
|
312
611
|
html += `<span class="tree-stats">${escapeHtml(formatTime(file.timeUs))} · ${escapeHtml(formatPct(file.timeUs, totalTimeUs))} · ${file.sampleCount} samples</span>`;
|
|
612
|
+
if (hasAsync) html += formatAsyncStats(file);
|
|
313
613
|
html += `</summary>`;
|
|
314
614
|
|
|
315
615
|
for (const fn of file.functions) {
|
|
@@ -317,6 +617,7 @@ function renderTree(
|
|
|
317
617
|
html += `<span class="tree-label fn">fn</span> `;
|
|
318
618
|
html += `<span class="tree-name">${escapeHtml(fn.name)}</span>`;
|
|
319
619
|
html += ` <span class="tree-stats">${escapeHtml(formatTime(fn.timeUs))} · ${escapeHtml(formatPct(fn.timeUs, totalTimeUs))} · ${fn.sampleCount} samples</span>`;
|
|
620
|
+
if (hasAsync) html += formatAsyncStats(fn);
|
|
320
621
|
html += `</div>`;
|
|
321
622
|
}
|
|
322
623
|
|
|
@@ -344,30 +645,52 @@ function renderTree(
|
|
|
344
645
|
|
|
345
646
|
/**
|
|
346
647
|
* Render a complete self-contained HTML report from aggregated profiling data.
|
|
648
|
+
*
|
|
649
|
+
* @param data - Aggregated report data (packages, timing, project name).
|
|
650
|
+
* @returns A full HTML document string with inline CSS/JS and no external dependencies.
|
|
347
651
|
*/
|
|
348
652
|
export function renderHtml(data: ReportData): string {
|
|
349
|
-
const
|
|
350
|
-
const
|
|
653
|
+
const hasAsync = !!(data.totalAsyncTimeUs && data.totalAsyncTimeUs > 0);
|
|
654
|
+
const summaryTable = renderSummaryTable(data.packages, data.otherCount, data.totalTimeUs, hasAsync);
|
|
655
|
+
const tree = renderTree(data.packages, data.otherCount, data.totalTimeUs, hasAsync);
|
|
351
656
|
const totalFormatted = escapeHtml(formatTime(data.totalTimeUs));
|
|
352
657
|
|
|
658
|
+
const titleName = escapeHtml(data.projectName);
|
|
659
|
+
|
|
660
|
+
let metaLine = `Generated ${escapeHtml(data.timestamp)} · Total wall time: ${totalFormatted}`;
|
|
661
|
+
if (hasAsync) {
|
|
662
|
+
metaLine += ` · Total async wait: ${escapeHtml(formatTime(data.totalAsyncTimeUs!))}`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Sanitize JSON for safe embedding in <script> — replace < to prevent </script> injection
|
|
666
|
+
const safeJson = JSON.stringify(data).replace(/</g, '\\u003c');
|
|
667
|
+
|
|
353
668
|
return `<!DOCTYPE html>
|
|
354
669
|
<html lang="en">
|
|
355
670
|
<head>
|
|
356
671
|
<meta charset="utf-8">
|
|
357
672
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
358
|
-
<title
|
|
673
|
+
<title>${titleName} · where-you-at report</title>
|
|
359
674
|
<style>${generateCss()}
|
|
360
675
|
</style>
|
|
361
676
|
</head>
|
|
362
677
|
<body>
|
|
363
|
-
<h1
|
|
364
|
-
<div class="meta"
|
|
678
|
+
<h1>${titleName}</h1>
|
|
679
|
+
<div class="meta">${metaLine}</div>
|
|
365
680
|
|
|
366
681
|
<h2>Summary</h2>
|
|
367
|
-
|
|
682
|
+
<div class="threshold-control">
|
|
683
|
+
<label>Threshold</label>
|
|
684
|
+
<input type="range" id="threshold-slider" min="0" max="20" step="0.5" value="5">
|
|
685
|
+
<span id="threshold-value">5.0%</span>
|
|
686
|
+
</div>
|
|
687
|
+
<div id="summary-container">${summaryTable}</div>
|
|
368
688
|
|
|
369
689
|
<h2>Details</h2>
|
|
370
|
-
|
|
690
|
+
<div id="tree-container">${tree}</div>
|
|
691
|
+
|
|
692
|
+
<script>var __REPORT_DATA__ = ${safeJson};</script>
|
|
693
|
+
<script>${generateJs()}</script>
|
|
371
694
|
</body>
|
|
372
695
|
</html>`;
|
|
373
696
|
}
|