next-leak 0.10.0 → 0.11.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
@@ -115,6 +115,7 @@ Every report prints the gate it used.
115
115
  |---|---|---|
116
116
  | `--routes <list>` | all | Only measure these routes (comma-separated templates or prefixes) |
117
117
  | `--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 |
118
+ | `--repeat <n>` | 1 | Measure each route `n` times, each from a fresh server, and judge it on all of them (max 10). The run takes `n` times as long. Routes whose repetitions disagree are reported `inconclusive`, and the spread of growth rates is printed either way. More cycles watch one process for longer; repetitions watch different ones, which is where the variance lives — three runs of one Next build measured 602, 826 and 875 MB retained, and a fourth measured 39 MB |
118
119
  | `--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 |
119
120
  | `--connections <n>` | 100 | Concurrent connections |
120
121
  | `--idle <seconds>` | 30 | **Maximum** wait before each sample; the run continues as soon as the heap settles |
@@ -440,6 +441,17 @@ through the build's source maps.
440
441
  that is almost nothing. A 1342.9 MB capture of a leaking route parsed 2.4 MB
441
442
  of JSON and diffs fine; a 2227.5 MB one whose `strings` section alone was
442
443
  868.9 MB is refused, and the message names 869 MB rather than 2227 MB.
444
+ - **A slow snapshot is not a failed route.** `v8.writeHeapSnapshot` blocks the
445
+ measured process until the file is on disk, so the control channel goes
446
+ silent for as long as the write takes — and the worse the leak, the longer
447
+ that is. The wait is judged by bytes reaching disk, not by a clock: a
448
+ snapshot that is still growing is given as long as it needs, while five
449
+ minutes with nothing written is treated as a wedged process and said so. If
450
+ the final snapshot cannot be taken at all, the run keeps its verdict — the
451
+ curve is already complete by then — and reports the missing attribution
452
+ instead of discarding the route. Measured on the
453
+ [#84648](https://github.com/vercel/next.js/issues/84648) reproduction, whose
454
+ 4.2 GB capture outlasted every fixed deadline worth setting.
443
455
  - **Architectures:** verified on **arm64 and x64** (linux/amd64 in Docker) — same app, same parameters, same verdicts.
444
456
  - **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.
445
457
  - Empirically validated on Next **15.5.4, 16.0.x, 16.1.5, 16.2.x, 16.3.0/16.3.1 and 16.3-canary** (incl. Sentry, OpenTelemetry, PPR and i18n apps), against the public reproductions attached to real issues (open and since-fixed). Most recent measurements, 2026-08-17/18: the runtime path on 16.2.12 and 16.3.1-canary.18, the build path on 16.2.12 and 16.3.1. The contracts it relies on are stable since Next 13–14, but older versions are untested.
@@ -277,7 +277,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
277
277
 
278
278
  ---
279
279
 
280
- ## @babel/types@7.29.7
280
+ ## @babel/types@7.29.8
281
281
 
282
282
  License: MIT
283
283
  https://babel.dev/docs/en/next/babel-types
@@ -31,23 +31,32 @@ function isStepwiseGrowth(samples, deltas, growingCycles, mean, minGrowth) {
31
31
  }
32
32
  return maxDrawdown(samples) <= netGrowth * STEPWISE_MAX_DRAWDOWN_RATIO;
33
33
  }
34
- function isSaturating(deltas) {
34
+ function isSaturating(deltas, minGrowth) {
35
35
  if (deltas.length < SATURATION_MIN_CYCLES) {
36
36
  return false;
37
37
  }
38
38
  const first = deltas[0];
39
39
  const last = deltas[deltas.length - 1];
40
- if (first === void 0 || last === void 0 || first <= 0) {
40
+ if (first === void 0 || last === void 0 || first < minGrowth) {
41
41
  return false;
42
42
  }
43
- for (let i = 1; i < deltas.length; i += 1) {
44
- const current = deltas[i];
45
- const previous = deltas[i - 1];
46
- if (current === void 0 || previous === void 0 || current >= previous) {
47
- return false;
48
- }
43
+ if (!deltas.every((delta) => delta > 0)) {
44
+ return false;
49
45
  }
50
- return last <= first * SATURATION_MAX_FINAL_RATIO;
46
+ if (last > first * SATURATION_MAX_FINAL_RATIO) {
47
+ return false;
48
+ }
49
+ return isHeadingDown(deltas);
50
+ }
51
+ function isHeadingDown(deltas) {
52
+ const midpoint = Math.floor(deltas.length / 2);
53
+ const head = deltas.slice(0, midpoint);
54
+ const tail = deltas.slice(midpoint);
55
+ if (head.length === 0 || tail.length === 0) {
56
+ return false;
57
+ }
58
+ const mean = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
59
+ return mean(tail) < mean(head);
51
60
  }
52
61
  function isTooLargeToAcquit(deltas, mean, minGrowth) {
53
62
  const netGrowth = deltas.reduce((sum, delta) => sum + delta, 0);
@@ -71,10 +80,10 @@ function classifyShape(samples, options) {
71
80
  const allGrow = deltas.every((d) => d >= minGrowth);
72
81
  const anyFlatOrDown = deltas.some((d) => d <= 0);
73
82
  const growingCycles = deltas.filter((d) => d >= minGrowth).length;
83
+ if (isSaturating(deltas, minGrowth)) {
84
+ return { verdict: "saturating", growthPerCycle: mean, deltas, source: "heap" };
85
+ }
74
86
  if (allGrow) {
75
- if (isSaturating(deltas)) {
76
- return { verdict: "saturating", growthPerCycle: mean, deltas, source: "heap" };
77
- }
78
87
  return { verdict: "leak", growthPerCycle: mean, deltas, source: "heap" };
79
88
  }
80
89
  if (isStepwiseGrowth(samples, deltas, growingCycles, mean, minGrowth)) {
@@ -115,7 +124,8 @@ function effectiveVerdict(report) {
115
124
  var VERDICT_WEAKENING = /* @__PURE__ */ new Set([
116
125
  "near-threshold",
117
126
  "spiky-growth",
118
- "thin-evidence"
127
+ "thin-evidence",
128
+ "repetitions-disagree"
119
129
  ]);
120
130
  function warrantsIssueDraft(report) {
121
131
  return effectiveVerdict(report) === "leak" && !report.confidence.warnings.some((warning) => VERDICT_WEAKENING.has(warning.code));