next-leak 0.1.3 → 0.2.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 CHANGED
@@ -79,21 +79,28 @@ npx next-leak .
79
79
  For each discovered route, in a fresh process, it runs the validated ritual:
80
80
 
81
81
  ```
82
- warm-up → forced GC → baseline snapshot → [load → idle → GC → sample] ×3 → snapshot
82
+ warm-up → forced GC → baseline snapshot → [load → idle → GC → sample] ×4 → snapshot
83
83
  ```
84
84
 
85
- The verdict comes from the **shape of the post-GC curve**: retained heap that keeps growing every cycle is a leak; growth that flattens is warm-up. Absolute sizes are noise; shapes are robust.
85
+ The verdict comes from the **shape of the post-GC curve**: retained heap that
86
+ keeps growing every cycle is a leak; growth that flattens is warm-up. Where the
87
+ heap sits is noise — 40 MB and 400 MB say nothing on their own — so only the
88
+ shape is judged. The one absolute number involved is the gate a cycle's growth
89
+ must clear to count, and it scales with the traffic that cycle served, so
90
+ changing `--requests` changes how long the run takes and not what it decides.
91
+ Every report prints the gate it used.
86
92
 
87
93
  ## Options
88
94
 
89
95
  | Flag | Default | What it does |
90
96
  |---|---|---|
91
97
  | `--routes <list>` | all | Only measure these routes (comma-separated templates or prefixes) |
92
- | `--cycles <n>` | 3 | Load cycles per route (min 3) |
93
- | `--requests <n>` | 5000 | Requests per cycle |
98
+ | `--cycles <n>` | 4 | Load cycles per route (min 3). The first is dropped as warm-up, so the verdict sees `n − 1` deltas — at 3 it sees two |
99
+ | `--requests <n>` | 5000 | Requests per cycle. Raises sensitivity as well as duration: the growth gate scales with it, down to a noise floor around 5000 |
94
100
  | `--connections <n>` | 100 | Concurrent connections |
95
101
  | `--idle <seconds>` | 30 | **Maximum** wait before each sample; the run continues as soon as the heap settles |
96
- | `--quick` | off | Fast preset (2000 requests × 4 cycles, 8s idle) the exact profile the real-app validation ran with. Explicit flags override it |
102
+ | `--max-old-space <mb>` | 512 | Heap cap of each measured process. Raise it for apps whose legitimate working set is larger, or they die under measurement |
103
+ | `--quick` | off | Fast preset (2000 requests × 4 cycles, 8s idle) — the exact profile the real-app validation ran with. Same cycle count as the default; what it trades away is traffic per cycle, so it sits on the noise floor and is less sensitive to slow leaks. Explicit flags override it |
97
104
  | `--diff-all` | off | Diff snapshots for stable routes too |
98
105
  | `--output <dir>` | `<app>/.next-leak` | Where runs are written |
99
106
 
@@ -138,13 +145,19 @@ separates them, because each one has a different fix:
138
145
  | Growth that pauses and resumes (stepwise) | `leak` | A healthy route gives back 20-30% of its growth; a stepwise leak gives back nothing |
139
146
  | Native/buffer memory with a flat JS heap | `leak (external)` or an explicit RSS note | Heap, `external` and RSS are sampled and judged separately |
140
147
  | A leak in your code vs a dependency vs Next itself | `culprit: src/app/x/page.tsx (your code)` — or the package, or framework internals | Retainer chains mapped through the build's source maps |
141
- | A run whose own evidence is weak | `low confidence` warnings, or the verdict is withdrawn | Every run audits itself: did the load land, did the heap settle, does one cycle carry the average |
148
+ | A run whose own evidence is weak | `low confidence` warnings, or the verdict is withdrawn | Every run audits itself: did the load land, did the heap settle, does one cycle carry the average, did the heap run into its own ceiling |
142
149
 
143
150
  ## Reading the verdicts
144
151
 
145
- - **`stable`** — done, stop hunting. The report proves it. If the heap is flat
146
- but RSS keeps climbing, the report says so explicitly: that is an allocator,
147
- external-buffer or fragmentation problem, not a JS-heap leak.
152
+ - **`stable`** — no growth this run could detect: across the cycles it ran, the
153
+ post-GC curve never cleared the growth gate printed at the foot of the
154
+ report. That is not proof of absence, and the wording matters — the verdict
155
+ is deliberately biased toward missing a leak rather than inventing one (a
156
+ single flat or falling cycle is enough to call a route stable), so a leak
157
+ that oscillates while it climbs can land here. To press harder, raise
158
+ `--cycles` and `--requests`: both make the run more sensitive. If the heap is
159
+ flat but RSS keeps climbing, the report says so explicitly: that is an
160
+ allocator, external-buffer or fragmentation problem, not a JS-heap leak.
148
161
  - **`leak`** — the report names the culprit when attribution resolves: your file (`culprit: src/app/x/page.tsx (your code)`), a dependency (package name), or framework internals. An `ISSUE-<route>.md` draft is generated; if the leak is app-owned, the draft tells you **not** to file it upstream.
149
162
  - **`inconclusive`** — sustained sub-threshold growth: measure longer. The CLI prints the exact re-run command (`--routes <those> --cycles 6`).
150
163
  - **`failed`** — the route errored under load (auth redirects, POST-only endpoints). >1% non-2xx aborts measurement instead of measuring garbage. That's by design.
@@ -163,8 +176,10 @@ its own evidence, and anything that undermines a verdict is printed next to it:
163
176
 
164
177
  What gets checked: whether the heap actually held still before each sample,
165
178
  whether the requests you asked for really landed, whether an early-disconnect
166
- run disconnected anything, whether one cycle dominates the average, and
167
- whether the growth barely clears the noise floor.
179
+ run disconnected anything, whether one cycle dominates the average, whether
180
+ the growth barely clears the noise floor, and whether the heap came close
181
+ enough to its own cap that the curve was clipped by the ceiling rather than by
182
+ the app.
168
183
 
169
184
  When the run didn't observe what a `leak` verdict requires — the heap never
170
185
  settled, or an abandonment run abandoned nothing — the verdict is **withdrawn**
@@ -212,6 +227,11 @@ through the build's source maps.
212
227
  - **Architectures:** verified on **arm64 and x64** (linux/amd64 in Docker) — same app, same parameters, same verdicts.
213
228
  - **Attribution** (naming the file) needs a Turbopack build with server sourcemaps — the Next 15+ default. On webpack builds the registry is empty by design and findings degrade to `unattributed` with raw retainer chains; measurement itself does not depend on it. Note that `output: "standalone"` + `--webpack` produced a bundle that could not start at all on `16.3.0-canary.90` (missing `@swc/helpers`), independently of this tool.
214
229
  - Empirically validated on Next **15.5.4, 16.0.x, 16.1.5, 16.2.x and 16.3-canary** (incl. Sentry, OpenTelemetry, PPR and i18n apps), against real reproductions from open issues. The contracts it relies on are stable since Next 13–14, but older versions are untested.
230
+ - **Each measured process runs under a 512 MB heap cap** by default, so a leak
231
+ reaches a ceiling in minutes instead of the hours a production container
232
+ takes. An app whose legitimate working set is larger needs
233
+ `--max-old-space`, or every route dies as an OOM that is not the app's
234
+ fault. When a run's heap gets close to the cap, the report says so.
215
235
  - Borderline routes can flip between `stable`/`leak` across runs — more cycles resolves this.
216
236
  - The measured app runs with its real environment: routes that call external services will call them under load. Scope with `--routes` and moderate `--requests` accordingly.
217
237
 
@@ -224,6 +244,11 @@ pnpm pack:smoke # release gate: installs the real tarball and measures the
224
244
  pnpm test:mutation # Stryker — slow; run before releases, weekly in CI
225
245
  ```
226
246
 
247
+ `pnpm build` also regenerates [THIRD-PARTY-NOTICES.md](./THIRD-PARTY-NOTICES.md)
248
+ from the build's metafile and runs `scripts/check-bundle.mjs`, which fails the
249
+ build if browser tooling ever gets back into `dist/`. Commit the regenerated
250
+ notices when a dependency changes.
251
+
227
252
  CI runs typecheck, tests, build, `pnpm audit --prod` and the pack smoke on
228
253
  Node 22 and 24; mutation testing runs weekly and uploads its report.
229
254