@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/LICENSE +21 -0
- package/dist/chunk-7HD2T2AD.mjs +261 -0
- package/dist/chunk-B5TADGOF.mjs +12 -0
- package/dist/chunk-IGYROJNQ.mjs +170 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +285 -0
- package/dist/cli.mjs +123 -0
- package/dist/esm-loader.d.mts +30 -0
- package/dist/esm-loader.d.ts +30 -0
- package/dist/esm-loader.js +125 -0
- package/dist/esm-loader.mjs +100 -0
- package/dist/index.d.mts +67 -0
- package/dist/index.d.ts +67 -0
- package/dist/index.js +816 -0
- package/dist/index.mjs +368 -0
- package/dist/register.d.mts +2 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +438 -0
- package/dist/register.mjs +181 -0
- package/package.json +63 -0
- package/readme.md +440 -0
package/readme.md
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# coldstart
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/coldstart)
|
|
4
|
+
[](https://www.npmjs.com/package/coldstart)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
Profile Node.js startup one module load at a time.
|
|
9
|
+
|
|
10
|
+
`coldstart` is a zero-dependency startup profiler for Node.js that instruments CommonJS and ESM startup loading, reconstructs the dependency tree, and points at the modules that actually slow boot time down.
|
|
11
|
+
|
|
12
|
+
Terminal tree, JSON, and self-contained flamegraph output are included.
|
|
13
|
+
|
|
14
|
+
It is designed for questions like:
|
|
15
|
+
|
|
16
|
+
- Why did startup go from `300ms` to `900ms`?
|
|
17
|
+
- Which dependency chain is dominating boot?
|
|
18
|
+
- Is the time going into my code or `node_modules`?
|
|
19
|
+
- Which modules are slow themselves vs slow only because of their children?
|
|
20
|
+
- Is startup blocking the event loop?
|
|
21
|
+
|
|
22
|
+
## Highlights
|
|
23
|
+
|
|
24
|
+
- Times every CommonJS `require()` with `performance.now()`
|
|
25
|
+
- Traces ESM startup loads through Node's loader hooks
|
|
26
|
+
- Builds a parent → child module load tree
|
|
27
|
+
- Computes inclusive and exclusive time per module
|
|
28
|
+
- Tracks builtins like `fs`, `path`, and `http`
|
|
29
|
+
- Measures event loop delay during startup with `perf_hooks`
|
|
30
|
+
- Ships a terminal report, JSON report, and self-contained HTML flamegraph
|
|
31
|
+
- Has no runtime dependencies
|
|
32
|
+
|
|
33
|
+
## Status
|
|
34
|
+
|
|
35
|
+
What works today:
|
|
36
|
+
|
|
37
|
+
- CommonJS startup profiling
|
|
38
|
+
- ESM startup profiling
|
|
39
|
+
- CLI profiling
|
|
40
|
+
- programmatic API
|
|
41
|
+
- text report
|
|
42
|
+
- JSON report
|
|
43
|
+
- single-file flamegraph HTML export
|
|
44
|
+
|
|
45
|
+
What does not work yet:
|
|
46
|
+
|
|
47
|
+
- dynamic `import()` tracing
|
|
48
|
+
|
|
49
|
+
## Requirements
|
|
50
|
+
|
|
51
|
+
- Node.js `18+`
|
|
52
|
+
- Node.js `18.19+` for ESM tracing through the loader hook path
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install coldstart
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
If you are working in this repository directly:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install
|
|
64
|
+
npm run build
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
### CLI
|
|
70
|
+
|
|
71
|
+
Profile a Node app:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
coldstart server.js
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Profile an ESM entry:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
coldstart server.mjs
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Pass Node flags through `-- node ...`:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
coldstart -- node --trace-warnings server.js
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Print JSON instead of the text tree:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
coldstart --json server.js
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If you are running from this repo before publishing:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
node dist/cli.js server.js
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Programmatic API
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { monitor, renderTextReport } from 'coldstart'
|
|
105
|
+
|
|
106
|
+
const done = monitor()
|
|
107
|
+
|
|
108
|
+
require('./bootstrap')
|
|
109
|
+
require('./server')
|
|
110
|
+
|
|
111
|
+
const report = done()
|
|
112
|
+
console.log(renderTextReport(report))
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Preload Mode
|
|
116
|
+
|
|
117
|
+
If you want just the loader patch:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
node --require coldstart/register server.js
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
For ESM or mixed apps, prefer:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
node --import coldstart/register server.mjs
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
From this repo:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
node --require ./dist/register.js server.js
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
node --import ./dist/register.mjs server.mjs
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## First Useful Test
|
|
140
|
+
|
|
141
|
+
Builtins are useful for verifying that the hook is active, but they are usually too fast to teach you much.
|
|
142
|
+
|
|
143
|
+
This works as a smoke test:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
node dist/cli.js -- node -e "require('module')"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
But this is a better test because it loads real file-backed code:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
node dist/cli.js -- node -e "require('typescript')"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
For ESM:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
node dist/cli.js -- node --input-type=module -e "await import('./dist/index.mjs')"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Or:
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
node dist/cli.js -- node -e "require('express'); require('sequelize')"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
If all you test is `fs`, `path`, or `http`, seeing `0ms` is expected.
|
|
168
|
+
|
|
169
|
+
## Example Output
|
|
170
|
+
|
|
171
|
+
```text
|
|
172
|
+
coldstart - 847ms total startup
|
|
173
|
+
|
|
174
|
+
┌─ express 234ms ████████████░░░░░░
|
|
175
|
+
│ ├─ body-parser 89ms █████░░░░░░░░░░░░
|
|
176
|
+
│ └─ qs 12ms █░░░░░░░░░░░░░░░░
|
|
177
|
+
└─ sequelize 401ms █████████████████ ! slow
|
|
178
|
+
|
|
179
|
+
event loop max 42ms, p99 17ms, mean 4.3ms
|
|
180
|
+
modules 312 total, 59 cached
|
|
181
|
+
time split 286ms first-party, 503ms node_modules
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Color thresholds in the text reporter:
|
|
185
|
+
|
|
186
|
+
- green: under `20ms`
|
|
187
|
+
- yellow: `20ms` to `100ms`
|
|
188
|
+
- red: over `100ms`
|
|
189
|
+
|
|
190
|
+
## CLI Usage
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
coldstart [--json] [--color] [--no-color] app.js [args...]
|
|
194
|
+
coldstart [--json] [--color] [--no-color] -- node [node-flags...] app.js [args...]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Examples:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
coldstart app.js
|
|
201
|
+
coldstart app.js --port 3000
|
|
202
|
+
coldstart --json app.js
|
|
203
|
+
coldstart -- node --inspect app.js
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
What the CLI does:
|
|
207
|
+
|
|
208
|
+
1. spawns your app in a child Node process
|
|
209
|
+
2. preloads `coldstart/register`
|
|
210
|
+
3. lets the app run normally
|
|
211
|
+
4. collects the startup report on exit
|
|
212
|
+
5. prints either text or JSON
|
|
213
|
+
|
|
214
|
+
## Programmatic API
|
|
215
|
+
|
|
216
|
+
### `monitor()`
|
|
217
|
+
|
|
218
|
+
Starts a fresh measurement and returns a function that finalizes the report.
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { monitor } from 'coldstart'
|
|
222
|
+
|
|
223
|
+
const done = monitor()
|
|
224
|
+
const report = done()
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### `report()`
|
|
228
|
+
|
|
229
|
+
Returns the current report immediately.
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { report } from 'coldstart'
|
|
233
|
+
|
|
234
|
+
const startupReport = report()
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
This is mainly useful if you already enabled the hook separately.
|
|
238
|
+
|
|
239
|
+
### `renderTextReport(report, options?)`
|
|
240
|
+
|
|
241
|
+
Returns the terminal report as a string.
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { renderTextReport } from 'coldstart'
|
|
245
|
+
|
|
246
|
+
console.log(renderTextReport(report))
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Options:
|
|
250
|
+
|
|
251
|
+
- `color?: boolean`
|
|
252
|
+
- `barWidth?: number`
|
|
253
|
+
- `showSummary?: boolean`
|
|
254
|
+
|
|
255
|
+
### `renderJsonReport(report, options?)`
|
|
256
|
+
|
|
257
|
+
Returns the report as JSON.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import { renderJsonReport } from 'coldstart'
|
|
261
|
+
|
|
262
|
+
process.stdout.write(renderJsonReport(report))
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Options:
|
|
266
|
+
|
|
267
|
+
- `pretty?: boolean`
|
|
268
|
+
|
|
269
|
+
### `renderFlamegraphHtml(report)`
|
|
270
|
+
|
|
271
|
+
Returns a self-contained HTML flamegraph.
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import { renderFlamegraphHtml } from 'coldstart'
|
|
275
|
+
import { writeFileSync } from 'fs'
|
|
276
|
+
|
|
277
|
+
writeFileSync('coldstart.html', renderFlamegraphHtml(report))
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Open the generated file in a browser to inspect the startup timeline visually.
|
|
281
|
+
|
|
282
|
+
## How It Works
|
|
283
|
+
|
|
284
|
+
`coldstart` records raw startup load events from both the CommonJS loader and the ESM loader hook path.
|
|
285
|
+
|
|
286
|
+
For CommonJS:
|
|
287
|
+
|
|
288
|
+
- the request is resolved
|
|
289
|
+
- the parent module is tracked
|
|
290
|
+
- wall-clock load time is measured with `performance.now()`
|
|
291
|
+
- cache hits are recorded
|
|
292
|
+
- builtins are recorded as dependency edges
|
|
293
|
+
|
|
294
|
+
For ESM:
|
|
295
|
+
|
|
296
|
+
- module resolution is captured through the loader `resolve` hook
|
|
297
|
+
- module execution completion is tracked through the loader `load` hook
|
|
298
|
+
- timing is bridged back to the main tracer through a message channel
|
|
299
|
+
|
|
300
|
+
After startup, the tracer converts those raw events into:
|
|
301
|
+
|
|
302
|
+
- a tree of module loads
|
|
303
|
+
- a flat list sorted by inclusive time
|
|
304
|
+
- a slowest list sorted by exclusive time
|
|
305
|
+
- summary stats for total startup, cached loads, event loop blocking, first-party time, and `node_modules` time
|
|
306
|
+
|
|
307
|
+
## Understanding the Numbers
|
|
308
|
+
|
|
309
|
+
Each module node includes both `inclusiveMs` and `exclusiveMs`.
|
|
310
|
+
|
|
311
|
+
Use `inclusiveMs` to answer:
|
|
312
|
+
|
|
313
|
+
- Which subtree made startup slow?
|
|
314
|
+
- Which dependency chain dominates boot time?
|
|
315
|
+
|
|
316
|
+
Use `exclusiveMs` to answer:
|
|
317
|
+
|
|
318
|
+
- Which module itself is slow?
|
|
319
|
+
- Where is the actual initialization work happening?
|
|
320
|
+
|
|
321
|
+
Rule of thumb:
|
|
322
|
+
|
|
323
|
+
- high inclusive, low exclusive: the module mostly pulls in slow children
|
|
324
|
+
- high exclusive: the module itself is expensive
|
|
325
|
+
|
|
326
|
+
## Report Shape
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
interface StartupReport {
|
|
330
|
+
totalStartupMs: number
|
|
331
|
+
eventLoop: {
|
|
332
|
+
maxBlockMs: number
|
|
333
|
+
meanBlockMs: number
|
|
334
|
+
p99BlockMs: number
|
|
335
|
+
}
|
|
336
|
+
tree: ModuleNode[]
|
|
337
|
+
flat: ModuleNode[]
|
|
338
|
+
slowest: ModuleNode[]
|
|
339
|
+
nodeModuleTime: number
|
|
340
|
+
firstPartyTime: number
|
|
341
|
+
totalModulesLoaded: number
|
|
342
|
+
cachedModulesCount: number
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
interface ModuleNode {
|
|
348
|
+
id: string
|
|
349
|
+
request: string
|
|
350
|
+
resolvedPath: string
|
|
351
|
+
parentPath: string
|
|
352
|
+
inclusiveMs: number
|
|
353
|
+
exclusiveMs: number
|
|
354
|
+
cached: boolean
|
|
355
|
+
isNodeModule: boolean
|
|
356
|
+
isBuiltin: boolean
|
|
357
|
+
children: ModuleNode[]
|
|
358
|
+
depth: number
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Troubleshooting
|
|
363
|
+
|
|
364
|
+
### Why do builtins like `fs`, `path`, or `http` show `0ms`?
|
|
365
|
+
|
|
366
|
+
Because builtins are not loaded from disk like normal package files. They are still useful to record as dependency edges, but their measured cost is usually effectively zero.
|
|
367
|
+
|
|
368
|
+
### Why do I mostly see builtin modules?
|
|
369
|
+
|
|
370
|
+
Your test is probably too small. Try profiling real modules instead:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
coldstart -- node -e "require('typescript')"
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
or:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
coldstart app.js
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
where `app.js` loads real package or first-party code.
|
|
383
|
+
|
|
384
|
+
### Why do I not see ESM imports?
|
|
385
|
+
|
|
386
|
+
Make sure you are on Node `18.19+`. The ESM path depends on `module.register()` and Node's loader hooks.
|
|
387
|
+
|
|
388
|
+
Also note that current support is for startup ESM loading. Dynamic `import()` tracing is still not implemented.
|
|
389
|
+
|
|
390
|
+
### Why is the CLI output short when my app keeps running?
|
|
391
|
+
|
|
392
|
+
The CLI prints the report when the child process exits. It is intended for startup profiling runs, not long-lived always-on monitoring.
|
|
393
|
+
|
|
394
|
+
If you need more control, use the programmatic API and decide when to call `done()`.
|
|
395
|
+
|
|
396
|
+
### Why do cached modules appear at all?
|
|
397
|
+
|
|
398
|
+
Because cached loads still describe the startup dependency graph. In text output, noisy zero-cost cached builtin rows are collapsed to keep the tree readable.
|
|
399
|
+
|
|
400
|
+
## Development
|
|
401
|
+
|
|
402
|
+
Build:
|
|
403
|
+
|
|
404
|
+
```bash
|
|
405
|
+
npm run build
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Type-check:
|
|
409
|
+
|
|
410
|
+
```bash
|
|
411
|
+
npx tsc -p tsconfig.json
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Smoke test:
|
|
415
|
+
|
|
416
|
+
```bash
|
|
417
|
+
node dist/cli.js -- node -e "require('typescript')"
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
ESM smoke test:
|
|
421
|
+
|
|
422
|
+
```bash
|
|
423
|
+
node dist/cli.js -- node --input-type=module -e "await import('./dist/index.mjs')"
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Programmatic smoke test:
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
node -e "const { monitor, renderTextReport } = require('./dist/index.js'); const done = monitor(); require('typescript'); console.log(renderTextReport(done()));"
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
## Why This Exists
|
|
433
|
+
|
|
434
|
+
Startup regressions are easy to introduce and easy to miss.
|
|
435
|
+
|
|
436
|
+
A single heavy dependency, synchronous work in module scope, or a deep require chain can quietly add hundreds of milliseconds before your app is ready. `coldstart` makes that visible fast.
|
|
437
|
+
|
|
438
|
+
## License
|
|
439
|
+
|
|
440
|
+
MIT
|