feat: add --heapProfile flag for per-file heap NDJSON dumps#1292
Closed
pkasarda wants to merge 2 commits into
Closed
feat: add --heapProfile flag for per-file heap NDJSON dumps#1292pkasarda wants to merge 2 commits into
pkasarda wants to merge 2 commits into
Conversation
When debugging slow suites under `isolate: false`, the per-test
`--logHeapUsage` flag tells you which TESTS allocated, but not how
the worker's heap grew across FILES — which is the trajectory you
actually care about for monotonic-growth bugs.
`--heapProfile [path]` (or `heapProfile: true | <path>` in config)
appends one NDJSON line per test file with the worker's full
`process.memoryUsage()` snapshot at file boundary. Plot with `jq`
or pandas to see heap-vs-file-sequence.
```bash
rstest run --heapProfile=./heap.jsonl
jq -s 'group_by(.pid) | map({pid: .[0].pid, growth: (max_by(.heapUsed).heapUsed - min_by(.heapUsed).heapUsed)})' heap.jsonl
```
Schema (one JSON object per line):
```json
{ "pid": 12345, "seq": 1, "test": "src/foo.test.ts", "heapUsed": 71000000, "heapTotal": 136000000, "rss": 254000000, "external": 133000000, "ts": 1714563210123 }
```
Multiple workers appending to the same path is safe — each
`appendFileSync` write is below `PIPE_BUF` (4 KiB on Linux, 512 B
on macOS) so writes don't interleave mid-line. Lines from different
workers will interleave but each is self-describing.
Defaults / disabled-paths are no-ops; if the file can't be written
(permissions, full disk), the error is swallowed — a profiler bug
must never break test runs.
Files:
- `packages/core/src/types/config.ts` — public `heapProfile?: boolean | string`
- `packages/core/src/types/worker.ts` — `RuntimeConfig` picks `heapProfile`
- `packages/core/src/config.ts` — default `heapProfile: false`
- `packages/core/src/cli/commands.ts` — `--heapProfile [path]` flag def
- `packages/core/src/cli/init.ts` — `CommonOptions` + merge key
- `packages/core/src/pool/index.ts` — pass through to runtime config
- `packages/core/src/runtime/worker/runInPool.ts` — `writeHeapProfileLine` helper, called at the top of `preparePool`
- `e2e/heap-profile/` — fixture asserts NDJSON shape and per-file count
`heapProfile?: boolean | string` was already optional on the public `RstestConfig`, but `NormalizedConfig = Required<Omit<RstestConfig, OptionalKeys | ...>>` re-promoted it to required, which propagated through `RuntimeConfig` (a `Pick<NormalizedConfig, ...>`). That broke @rstest/browser's `getRuntimeConfigFromProject`: src/hostController.ts:831 - error TS2741: Property 'heapProfile' is missing in type '...' but required in type 'RuntimeConfig'. The browser host doesn't surface heap profiling (no fs in the browser sandbox), so undefined-means-off is the right semantic. Add `heapProfile` to `OptionalKeys` so it stays optional after normalization; downstream readers (`writeHeapProfileLine` in the worker) already early-return on falsy values. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Closing — on reflection this overlaps with the existing Keeping #1291 (pool.memoryLimit) as the single focused contribution from this work. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft — opening for visibility, no CC, no reviewers.
What
Adds
--heapProfile [path](CLI) andheapProfile?: boolean | string(config) — appends one NDJSON line per test file with the worker's fullprocess.memoryUsage()snapshot at file boundary.Why
--logHeapUsage(existing) records per-TESTheapUsedinto the test result. Useful for "which test allocated?" Useless for "is the worker leaking heap across FILES?" — the question that matters underisolate: false(or anything reusing workers).When debugging a long jsdom-heavy suite under reuse, we needed to see the per-worker trajectory: file 1 = 71 MB, file 10 = 745 MB, file 50 = 3.5 GB. The existing per-test heap stats don't aggregate by worker pid or by file sequence — you'd have to scrape the JSON reporter output and reconstruct it.
--heapProfilewrites the trajectory directly sojq/pandas can plot it:jq -s 'group_by(.pid)[] | { pid: .[0].pid, files: length, growth: (max_by(.heapUsed).heapUsed - min_by(.heapUsed).heapUsed) }' heap.jsonlSchema
{ "pid": 12345, "seq": 1, "test": "src/foo.test.ts", "heapUsed": 71000000, "heapTotal": 136000000, "rss": 254000000, "external": 133000000, "ts": 1714563210123 }pid— worker pid (use to group lines from the same process)seq— 1-indexed per-worker sequence number (use to detect monotonic growth)test— absolute path of the test fileheapUsed,heapTotal,rss,external—process.memoryUsage()snapshot at file-boundary entryts—Date.now()of the sampleMechanism
The snapshot is taken at the top of
preparePool— i.e. before the per-file rspack compile + setup-file evaluation — so consecutive lines for the same pid form a clean per-file trajectory.Multiple workers appending to the same path is safe: each
appendFileSyncwrite is belowPIPE_BUF(4 KiB on Linux, 512 B on macOS) so writes don't interleave mid-line. Lines from different workers will interleave but each is self-describing viapid/seq.If the file can't be written (permissions, full disk), the error is swallowed — a profiler bug must never break test runs.
What doesn't change
--logHeapUsage— unchanged. Different audience: per-test, in-band, machine-readable via JSON reporter.Files
packages/core/src/types/config.ts— publicheapProfile?: boolean | stringpackages/core/src/types/worker.ts—RuntimeConfigpicksheapProfilepackages/core/src/config.ts— defaultheapProfile: falsepackages/core/src/cli/commands.ts—--heapProfile [path]flag defpackages/core/src/cli/init.ts—CommonOptions+ merge keypackages/core/src/pool/index.ts— passes through toruntimeConfigpackages/core/src/runtime/worker/runInPool.ts—writeHeapProfileLinehelper, invoked at the top ofpreparePoole2e/heap-profile/— fixture passes--heapProfile=<tmpfile>and asserts the NDJSON shape + line countTesting
pnpm --filter @rstest/core test— 242/242 (snapshots updated for the new defaultheapProfile: false)e2e/heap-profile— 1/1Status
Draft. Opening for visibility, no CC, no reviewers. Will mark ready when the bigger memory-debugging story is sketched (the pool.memoryLimit PR is the natural companion).