@mtharrison/pkg-profiler
v2.4.0
Published
Zero-dependency sampling profiler that shows which npm packages consume your wall time
Downloads
689
Maintainers
Readme
Installation
npm install @mtharrison/pkg-profilerWorks with both ESM (import) and CommonJS (require).
Quick Start
import { start, stop } from "@mtharrison/pkg-profiler";
await start();
// ... your code here ...
const result = await stop();
result.writeHtml(); // writes an HTML report to cwdWith async I/O tracking:
import { start, stop } from "@mtharrison/pkg-profiler";
await start({ trackAsync: true });
// ... your code with network/file I/O ...
const result = await stop();
console.log(`Wall time: ${result.wallTimeUs}us`);
console.log(`Async I/O wait: ${result.totalAsyncTimeUs}us`);
result.writeHtml();Or use the convenience wrapper:
import { profile } from "@mtharrison/pkg-profiler";
const result = await profile(async () => {
await build();
});
result.writeHtml();What You Get
A self-contained HTML report that shows exactly which npm packages are eating your wall time. The summary table gives you the top-level picture; expand the tree to drill into individual files and functions. First-party code is highlighted so you can instantly see whether the bottleneck is yours or a dependency's.
API
start(options?)
Start the V8 CPU sampling profiler. Safe no-op if already profiling.
| Option | Type | Default | Description |
| ------------ | --------- | ------- | ----------------------------------------------------- |
| interval | number | 1000 | Sampling interval in microseconds |
| trackAsync | boolean | false | Enable async I/O wait time tracking via async_hooks |
stop()
Stop the profiler and return a PkgProfile containing the aggregated data. Resets the sample store afterward.
clear()
Stop profiling and discard all data without generating a profile.
profile(fn)
Profile a block of code. Starts the profiler, runs fn, stops the profiler, and returns a PkgProfile.
const result = await profile(async () => {
await runBuild();
await runTests();
});
const path = result.writeHtml();profile({ onExit })
Long-running mode for servers. Starts the profiler and registers shutdown handlers for SIGINT, SIGTERM, and beforeExit. When triggered, stops the profiler and calls onExit with the result.
StartOptions (interval, trackAsync) can be passed alongside onExit:
await profile({
trackAsync: true,
onExit: (result) => result.writeHtml(),
});
const app = createApp();
app.listen(3000);
// Ctrl+C -> stop() called -> onExit fires -> writeHtml() -> process exitsPkgProfile
Returned by stop() and profile(). Contains aggregated profiling data.
| Property | Type | Description |
| ------------------ | --------------------- | ----------------------------------------------------------------- |
| timestamp | string | When the profile was captured |
| totalTimeUs | number | Total sampled CPU time in microseconds |
| wallTimeUs | number \| undefined | Elapsed wall time from start() to stop() in microseconds |
| totalAsyncTimeUs | number \| undefined | Total async I/O wait time in microseconds (requires trackAsync) |
| packages | PackageEntry[] | Package breakdown sorted by time descending |
| otherCount | number | Number of packages below reporting threshold |
| projectName | string | Project name from package.json |
writeHtml(path?)
Write a self-contained HTML report to disk. Returns the absolute path to the written file.
- Default: writes to
./where-you-at-{timestamp}.htmlin the current directory - With path: writes to the specified location
Async I/O Tracking
Enable trackAsync: true to measure time spent waiting on async I/O (network requests, file reads, timers, etc.) in addition to CPU sampling. This uses Node.js async_hooks to track when async operations start and complete, attributing wait time to the originating package.
When enabled, each PackageEntry gains additional fields:
| Property | Type | Description |
| -------------- | --------------------- | -------------------------------------------- |
| asyncTimeUs | number \| undefined | Async wait time for this package |
| asyncPct | number \| undefined | Percentage of total async time |
| asyncOpCount | number \| undefined | Number of async operations from this package |
The HTML report will include async timing data alongside CPU time when available.
Dependency Chains
Each PackageEntry includes an optional depChain field showing how a transitive dependency is reached. For example, if raw-body is used via express -> body-parser -> raw-body, the depChain will be ["express", "body-parser", "raw-body"]. Direct dependencies and first-party code will not have a depChain.
How It Works
Uses the V8 CPU profiler (node:inspector) to sample the call stack at regular intervals. Each sample's leaf frame is attributed the elapsed wall time, then file paths are resolved to npm packages by walking up through node_modules. No code instrumentation required.
When trackAsync is enabled, async_hooks are used to additionally measure time spent waiting on async I/O and attribute it to the originating package.
Requirements
Node.js >= 20.0.0
License
MIT
