next-leak 0.4.0 → 0.4.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 +2 -1
- package/dist/{chunk-NYWCWKS6.js → chunk-BTWQYJOW.js} +1 -1
- package/dist/{chunk-BDIPW6FU.js → chunk-FFCY6POI.js} +27 -2
- package/dist/{chunk-USNLL625.js → chunk-OJIXF7RP.js} +41 -9
- package/dist/cli-args.d.ts +1 -0
- package/dist/cli.js +3 -2
- package/dist/confidence.d.ts +2 -1
- package/dist/{html-report-VW7VQOCH.js → html-report-76DP5ADD.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/runner.d.ts +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,6 +104,7 @@ Every report prints the gate it used.
|
|
|
104
104
|
| `--idle <seconds>` | 30 | **Maximum** wait before each sample; the run continues as soon as the heap settles |
|
|
105
105
|
| `--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 |
|
|
106
106
|
| `--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 |
|
|
107
|
+
| `--no-resolve` | off | Skip the second pass on inconclusive routes |
|
|
107
108
|
| `--diff-all` | off | Diff snapshots for stable routes too |
|
|
108
109
|
| `--output <dir>` | `<app>/.next-leak` | Where runs are written |
|
|
109
110
|
|
|
@@ -168,7 +169,7 @@ separates them, because each one has a different fix:
|
|
|
168
169
|
flat but RSS keeps climbing, the report says so explicitly: that is an
|
|
169
170
|
allocator, external-buffer or fragmentation problem, not a JS-heap leak.
|
|
170
171
|
- **`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.
|
|
171
|
-
- **`inconclusive`** —
|
|
172
|
+
- **`inconclusive`** — the evidence does not decide. The run does not stop there: any inconclusive route is **measured again automatically**, with twice the cycles, and the second pass is what you see (`resolved at 8 cycles` next to the verdict). On the reproduction for [#95094](https://github.com/vercel/next.js/issues/95094), `--quick` alone reports `inconclusive` on three deltas and then comes back with the leak. `--no-resolve` turns the second pass off; when even that is undecided, the re-run command is still printed.
|
|
172
173
|
- **`failed`** — the route errored under load (auth redirects, POST-only endpoints). >1% non-2xx aborts measurement instead of measuring garbage. That's by design.
|
|
173
174
|
|
|
174
175
|
## Peak pressure: `stable` is not the same as safe
|
|
@@ -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-FFCY6POI.js";
|
|
5
5
|
import {
|
|
6
6
|
assessPeakPressure,
|
|
7
7
|
describePeakPressure
|
|
@@ -73,7 +73,8 @@ function effectiveVerdict(report) {
|
|
|
73
73
|
}
|
|
74
74
|
var VERDICT_WEAKENING = /* @__PURE__ */ new Set([
|
|
75
75
|
"near-threshold",
|
|
76
|
-
"spiky-growth"
|
|
76
|
+
"spiky-growth",
|
|
77
|
+
"thin-evidence"
|
|
77
78
|
]);
|
|
78
79
|
function warrantsIssueDraft(report) {
|
|
79
80
|
return effectiveVerdict(report) === "leak" && !report.confidence.warnings.some((warning) => VERDICT_WEAKENING.has(warning.code));
|
|
@@ -182,13 +183,36 @@ function heapCeilingWarnings(input) {
|
|
|
182
183
|
detail: `the heap peaked at ${mb(peak)} against a ${capMb} MB cap (${pct(peak, capBytes)}) \u2014 the curve may have been clipped by the ceiling rather than by the app; re-run with a larger --max-old-space`
|
|
183
184
|
}];
|
|
184
185
|
}
|
|
186
|
+
var THIN_EVIDENCE_MIN_DELTAS = 5;
|
|
187
|
+
var THIN_EVIDENCE_MIN_DELTA_RATIO = 4;
|
|
188
|
+
function isThinEvidence(trend, minGrowth) {
|
|
189
|
+
if (trend.deltas.length >= THIN_EVIDENCE_MIN_DELTAS || trend.deltas.length === 0) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
const smallest = Math.min(...trend.deltas);
|
|
193
|
+
return smallest < minGrowth * THIN_EVIDENCE_MIN_DELTA_RATIO;
|
|
194
|
+
}
|
|
195
|
+
function thinEvidenceWarnings(trend, minGrowth) {
|
|
196
|
+
if (trend.verdict !== "leak" || !isThinEvidence(trend, minGrowth)) {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
const smallest = Math.min(...trend.deltas);
|
|
200
|
+
return [{
|
|
201
|
+
code: "thin-evidence",
|
|
202
|
+
detail: `judged on ${trend.deltas.length} cycles, and its weakest grew ${mb(smallest)} against a ${mb(minGrowth)} gate \u2014 on that few cycles ordinary oscillation reaches that, and a repeat run often disagrees; measure more cycles to resolve it`
|
|
203
|
+
}];
|
|
204
|
+
}
|
|
185
205
|
function isVerdictInvalid(input) {
|
|
186
206
|
if (input.trend.verdict !== "leak") {
|
|
187
207
|
return false;
|
|
188
208
|
}
|
|
189
209
|
const neverSettled = input.settleOutcomes.length > 0 && input.settleOutcomes.every((outcome) => outcome.status === "moving");
|
|
190
210
|
const abandonedNothing = input.abandonAfterMs !== void 0 && input.loadOutcomes.length > 0 && input.loadOutcomes.every((outcome) => (outcome.abandoned ?? 0) === 0);
|
|
191
|
-
|
|
211
|
+
const thinEvidence = isThinEvidence(
|
|
212
|
+
input.trend,
|
|
213
|
+
input.minGrowthPerCycle ?? MIN_GROWTH_NOISE_FLOOR
|
|
214
|
+
);
|
|
215
|
+
return neverSettled || abandonedNothing || thinEvidence;
|
|
192
216
|
}
|
|
193
217
|
function assessConfidence(input) {
|
|
194
218
|
const minGrowth = input.minGrowthPerCycle ?? MIN_GROWTH_NOISE_FLOOR;
|
|
@@ -197,6 +221,7 @@ function assessConfidence(input) {
|
|
|
197
221
|
...loadWarnings(input.loadOutcomes, input.abandonAfterMs),
|
|
198
222
|
...growthShapeWarnings(input.trend),
|
|
199
223
|
...noiseFloorWarnings(input.trend, minGrowth),
|
|
224
|
+
...thinEvidenceWarnings(input.trend, minGrowth),
|
|
200
225
|
...heapCeilingWarnings(input)
|
|
201
226
|
];
|
|
202
227
|
return {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
effectiveVerdict,
|
|
7
7
|
minGrowthFor,
|
|
8
8
|
warrantsIssueDraft
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-FFCY6POI.js";
|
|
10
10
|
import {
|
|
11
11
|
assessPeakPressure,
|
|
12
12
|
describePeakPressure
|
|
@@ -91992,6 +91992,11 @@ var FLAGS = [
|
|
|
91992
91992
|
help: "Fast preset: 2000 requests x 4 cycles, 8s idle \u2014 same cycle count as the default, less traffic per cycle"
|
|
91993
91993
|
},
|
|
91994
91994
|
{ flag: "--diff-all", value: "none", help: "Diff snapshots for stable routes too (slow)" },
|
|
91995
|
+
{
|
|
91996
|
+
flag: "--no-resolve",
|
|
91997
|
+
value: "none",
|
|
91998
|
+
help: "Do not re-measure inconclusive routes with more cycles (default: re-measure once)"
|
|
91999
|
+
},
|
|
91995
92000
|
{ flag: "--output", value: "string", argName: "<dir>", help: "Where to write runs (default <app-dir>/.next-leak)" },
|
|
91996
92001
|
{ flag: "--help", alias: "-h", value: "none", help: "Show this help" },
|
|
91997
92002
|
{ flag: "--version", alias: "-v", value: "none", help: "Print the version" }
|
|
@@ -92087,6 +92092,9 @@ function applyFlag(spec, value, options) {
|
|
|
92087
92092
|
case "--diff-all":
|
|
92088
92093
|
options.diffAll = true;
|
|
92089
92094
|
return FLAG_OK;
|
|
92095
|
+
case "--no-resolve":
|
|
92096
|
+
options.noResolve = true;
|
|
92097
|
+
return FLAG_OK;
|
|
92090
92098
|
case "--output":
|
|
92091
92099
|
options.output = value;
|
|
92092
92100
|
return FLAG_OK;
|
|
@@ -92137,6 +92145,7 @@ function parseCliArgs(argv) {
|
|
|
92137
92145
|
idleSeconds: null,
|
|
92138
92146
|
maxOldSpaceMb: null,
|
|
92139
92147
|
quick: false,
|
|
92148
|
+
noResolve: false,
|
|
92140
92149
|
diffAll: false,
|
|
92141
92150
|
output: null
|
|
92142
92151
|
};
|
|
@@ -92441,10 +92450,11 @@ function routeLines(route, parameters) {
|
|
|
92441
92450
|
}
|
|
92442
92451
|
const verdict = effectiveVerdict(route);
|
|
92443
92452
|
const curve = route.samples.map(formatMb).join(" \u2192 ");
|
|
92453
|
+
const resolved = route.resolvedWithCycles === void 0 ? "" : ` (resolved at ${route.resolvedWithCycles} cycles)`;
|
|
92444
92454
|
return [
|
|
92445
92455
|
` ${VERDICT_ICON[verdict]} ${route.route} ${verdict} (${formatGrowth(
|
|
92446
92456
|
route.growthPer1000Requests
|
|
92447
|
-
)}) heap ${curve}`,
|
|
92457
|
+
)}) heap ${curve}${resolved}`,
|
|
92448
92458
|
...confidenceLines(route),
|
|
92449
92459
|
...memorySourceLines(route, verdict),
|
|
92450
92460
|
...peakPressureLines(route, parameters),
|
|
@@ -92457,9 +92467,17 @@ function formatReport(report) {
|
|
|
92457
92467
|
lines.push(...routeLines(route, report.parameters));
|
|
92458
92468
|
}
|
|
92459
92469
|
const { minGrowthPerCycle, loadRequests, cycles, maxOldSpaceMb } = report.parameters;
|
|
92470
|
+
const resolvedCycles = [
|
|
92471
|
+
...new Set(
|
|
92472
|
+
report.routes.flatMap(
|
|
92473
|
+
(route) => route.status === "measured" && route.resolvedWithCycles !== void 0 ? [route.resolvedWithCycles] : []
|
|
92474
|
+
)
|
|
92475
|
+
)
|
|
92476
|
+
].sort((a, b) => a - b);
|
|
92477
|
+
const cyclesLabel = resolvedCycles.length === 0 ? `${cycles} cycles` : `${cycles} cycles (${resolvedCycles.join(", ")} where resolved)`;
|
|
92460
92478
|
lines.push(
|
|
92461
92479
|
"",
|
|
92462
|
-
`judged over ${
|
|
92480
|
+
`judged over ${cyclesLabel} \xD7 ${loadRequests} requests, growth gate ${(minGrowthPerCycle / 1024).toFixed(0)} KiB/cycle (${formatGrowth(minGrowthPerCycle / loadRequests * 1e3)}), heap cap ${maxOldSpaceMb} MB`,
|
|
92463
92481
|
`snapshots and run.json: ${report.workDir}`,
|
|
92464
92482
|
`report: ${report.bundle.htmlReport}`,
|
|
92465
92483
|
...report.bundle.issues.map((issue) => `issue draft (${issue.route}): ${issue.file}`)
|
|
@@ -93851,18 +93869,21 @@ function skipReason(route, requestPath) {
|
|
|
93851
93869
|
}
|
|
93852
93870
|
return null;
|
|
93853
93871
|
}
|
|
93854
|
-
async function measureRoute(context, route, requestPath, index) {
|
|
93872
|
+
async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
93855
93873
|
const { deps, options, target, workDir, routeConfig, registry, nextVersion, progress } = context;
|
|
93856
93874
|
const result = await deps.ritual({
|
|
93857
93875
|
serverPath: target.standaloneServer,
|
|
93858
93876
|
route: requestPath,
|
|
93859
|
-
workDir: path6.join(
|
|
93877
|
+
workDir: path6.join(
|
|
93878
|
+
workDir,
|
|
93879
|
+
`${String(index + 1).padStart(2, "0")}-${routeSlug(route.path)}${pass?.dirSuffix ?? ""}`
|
|
93880
|
+
),
|
|
93860
93881
|
bootstrapPath: options.bootstrapPath,
|
|
93861
93882
|
appPort: await deps.freePort(),
|
|
93862
93883
|
...options.warmupRequests !== void 0 && { warmupRequests: options.warmupRequests },
|
|
93863
93884
|
...options.loadRequests !== void 0 && { loadRequests: options.loadRequests },
|
|
93864
93885
|
...options.connections !== void 0 && { connections: options.connections },
|
|
93865
|
-
...options.cycles !== void 0 && { cycles: options.cycles },
|
|
93886
|
+
...pass !== void 0 ? { cycles: pass.cycles } : options.cycles !== void 0 && { cycles: options.cycles },
|
|
93866
93887
|
...options.idleMs !== void 0 && { idleMs: options.idleMs },
|
|
93867
93888
|
...options.maxOldSpaceMb !== void 0 && { maxOldSpaceMb: options.maxOldSpaceMb },
|
|
93868
93889
|
...routeConfig.headers !== void 0 && { headers: routeConfig.headers },
|
|
@@ -93896,6 +93917,7 @@ async function measureRoute(context, route, requestPath, index) {
|
|
|
93896
93917
|
route: route.path,
|
|
93897
93918
|
status: "measured",
|
|
93898
93919
|
requestPath,
|
|
93920
|
+
...pass !== void 0 && { resolvedWithCycles: pass.cycles },
|
|
93899
93921
|
samples: result.samples,
|
|
93900
93922
|
memorySamples: result.memorySamples,
|
|
93901
93923
|
peaks: result.peaks,
|
|
@@ -93914,7 +93936,7 @@ async function measureRoute(context, route, requestPath, index) {
|
|
|
93914
93936
|
};
|
|
93915
93937
|
}
|
|
93916
93938
|
async function writeEvidenceBundle(report, workDir) {
|
|
93917
|
-
const { renderHtmlReport } = await import("./html-report-
|
|
93939
|
+
const { renderHtmlReport } = await import("./html-report-76DP5ADD.js");
|
|
93918
93940
|
const { renderIssueMarkdown } = await import("./issue-report-HKA26WNV.js");
|
|
93919
93941
|
for (const route of report.routes) {
|
|
93920
93942
|
if (route.status === "measured" && warrantsIssueDraft(route)) {
|
|
@@ -93925,6 +93947,7 @@ async function writeEvidenceBundle(report, workDir) {
|
|
|
93925
93947
|
}
|
|
93926
93948
|
await writeFile(report.bundle.htmlReport, renderHtmlReport(report));
|
|
93927
93949
|
}
|
|
93950
|
+
var resolveCycles = (cycles) => Math.max(cycles * 2, 6);
|
|
93928
93951
|
async function routeReportFor(context, route, index, total) {
|
|
93929
93952
|
const { routeConfig, progress } = context;
|
|
93930
93953
|
const label = `${route.path} (${index + 1}/${total})`;
|
|
@@ -93937,7 +93960,16 @@ async function routeReportFor(context, route, index, total) {
|
|
|
93937
93960
|
try {
|
|
93938
93961
|
const asPath = requestPath === route.path ? "" : ` as ${requestPath}`;
|
|
93939
93962
|
progress(`measuring ${label}${asPath}`);
|
|
93940
|
-
|
|
93963
|
+
const first = await measureRoute(context, route, requestPath, index);
|
|
93964
|
+
if (first.status !== "measured" || context.options.resolveInconclusive === false || effectiveVerdict(first) !== "inconclusive") {
|
|
93965
|
+
return first;
|
|
93966
|
+
}
|
|
93967
|
+
const cycles = resolveCycles(context.options.cycles ?? RITUAL_DEFAULTS.cycles);
|
|
93968
|
+
progress(`re-measuring ${route.path} with ${cycles} cycles: the first pass could not call it`);
|
|
93969
|
+
return await measureRoute(context, route, requestPath, index, {
|
|
93970
|
+
cycles,
|
|
93971
|
+
dirSuffix: "-resolve"
|
|
93972
|
+
});
|
|
93941
93973
|
} catch (cause) {
|
|
93942
93974
|
const failure = cause instanceof Error ? cause.message : String(cause);
|
|
93943
93975
|
progress(`failed ${label}: ${failure}`);
|
|
@@ -93972,7 +94004,7 @@ async function planRun(options, target, deps, progress) {
|
|
|
93972
94004
|
).length;
|
|
93973
94005
|
const estimate = estimateRun(measurable, parameters);
|
|
93974
94006
|
progress(
|
|
93975
|
-
`${routes.length} routes discovered` + (measurable === routes.length ? "" : ` \xB7 ${measurable} measurable`) + ` \xB7 estimated ${formatEstimate(estimate)}` + // Long default runs are where first-time users give up; point at the two
|
|
94007
|
+
`${routes.length} routes discovered` + (measurable === routes.length ? "" : ` \xB7 ${measurable} measurable`) + ` \xB7 estimated ${formatEstimate(estimate)}` + (options.resolveInconclusive === false ? "" : " (inconclusive routes are measured again)") + // Long default runs are where first-time users give up; point at the two
|
|
93976
94008
|
// ways out. Suppressed once load parameters were tuned by hand (or by
|
|
93977
94009
|
// --quick, which arrives here as explicit loadRequests/idleMs).
|
|
93978
94010
|
(estimate.slowSeconds > 15 * 60 && options.loadRequests === void 0 && options.idleMs === void 0 ? " \u2014 use --quick for the fast validated preset, or narrow with --routes" : "")
|
package/dist/cli-args.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
killActiveChildren,
|
|
10
10
|
parseCliArgs,
|
|
11
11
|
runMeasurement
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import "./chunk-
|
|
12
|
+
} from "./chunk-OJIXF7RP.js";
|
|
13
|
+
import "./chunk-FFCY6POI.js";
|
|
14
14
|
import "./chunk-XHPUAMJG.js";
|
|
15
15
|
import "./chunk-6XYFBOL2.js";
|
|
16
16
|
|
|
@@ -100,6 +100,7 @@ async function main() {
|
|
|
100
100
|
...options.idleSeconds !== null && { idleMs: options.idleSeconds * 1e3 },
|
|
101
101
|
...options.maxOldSpaceMb !== null && { maxOldSpaceMb: options.maxOldSpaceMb },
|
|
102
102
|
...options.diffAll && { diffAll: true },
|
|
103
|
+
...options.noResolve && { resolveInconclusive: false },
|
|
103
104
|
...options.output !== null && { outputDir: options.output },
|
|
104
105
|
onProgress: (message) => console.error(`\xB7 ${message}`)
|
|
105
106
|
});
|
package/dist/confidence.d.ts
CHANGED
|
@@ -12,10 +12,11 @@ import { type TrendResult, type TrendVerdict } from "./trend.js";
|
|
|
12
12
|
* mid-stream teardown path was never reached
|
|
13
13
|
* `spiky-growth` — one cycle dominates, so the mean describes little
|
|
14
14
|
* `near-threshold` — growth barely clears the noise floor
|
|
15
|
+
* `thin-evidence` — a leak called on too few cycles for its size
|
|
15
16
|
* `near-heap-ceiling` — the heap approached the cap the process ran under,
|
|
16
17
|
* so the curve was measured against a ceiling instead of running free
|
|
17
18
|
*/
|
|
18
|
-
export type WarningCode = "unsettled" | "settle-unverified" | "load-incomplete" | "abandon-ineffective" | "abandon-before-response" | "spiky-growth" | "near-threshold" | "near-heap-ceiling";
|
|
19
|
+
export type WarningCode = "unsettled" | "settle-unverified" | "load-incomplete" | "abandon-ineffective" | "abandon-before-response" | "spiky-growth" | "near-threshold" | "thin-evidence" | "near-heap-ceiling";
|
|
19
20
|
export type MeasurementWarning = {
|
|
20
21
|
code: WarningCode;
|
|
21
22
|
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-BTWQYJOW.js";
|
|
5
|
+
import "./chunk-FFCY6POI.js";
|
|
6
6
|
import "./chunk-XHPUAMJG.js";
|
|
7
7
|
import "./chunk-6XYFBOL2.js";
|
|
8
8
|
export {
|
package/dist/index.js
CHANGED
|
@@ -39,13 +39,13 @@ import {
|
|
|
39
39
|
sourceIndexAt,
|
|
40
40
|
summarizeBaseline,
|
|
41
41
|
validateTarget
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-OJIXF7RP.js";
|
|
43
43
|
import {
|
|
44
44
|
renderHtmlReport
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-BTWQYJOW.js";
|
|
46
46
|
import {
|
|
47
47
|
classifyTrend
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-FFCY6POI.js";
|
|
49
49
|
import {
|
|
50
50
|
renderIssueMarkdown
|
|
51
51
|
} from "./chunk-WXAFXVWS.js";
|
package/dist/runner.d.ts
CHANGED
|
@@ -56,6 +56,11 @@ export type RouteReport = {
|
|
|
56
56
|
/** Null when there is no diff or no module registry. */
|
|
57
57
|
attribution: AttributedDiff | null;
|
|
58
58
|
signatures: MatchedSignature[];
|
|
59
|
+
/**
|
|
60
|
+
* Cycles used by the second pass, when the first came back
|
|
61
|
+
* `inconclusive` and the run went back for more evidence.
|
|
62
|
+
*/
|
|
63
|
+
resolvedWithCycles?: number;
|
|
59
64
|
};
|
|
60
65
|
export type MeasuredRoute = Extract<RouteReport, {
|
|
61
66
|
status: "measured";
|
|
@@ -111,6 +116,11 @@ export type RunOptions = {
|
|
|
111
116
|
diffAll?: boolean;
|
|
112
117
|
/** Only measure routes matching these templates or prefixes. */
|
|
113
118
|
routeFilter?: string[];
|
|
119
|
+
/**
|
|
120
|
+
* Measure a route again, with more cycles, when the first pass could not
|
|
121
|
+
* call it. Default true: the run should answer the question it was asked.
|
|
122
|
+
*/
|
|
123
|
+
resolveInconclusive?: boolean;
|
|
114
124
|
/** Abort between phases; remaining routes are reported as interrupted. */
|
|
115
125
|
signal?: AbortSignal;
|
|
116
126
|
onProgress?: (message: string) => void;
|
|
@@ -143,6 +153,11 @@ export type RunnerDeps = {
|
|
|
143
153
|
};
|
|
144
154
|
export declare function freePort(): Promise<number>;
|
|
145
155
|
export declare function routeSlug(route: string): string;
|
|
156
|
+
/**
|
|
157
|
+
* Cycles a second pass uses — the same figure the report recommends for a
|
|
158
|
+
* manual re-run, so the tool follows its own advice.
|
|
159
|
+
*/
|
|
160
|
+
export declare const resolveCycles: (cycles: number) => number;
|
|
146
161
|
/**
|
|
147
162
|
* Full measurement run: validate the target, discover routes, run the ritual
|
|
148
163
|
* per route in a fresh process, diff snapshots for non-stable verdicts, and
|