@yetanotheraryan/coldstart 1.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/dist/index.js ADDED
@@ -0,0 +1,816 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ monitor: () => monitor,
24
+ renderFlamegraphHtml: () => renderFlamegraphHtml,
25
+ renderJsonReport: () => renderJsonReport,
26
+ renderTextReport: () => renderTextReport,
27
+ report: () => report
28
+ });
29
+ module.exports = __toCommonJS(src_exports);
30
+
31
+ // src/cjs.ts
32
+ var import_module = require("module");
33
+
34
+ // src/tracer.ts
35
+ var import_perf_hooks = require("perf_hooks");
36
+ var Tracer = class {
37
+ events = [];
38
+ startTime = performance.now();
39
+ histogram = null;
40
+ constructor() {
41
+ try {
42
+ this.histogram = (0, import_perf_hooks.monitorEventLoopDelay)({ resolution: 5 });
43
+ this.histogram.enable();
44
+ } catch {
45
+ this.histogram = null;
46
+ }
47
+ }
48
+ /**
49
+ * Called by cjs.ts for every require() interception.
50
+ * Appends to the flat event list — tree is built lazily on report().
51
+ */
52
+ record(event) {
53
+ this.events.push(event);
54
+ }
55
+ /**
56
+ * Mark startup as complete and return the full report.
57
+ * Call this after your server/app signals it's ready.
58
+ */
59
+ report() {
60
+ const totalStartupMs = performance.now() - this.startTime;
61
+ if (this.histogram) {
62
+ this.histogram.disable();
63
+ }
64
+ const tree = this.buildTree();
65
+ const flat = this.flatten(tree).sort((a, b) => b.inclusiveMs - a.inclusiveMs);
66
+ const slowest = [...flat].filter((n) => !n.cached && !n.isBuiltin).sort((a, b) => b.exclusiveMs - a.exclusiveMs).slice(0, 10);
67
+ const nodeModuleTime = flat.filter((n) => n.isNodeModule && !n.cached).reduce((sum, n) => sum + n.exclusiveMs, 0);
68
+ const firstPartyTime = flat.filter((n) => !n.isNodeModule && !n.isBuiltin && !n.cached).reduce((sum, n) => sum + n.exclusiveMs, 0);
69
+ return {
70
+ totalStartupMs,
71
+ eventLoop: {
72
+ maxBlockMs: this.histogram ? this.histogram.max / 1e6 : 0,
73
+ meanBlockMs: this.histogram ? this.histogram.mean / 1e6 : 0,
74
+ p99BlockMs: this.histogram ? this.histogram.percentile(99) / 1e6 : 0
75
+ },
76
+ tree,
77
+ flat,
78
+ slowest,
79
+ nodeModuleTime,
80
+ firstPartyTime,
81
+ totalModulesLoaded: this.events.length,
82
+ cachedModulesCount: this.events.filter((e) => e.cached).length
83
+ };
84
+ }
85
+ /**
86
+ * Build the parent→child tree from the flat event list.
87
+ *
88
+ * Strategy:
89
+ * 1. Create a ModuleNode for each event
90
+ * 2. Wire children to parents using parentPath
91
+ * 3. Compute exclusive time = inclusive - sum(children inclusive)
92
+ * 4. Assign depth via BFS from roots
93
+ */
94
+ buildTree() {
95
+ const nodes = this.events.map((e) => ({
96
+ id: e.id,
97
+ request: e.request,
98
+ resolvedPath: e.resolvedPath,
99
+ parentPath: e.parentPath,
100
+ inclusiveMs: e.durationMs,
101
+ exclusiveMs: e.durationMs,
102
+ // will be adjusted below
103
+ cached: e.cached,
104
+ isNodeModule: e.isNodeModule,
105
+ isBuiltin: e.isBuiltin,
106
+ children: [],
107
+ depth: 0
108
+ }));
109
+ const byPath = /* @__PURE__ */ new Map();
110
+ for (const node of nodes) {
111
+ const existing = byPath.get(node.resolvedPath) ?? [];
112
+ existing.push(node);
113
+ byPath.set(node.resolvedPath, existing);
114
+ }
115
+ const roots = [];
116
+ for (const node of nodes) {
117
+ if (node.parentPath === "<entry>") {
118
+ roots.push(node);
119
+ continue;
120
+ }
121
+ const parentNodes = byPath.get(node.parentPath);
122
+ if (parentNodes && parentNodes.length > 0) {
123
+ parentNodes[parentNodes.length - 1].children.push(node);
124
+ } else {
125
+ roots.push(node);
126
+ }
127
+ }
128
+ const computeExclusive = (node) => {
129
+ for (const child of node.children) {
130
+ computeExclusive(child);
131
+ }
132
+ const childrenInclusiveSum = node.children.reduce(
133
+ (sum, c) => sum + c.inclusiveMs,
134
+ 0
135
+ );
136
+ node.exclusiveMs = Math.max(0, node.inclusiveMs - childrenInclusiveSum);
137
+ };
138
+ for (const root of roots) {
139
+ computeExclusive(root);
140
+ }
141
+ const assignDepth = (node, depth) => {
142
+ node.depth = depth;
143
+ for (const child of node.children) {
144
+ assignDepth(child, depth + 1);
145
+ }
146
+ };
147
+ for (const root of roots) {
148
+ assignDepth(root, 0);
149
+ }
150
+ return roots;
151
+ }
152
+ /**
153
+ * Flatten the tree into a list (pre-order DFS).
154
+ */
155
+ flatten(nodes) {
156
+ const result = [];
157
+ const visit = (node) => {
158
+ result.push(node);
159
+ for (const child of node.children) {
160
+ visit(child);
161
+ }
162
+ };
163
+ for (const node of nodes) {
164
+ visit(node);
165
+ }
166
+ return result;
167
+ }
168
+ /**
169
+ * Reset — useful for testing multiple startups in the same process.
170
+ */
171
+ reset() {
172
+ this.events = [];
173
+ this.startTime = performance.now();
174
+ if (this.histogram) {
175
+ this.histogram.reset();
176
+ this.histogram.enable();
177
+ }
178
+ }
179
+ /** Expose raw events for testing */
180
+ getRawEvents() {
181
+ return [...this.events];
182
+ }
183
+ };
184
+ var tracer = new Tracer();
185
+
186
+ // src/cjs.ts
187
+ var originalLoad = import_module.Module._load;
188
+ var callStack = [];
189
+ import_module.Module._load = function coldstartLoad(request, parent, isMain) {
190
+ let resolvedFilename;
191
+ try {
192
+ resolvedFilename = import_module.Module._resolveFilename(request, parent, isMain);
193
+ } catch {
194
+ return originalLoad.apply(this, arguments);
195
+ }
196
+ const isCached = !!import_module.Module._cache[resolvedFilename];
197
+ const isBuiltin = import_module.Module.isBuiltin?.(request) ?? isBuiltinModule(request);
198
+ const parentFilename = callStack.length > 0 ? callStack[callStack.length - 1] : parent?.filename ?? "<entry>";
199
+ callStack.push(resolvedFilename);
200
+ const startTime = performance.now();
201
+ let result;
202
+ try {
203
+ result = originalLoad.apply(this, arguments);
204
+ } finally {
205
+ callStack.pop();
206
+ if (!isBuiltin) {
207
+ const duration = performance.now() - startTime;
208
+ tracer.record({
209
+ id: resolvedFilename,
210
+ request,
211
+ // original string passed to require()
212
+ resolvedPath: resolvedFilename,
213
+ parentPath: parentFilename,
214
+ durationMs: duration,
215
+ startTime,
216
+ cached: isCached,
217
+ isNodeModule: resolvedFilename.includes("node_modules"),
218
+ isBuiltin: false
219
+ });
220
+ }
221
+ }
222
+ return result;
223
+ };
224
+ var originalResolveFilename = import_module.Module._resolveFilename;
225
+ import_module.Module._resolveFilename = function coldstartResolve(request, parent, isMain, options) {
226
+ if (import_module.Module.isBuiltin?.(request) ?? isBuiltinModule(request)) {
227
+ const parentFilename = callStack.length > 0 ? callStack[callStack.length - 1] : parent?.filename ?? "<entry>";
228
+ tracer.record({
229
+ id: `builtin:${request}`,
230
+ request,
231
+ resolvedPath: `builtin:${request}`,
232
+ parentPath: parentFilename,
233
+ durationMs: 0,
234
+ startTime: performance.now(),
235
+ cached: true,
236
+ isNodeModule: false,
237
+ isBuiltin: true
238
+ });
239
+ }
240
+ return originalResolveFilename.apply(this, arguments);
241
+ };
242
+ function isBuiltinModule(name) {
243
+ const builtins = /* @__PURE__ */ new Set([
244
+ "assert",
245
+ "async_hooks",
246
+ "buffer",
247
+ "child_process",
248
+ "cluster",
249
+ "console",
250
+ "constants",
251
+ "crypto",
252
+ "dgram",
253
+ "diagnostics_channel",
254
+ "dns",
255
+ "domain",
256
+ "events",
257
+ "fs",
258
+ "http",
259
+ "http2",
260
+ "https",
261
+ "inspector",
262
+ "module",
263
+ "net",
264
+ "os",
265
+ "path",
266
+ "perf_hooks",
267
+ "process",
268
+ "punycode",
269
+ "querystring",
270
+ "readline",
271
+ "repl",
272
+ "stream",
273
+ "string_decoder",
274
+ "sys",
275
+ "timers",
276
+ "tls",
277
+ "trace_events",
278
+ "tty",
279
+ "url",
280
+ "util",
281
+ "v8",
282
+ "vm",
283
+ "wasi",
284
+ "worker_threads",
285
+ "zlib"
286
+ ]);
287
+ const stripped = name.startsWith("node:") ? name.slice(5) : name;
288
+ return builtins.has(stripped);
289
+ }
290
+
291
+ // src/reporter/text.ts
292
+ var BAR_WIDTH = 18;
293
+ var ANSI_RESET = "\x1B[0m";
294
+ var ANSI_RED = "\x1B[31m";
295
+ var ANSI_YELLOW = "\x1B[33m";
296
+ var ANSI_GREEN = "\x1B[32m";
297
+ var ANSI_DIM = "\x1B[2m";
298
+ function renderTextReport(report2, options = {}) {
299
+ const color = options.color ?? true;
300
+ const barWidth = options.barWidth ?? BAR_WIDTH;
301
+ const showSummary = options.showSummary ?? true;
302
+ const displayTree = collapseDisplayNoise(report2.tree);
303
+ const maxInclusiveMs = Math.max(
304
+ 1,
305
+ ...report2.flat.filter((node) => !node.cached).map((node) => node.inclusiveMs)
306
+ );
307
+ const lines = [
308
+ `coldstart - ${formatDuration(report2.totalStartupMs)} total startup`,
309
+ ""
310
+ ];
311
+ for (let index = 0; index < displayTree.length; index += 1) {
312
+ const isLastRoot = index === displayTree.length - 1;
313
+ const root = displayTree[index];
314
+ lines.push(...renderNode(root, "", isLastRoot, maxInclusiveMs, barWidth, color));
315
+ }
316
+ if (showSummary) {
317
+ if (displayTree.length > 0) {
318
+ lines.push("");
319
+ }
320
+ lines.push(
321
+ `${dim("event loop")} max ${formatDuration(report2.eventLoop.maxBlockMs)}, p99 ${formatDuration(report2.eventLoop.p99BlockMs)}, mean ${formatDuration(report2.eventLoop.meanBlockMs)}`,
322
+ `${dim("modules")} ${report2.totalModulesLoaded} total, ${report2.cachedModulesCount} cached`,
323
+ `${dim("time split")} ${formatDuration(report2.firstPartyTime)} first-party, ${formatDuration(report2.nodeModuleTime)} node_modules`
324
+ );
325
+ }
326
+ return lines.join("\n");
327
+ function dim(value) {
328
+ return colorize(value, ANSI_DIM, color);
329
+ }
330
+ }
331
+ function collapseDisplayNoise(nodes) {
332
+ const result = [];
333
+ const seen = /* @__PURE__ */ new Set();
334
+ for (const node of nodes) {
335
+ const collapsedChildren = collapseDisplayNoise(node.children);
336
+ const collapsedNode = {
337
+ ...node,
338
+ children: collapsedChildren
339
+ };
340
+ if (shouldCollapseBuiltinNode(collapsedNode)) {
341
+ const key = [
342
+ collapsedNode.request,
343
+ collapsedNode.depth
344
+ ].join("|");
345
+ if (seen.has(key)) {
346
+ continue;
347
+ }
348
+ seen.add(key);
349
+ }
350
+ result.push(collapsedNode);
351
+ }
352
+ return result;
353
+ }
354
+ function shouldCollapseBuiltinNode(node) {
355
+ return node.isBuiltin && node.cached && node.inclusiveMs === 0 && node.children.length === 0;
356
+ }
357
+ function renderNode(node, prefix, isLast, maxInclusiveMs, barWidth, color) {
358
+ const branch = prefix.length === 0 ? isLast ? "\u2514\u2500 " : "\u250C\u2500 " : `${prefix}${isLast ? "\u2514\u2500 " : "\u251C\u2500 "}`;
359
+ const nextPrefix = prefix.length === 0 ? isLast ? " " : "\u2502 " : `${prefix}${isLast ? " " : "\u2502 "}`;
360
+ const label = formatLabel(node);
361
+ const duration = formatDuration(node.inclusiveMs).padStart(6);
362
+ const durationColor = getDurationColor(node.inclusiveMs);
363
+ const bar = renderBar(node.inclusiveMs, maxInclusiveMs, barWidth);
364
+ const suffix = node.inclusiveMs >= 100 ? ` ${colorize("! slow", ANSI_RED, color)}` : "";
365
+ const line = `${branch}${label.padEnd(18)} ${colorize(duration, durationColor, color)} ${colorize(bar, durationColor, color)}${suffix}`;
366
+ const lines = [line];
367
+ for (let index = 0; index < node.children.length; index += 1) {
368
+ const child = node.children[index];
369
+ const isLastChild = index === node.children.length - 1;
370
+ lines.push(...renderNode(child, nextPrefix, isLastChild, maxInclusiveMs, barWidth, color));
371
+ }
372
+ return lines;
373
+ }
374
+ function formatLabel(node) {
375
+ if (node.isBuiltin) {
376
+ return node.request.replace(/^node:/, "");
377
+ }
378
+ if (node.isNodeModule) {
379
+ return packageNameFromRequest(node.request);
380
+ }
381
+ const normalizedPath = normalizeModulePath(node);
382
+ const segments = normalizedPath.split("/");
383
+ const base = segments[segments.length - 1] ?? "";
384
+ return base.length > 0 ? base : node.request;
385
+ }
386
+ function normalizeModulePath(node) {
387
+ if (looksLikeRelativeRequest(node.request)) {
388
+ return node.request.replace(/\\/g, "/");
389
+ }
390
+ if (node.resolvedPath.startsWith("file://")) {
391
+ try {
392
+ return decodeURIComponent(new URL(node.resolvedPath).pathname).replace(/\\/g, "/");
393
+ } catch {
394
+ return node.resolvedPath.replace(/\\/g, "/");
395
+ }
396
+ }
397
+ return node.resolvedPath.replace(/\\/g, "/");
398
+ }
399
+ function looksLikeRelativeRequest(request) {
400
+ return request.startsWith("./") || request.startsWith("../") || request.startsWith("/");
401
+ }
402
+ function packageNameFromRequest(request) {
403
+ if (request.startsWith("@")) {
404
+ const [scope, name2] = request.split("/");
405
+ return name2 ? `${scope}/${name2}` : request;
406
+ }
407
+ const [name] = request.split("/");
408
+ return name || request;
409
+ }
410
+ function renderBar(durationMs, maxInclusiveMs, barWidth) {
411
+ const filled = Math.max(1, Math.round(durationMs / maxInclusiveMs * barWidth));
412
+ return `${"\u2588".repeat(Math.min(barWidth, filled))}${"\u2591".repeat(Math.max(0, barWidth - filled))}`;
413
+ }
414
+ function formatDuration(valueMs) {
415
+ if (!Number.isFinite(valueMs)) {
416
+ return "0ms";
417
+ }
418
+ if (valueMs >= 1e3) {
419
+ return `${Math.round(valueMs)}ms`;
420
+ }
421
+ if (valueMs >= 100) {
422
+ return `${Math.round(valueMs)}ms`;
423
+ }
424
+ if (valueMs >= 10) {
425
+ return `${valueMs.toFixed(1).replace(/\.0$/, "")}ms`;
426
+ }
427
+ return `${valueMs.toFixed(2).replace(/0+$/, "").replace(/\.$/, "")}ms`;
428
+ }
429
+ function getDurationColor(durationMs) {
430
+ if (durationMs > 100) {
431
+ return ANSI_RED;
432
+ }
433
+ if (durationMs >= 20) {
434
+ return ANSI_YELLOW;
435
+ }
436
+ return ANSI_GREEN;
437
+ }
438
+ function colorize(value, code, enabled) {
439
+ if (!enabled) {
440
+ return value;
441
+ }
442
+ return `${code}${value}${ANSI_RESET}`;
443
+ }
444
+
445
+ // src/reporter/json.ts
446
+ function renderJsonReport(report2, options = {}) {
447
+ return `${JSON.stringify(report2, jsonReplacer, options.pretty === false ? 0 : 2)}
448
+ `;
449
+ }
450
+ function jsonReplacer(_key, value) {
451
+ if (typeof value === "number" && !Number.isFinite(value)) {
452
+ return 0;
453
+ }
454
+ return value;
455
+ }
456
+
457
+ // src/reporter/flamegraph.ts
458
+ function renderFlamegraphHtml(report2) {
459
+ const total = Math.max(report2.totalStartupMs, 1);
460
+ const frames = buildFrames(report2.tree);
461
+ const maxDepth = frames.reduce((depth, frame) => Math.max(depth, frame.depth), 0);
462
+ const payload = JSON.stringify({
463
+ totalStartupMs: report2.totalStartupMs,
464
+ eventLoop: report2.eventLoop,
465
+ totalModulesLoaded: report2.totalModulesLoaded,
466
+ cachedModulesCount: report2.cachedModulesCount,
467
+ frames,
468
+ maxDepth
469
+ });
470
+ return `<!doctype html>
471
+ <html lang="en">
472
+ <head>
473
+ <meta charset="utf-8">
474
+ <meta name="viewport" content="width=device-width, initial-scale=1">
475
+ <title>coldstart flamegraph</title>
476
+ <style>
477
+ :root {
478
+ --bg: #f7f2e8;
479
+ --panel: rgba(255, 252, 246, 0.92);
480
+ --border: #d8c9ae;
481
+ --text: #2c2418;
482
+ --muted: #6b5c48;
483
+ --grid: rgba(117, 95, 63, 0.15);
484
+ --builtin: #8fb8a8;
485
+ --node-module: #e09f5a;
486
+ --first-party: #c8553d;
487
+ --cached: #b9b0a3;
488
+ }
489
+
490
+ * { box-sizing: border-box; }
491
+ body {
492
+ margin: 0;
493
+ font-family: "IBM Plex Sans", "Avenir Next", sans-serif;
494
+ color: var(--text);
495
+ background:
496
+ radial-gradient(circle at top left, rgba(232, 171, 96, 0.28), transparent 28%),
497
+ radial-gradient(circle at top right, rgba(114, 155, 121, 0.18), transparent 25%),
498
+ linear-gradient(180deg, #fbf6ee 0%, var(--bg) 100%);
499
+ }
500
+
501
+ .page {
502
+ max-width: 1280px;
503
+ margin: 0 auto;
504
+ padding: 32px 20px 48px;
505
+ }
506
+
507
+ .hero {
508
+ display: grid;
509
+ gap: 10px;
510
+ margin-bottom: 20px;
511
+ }
512
+
513
+ h1 {
514
+ margin: 0;
515
+ font-size: clamp(28px, 4vw, 48px);
516
+ line-height: 0.95;
517
+ letter-spacing: -0.04em;
518
+ }
519
+
520
+ .subtle {
521
+ color: var(--muted);
522
+ font-size: 14px;
523
+ }
524
+
525
+ .stats {
526
+ display: flex;
527
+ flex-wrap: wrap;
528
+ gap: 12px;
529
+ margin: 18px 0 24px;
530
+ }
531
+
532
+ .stat {
533
+ background: var(--panel);
534
+ border: 1px solid var(--border);
535
+ border-radius: 14px;
536
+ padding: 12px 14px;
537
+ min-width: 150px;
538
+ backdrop-filter: blur(6px);
539
+ }
540
+
541
+ .stat strong {
542
+ display: block;
543
+ font-size: 20px;
544
+ margin-bottom: 4px;
545
+ }
546
+
547
+ .legend {
548
+ display: flex;
549
+ flex-wrap: wrap;
550
+ gap: 12px;
551
+ margin-bottom: 14px;
552
+ font-size: 13px;
553
+ color: var(--muted);
554
+ }
555
+
556
+ .legend span {
557
+ display: inline-flex;
558
+ align-items: center;
559
+ gap: 8px;
560
+ }
561
+
562
+ .swatch {
563
+ width: 12px;
564
+ height: 12px;
565
+ border-radius: 3px;
566
+ border: 1px solid rgba(0, 0, 0, 0.08);
567
+ }
568
+
569
+ .chart {
570
+ position: relative;
571
+ overflow: auto;
572
+ background: var(--panel);
573
+ border: 1px solid var(--border);
574
+ border-radius: 18px;
575
+ padding: 14px;
576
+ box-shadow: 0 20px 50px rgba(102, 75, 44, 0.08);
577
+ }
578
+
579
+ .chart-grid {
580
+ position: absolute;
581
+ inset: 14px;
582
+ pointer-events: none;
583
+ background-image:
584
+ linear-gradient(to right, var(--grid) 1px, transparent 1px),
585
+ linear-gradient(to bottom, rgba(117, 95, 63, 0.08) 1px, transparent 1px);
586
+ background-size: 10% 100%, 100% 32px;
587
+ }
588
+
589
+ #flamegraph {
590
+ position: relative;
591
+ min-width: 960px;
592
+ }
593
+
594
+ .frame {
595
+ position: absolute;
596
+ height: 26px;
597
+ border-radius: 6px;
598
+ overflow: hidden;
599
+ border: 1px solid rgba(44, 36, 24, 0.08);
600
+ cursor: default;
601
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
602
+ }
603
+
604
+ .frame span {
605
+ display: block;
606
+ padding: 5px 8px;
607
+ white-space: nowrap;
608
+ overflow: hidden;
609
+ text-overflow: ellipsis;
610
+ font-size: 12px;
611
+ font-weight: 600;
612
+ }
613
+
614
+ .tooltip {
615
+ position: fixed;
616
+ z-index: 10;
617
+ max-width: 320px;
618
+ padding: 10px 12px;
619
+ border-radius: 12px;
620
+ background: rgba(44, 36, 24, 0.96);
621
+ color: #fff9ef;
622
+ font-size: 12px;
623
+ line-height: 1.4;
624
+ pointer-events: none;
625
+ opacity: 0;
626
+ transform: translateY(6px);
627
+ transition: opacity 120ms ease, transform 120ms ease;
628
+ box-shadow: 0 12px 30px rgba(0, 0, 0, 0.25);
629
+ }
630
+
631
+ .tooltip.visible {
632
+ opacity: 1;
633
+ transform: translateY(0);
634
+ }
635
+ </style>
636
+ </head>
637
+ <body>
638
+ <div class="page">
639
+ <div class="hero">
640
+ <div class="subtle">Node.js startup profile</div>
641
+ <h1>coldstart flamegraph</h1>
642
+ <div class="subtle">Inclusive module load time laid out across startup from left to right.</div>
643
+ </div>
644
+
645
+ <div class="stats">
646
+ <div class="stat"><strong>${formatMs(report2.totalStartupMs)}</strong><span>Total startup</span></div>
647
+ <div class="stat"><strong>${report2.totalModulesLoaded}</strong><span>Modules loaded</span></div>
648
+ <div class="stat"><strong>${report2.cachedModulesCount}</strong><span>Cached requires</span></div>
649
+ <div class="stat"><strong>${formatMs(report2.eventLoop.maxBlockMs)}</strong><span>Max event loop block</span></div>
650
+ </div>
651
+
652
+ <div class="legend">
653
+ <span><i class="swatch" style="background: var(--first-party)"></i>First-party</span>
654
+ <span><i class="swatch" style="background: var(--node-module)"></i>node_modules</span>
655
+ <span><i class="swatch" style="background: var(--builtin)"></i>Builtins</span>
656
+ <span><i class="swatch" style="background: var(--cached)"></i>Cached</span>
657
+ </div>
658
+
659
+ <div class="chart">
660
+ <div class="chart-grid"></div>
661
+ <div id="flamegraph"></div>
662
+ </div>
663
+ </div>
664
+
665
+ <div id="tooltip" class="tooltip"></div>
666
+
667
+ <script>
668
+ const data = ${payload};
669
+ const total = Math.max(data.totalStartupMs || ${total}, 1);
670
+ const rowHeight = 32;
671
+ const flamegraph = document.getElementById('flamegraph');
672
+ const tooltip = document.getElementById('tooltip');
673
+ flamegraph.style.height = ((data.maxDepth + 1) * rowHeight) + 'px';
674
+
675
+ const colorFor = (frame) => {
676
+ if (frame.cached) return 'var(--cached)';
677
+ if (frame.isBuiltin) return 'var(--builtin)';
678
+ if (frame.isNodeModule) return 'var(--node-module)';
679
+ return 'var(--first-party)';
680
+ };
681
+
682
+ for (const frame of data.frames) {
683
+ const element = document.createElement('div');
684
+ const width = Math.max((frame.value / total) * 100, 0.25);
685
+ element.className = 'frame';
686
+ element.style.left = (frame.start / total) * 100 + '%';
687
+ element.style.top = (frame.depth * rowHeight) + 'px';
688
+ element.style.width = width + '%';
689
+ element.style.background = colorFor(frame);
690
+ element.innerHTML = '<span>' + escapeHtml(frame.name) + '</span>';
691
+ element.addEventListener('mousemove', (event) => {
692
+ tooltip.innerHTML =
693
+ '<strong>' + escapeHtml(frame.name) + '</strong><br>' +
694
+ 'Inclusive: ' + formatMs(frame.value) + '<br>' +
695
+ 'Range: ' + formatMs(frame.start) + ' - ' + formatMs(frame.end) + '<br>' +
696
+ 'Path: ' + escapeHtml(frame.path);
697
+ tooltip.style.left = (event.clientX + 14) + 'px';
698
+ tooltip.style.top = (event.clientY + 14) + 'px';
699
+ tooltip.classList.add('visible');
700
+ });
701
+ element.addEventListener('mouseleave', () => {
702
+ tooltip.classList.remove('visible');
703
+ });
704
+ flamegraph.appendChild(element);
705
+ }
706
+
707
+ function formatMs(value) {
708
+ if (!Number.isFinite(value)) return '0ms';
709
+ if (value >= 100) return Math.round(value) + 'ms';
710
+ if (value >= 10) return value.toFixed(1).replace(/\\.0$/, '') + 'ms';
711
+ return value.toFixed(2).replace(/0+$/, '').replace(/\\.$/, '') + 'ms';
712
+ }
713
+
714
+ function escapeHtml(value) {
715
+ return value
716
+ .replaceAll('&', '&amp;')
717
+ .replaceAll('<', '&lt;')
718
+ .replaceAll('>', '&gt;')
719
+ .replaceAll('"', '&quot;')
720
+ .replaceAll("'", '&#39;');
721
+ }
722
+ </script>
723
+ </body>
724
+ </html>`;
725
+ }
726
+ function buildFrames(nodes) {
727
+ const frames = [];
728
+ for (const node of nodes) {
729
+ walk(node, 0, formatLabel2(node));
730
+ }
731
+ return frames;
732
+ function walk(node, start, trail) {
733
+ const end = start + node.inclusiveMs;
734
+ frames.push({
735
+ name: formatLabel2(node),
736
+ value: node.inclusiveMs,
737
+ depth: node.depth,
738
+ start,
739
+ end,
740
+ path: trail,
741
+ isBuiltin: node.isBuiltin,
742
+ isNodeModule: node.isNodeModule,
743
+ cached: node.cached
744
+ });
745
+ let childOffset = start;
746
+ for (const child of node.children) {
747
+ childOffset = walk(child, childOffset, `${trail} -> ${formatLabel2(child)}`);
748
+ }
749
+ return end;
750
+ }
751
+ }
752
+ function formatLabel2(node) {
753
+ if (node.isBuiltin) {
754
+ return node.request.replace(/^node:/, "");
755
+ }
756
+ if (node.isNodeModule) {
757
+ return packageNameFromRequest2(node.request);
758
+ }
759
+ const normalizedPath = normalizeModulePath2(node);
760
+ const segments = normalizedPath.split("/");
761
+ const base = segments[segments.length - 1] ?? "";
762
+ return base.length > 0 ? base : node.request || node.resolvedPath;
763
+ }
764
+ function normalizeModulePath2(node) {
765
+ if (looksLikeRelativeRequest2(node.request)) {
766
+ return node.request.replace(/\\/g, "/");
767
+ }
768
+ if (node.resolvedPath.startsWith("file://")) {
769
+ try {
770
+ return decodeURIComponent(new URL(node.resolvedPath).pathname).replace(/\\/g, "/");
771
+ } catch {
772
+ return node.resolvedPath.replace(/\\/g, "/");
773
+ }
774
+ }
775
+ return node.resolvedPath.replace(/\\/g, "/");
776
+ }
777
+ function looksLikeRelativeRequest2(request) {
778
+ return request.startsWith("./") || request.startsWith("../") || request.startsWith("/");
779
+ }
780
+ function packageNameFromRequest2(request) {
781
+ if (request.startsWith("@")) {
782
+ const [scope, name2] = request.split("/");
783
+ return name2 ? `${scope}/${name2}` : request;
784
+ }
785
+ const [name] = request.split("/");
786
+ return name || request;
787
+ }
788
+ function formatMs(value) {
789
+ if (!Number.isFinite(value)) {
790
+ return "0ms";
791
+ }
792
+ if (value >= 100) {
793
+ return `${Math.round(value)}ms`;
794
+ }
795
+ if (value >= 10) {
796
+ return `${value.toFixed(1).replace(/\.0$/, "")}ms`;
797
+ }
798
+ return `${value.toFixed(2).replace(/0+$/, "").replace(/\.$/, "")}ms`;
799
+ }
800
+
801
+ // src/index.ts
802
+ function monitor() {
803
+ tracer.reset();
804
+ return () => tracer.report();
805
+ }
806
+ function report() {
807
+ return tracer.report();
808
+ }
809
+ // Annotate the CommonJS export names for ESM import in node:
810
+ 0 && (module.exports = {
811
+ monitor,
812
+ renderFlamegraphHtml,
813
+ renderJsonReport,
814
+ renderTextReport,
815
+ report
816
+ });