next-leak 0.10.1 → 0.11.1
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 +18 -3
- package/dist/{chunk-5ZBIS5WM.js → chunk-4FYSLLSX.js} +23 -13
- package/dist/{chunk-OIJE43WX.js → chunk-672J2BQO.js} +149 -20
- package/dist/{chunk-SV47LIER.js → chunk-YLHE4N5G.js} +1 -1
- package/dist/cli-args.d.ts +11 -0
- package/dist/cli.js +6 -2
- package/dist/confidence.d.ts +10 -1
- package/dist/{html-report-YWIK5EDQ.js → html-report-O7ILFOF5.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/launcher.d.ts +23 -0
- package/dist/ritual.d.ts +2 -0
- package/dist/runner.d.ts +36 -1
- package/dist/target.d.ts +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,13 +29,15 @@ next-leak found the growth, the retaining object and the chain that holds it —
|
|
|
29
29
|
without being told what to look for. Next.js 16.3.0 has since fixed it.
|
|
30
30
|
|
|
31
31
|
**Verified against real Next.js issues**, not synthetic fixtures. Issue states
|
|
32
|
-
checked 2026-
|
|
32
|
+
checked 2026-09-05:
|
|
33
33
|
|
|
34
34
|
| Issue | What it is | Measured | State today |
|
|
35
35
|
|---|---|---|---|
|
|
36
|
-
| [#
|
|
36
|
+
| [#97776](https://github.com/vercel/next.js/issues/97776) | `use cache`: `AbortSignal.any` composites never released (canonical issue; #97938 and #97464 are duplicates) | +705 KB per request on 16.3.3, flat on 16.2.6 (the #97938 run) | **open** — fix [#97476](https://github.com/vercel/next.js/pull/97476) merged Aug 19, in no stable release as of 16.3.4 |
|
|
37
|
+
| [#97938](https://github.com/vercel/next.js/issues/97938) | `cacheComponents`: composite abort signal never released | +705 KB per request on 16.3.3, flat on 16.2.6 | closed Aug 31 as a duplicate of #97776; fix in no stable release |
|
|
37
38
|
| [#96533](https://github.com/vercel/next.js/issues/96533) | ISR revalidation holds RSC buffers between collections | 4–5 MB of `arrayBuffers` held vs 0.32 MB retained | **open** |
|
|
38
|
-
| [#97464](https://github.com/vercel/next.js/issues/97464) | Static-gen worker retains per prerendered page | OOM after 1617 and 1525 of 2504 pages on 16.3.3; 16.2.12 finishes at 0.05 MB/page |
|
|
39
|
+
| [#97464](https://github.com/vercel/next.js/issues/97464) | Static-gen worker retains per prerendered page | OOM after 1617 and 1525 of 2504 pages on 16.3.3; 16.2.12 finishes at 0.05 MB/page | closed Aug 31 as a duplicate of #97776; fix in no stable release |
|
|
40
|
+
| [#97802](https://github.com/vercel/next.js/issues/97802) | Turbopack compilation saturates the container before rendering anything | Reporter's reproduction in a 2 CPU / 4 GB container: 16.2.12 peaks ~2 GB and finishes; 16.3.0 pins 4 GB and stalls | **open** — bisected to 16.3.0; no fix known |
|
|
39
41
|
| [#92287](https://github.com/vercel/next.js/issues/92287) | Cache Components: unbounded `arrayBuffers` under load | 37.5 MB of arrayBuffers held between collections, 37x what it retains (16.3.1) | **open** |
|
|
40
42
|
| [#84884](https://github.com/vercel/next.js/issues/84884) | axios + `AbortSignal` in middleware | 32.8 → 369.9 MB | **open** |
|
|
41
43
|
| [#89091](https://github.com/vercel/next.js/issues/89091) | zlib retention on mid-stream aborts | +42.5 MB/1000 aborted req on 16.1.5; **+0.03 on 16.3.1** | closed |
|
|
@@ -115,11 +117,13 @@ Every report prints the gate it used.
|
|
|
115
117
|
|---|---|---|
|
|
116
118
|
| `--routes <list>` | all | Only measure these routes (comma-separated templates or prefixes) |
|
|
117
119
|
| `--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 |
|
|
120
|
+
| `--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
121
|
| `--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
122
|
| `--connections <n>` | 100 | Concurrent connections |
|
|
120
123
|
| `--idle <seconds>` | 30 | **Maximum** wait before each sample; the run continues as soon as the heap settles |
|
|
121
124
|
| `--warmup <n>` | 200 | Requests before the baseline snapshot. Lower it on apps that cache per request: warm-up fills those caches and the baseline then measures the warm-up, not the app. The run says so when it happens |
|
|
122
125
|
| `--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 |
|
|
126
|
+
| `--ready-timeout <seconds>` | 60 | How long each measured process gets to start listening. Raise it for apps that boot slowly. A process that never listens reports whatever it wrote to stderr while trying |
|
|
123
127
|
| `--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 |
|
|
124
128
|
| `--no-resolve` | off | Skip the second pass on inconclusive routes |
|
|
125
129
|
| `--self-check` | off | Measure a planted leak first to prove the harness works here. Costs one route's worth of time; a run that cannot detect 8 KB per request produces verdicts worth nothing |
|
|
@@ -406,6 +410,17 @@ through the build's source maps.
|
|
|
406
410
|
|
|
407
411
|
|
|
408
412
|
- **Supported (default command):** App Router · `output: "standalone"` · Node ≥ 22 · Linux/macOS. Pages Router, non-standalone, and Windows are rejected with a clear message.
|
|
413
|
+
- **Monorepos work unchanged.** A workspace build does not put `server.js` at
|
|
414
|
+
the root of `.next/standalone`: Next keeps the app's path relative to the
|
|
415
|
+
workspace root, so an app in `client/` ships its server at
|
|
416
|
+
`.next/standalone/client/server.js` with `node_modules` hoisted above it.
|
|
417
|
+
Point the tool at the app directory as usual — it looks there too. Do not
|
|
418
|
+
flatten that tree by hand: on a pnpm workspace the moved `server.js` cannot
|
|
419
|
+
resolve `next` any more, because what sits in `standalone/<app>/node_modules`
|
|
420
|
+
are relative symlinks into `../../node_modules/.pnpm/`, and a level up they
|
|
421
|
+
point outside the tree. An npm workspace survives the move — it puts no
|
|
422
|
+
`node_modules` beside the app at all — which is why the same build can serve
|
|
423
|
+
by hand and still be unmeasurable.
|
|
409
424
|
- **Sample values can vary per request.** `"slug": "post-{n}"` gives every
|
|
410
425
|
request its own URL — the shape of bot traffic and of a cache that never
|
|
411
426
|
repeats a key. `"slug": "post-{n%200}"` cycles through exactly 200 distinct
|
|
@@ -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));
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
minGrowthFor,
|
|
8
8
|
resolveCycles,
|
|
9
9
|
warrantsIssueDraft
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-4FYSLLSX.js";
|
|
11
11
|
import {
|
|
12
12
|
assessPeakPressure,
|
|
13
13
|
describePeakPressure
|
|
@@ -92698,6 +92698,26 @@ async function readManifest(file, parse2) {
|
|
|
92698
92698
|
);
|
|
92699
92699
|
}
|
|
92700
92700
|
}
|
|
92701
|
+
async function findStandaloneServer(appDir, standaloneDir) {
|
|
92702
|
+
const atRoot = path2.join(standaloneDir, "server.js");
|
|
92703
|
+
if (await exists(atRoot)) {
|
|
92704
|
+
return atRoot;
|
|
92705
|
+
}
|
|
92706
|
+
let current = path2.resolve(appDir);
|
|
92707
|
+
let suffix = "";
|
|
92708
|
+
for (; ; ) {
|
|
92709
|
+
const parent = path2.dirname(current);
|
|
92710
|
+
if (parent === current) {
|
|
92711
|
+
return void 0;
|
|
92712
|
+
}
|
|
92713
|
+
suffix = suffix === "" ? path2.basename(current) : path2.join(path2.basename(current), suffix);
|
|
92714
|
+
const candidate = path2.join(standaloneDir, suffix, "server.js");
|
|
92715
|
+
if (await exists(candidate)) {
|
|
92716
|
+
return candidate;
|
|
92717
|
+
}
|
|
92718
|
+
current = parent;
|
|
92719
|
+
}
|
|
92720
|
+
}
|
|
92701
92721
|
async function validateTarget(appDir) {
|
|
92702
92722
|
const nextDir = path2.resolve(appDir, ".next");
|
|
92703
92723
|
if (!await exists(nextDir)) {
|
|
@@ -92706,11 +92726,11 @@ async function validateTarget(appDir) {
|
|
|
92706
92726
|
`No .next directory in ${appDir}. Run "next build" first.`
|
|
92707
92727
|
);
|
|
92708
92728
|
}
|
|
92709
|
-
const standaloneServer = path2.join(nextDir, "standalone"
|
|
92710
|
-
if (
|
|
92729
|
+
const standaloneServer = await findStandaloneServer(appDir, path2.join(nextDir, "standalone"));
|
|
92730
|
+
if (standaloneServer === void 0) {
|
|
92711
92731
|
throw new TargetError(
|
|
92712
92732
|
"NO_STANDALONE",
|
|
92713
|
-
`No ${
|
|
92733
|
+
`No ${path2.join(nextDir, "standalone", "server.js")}.
|
|
92714
92734
|
next-leak measures the standalone server bundle. Enable it once in next.config:
|
|
92715
92735
|
|
|
92716
92736
|
const nextConfig = {
|
|
@@ -92754,6 +92774,7 @@ import { pathToFileURL } from "url";
|
|
|
92754
92774
|
import { z as z3 } from "zod";
|
|
92755
92775
|
var controlFileSchema = z3.object({ port: z3.number(), pid: z3.number() });
|
|
92756
92776
|
var DEFAULT_MAX_OLD_SPACE_MB = 512;
|
|
92777
|
+
var DEFAULT_READY_TIMEOUT_MS = 6e4;
|
|
92757
92778
|
var LaunchError = class extends Error {
|
|
92758
92779
|
constructor(message) {
|
|
92759
92780
|
super(message);
|
|
@@ -92794,8 +92815,16 @@ function explainRuntimeFailure(stderr, maxOldSpaceMb) {
|
|
|
92794
92815
|
}
|
|
92795
92816
|
return `the measured process exited mid-run. ${explainStartupFailure(stderr)}`;
|
|
92796
92817
|
}
|
|
92818
|
+
function appNeverListened(hostname, port, budgetMs, stderr) {
|
|
92819
|
+
const head = `app on ${hostname}:${port} \u2014 the process started and answered on its control channel, but never listened within ${Math.round(budgetMs / 1e3)}s`;
|
|
92820
|
+
if (stderr.trim() !== "") {
|
|
92821
|
+
return `${head}. It wrote this while starting:
|
|
92822
|
+
${stderr.trim()}`;
|
|
92823
|
+
}
|
|
92824
|
+
return `${head}, and wrote nothing to stderr. An app that boots slower than that needs a larger budget: --ready-timeout <seconds>. Otherwise it is hanging during startup \u2014 starting the same server.js by hand, with PORT set, hangs the same way and can be interrupted to see where`;
|
|
92825
|
+
}
|
|
92797
92826
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
92798
|
-
async function pollUntil(deadline,
|
|
92827
|
+
async function pollUntil(deadline, describe, probe, failed) {
|
|
92799
92828
|
for (; ; ) {
|
|
92800
92829
|
const failure = failed();
|
|
92801
92830
|
if (failure !== void 0) {
|
|
@@ -92806,14 +92835,14 @@ async function pollUntil(deadline, what, probe, failed) {
|
|
|
92806
92835
|
return result;
|
|
92807
92836
|
}
|
|
92808
92837
|
if (Date.now() > deadline) {
|
|
92809
|
-
throw new LaunchError(`timed out waiting for ${
|
|
92838
|
+
throw new LaunchError(`timed out waiting for ${describe()}`);
|
|
92810
92839
|
}
|
|
92811
92840
|
await sleep(100);
|
|
92812
92841
|
}
|
|
92813
92842
|
}
|
|
92814
92843
|
async function launchInstrumented(options) {
|
|
92815
92844
|
const hostname = options.hostname ?? "127.0.0.1";
|
|
92816
|
-
const
|
|
92845
|
+
const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
|
|
92817
92846
|
const child = spawn(
|
|
92818
92847
|
process.execPath,
|
|
92819
92848
|
[
|
|
@@ -92857,8 +92886,8 @@ ${stderrTailBuffer}`;
|
|
|
92857
92886
|
const failed = () => exited ? `server exited before becoming ready. ${explainStartupFailure(stderrWindow())}` : void 0;
|
|
92858
92887
|
try {
|
|
92859
92888
|
const controlPort = await pollUntil(
|
|
92860
|
-
|
|
92861
|
-
`control channel in ${options.workDir}`,
|
|
92889
|
+
Date.now() + readyTimeoutMs,
|
|
92890
|
+
() => `control channel in ${options.workDir}`,
|
|
92862
92891
|
// Several processes may announce a channel (clustered servers); accept
|
|
92863
92892
|
// the first one that actually answers instead of trusting a filename.
|
|
92864
92893
|
async () => {
|
|
@@ -92888,8 +92917,8 @@ ${stderrTailBuffer}`;
|
|
|
92888
92917
|
failed
|
|
92889
92918
|
);
|
|
92890
92919
|
await pollUntil(
|
|
92891
|
-
|
|
92892
|
-
|
|
92920
|
+
Date.now() + readyTimeoutMs,
|
|
92921
|
+
() => appNeverListened(hostname, options.appPort, readyTimeoutMs, stderrWindow()),
|
|
92893
92922
|
async () => {
|
|
92894
92923
|
try {
|
|
92895
92924
|
await fetch(`http://${hostname}:${options.appPort}/`, { method: "HEAD" });
|
|
@@ -92939,11 +92968,13 @@ var BUILD_COMMAND = "build";
|
|
|
92939
92968
|
var RUN_ONLY_FLAGS = [
|
|
92940
92969
|
["routes", "--routes"],
|
|
92941
92970
|
["cycles", "--cycles"],
|
|
92971
|
+
["repeat", "--repeat"],
|
|
92942
92972
|
["requests", "--requests"],
|
|
92943
92973
|
["connections", "--connections"],
|
|
92944
92974
|
["idleSeconds", "--idle"],
|
|
92945
92975
|
["warmupRequests", "--warmup"],
|
|
92946
92976
|
["maxOldSpaceMb", "--max-old-space"],
|
|
92977
|
+
["readyTimeoutSeconds", "--ready-timeout"],
|
|
92947
92978
|
["quick", "--quick"],
|
|
92948
92979
|
["noResolve", "--no-resolve"],
|
|
92949
92980
|
["selfCheck", "--self-check"],
|
|
@@ -92967,6 +92998,12 @@ var FLAGS = [
|
|
|
92967
92998
|
help: "Only measure these routes \u2014 comma-separated templates or prefixes (e.g. /api,/dashboard)"
|
|
92968
92999
|
},
|
|
92969
93000
|
{ flag: "--cycles", value: "int", argName: "<n>", help: "Load cycles per route (default 4, minimum 3)" },
|
|
93001
|
+
{
|
|
93002
|
+
flag: "--repeat",
|
|
93003
|
+
value: "int",
|
|
93004
|
+
argName: "<n>",
|
|
93005
|
+
help: "Measure each route n times from a fresh server (default 1) \u2014 the run takes n times as long, and routes whose repetitions disagree are reported inconclusive"
|
|
93006
|
+
},
|
|
92970
93007
|
{ flag: "--requests", value: "int", argName: "<n>", help: "Requests per cycle (default 5000)" },
|
|
92971
93008
|
{ flag: "--connections", value: "int", argName: "<n>", help: "Concurrent connections (default 100)" },
|
|
92972
93009
|
{ flag: "--idle", value: "int", argName: "<seconds>", help: "Idle seconds before each sample (default 30)" },
|
|
@@ -92982,6 +93019,12 @@ var FLAGS = [
|
|
|
92982
93019
|
argName: "<mb>",
|
|
92983
93020
|
help: "Heap cap of each measured process (default 512) \u2014 raise it for apps whose working set is larger"
|
|
92984
93021
|
},
|
|
93022
|
+
{
|
|
93023
|
+
flag: "--ready-timeout",
|
|
93024
|
+
value: "int",
|
|
93025
|
+
argName: "<seconds>",
|
|
93026
|
+
help: "Seconds each measured process gets to start listening (default 60) \u2014 raise it for slow-booting apps"
|
|
93027
|
+
},
|
|
92985
93028
|
{
|
|
92986
93029
|
flag: "--quick",
|
|
92987
93030
|
value: "none",
|
|
@@ -93048,11 +93091,13 @@ interrupted, 1 on errors.
|
|
|
93048
93091
|
}
|
|
93049
93092
|
var LIMITS = {
|
|
93050
93093
|
"--cycles": 100,
|
|
93094
|
+
"--repeat": 10,
|
|
93051
93095
|
"--requests": 1e6,
|
|
93052
93096
|
"--connections": 1e4,
|
|
93053
93097
|
"--idle": 3600,
|
|
93054
93098
|
"--warmup": 1e6,
|
|
93055
|
-
"--max-old-space": 65536
|
|
93099
|
+
"--max-old-space": 65536,
|
|
93100
|
+
"--ready-timeout": 600
|
|
93056
93101
|
};
|
|
93057
93102
|
var MIN_MAX_OLD_SPACE_MB = 128;
|
|
93058
93103
|
function findSpec(argument) {
|
|
@@ -93088,11 +93133,13 @@ function applyNumericFlag(flag, value, options) {
|
|
|
93088
93133
|
);
|
|
93089
93134
|
}
|
|
93090
93135
|
if (flag === "--cycles") options.cycles = parsed;
|
|
93136
|
+
if (flag === "--repeat") options.repeat = parsed;
|
|
93091
93137
|
if (flag === "--requests") options.requests = parsed;
|
|
93092
93138
|
if (flag === "--connections") options.connections = parsed;
|
|
93093
93139
|
if (flag === "--idle") options.idleSeconds = parsed;
|
|
93094
93140
|
if (flag === "--warmup") options.warmupRequests = parsed;
|
|
93095
93141
|
if (flag === "--max-old-space") options.maxOldSpaceMb = parsed;
|
|
93142
|
+
if (flag === "--ready-timeout") options.readyTimeoutSeconds = parsed;
|
|
93096
93143
|
return FLAG_OK;
|
|
93097
93144
|
}
|
|
93098
93145
|
function applyFlag(spec, value, options) {
|
|
@@ -93100,11 +93147,13 @@ function applyFlag(spec, value, options) {
|
|
|
93100
93147
|
case "--routes":
|
|
93101
93148
|
return applyRoutesFlag(value, options);
|
|
93102
93149
|
case "--cycles":
|
|
93150
|
+
case "--repeat":
|
|
93103
93151
|
case "--requests":
|
|
93104
93152
|
case "--connections":
|
|
93105
93153
|
case "--idle":
|
|
93106
93154
|
case "--warmup":
|
|
93107
93155
|
case "--max-old-space":
|
|
93156
|
+
case "--ready-timeout":
|
|
93108
93157
|
return applyNumericFlag(spec.flag, value, options);
|
|
93109
93158
|
case "--quick":
|
|
93110
93159
|
options.quick = true;
|
|
@@ -93171,11 +93220,13 @@ function parseCliArgs(argv) {
|
|
|
93171
93220
|
appDir: "",
|
|
93172
93221
|
routes: null,
|
|
93173
93222
|
cycles: null,
|
|
93223
|
+
repeat: null,
|
|
93174
93224
|
requests: null,
|
|
93175
93225
|
connections: null,
|
|
93176
93226
|
idleSeconds: null,
|
|
93177
93227
|
warmupRequests: null,
|
|
93178
93228
|
maxOldSpaceMb: null,
|
|
93229
|
+
readyTimeoutSeconds: null,
|
|
93179
93230
|
quick: false,
|
|
93180
93231
|
noResolve: false,
|
|
93181
93232
|
selfCheck: false,
|
|
@@ -93858,7 +93909,8 @@ async function runRitual(options, deps = defaultDeps) {
|
|
|
93858
93909
|
serverPath: options.serverPath,
|
|
93859
93910
|
workDir: options.workDir,
|
|
93860
93911
|
bootstrapPath: options.bootstrapPath,
|
|
93861
|
-
...options.maxOldSpaceMb !== void 0 && { maxOldSpaceMb: options.maxOldSpaceMb }
|
|
93912
|
+
...options.maxOldSpaceMb !== void 0 && { maxOldSpaceMb: options.maxOldSpaceMb },
|
|
93913
|
+
...options.readyTimeoutMs !== void 0 && { readyTimeoutMs: options.readyTimeoutMs }
|
|
93862
93914
|
};
|
|
93863
93915
|
let app;
|
|
93864
93916
|
try {
|
|
@@ -94242,6 +94294,19 @@ function revalidationLines(route) {
|
|
|
94242
94294
|
` driven through ISR revalidation (revalidates every ${route.revalidatedEverySeconds}s; without it the load would serve the cache)`
|
|
94243
94295
|
];
|
|
94244
94296
|
}
|
|
94297
|
+
function repetitionLines(route) {
|
|
94298
|
+
const repetitions = route.repetitions;
|
|
94299
|
+
if (repetitions === void 0 || repetitions.length < 2) {
|
|
94300
|
+
return [];
|
|
94301
|
+
}
|
|
94302
|
+
const rates = repetitions.map((entry) => entry.growthPer1000Requests);
|
|
94303
|
+
const low = Math.min(...rates);
|
|
94304
|
+
const high = Math.max(...rates);
|
|
94305
|
+
const verdicts = [...new Set(repetitions.map((entry) => entry.verdict))];
|
|
94306
|
+
const agreement = verdicts.length === 1 ? `all ${repetitions.length} agreed on ${verdicts[0]}` : `they disagreed (${verdicts.join(", ")}), so no single verdict is reported`;
|
|
94307
|
+
const range = `${low >= 0 ? "+" : ""}${(low / MB2).toFixed(2)} to ${formatGrowth(high)}`;
|
|
94308
|
+
return [` across ${repetitions.length} repetitions: ${range} \u2014 ${agreement}`];
|
|
94309
|
+
}
|
|
94245
94310
|
function cacheLines(route) {
|
|
94246
94311
|
const lines = [];
|
|
94247
94312
|
if (route.trend.verdict === "saturating") {
|
|
@@ -94363,6 +94428,7 @@ function routeLines(route, parameters) {
|
|
|
94363
94428
|
` ${VERDICT_ICON[verdict]} ${route.route} ${verdict} (${formatGrowth(
|
|
94364
94429
|
route.growthPer1000Requests
|
|
94365
94430
|
)}) heap ${curve}${resolved}`,
|
|
94431
|
+
...repetitionLines(route),
|
|
94366
94432
|
...revalidationLines(route),
|
|
94367
94433
|
...cacheLines(route),
|
|
94368
94434
|
...abandonLines(route),
|
|
@@ -94752,9 +94818,10 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
94752
94818
|
...options.warmupRequests !== void 0 && { warmupRequests: options.warmupRequests },
|
|
94753
94819
|
...options.loadRequests !== void 0 && { loadRequests: options.loadRequests },
|
|
94754
94820
|
...options.connections !== void 0 && { connections: options.connections },
|
|
94755
|
-
...pass !== void 0 ? { cycles: pass.cycles } : options.cycles !== void 0 && { cycles: options.cycles },
|
|
94821
|
+
...pass?.cycles !== void 0 ? { cycles: pass.cycles } : options.cycles !== void 0 && { cycles: options.cycles },
|
|
94756
94822
|
...options.idleMs !== void 0 && { idleMs: options.idleMs },
|
|
94757
94823
|
...options.maxOldSpaceMb !== void 0 && { maxOldSpaceMb: options.maxOldSpaceMb },
|
|
94824
|
+
...options.readyTimeoutMs !== void 0 && { readyTimeoutMs: options.readyTimeoutMs },
|
|
94758
94825
|
...headers !== void 0 && { headers },
|
|
94759
94826
|
...routeConfig.abandonAfterMs !== void 0 && {
|
|
94760
94827
|
abandonAfterMs: routeConfig.abandonAfterMs
|
|
@@ -94810,7 +94877,7 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
94810
94877
|
route: route.path,
|
|
94811
94878
|
status: "measured",
|
|
94812
94879
|
requestPath,
|
|
94813
|
-
...pass !== void 0 && { resolvedWithCycles: pass.cycles },
|
|
94880
|
+
...pass?.cycles !== void 0 && { resolvedWithCycles: pass.cycles },
|
|
94814
94881
|
samples: result.samples,
|
|
94815
94882
|
memorySamples: result.memorySamples,
|
|
94816
94883
|
peaks: result.peaks,
|
|
@@ -94841,7 +94908,7 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
94841
94908
|
};
|
|
94842
94909
|
}
|
|
94843
94910
|
async function writeEvidenceBundle(report, workDir) {
|
|
94844
|
-
const { renderHtmlReport } = await import("./html-report-
|
|
94911
|
+
const { renderHtmlReport } = await import("./html-report-O7ILFOF5.js");
|
|
94845
94912
|
const { renderIssueMarkdown } = await import("./issue-report-FPVOZ6HU.js");
|
|
94846
94913
|
for (const route of report.routes) {
|
|
94847
94914
|
if (route.status === "measured" && warrantsIssueDraft(route)) {
|
|
@@ -94879,7 +94946,7 @@ async function routeReportFor(context, route, index, total) {
|
|
|
94879
94946
|
progress(`not measuring ${label}: ${plan.reason}`);
|
|
94880
94947
|
return { route: route.path, status: "not-exercised", reason: plan.reason };
|
|
94881
94948
|
}
|
|
94882
|
-
return
|
|
94949
|
+
return measureRepeatedly(context, route, requestPath, index, label);
|
|
94883
94950
|
}
|
|
94884
94951
|
function reasonToResolve(verdict) {
|
|
94885
94952
|
switch (verdict) {
|
|
@@ -94892,12 +94959,74 @@ function reasonToResolve(verdict) {
|
|
|
94892
94959
|
return null;
|
|
94893
94960
|
}
|
|
94894
94961
|
}
|
|
94895
|
-
async function
|
|
94962
|
+
async function measureRepeatedly(context, route, requestPath, index, label) {
|
|
94963
|
+
const repeat = context.options.repeat ?? 1;
|
|
94964
|
+
if (repeat <= 1) {
|
|
94965
|
+
return measureWithResolution(context, route, requestPath, index, label);
|
|
94966
|
+
}
|
|
94967
|
+
const passes = [];
|
|
94968
|
+
for (let attempt = 1; attempt <= repeat; attempt += 1) {
|
|
94969
|
+
context.progress(`repetition ${attempt}/${repeat} of ${label}`);
|
|
94970
|
+
passes.push(
|
|
94971
|
+
await measureWithResolution(context, route, requestPath, index, label, `-rep${attempt}`)
|
|
94972
|
+
);
|
|
94973
|
+
if (context.options.signal?.aborted === true) {
|
|
94974
|
+
break;
|
|
94975
|
+
}
|
|
94976
|
+
}
|
|
94977
|
+
return aggregateRepetitions(passes);
|
|
94978
|
+
}
|
|
94979
|
+
function aggregateRepetitions(passes) {
|
|
94980
|
+
const first = passes[0];
|
|
94981
|
+
if (first === void 0) {
|
|
94982
|
+
return { route: "", status: "failed", reason: "no repetition produced a result" };
|
|
94983
|
+
}
|
|
94984
|
+
const measured = passes.filter((pass) => pass.status === "measured");
|
|
94985
|
+
if (measured.length !== passes.length || measured.length === 0) {
|
|
94986
|
+
return passes.find((pass) => pass.status !== "measured") ?? first;
|
|
94987
|
+
}
|
|
94988
|
+
const repetitions = measured.map((pass) => ({
|
|
94989
|
+
verdict: pass.trend.verdict,
|
|
94990
|
+
growthPer1000Requests: pass.trend.growthPerCycle / pass.requestsPerCycle * 1e3
|
|
94991
|
+
}));
|
|
94992
|
+
const verdicts = [...new Set(repetitions.map((entry) => entry.verdict))];
|
|
94993
|
+
const winner = measured[0];
|
|
94994
|
+
if (winner === void 0) {
|
|
94995
|
+
return first;
|
|
94996
|
+
}
|
|
94997
|
+
if (verdicts.length === 1) {
|
|
94998
|
+
return { ...winner, repetitions };
|
|
94999
|
+
}
|
|
95000
|
+
const observed = verdicts.join(", ");
|
|
95001
|
+
return {
|
|
95002
|
+
...winner,
|
|
95003
|
+
repetitions,
|
|
95004
|
+
confidence: {
|
|
95005
|
+
...winner.confidence,
|
|
95006
|
+
level: "low",
|
|
95007
|
+
warnings: [
|
|
95008
|
+
...winner.confidence.warnings,
|
|
95009
|
+
{
|
|
95010
|
+
code: "repetitions-disagree",
|
|
95011
|
+
detail: `${repetitions.length} repetitions of this route disagreed (${observed}) \u2014 the measurement did not settle, so no single verdict is reported`
|
|
95012
|
+
}
|
|
95013
|
+
],
|
|
95014
|
+
supersededVerdict: "inconclusive"
|
|
95015
|
+
}
|
|
95016
|
+
};
|
|
95017
|
+
}
|
|
95018
|
+
async function measureWithResolution(context, route, requestPath, index, label, repetitionSuffix = "") {
|
|
94896
95019
|
const { progress } = context;
|
|
94897
95020
|
try {
|
|
94898
95021
|
const asPath = requestPath === route.path ? "" : ` as ${requestPath}`;
|
|
94899
95022
|
progress(`measuring ${label}${asPath}`);
|
|
94900
|
-
const first = await measureRoute(
|
|
95023
|
+
const first = await measureRoute(
|
|
95024
|
+
context,
|
|
95025
|
+
route,
|
|
95026
|
+
requestPath,
|
|
95027
|
+
index,
|
|
95028
|
+
repetitionSuffix === "" ? void 0 : { dirSuffix: repetitionSuffix }
|
|
95029
|
+
);
|
|
94901
95030
|
const reason = first.status === "measured" ? reasonToResolve(effectiveVerdict(first)) : null;
|
|
94902
95031
|
if (first.status !== "measured" || context.options.resolveInconclusive === false || reason === null) {
|
|
94903
95032
|
return first;
|
|
@@ -94910,7 +95039,7 @@ async function measureWithResolution(context, route, requestPath, index, label)
|
|
|
94910
95039
|
try {
|
|
94911
95040
|
return await measureRoute(context, route, requestPath, index, {
|
|
94912
95041
|
cycles,
|
|
94913
|
-
dirSuffix:
|
|
95042
|
+
dirSuffix: `${repetitionSuffix}-resolve`
|
|
94914
95043
|
});
|
|
94915
95044
|
} catch (cause) {
|
|
94916
95045
|
progress(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createRequire as __nextLeakCreateRequire } from 'node:module';import { fileURLToPath as __nextLeakFileURLToPath } from 'node:url';import { dirname as __nextLeakDirname } from 'node:path';const require = __nextLeakCreateRequire(import.meta.url);const __filename = __nextLeakFileURLToPath(import.meta.url);const __dirname = __nextLeakDirname(__filename);
|
|
2
2
|
import {
|
|
3
3
|
effectiveVerdict
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-4FYSLLSX.js";
|
|
5
5
|
import {
|
|
6
6
|
assessPeakPressure,
|
|
7
7
|
describePeakPressure
|
package/dist/cli-args.d.ts
CHANGED
|
@@ -2,11 +2,22 @@ export type CliRunOptions = {
|
|
|
2
2
|
appDir: string;
|
|
3
3
|
routes: string[] | null;
|
|
4
4
|
cycles: number | null;
|
|
5
|
+
/**
|
|
6
|
+
* How many times to measure each route, from a fresh server each time.
|
|
7
|
+
*
|
|
8
|
+
* More cycles watch one process for longer; repetitions watch different
|
|
9
|
+
* processes. The spreads that make a verdict unpublishable live between
|
|
10
|
+
* processes: vercel/next.js#84648 gave 602, 826 and 875 MB on three runs of
|
|
11
|
+
* one build and 39 MB on a fourth.
|
|
12
|
+
*/
|
|
13
|
+
repeat: number | null;
|
|
5
14
|
requests: number | null;
|
|
6
15
|
connections: number | null;
|
|
7
16
|
idleSeconds: number | null;
|
|
8
17
|
warmupRequests: number | null;
|
|
9
18
|
maxOldSpaceMb: number | null;
|
|
19
|
+
/** Seconds each measured process gets to boot before the route is failed. */
|
|
20
|
+
readyTimeoutSeconds: number | null;
|
|
10
21
|
quick: boolean;
|
|
11
22
|
noResolve: boolean;
|
|
12
23
|
selfCheck: boolean;
|
package/dist/cli.js
CHANGED
|
@@ -22,10 +22,10 @@ import {
|
|
|
22
22
|
runSelfCheck,
|
|
23
23
|
unregisterChild,
|
|
24
24
|
validateTarget
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-672J2BQO.js";
|
|
26
26
|
import {
|
|
27
27
|
classifyTrend
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-4FYSLLSX.js";
|
|
29
29
|
import "./chunk-XHPUAMJG.js";
|
|
30
30
|
import "./chunk-6XYFBOL2.js";
|
|
31
31
|
|
|
@@ -927,11 +927,15 @@ async function main() {
|
|
|
927
927
|
...quickPreset,
|
|
928
928
|
...options.routes !== null && { routeFilter: options.routes },
|
|
929
929
|
...options.cycles !== null && { cycles: options.cycles },
|
|
930
|
+
...options.repeat !== null && { repeat: options.repeat },
|
|
930
931
|
...options.requests !== null && { loadRequests: options.requests },
|
|
931
932
|
...options.connections !== null && { connections: options.connections },
|
|
932
933
|
...options.idleSeconds !== null && { idleMs: options.idleSeconds * 1e3 },
|
|
933
934
|
...options.warmupRequests !== null && { warmupRequests: options.warmupRequests },
|
|
934
935
|
...options.maxOldSpaceMb !== null && { maxOldSpaceMb: options.maxOldSpaceMb },
|
|
936
|
+
...options.readyTimeoutSeconds !== null && {
|
|
937
|
+
readyTimeoutMs: options.readyTimeoutSeconds * 1e3
|
|
938
|
+
},
|
|
935
939
|
...options.diffAll && { diffAll: true },
|
|
936
940
|
...options.noResolve && { resolveInconclusive: false },
|
|
937
941
|
...options.output !== null && { outputDir: options.output },
|
package/dist/confidence.d.ts
CHANGED
|
@@ -20,7 +20,16 @@ import { type TrendResult, type TrendVerdict } from "./trend.js";
|
|
|
20
20
|
* settled at, so it carries warm-up's own retention rather than the app's
|
|
21
21
|
* resting size
|
|
22
22
|
*/
|
|
23
|
-
export type WarningCode = "unsettled" | "settle-unverified" | "load-incomplete" | "abandon-ineffective" | "abandon-before-response" | "spiky-growth" | "near-threshold" | "thin-evidence" | "near-heap-ceiling" | "warm-up-baseline"
|
|
23
|
+
export type WarningCode = "unsettled" | "settle-unverified" | "load-incomplete" | "abandon-ineffective" | "abandon-before-response" | "spiky-growth" | "near-threshold" | "thin-evidence" | "near-heap-ceiling" | "warm-up-baseline"
|
|
24
|
+
/**
|
|
25
|
+
* Repeated measurements of the same route did not agree.
|
|
26
|
+
*
|
|
27
|
+
* More cycles watch one process for longer; repetitions watch different
|
|
28
|
+
* ones, and that is where the spread lives — vercel/next.js#84648 gave 602,
|
|
29
|
+
* 826 and 875 MB on three runs of one build and 39 MB on a fourth. A verdict
|
|
30
|
+
* a second run contradicts is not a verdict.
|
|
31
|
+
*/
|
|
32
|
+
| "repetitions-disagree";
|
|
24
33
|
export type MeasurementWarning = {
|
|
25
34
|
code: WarningCode;
|
|
26
35
|
detail: string;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createRequire as __nextLeakCreateRequire } from 'node:module';import { fileURLToPath as __nextLeakFileURLToPath } from 'node:url';import { dirname as __nextLeakDirname } from 'node:path';const require = __nextLeakCreateRequire(import.meta.url);const __filename = __nextLeakFileURLToPath(import.meta.url);const __dirname = __nextLeakDirname(__filename);
|
|
2
2
|
import {
|
|
3
3
|
renderHtmlReport
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-YLHE4N5G.js";
|
|
5
|
+
import "./chunk-4FYSLLSX.js";
|
|
6
6
|
import "./chunk-XHPUAMJG.js";
|
|
7
7
|
import "./chunk-6XYFBOL2.js";
|
|
8
8
|
export {
|
package/dist/index.js
CHANGED
|
@@ -41,13 +41,13 @@ import {
|
|
|
41
41
|
sourceIndexAt,
|
|
42
42
|
summarizeBaseline,
|
|
43
43
|
validateTarget
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-672J2BQO.js";
|
|
45
45
|
import {
|
|
46
46
|
renderHtmlReport
|
|
47
|
-
} from "./chunk-
|
|
47
|
+
} from "./chunk-YLHE4N5G.js";
|
|
48
48
|
import {
|
|
49
49
|
classifyTrend
|
|
50
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-4FYSLLSX.js";
|
|
51
51
|
import {
|
|
52
52
|
renderIssueMarkdown
|
|
53
53
|
} from "./chunk-AUUMRTZZ.js";
|
package/dist/launcher.d.ts
CHANGED
|
@@ -8,6 +8,18 @@ import { type ChildProcess } from "node:child_process";
|
|
|
8
8
|
* so `--max-old-space` exists and the chosen value is recorded in `run.json`.
|
|
9
9
|
*/
|
|
10
10
|
export declare const DEFAULT_MAX_OLD_SPACE_MB = 512;
|
|
11
|
+
/**
|
|
12
|
+
* How long each startup wait gets before the route is called failed.
|
|
13
|
+
*
|
|
14
|
+
* It was 15 s, shared between waiting for the control channel and waiting for
|
|
15
|
+
* the app to listen — so a slow channel spent the app's budget too. Both
|
|
16
|
+
* numbers were also invisible: a real app that needs longer to boot than the
|
|
17
|
+
* tool was willing to wait had no way to say so, and got
|
|
18
|
+
* `timed out waiting for app` on every route (#71). 60 s is what a cold
|
|
19
|
+
* standalone bundle of a couple of thousand modules takes on a laptop with
|
|
20
|
+
* a busy disk, with room to spare; `--ready-timeout` moves it.
|
|
21
|
+
*/
|
|
22
|
+
export declare const DEFAULT_READY_TIMEOUT_MS = 60000;
|
|
11
23
|
export type LaunchOptions = {
|
|
12
24
|
/** Absolute path to the standalone `server.js` (or any PORT/HOSTNAME-honoring server). */
|
|
13
25
|
serverPath: string;
|
|
@@ -19,6 +31,10 @@ export type LaunchOptions = {
|
|
|
19
31
|
bootstrapPath: string;
|
|
20
32
|
hostname?: string;
|
|
21
33
|
maxOldSpaceMb?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Budget for each of the two waits below, not for the pair. Default:
|
|
36
|
+
* `DEFAULT_READY_TIMEOUT_MS`.
|
|
37
|
+
*/
|
|
22
38
|
readyTimeoutMs?: number;
|
|
23
39
|
env?: Record<string, string>;
|
|
24
40
|
};
|
|
@@ -81,6 +97,13 @@ export declare function explainStartupFailure(stderr: string): string;
|
|
|
81
97
|
*/
|
|
82
98
|
export declare function stderrShowsHeapExhaustion(stderr: string): boolean;
|
|
83
99
|
export declare function explainRuntimeFailure(stderr: string, maxOldSpaceMb: number): string;
|
|
100
|
+
/**
|
|
101
|
+
* What to say when the process is alive and answering on its control channel,
|
|
102
|
+
* but never opened the app port. The old message named the port and stopped
|
|
103
|
+
* there, which reads like the tool failed to connect to something that was
|
|
104
|
+
* running. The process is up: what did not happen is the listen.
|
|
105
|
+
*/
|
|
106
|
+
export declare function appNeverListened(hostname: string, port: number, budgetMs: number, stderr: string): string;
|
|
84
107
|
/**
|
|
85
108
|
* Spawns the measured server in a fresh child process with GC exposed and the
|
|
86
109
|
* control-channel bootstrap preloaded, and waits until both the app port and
|
package/dist/ritual.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export type RitualOptions = {
|
|
|
20
20
|
idleMs?: number;
|
|
21
21
|
/** Old-space cap for the measured process (MB). Default: 512. */
|
|
22
22
|
maxOldSpaceMb?: number;
|
|
23
|
+
/** How long the process gets to start listening (ms). Default: 60_000. */
|
|
24
|
+
readyTimeoutMs?: number;
|
|
23
25
|
/** Headers sent with every request during warm-up and load. */
|
|
24
26
|
headers?: Record<string, string>;
|
|
25
27
|
/**
|
package/dist/runner.d.ts
CHANGED
|
@@ -8,7 +8,13 @@ import { type PrerenderManifest } from "./manifests.js";
|
|
|
8
8
|
import { extractModuleRegistry } from "./module-registry.js";
|
|
9
9
|
import { runRitual, type LoadOutcome, type PeakSample, type PhaseTiming, type SettleOutcome } from "./ritual.js";
|
|
10
10
|
import { readNextVersion, type MatchedSignature } from "./signatures.js";
|
|
11
|
-
import { type TrendResult } from "./trend.js";
|
|
11
|
+
import { type TrendResult, type TrendVerdict } from "./trend.js";
|
|
12
|
+
/** What one repetition of a route concluded, for disclosing the spread. */
|
|
13
|
+
export type RepetitionSummary = {
|
|
14
|
+
verdict: TrendVerdict;
|
|
15
|
+
/** Growth normalized by traffic, so repetitions stay comparable. */
|
|
16
|
+
growthPer1000Requests: number;
|
|
17
|
+
};
|
|
12
18
|
export type RouteReport = {
|
|
13
19
|
route: string;
|
|
14
20
|
status: "skipped";
|
|
@@ -79,6 +85,12 @@ export type RouteReport = {
|
|
|
79
85
|
unreclaimedTrend: TrendResult;
|
|
80
86
|
/** Requests each cycle served — what the growth rates normalize by. */
|
|
81
87
|
requestsPerCycle: number;
|
|
88
|
+
/**
|
|
89
|
+
* One entry per repetition when `repeat` was greater than 1, in the
|
|
90
|
+
* order they ran. Absent for a single measurement, so `run.json` keeps
|
|
91
|
+
* its existing shape unless repetition was asked for.
|
|
92
|
+
*/
|
|
93
|
+
repetitions?: RepetitionSummary[];
|
|
82
94
|
/**
|
|
83
95
|
* The early-disconnect regime, when the route asked for one. Recorded
|
|
84
96
|
* because a curve measured with cuts landing mid-stream and one measured
|
|
@@ -218,6 +230,11 @@ export type RunOptions = {
|
|
|
218
230
|
idleMs?: number;
|
|
219
231
|
/** Old-space cap for each measured process (MB). Default 512. */
|
|
220
232
|
maxOldSpaceMb?: number;
|
|
233
|
+
/**
|
|
234
|
+
* How long each measured process gets to start listening, in milliseconds.
|
|
235
|
+
* Default: `DEFAULT_READY_TIMEOUT_MS`.
|
|
236
|
+
*/
|
|
237
|
+
readyTimeoutMs?: number;
|
|
221
238
|
/** Also diff routes with a stable verdict. Default false: diffs are slow. */
|
|
222
239
|
diffAll?: boolean;
|
|
223
240
|
/** Only measure routes matching these templates or prefixes. */
|
|
@@ -227,6 +244,13 @@ export type RunOptions = {
|
|
|
227
244
|
* call it. Default true: the run should answer the question it was asked.
|
|
228
245
|
*/
|
|
229
246
|
resolveInconclusive?: boolean;
|
|
247
|
+
/**
|
|
248
|
+
* How many times to measure each route, each from a fresh server. Default 1.
|
|
249
|
+
*
|
|
250
|
+
* Distinct from `cycles`, which watches a single process for longer. The
|
|
251
|
+
* variance that makes a number unpublishable is between processes.
|
|
252
|
+
*/
|
|
253
|
+
repeat?: number;
|
|
230
254
|
/**
|
|
231
255
|
* Growth the self-check measured on its planted leak, when one ran before
|
|
232
256
|
* this measurement. Its presence is what lets the report say a `stable`
|
|
@@ -265,6 +289,17 @@ export type RunnerDeps = {
|
|
|
265
289
|
};
|
|
266
290
|
export declare function freePort(): Promise<number>;
|
|
267
291
|
export declare function routeSlug(route: string): string;
|
|
292
|
+
/**
|
|
293
|
+
* Combines repeated measurements of one route into the verdict it earned.
|
|
294
|
+
*
|
|
295
|
+
* Unanimity or nothing. Taking the most severe would let one noisy repetition
|
|
296
|
+
* in five accuse a healthy route, and a false accusation is the expensive
|
|
297
|
+
* error; taking the majority would discard exactly the disagreement the
|
|
298
|
+
* repetitions were run to find. When they disagree the honest report is that
|
|
299
|
+
* the measurement did not settle, which is what `inconclusive` means and what
|
|
300
|
+
* already routes to re-measurement and away from issue drafts.
|
|
301
|
+
*/
|
|
302
|
+
export declare function aggregateRepetitions(passes: readonly RouteReport[]): RouteReport;
|
|
268
303
|
/**
|
|
269
304
|
* Full measurement run: validate the target, discover routes, run the ritual
|
|
270
305
|
* per route in a fresh process, diff snapshots for non-stable verdicts, and
|
package/dist/target.d.ts
CHANGED
|
@@ -6,7 +6,11 @@ export declare class TargetError extends Error {
|
|
|
6
6
|
}
|
|
7
7
|
export type ValidatedTarget = {
|
|
8
8
|
appDir: string;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Absolute path to the standalone `server.js`. At the root of
|
|
11
|
+
* `.next/standalone` for a single-package app; under the app's path relative
|
|
12
|
+
* to the workspace root for a monorepo.
|
|
13
|
+
*/
|
|
10
14
|
standaloneServer: string;
|
|
11
15
|
appPaths: AppPathsManifest;
|
|
12
16
|
pages: PagesManifest;
|