next-leak 0.11.0 → 0.11.2
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 +17 -3
- package/dist/{chunk-JHOE2RCM.js → chunk-QNYUT2XF.js} +88 -15
- package/dist/cli-args.d.ts +2 -0
- package/dist/cli.js +4 -1
- package/dist/index.js +1 -1
- package/dist/launcher.d.ts +38 -0
- package/dist/ritual.d.ts +2 -0
- package/dist/runner.d.ts +5 -0
- 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 |
|
|
@@ -121,6 +123,7 @@ Every report prints the gate it used.
|
|
|
121
123
|
| `--idle <seconds>` | 30 | **Maximum** wait before each sample; the run continues as soon as the heap settles |
|
|
122
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 |
|
|
123
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 |
|
|
124
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 |
|
|
125
128
|
| `--no-resolve` | off | Skip the second pass on inconclusive routes |
|
|
126
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 |
|
|
@@ -407,6 +410,17 @@ through the build's source maps.
|
|
|
407
410
|
|
|
408
411
|
|
|
409
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.
|
|
410
424
|
- **Sample values can vary per request.** `"slug": "post-{n}"` gives every
|
|
411
425
|
request its own URL — the shape of bot traffic and of a cache that never
|
|
412
426
|
repeats a key. `"slug": "post-{n%200}"` cycles through exactly 200 distinct
|
|
@@ -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,38 @@ 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, lastFailure = void 0) {
|
|
92819
|
+
const refused = lastFailure === void 0 ? "" : ` (${lastFailure})`;
|
|
92820
|
+
const head = `app on ${hostname}:${port} \u2014 the process started and answered on its control channel, but never accepted a connection${refused} within ${Math.round(budgetMs / 1e3)}s`;
|
|
92821
|
+
if (stderr.trim() !== "") {
|
|
92822
|
+
return `${head}. It wrote this while starting:
|
|
92823
|
+
${stderr.trim()}`;
|
|
92824
|
+
}
|
|
92825
|
+
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`;
|
|
92826
|
+
}
|
|
92827
|
+
var NOT_LISTENING_CODES = /* @__PURE__ */ new Set([
|
|
92828
|
+
"ECONNREFUSED",
|
|
92829
|
+
"EHOSTUNREACH",
|
|
92830
|
+
"ENETUNREACH",
|
|
92831
|
+
"ENOTFOUND"
|
|
92832
|
+
]);
|
|
92833
|
+
function probeFailure(cause) {
|
|
92834
|
+
const inner = cause?.cause;
|
|
92835
|
+
const code = inner?.code;
|
|
92836
|
+
if (typeof code === "string") {
|
|
92837
|
+
return code;
|
|
92838
|
+
}
|
|
92839
|
+
const message = inner?.message;
|
|
92840
|
+
if (typeof message === "string" && message !== "") {
|
|
92841
|
+
return message;
|
|
92842
|
+
}
|
|
92843
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
92844
|
+
}
|
|
92845
|
+
function meansNotListening(failure) {
|
|
92846
|
+
return NOT_LISTENING_CODES.has(failure);
|
|
92847
|
+
}
|
|
92797
92848
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
92798
|
-
async function pollUntil(deadline,
|
|
92849
|
+
async function pollUntil(deadline, describe, probe, failed) {
|
|
92799
92850
|
for (; ; ) {
|
|
92800
92851
|
const failure = failed();
|
|
92801
92852
|
if (failure !== void 0) {
|
|
@@ -92806,14 +92857,14 @@ async function pollUntil(deadline, what, probe, failed) {
|
|
|
92806
92857
|
return result;
|
|
92807
92858
|
}
|
|
92808
92859
|
if (Date.now() > deadline) {
|
|
92809
|
-
throw new LaunchError(`timed out waiting for ${
|
|
92860
|
+
throw new LaunchError(`timed out waiting for ${describe()}`);
|
|
92810
92861
|
}
|
|
92811
92862
|
await sleep(100);
|
|
92812
92863
|
}
|
|
92813
92864
|
}
|
|
92814
92865
|
async function launchInstrumented(options) {
|
|
92815
92866
|
const hostname = options.hostname ?? "127.0.0.1";
|
|
92816
|
-
const
|
|
92867
|
+
const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
|
|
92817
92868
|
const child = spawn(
|
|
92818
92869
|
process.execPath,
|
|
92819
92870
|
[
|
|
@@ -92857,8 +92908,8 @@ ${stderrTailBuffer}`;
|
|
|
92857
92908
|
const failed = () => exited ? `server exited before becoming ready. ${explainStartupFailure(stderrWindow())}` : void 0;
|
|
92858
92909
|
try {
|
|
92859
92910
|
const controlPort = await pollUntil(
|
|
92860
|
-
|
|
92861
|
-
`control channel in ${options.workDir}`,
|
|
92911
|
+
Date.now() + readyTimeoutMs,
|
|
92912
|
+
() => `control channel in ${options.workDir}`,
|
|
92862
92913
|
// Several processes may announce a channel (clustered servers); accept
|
|
92863
92914
|
// the first one that actually answers instead of trusting a filename.
|
|
92864
92915
|
async () => {
|
|
@@ -92887,15 +92938,20 @@ ${stderrTailBuffer}`;
|
|
|
92887
92938
|
},
|
|
92888
92939
|
failed
|
|
92889
92940
|
);
|
|
92941
|
+
let lastFailure;
|
|
92890
92942
|
await pollUntil(
|
|
92891
|
-
|
|
92892
|
-
|
|
92943
|
+
Date.now() + readyTimeoutMs,
|
|
92944
|
+
() => appNeverListened(hostname, options.appPort, readyTimeoutMs, stderrWindow(), lastFailure),
|
|
92893
92945
|
async () => {
|
|
92946
|
+
const url = `http://${hostname}:${options.appPort}${options.readyPath ?? "/"}`;
|
|
92894
92947
|
try {
|
|
92895
|
-
await fetch(
|
|
92948
|
+
const response = await fetch(url, { method: "GET", redirect: "manual" });
|
|
92949
|
+
await response.body?.cancel();
|
|
92896
92950
|
return true;
|
|
92897
|
-
} catch {
|
|
92898
|
-
|
|
92951
|
+
} catch (cause) {
|
|
92952
|
+
const failure = probeFailure(cause);
|
|
92953
|
+
lastFailure = failure;
|
|
92954
|
+
return meansNotListening(failure) ? void 0 : true;
|
|
92899
92955
|
}
|
|
92900
92956
|
},
|
|
92901
92957
|
failed
|
|
@@ -92945,6 +93001,7 @@ var RUN_ONLY_FLAGS = [
|
|
|
92945
93001
|
["idleSeconds", "--idle"],
|
|
92946
93002
|
["warmupRequests", "--warmup"],
|
|
92947
93003
|
["maxOldSpaceMb", "--max-old-space"],
|
|
93004
|
+
["readyTimeoutSeconds", "--ready-timeout"],
|
|
92948
93005
|
["quick", "--quick"],
|
|
92949
93006
|
["noResolve", "--no-resolve"],
|
|
92950
93007
|
["selfCheck", "--self-check"],
|
|
@@ -92989,6 +93046,12 @@ var FLAGS = [
|
|
|
92989
93046
|
argName: "<mb>",
|
|
92990
93047
|
help: "Heap cap of each measured process (default 512) \u2014 raise it for apps whose working set is larger"
|
|
92991
93048
|
},
|
|
93049
|
+
{
|
|
93050
|
+
flag: "--ready-timeout",
|
|
93051
|
+
value: "int",
|
|
93052
|
+
argName: "<seconds>",
|
|
93053
|
+
help: "Seconds each measured process gets to start listening (default 60) \u2014 raise it for slow-booting apps"
|
|
93054
|
+
},
|
|
92992
93055
|
{
|
|
92993
93056
|
flag: "--quick",
|
|
92994
93057
|
value: "none",
|
|
@@ -93060,7 +93123,8 @@ var LIMITS = {
|
|
|
93060
93123
|
"--connections": 1e4,
|
|
93061
93124
|
"--idle": 3600,
|
|
93062
93125
|
"--warmup": 1e6,
|
|
93063
|
-
"--max-old-space": 65536
|
|
93126
|
+
"--max-old-space": 65536,
|
|
93127
|
+
"--ready-timeout": 600
|
|
93064
93128
|
};
|
|
93065
93129
|
var MIN_MAX_OLD_SPACE_MB = 128;
|
|
93066
93130
|
function findSpec(argument) {
|
|
@@ -93102,6 +93166,7 @@ function applyNumericFlag(flag, value, options) {
|
|
|
93102
93166
|
if (flag === "--idle") options.idleSeconds = parsed;
|
|
93103
93167
|
if (flag === "--warmup") options.warmupRequests = parsed;
|
|
93104
93168
|
if (flag === "--max-old-space") options.maxOldSpaceMb = parsed;
|
|
93169
|
+
if (flag === "--ready-timeout") options.readyTimeoutSeconds = parsed;
|
|
93105
93170
|
return FLAG_OK;
|
|
93106
93171
|
}
|
|
93107
93172
|
function applyFlag(spec, value, options) {
|
|
@@ -93115,6 +93180,7 @@ function applyFlag(spec, value, options) {
|
|
|
93115
93180
|
case "--idle":
|
|
93116
93181
|
case "--warmup":
|
|
93117
93182
|
case "--max-old-space":
|
|
93183
|
+
case "--ready-timeout":
|
|
93118
93184
|
return applyNumericFlag(spec.flag, value, options);
|
|
93119
93185
|
case "--quick":
|
|
93120
93186
|
options.quick = true;
|
|
@@ -93187,6 +93253,7 @@ function parseCliArgs(argv) {
|
|
|
93187
93253
|
idleSeconds: null,
|
|
93188
93254
|
warmupRequests: null,
|
|
93189
93255
|
maxOldSpaceMb: null,
|
|
93256
|
+
readyTimeoutSeconds: null,
|
|
93190
93257
|
quick: false,
|
|
93191
93258
|
noResolve: false,
|
|
93192
93259
|
selfCheck: false,
|
|
@@ -93869,7 +93936,12 @@ async function runRitual(options, deps = defaultDeps) {
|
|
|
93869
93936
|
serverPath: options.serverPath,
|
|
93870
93937
|
workDir: options.workDir,
|
|
93871
93938
|
bootstrapPath: options.bootstrapPath,
|
|
93872
|
-
|
|
93939
|
+
// Wait on the route this ritual is about to measure. `/` is a different
|
|
93940
|
+
// page with different failure modes, and readiness judged on it withdrew
|
|
93941
|
+
// routes that were serving fine (#74).
|
|
93942
|
+
readyPath: options.route,
|
|
93943
|
+
...options.maxOldSpaceMb !== void 0 && { maxOldSpaceMb: options.maxOldSpaceMb },
|
|
93944
|
+
...options.readyTimeoutMs !== void 0 && { readyTimeoutMs: options.readyTimeoutMs }
|
|
93873
93945
|
};
|
|
93874
93946
|
let app;
|
|
93875
93947
|
try {
|
|
@@ -94780,6 +94852,7 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
94780
94852
|
...pass?.cycles !== void 0 ? { cycles: pass.cycles } : options.cycles !== void 0 && { cycles: options.cycles },
|
|
94781
94853
|
...options.idleMs !== void 0 && { idleMs: options.idleMs },
|
|
94782
94854
|
...options.maxOldSpaceMb !== void 0 && { maxOldSpaceMb: options.maxOldSpaceMb },
|
|
94855
|
+
...options.readyTimeoutMs !== void 0 && { readyTimeoutMs: options.readyTimeoutMs },
|
|
94783
94856
|
...headers !== void 0 && { headers },
|
|
94784
94857
|
...routeConfig.abandonAfterMs !== void 0 && {
|
|
94785
94858
|
abandonAfterMs: routeConfig.abandonAfterMs
|
package/dist/cli-args.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export type CliRunOptions = {
|
|
|
16
16
|
idleSeconds: number | null;
|
|
17
17
|
warmupRequests: number | null;
|
|
18
18
|
maxOldSpaceMb: number | null;
|
|
19
|
+
/** Seconds each measured process gets to boot before the route is failed. */
|
|
20
|
+
readyTimeoutSeconds: number | null;
|
|
19
21
|
quick: boolean;
|
|
20
22
|
noResolve: boolean;
|
|
21
23
|
selfCheck: boolean;
|
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
runSelfCheck,
|
|
23
23
|
unregisterChild,
|
|
24
24
|
validateTarget
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-QNYUT2XF.js";
|
|
26
26
|
import {
|
|
27
27
|
classifyTrend
|
|
28
28
|
} from "./chunk-4FYSLLSX.js";
|
|
@@ -933,6 +933,9 @@ async function main() {
|
|
|
933
933
|
...options.idleSeconds !== null && { idleMs: options.idleSeconds * 1e3 },
|
|
934
934
|
...options.warmupRequests !== null && { warmupRequests: options.warmupRequests },
|
|
935
935
|
...options.maxOldSpaceMb !== null && { maxOldSpaceMb: options.maxOldSpaceMb },
|
|
936
|
+
...options.readyTimeoutSeconds !== null && {
|
|
937
|
+
readyTimeoutMs: options.readyTimeoutSeconds * 1e3
|
|
938
|
+
},
|
|
936
939
|
...options.diffAll && { diffAll: true },
|
|
937
940
|
...options.noResolve && { resolveInconclusive: false },
|
|
938
941
|
...options.output !== null && { outputDir: options.output },
|
package/dist/index.js
CHANGED
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;
|
|
@@ -18,7 +30,18 @@ export type LaunchOptions = {
|
|
|
18
30
|
/** Path to the built bootstrap module loaded with `--import`. */
|
|
19
31
|
bootstrapPath: string;
|
|
20
32
|
hostname?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Path the readiness probe asks for. Defaults to `/`, but a run should pass
|
|
35
|
+
* the route it is about to measure: an app can serve that route perfectly
|
|
36
|
+
* and still fail on `/` — an i18n redirect, an auth wall, a rewrite — and
|
|
37
|
+
* the wait would then be judging a page nobody asked about (#74).
|
|
38
|
+
*/
|
|
39
|
+
readyPath?: string;
|
|
21
40
|
maxOldSpaceMb?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Budget for each of the two waits below, not for the pair. Default:
|
|
43
|
+
* `DEFAULT_READY_TIMEOUT_MS`.
|
|
44
|
+
*/
|
|
22
45
|
readyTimeoutMs?: number;
|
|
23
46
|
env?: Record<string, string>;
|
|
24
47
|
};
|
|
@@ -81,6 +104,21 @@ export declare function explainStartupFailure(stderr: string): string;
|
|
|
81
104
|
*/
|
|
82
105
|
export declare function stderrShowsHeapExhaustion(stderr: string): boolean;
|
|
83
106
|
export declare function explainRuntimeFailure(stderr: string, maxOldSpaceMb: number): string;
|
|
107
|
+
/**
|
|
108
|
+
* What to say when the process is alive and answering on its control channel,
|
|
109
|
+
* but never opened the app port. The old message named the port and stopped
|
|
110
|
+
* there, which reads like the tool failed to connect to something that was
|
|
111
|
+
* running. The process is up: what did not happen is the listen.
|
|
112
|
+
*/
|
|
113
|
+
export declare function appNeverListened(hostname: string, port: number, budgetMs: number, stderr: string, lastFailure?: string | undefined): string;
|
|
114
|
+
/**
|
|
115
|
+
* `fetch` rejects with a bare `TypeError: fetch failed`; what happened is one
|
|
116
|
+
* level down, and not always as a `code` — a redirect loop arrives as a
|
|
117
|
+
* message with none.
|
|
118
|
+
*/
|
|
119
|
+
export declare function probeFailure(cause: unknown): string;
|
|
120
|
+
/** Whether a probe failure means the port is not open yet. */
|
|
121
|
+
export declare function meansNotListening(failure: string): boolean;
|
|
84
122
|
/**
|
|
85
123
|
* Spawns the measured server in a fresh child process with GC exposed and the
|
|
86
124
|
* 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
|
@@ -230,6 +230,11 @@ export type RunOptions = {
|
|
|
230
230
|
idleMs?: number;
|
|
231
231
|
/** Old-space cap for each measured process (MB). Default 512. */
|
|
232
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;
|
|
233
238
|
/** Also diff routes with a stable verdict. Default false: diffs are slow. */
|
|
234
239
|
diffAll?: boolean;
|
|
235
240
|
/** Only measure routes matching these templates or prefixes. */
|
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;
|