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 +12 -0
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/dist/{chunk-5ZBIS5WM.js → chunk-4FYSLLSX.js} +23 -13
- package/dist/{chunk-75W2KIXN.js → chunk-JHOE2RCM.js} +1233 -1042
- package/dist/{chunk-SV47LIER.js → chunk-YLHE4N5G.js} +1 -1
- package/dist/cli-args.d.ts +9 -0
- package/dist/cli.js +3 -2
- package/dist/confidence.d.ts +10 -1
- package/dist/control-client.d.ts +30 -3
- package/dist/{html-report-YWIK5EDQ.js → html-report-O7ILFOF5.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/ritual.d.ts +7 -0
- package/dist/runner.d.ts +37 -2
- package/package.json +1 -1
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.
|
package/THIRD-PARTY-NOTICES.md
CHANGED
|
@@ -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
|
|
40
|
+
if (first === void 0 || last === void 0 || first < minGrowth) {
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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));
|