next-leak 0.5.1 → 0.7.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 +129 -22
- package/dist/abandon-load.d.ts +12 -2
- package/dist/build-report.d.ts +11 -0
- package/dist/build-run.d.ts +58 -0
- package/dist/build-target.d.ts +13 -0
- package/dist/build-verdict.d.ts +80 -0
- package/dist/{chunk-KJFYTJCF.js → chunk-BXDUPN6Z.js} +867 -420
- package/dist/{chunk-2FZXZLZW.js → chunk-EJ26OBZI.js} +36 -5
- package/dist/{chunk-NNDGXIGQ.js → chunk-RS3AO7HB.js} +1 -1
- package/dist/cli-args.d.ts +9 -0
- package/dist/cli.js +508 -6
- package/dist/confidence.d.ts +9 -1
- package/dist/{html-report-X2LUOZRH.js → html-report-TN7PYVU4.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/isr.d.ts +39 -0
- package/dist/launcher.d.ts +8 -0
- package/dist/manifests.d.ts +20 -0
- package/dist/process-tree.d.ts +44 -0
- package/dist/report.d.ts +0 -1
- package/dist/ritual.d.ts +25 -1
- package/dist/route-config.d.ts +23 -5
- package/dist/route-guidance.d.ts +21 -0
- package/dist/runner.d.ts +49 -0
- package/dist/target.d.ts +6 -1
- package/dist/unreclaimed-retention.d.ts +64 -0
- package/package.json +6 -2
package/dist/confidence.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AbandonOrigin } from "./abandon-load.js";
|
|
1
2
|
import type { HeapSample } from "./control-server.js";
|
|
2
3
|
import type { LoadOutcome, SettleOutcome } from "./ritual.js";
|
|
3
4
|
import { type TrendResult, type TrendVerdict } from "./trend.js";
|
|
@@ -15,8 +16,11 @@ import { type TrendResult, type TrendVerdict } from "./trend.js";
|
|
|
15
16
|
* `thin-evidence` — a leak called on too few cycles for its size
|
|
16
17
|
* `near-heap-ceiling` — the heap approached the cap the process ran under,
|
|
17
18
|
* so the curve was measured against a ceiling instead of running free
|
|
19
|
+
* `warm-up-baseline` — the baseline sat far above the level the cycles
|
|
20
|
+
* settled at, so it carries warm-up's own retention rather than the app's
|
|
21
|
+
* resting size
|
|
18
22
|
*/
|
|
19
|
-
export type WarningCode = "unsettled" | "settle-unverified" | "load-incomplete" | "abandon-ineffective" | "abandon-before-response" | "spiky-growth" | "near-threshold" | "thin-evidence" | "near-heap-ceiling";
|
|
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";
|
|
20
24
|
export type MeasurementWarning = {
|
|
21
25
|
code: WarningCode;
|
|
22
26
|
detail: string;
|
|
@@ -38,12 +42,16 @@ export type ConfidenceInput = {
|
|
|
38
42
|
settleOutcomes: readonly SettleOutcome[];
|
|
39
43
|
/** Set when the run asked for early disconnects. */
|
|
40
44
|
abandonAfterMs?: number;
|
|
45
|
+
/** Which deadline origin those disconnects used. Defaults to `first-byte`. */
|
|
46
|
+
abandonFrom?: AbandonOrigin;
|
|
41
47
|
/** Threshold the verdict used, for the noise-floor check. */
|
|
42
48
|
minGrowthPerCycle?: number;
|
|
43
49
|
/** Post-GC samples, for the heap-ceiling check. */
|
|
44
50
|
memorySamples?: readonly HeapSample[];
|
|
45
51
|
/** Old-space cap the measured process ran under (MB). */
|
|
46
52
|
maxOldSpaceMb?: number;
|
|
53
|
+
/** Warm-up requests the run sent before the baseline, for the warm-up check. */
|
|
54
|
+
warmupRequests?: number;
|
|
47
55
|
};
|
|
48
56
|
/**
|
|
49
57
|
* Cycles a re-measurement uses when a verdict came back `inconclusive` — the
|
|
@@ -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-RS3AO7HB.js";
|
|
5
|
+
import "./chunk-EJ26OBZI.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-BXDUPN6Z.js";
|
|
43
43
|
import {
|
|
44
44
|
renderHtmlReport
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-RS3AO7HB.js";
|
|
46
46
|
import {
|
|
47
47
|
classifyTrend
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-EJ26OBZI.js";
|
|
49
49
|
import {
|
|
50
50
|
renderIssueMarkdown
|
|
51
51
|
} from "./chunk-WXAFXVWS.js";
|
package/dist/isr.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PrerenderManifest } from "./manifests.js";
|
|
2
|
+
/**
|
|
3
|
+
* The header Next accepts as an authentic revalidation request.
|
|
4
|
+
*
|
|
5
|
+
* Sent with the build's own `previewModeId`, it makes the server re-render the
|
|
6
|
+
* page instead of serving the cached one. Without it, load against an ISR route
|
|
7
|
+
* exercises the static cache and nothing else — measured on the
|
|
8
|
+
* vercel/next.js#96533 reproduction, the same app reports `leak` with this
|
|
9
|
+
* header and `stable` without it.
|
|
10
|
+
*/
|
|
11
|
+
export declare const REVALIDATE_HEADER = "x-prerender-revalidate";
|
|
12
|
+
/**
|
|
13
|
+
* The revalidation period in force for a route, or null when it has none.
|
|
14
|
+
*
|
|
15
|
+
* Accepts either a concrete path (`/posts/post-1`) or a dynamic template
|
|
16
|
+
* (`/posts/[slug]`): the period lives on concrete entries, and a template
|
|
17
|
+
* inherits it from any entry whose `srcRoute` points back at it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function revalidateSecondsFor(manifest: PrerenderManifest | undefined, route: string): number | null;
|
|
20
|
+
/** Whether a route is served from the ISR cache and needs driving. */
|
|
21
|
+
export declare function revalidates(manifest: PrerenderManifest | undefined, route: string): boolean;
|
|
22
|
+
export type RevalidationPlan = {
|
|
23
|
+
kind: "not-isr";
|
|
24
|
+
} | {
|
|
25
|
+
kind: "drive";
|
|
26
|
+
headers: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
/** ISR, but the manifest cannot supply what an authentic request needs. */
|
|
29
|
+
| {
|
|
30
|
+
kind: "cannot-drive";
|
|
31
|
+
reason: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* How to make a route re-render, given what the build tells us.
|
|
35
|
+
*
|
|
36
|
+
* A header the user set themselves wins untouched: someone driving a bespoke
|
|
37
|
+
* revalidation path knows more about it than the manifest does.
|
|
38
|
+
*/
|
|
39
|
+
export declare function planRevalidation(manifest: PrerenderManifest | undefined, route: string, userHeaders: Record<string, string> | undefined): RevalidationPlan;
|
package/dist/launcher.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
1
2
|
/**
|
|
2
3
|
* Old-space cap for measured processes, when the caller does not pick one.
|
|
3
4
|
*
|
|
@@ -40,6 +41,13 @@ export declare class LaunchError extends Error {
|
|
|
40
41
|
}
|
|
41
42
|
/** Interrupt safety: no measured-app process may outlive the CLI. */
|
|
42
43
|
export declare function killActiveChildren(): void;
|
|
44
|
+
/**
|
|
45
|
+
* Puts a process this tool spawned under the same interrupt safety as a
|
|
46
|
+
* measured app — a build launched by `next-leak build` must not outlive a
|
|
47
|
+
* Ctrl+C either.
|
|
48
|
+
*/
|
|
49
|
+
export declare function registerChild(child: ChildProcess): void;
|
|
50
|
+
export declare function unregisterChild(child: ChildProcess): void;
|
|
43
51
|
/**
|
|
44
52
|
* Turns a stack dump into a sentence when the cause is recognisable. Seen in
|
|
45
53
|
* the wild: a webpack `output: standalone` build that ships without
|
package/dist/manifests.d.ts
CHANGED
|
@@ -19,6 +19,26 @@ export declare const routesManifestSchema: z.ZodObject<{
|
|
|
19
19
|
}, z.core.$loose>>;
|
|
20
20
|
}, z.core.$loose>;
|
|
21
21
|
export type RoutesManifest = z.infer<typeof routesManifestSchema>;
|
|
22
|
+
/**
|
|
23
|
+
* `.next/prerender-manifest.json` — only the fields ISR handling relies on.
|
|
24
|
+
*
|
|
25
|
+
* `routes` holds concrete prerendered paths; a dynamic template's revalidation
|
|
26
|
+
* period lives on those entries, linked back by `srcRoute`, not on the
|
|
27
|
+
* `dynamicRoutes` entry for the template itself. On the vercel/next.js#96533
|
|
28
|
+
* reproduction, `/posts/[slug]` carries no period while its 200 concrete
|
|
29
|
+
* `/posts/post-N` entries each carry `initialRevalidateSeconds: 3600` with
|
|
30
|
+
* `srcRoute: "/posts/[slug]"`.
|
|
31
|
+
*/
|
|
32
|
+
export declare const prerenderManifestSchema: z.ZodObject<{
|
|
33
|
+
routes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
34
|
+
initialRevalidateSeconds: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodLiteral<false>]>>;
|
|
35
|
+
srcRoute: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
36
|
+
}, z.core.$loose>>>;
|
|
37
|
+
preview: z.ZodOptional<z.ZodObject<{
|
|
38
|
+
previewModeId: z.ZodString;
|
|
39
|
+
}, z.core.$loose>>;
|
|
40
|
+
}, z.core.$loose>;
|
|
41
|
+
export type PrerenderManifest = z.infer<typeof prerenderManifestSchema>;
|
|
22
42
|
export type RouteKind = "page" | "route-handler";
|
|
23
43
|
export type DiscoveredRoute = {
|
|
24
44
|
/** Request path, e.g. "/" or "/products/[id]". */
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type ProcessRow = {
|
|
2
|
+
pid: number;
|
|
3
|
+
ppid: number;
|
|
4
|
+
/** Resident set size in bytes. `ps` reports KB; converted once, here. */
|
|
5
|
+
rssBytes: number;
|
|
6
|
+
command: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Parses `ps -axo pid,ppid,rss,command` output.
|
|
10
|
+
*
|
|
11
|
+
* Columns are right-aligned to widths that vary with the values, and the
|
|
12
|
+
* command itself contains spaces — so the first three fields are split off and
|
|
13
|
+
* everything after them is the command. A line that does not start with three
|
|
14
|
+
* numbers is not a process row (the header, or a wrapped line) and is skipped
|
|
15
|
+
* rather than failing the sample.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseProcessTable(output: string): ProcessRow[];
|
|
18
|
+
/**
|
|
19
|
+
* Every process below `rootPid`, at any depth, excluding the root itself.
|
|
20
|
+
*
|
|
21
|
+
* Walks generation by generation rather than recursing per row: a build tree is
|
|
22
|
+
* shallow but the machine's process table is not, and this visits each row once
|
|
23
|
+
* per generation instead of once per ancestor.
|
|
24
|
+
*/
|
|
25
|
+
export declare function descendantsOf(rows: readonly ProcessRow[], rootPid: number): ProcessRow[];
|
|
26
|
+
/** Whether a row is one of the build's static-generation workers. */
|
|
27
|
+
export declare function isStaticGenWorker(row: ProcessRow): boolean;
|
|
28
|
+
export type ProcessTableSample = {
|
|
29
|
+
ok: true;
|
|
30
|
+
rows: ProcessRow[];
|
|
31
|
+
} | {
|
|
32
|
+
ok: false;
|
|
33
|
+
reason: string;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* One sample of the whole process table.
|
|
37
|
+
*
|
|
38
|
+
* The failure is reported rather than swallowed. An empty table and an
|
|
39
|
+
* unreadable one look identical downstream — both yield no workers — and
|
|
40
|
+
* reporting "nothing to measure" when the truth is "could not look" is the
|
|
41
|
+
* silent false negative this tool exists to avoid. Seen for real: a sandbox
|
|
42
|
+
* that denies `ps` turned a leaking build into a clean run.
|
|
43
|
+
*/
|
|
44
|
+
export declare function sampleProcessTable(): Promise<ProcessTableSample>;
|
package/dist/report.d.ts
CHANGED
package/dist/ritual.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HeapSample } from "./control-server.js";
|
|
2
2
|
import { launchInstrumented } from "./launcher.js";
|
|
3
|
-
import { runAbandonPhase } from "./abandon-load.js";
|
|
3
|
+
import { runAbandonPhase, type AbandonOrigin } from "./abandon-load.js";
|
|
4
4
|
import { runLoadPhase } from "./load.js";
|
|
5
5
|
import { type TrendResult } from "./trend.js";
|
|
6
6
|
export type RitualOptions = {
|
|
@@ -24,6 +24,8 @@ export type RitualOptions = {
|
|
|
24
24
|
headers?: Record<string, string>;
|
|
25
25
|
/** Emulate clients that disconnect before the response arrives. */
|
|
26
26
|
abandonAfterMs?: number;
|
|
27
|
+
/** Where that deadline starts. Defaults to `first-byte`. */
|
|
28
|
+
abandonFrom?: AbandonOrigin;
|
|
27
29
|
};
|
|
28
30
|
export type PhaseTiming = {
|
|
29
31
|
phase: string;
|
|
@@ -89,6 +91,21 @@ export type RitualResult = {
|
|
|
89
91
|
memorySamples: HeapSample[];
|
|
90
92
|
/** Highest memory seen during each load cycle, sampled without collecting. */
|
|
91
93
|
peaks: PeakSample[];
|
|
94
|
+
/**
|
|
95
|
+
* One reading per cycle taken before any collection is forced — what the
|
|
96
|
+
* process is holding on its own.
|
|
97
|
+
*
|
|
98
|
+
* Every other number here is post-GC, which is what makes a verdict mean
|
|
99
|
+
* something and is also blind to a whole class of leak: memory a full GC
|
|
100
|
+
* would reclaim that a production process never runs one often enough to
|
|
101
|
+
* (vercel/next.js#96533). Absolute values overstate retention — this is
|
|
102
|
+
* taken seconds after load, not hours, so it includes garbage not yet
|
|
103
|
+
* collected — but a roughly constant offset across identical cycles cancels
|
|
104
|
+
* out of the deltas the trend is read from.
|
|
105
|
+
*/
|
|
106
|
+
unreclaimedSamples: HeapSample[];
|
|
107
|
+
/** Trend over `unreclaimedSamples`. Reported, never the verdict. */
|
|
108
|
+
unreclaimedTrend: TrendResult;
|
|
92
109
|
baselineSnapshot: string;
|
|
93
110
|
afterSnapshot: string;
|
|
94
111
|
trend: TrendResult;
|
|
@@ -110,6 +127,13 @@ export type RitualDeps = {
|
|
|
110
127
|
/** GC-free read, polled while the app is under load. */
|
|
111
128
|
readMemory: (port: number) => Promise<HeapSample>;
|
|
112
129
|
};
|
|
130
|
+
/**
|
|
131
|
+
* The pre-collection wait, taken *out of* the idle rather than added to it.
|
|
132
|
+
*
|
|
133
|
+
* Capped at a quarter of the idle so short profiles keep their settle budget:
|
|
134
|
+
* `--quick` idles for 8 s, and a flat 3 s would be nearly half of it.
|
|
135
|
+
*/
|
|
136
|
+
export declare function unreclaimedSettleFor(idleMs: number): number;
|
|
113
137
|
/**
|
|
114
138
|
* Single source of truth for ritual defaults — reports must echo them.
|
|
115
139
|
*
|
package/dist/route-config.d.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
/**
|
|
3
|
-
* `next-leak.config.json` in the target app dir: sample values for dynamic
|
|
4
|
-
* route params. `params` applies globally; `routes` overrides per route
|
|
5
|
-
* template (keys as discovered, e.g. `/[lang]/candidate/[candidateId]`).
|
|
6
|
-
*/
|
|
7
2
|
declare const routeConfigSchema: z.ZodObject<{
|
|
8
3
|
params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
9
4
|
routes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
10
5
|
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
11
6
|
query: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
12
7
|
abandonAfterMs: z.ZodOptional<z.ZodNumber>;
|
|
8
|
+
abandonFrom: z.ZodOptional<z.ZodEnum<{
|
|
9
|
+
"first-byte": "first-byte";
|
|
10
|
+
request: "request";
|
|
11
|
+
}>>;
|
|
13
12
|
}, z.core.$strict>;
|
|
14
13
|
export type RouteConfig = z.infer<typeof routeConfigSchema>;
|
|
15
14
|
export declare class RouteConfigError extends Error {
|
|
@@ -18,12 +17,31 @@ export declare class RouteConfigError extends Error {
|
|
|
18
17
|
export declare const ROUTE_CONFIG_FILE = "next-leak.config.json";
|
|
19
18
|
/** Missing file → empty config. Present but invalid → loud failure. */
|
|
20
19
|
export declare function loadRouteConfig(appDir: string): Promise<RouteConfig>;
|
|
20
|
+
export type DynamicSegment = {
|
|
21
|
+
name: string;
|
|
22
|
+
/** `[...slug]` — the value may span several path segments. */
|
|
23
|
+
catchAll: boolean;
|
|
24
|
+
/** `[[...slug]]` — the segment can be omitted entirely. */
|
|
25
|
+
optional: boolean;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The dynamic segments of a route template, in order.
|
|
29
|
+
*
|
|
30
|
+
* Exported so guidance and resolution read the same grammar: a skeleton that
|
|
31
|
+
* named parameters the resolver does not recognise would be worse than none.
|
|
32
|
+
*/
|
|
33
|
+
export declare function dynamicSegmentsOf(routeTemplate: string): DynamicSegment[];
|
|
21
34
|
/**
|
|
22
35
|
* Sample values may contain `{n}`, which the load phase replaces with a
|
|
23
36
|
* per-request counter. Leaks keyed by URL (route caches, LRUs, bot traffic
|
|
24
37
|
* with unique tails) are invisible when every request hits the same path.
|
|
25
38
|
*/
|
|
26
39
|
export declare const UNIQUE_MARKER = "{n}";
|
|
40
|
+
/** The bound in a sample value, or null when it carries none. */
|
|
41
|
+
export declare function boundedMarkerOf(value: string): {
|
|
42
|
+
marker: string;
|
|
43
|
+
bound: number;
|
|
44
|
+
} | null;
|
|
27
45
|
/**
|
|
28
46
|
* Substitutes sample values into a dynamic route template and returns a
|
|
29
47
|
* URL-safe request path (or null when a param has no configured value;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PrerenderManifest } from "./manifests.js";
|
|
2
|
+
/**
|
|
3
|
+
* Sample values lifted from paths the build actually prerendered.
|
|
4
|
+
*
|
|
5
|
+
* A skeleton full of placeholders still leaves the user guessing what a valid
|
|
6
|
+
* value looks like. The build already knows: `prerender-manifest.json` lists
|
|
7
|
+
* concrete paths, each pointing back at its template through `srcRoute`, so a
|
|
8
|
+
* template like `/posts/[slug]` can be filled with a real `post-0` that will
|
|
9
|
+
* resolve on the first try.
|
|
10
|
+
*/
|
|
11
|
+
export declare function sampleValuesFromManifest(manifest: PrerenderManifest | undefined, routeTemplate: string): Record<string, string> | null;
|
|
12
|
+
/**
|
|
13
|
+
* The `next-leak.config.json` that would measure the routes a run had to skip.
|
|
14
|
+
*
|
|
15
|
+
* Printed rather than merely referenced: telling someone a config file exists
|
|
16
|
+
* and leaving them to work out its shape is the difference between a run they
|
|
17
|
+
* can fix in ten seconds and one they abandon.
|
|
18
|
+
*/
|
|
19
|
+
export declare function renderConfigSkeleton(routeTemplates: readonly string[], manifest?: PrerenderManifest | undefined): string | null;
|
|
20
|
+
/** Whether a rendered skeleton still needs the user to fill anything in. */
|
|
21
|
+
export declare function hasPlaceholders(skeleton: string): boolean;
|
package/dist/runner.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { AbandonOrigin } from "./abandon-load.js";
|
|
1
2
|
import { type AttributedDiff } from "./attribution.js";
|
|
2
3
|
import { type ConfidenceReport } from "./confidence.js";
|
|
3
4
|
import type { HeapSample } from "./control-server.js";
|
|
4
5
|
import { type MeasurementEnvironment } from "./environment.js";
|
|
5
6
|
import { diffSnapshotFiles, type HeapDiff } from "./heap-diff.js";
|
|
7
|
+
import { type PrerenderManifest } from "./manifests.js";
|
|
6
8
|
import { extractModuleRegistry } from "./module-registry.js";
|
|
7
9
|
import { runRitual, type LoadOutcome, type PeakSample, type PhaseTiming, type SettleOutcome } from "./ritual.js";
|
|
8
10
|
import { readNextVersion, type MatchedSignature } from "./signatures.js";
|
|
@@ -15,6 +17,17 @@ export type RouteReport = {
|
|
|
15
17
|
route: string;
|
|
16
18
|
status: "failed";
|
|
17
19
|
reason: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The route was reachable, but the load could not have exercised the code
|
|
23
|
+
* path it represents — so no verdict is emitted. Measuring an ISR route
|
|
24
|
+
* without driving revalidation reports a flat curve about the static cache,
|
|
25
|
+
* which reads as health and is not.
|
|
26
|
+
*/
|
|
27
|
+
| {
|
|
28
|
+
route: string;
|
|
29
|
+
status: "not-exercised";
|
|
30
|
+
reason: string;
|
|
18
31
|
} | {
|
|
19
32
|
route: string;
|
|
20
33
|
status: "measured";
|
|
@@ -33,6 +46,40 @@ export type RouteReport = {
|
|
|
33
46
|
* here is post-GC; this is the one a container limit is judged against.
|
|
34
47
|
*/
|
|
35
48
|
peaks: PeakSample[];
|
|
49
|
+
/**
|
|
50
|
+
* One reading per cycle taken before any forced collection, and the
|
|
51
|
+
* trend over them. What a production process holds between full GCs is
|
|
52
|
+
* not what this tool's verdict measures — see `unreclaimed-retention`.
|
|
53
|
+
* Empty when a reading was lost: a hole would make every later delta
|
|
54
|
+
* span two cycles.
|
|
55
|
+
*/
|
|
56
|
+
unreclaimedSamples: HeapSample[];
|
|
57
|
+
unreclaimedTrend: TrendResult;
|
|
58
|
+
/** Requests each cycle served — what the growth rates normalize by. */
|
|
59
|
+
requestsPerCycle: number;
|
|
60
|
+
/**
|
|
61
|
+
* The early-disconnect regime, when the route asked for one. Recorded
|
|
62
|
+
* because a curve measured with cuts landing mid-stream and one measured
|
|
63
|
+
* with cuts landing before the response are different experiments, and
|
|
64
|
+
* the counters alone do not say which was intended.
|
|
65
|
+
*/
|
|
66
|
+
abandon?: {
|
|
67
|
+
afterMs: number;
|
|
68
|
+
from: AbandonOrigin;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Distinct keys the load cycled through, when the route asked for a
|
|
72
|
+
* bounded set. A verdict about a cache depends on how many keys it saw,
|
|
73
|
+
* so the number belongs on the record with the rest of the regime.
|
|
74
|
+
*/
|
|
75
|
+
keyCardinality?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Seconds of the ISR revalidation period this route was driven through,
|
|
78
|
+
* when it has one. Absent on routes not served from the ISR cache.
|
|
79
|
+
* Recorded because a curve measured against a cache and one measured
|
|
80
|
+
* against a re-render are different experiments.
|
|
81
|
+
*/
|
|
82
|
+
revalidatedEverySeconds?: number;
|
|
36
83
|
/** RSS growth per 1000 requests, computed like the heap figure. */
|
|
37
84
|
rssPer1000Requests: number;
|
|
38
85
|
/** Wall-clock per phase — explains where a long run spent its time. */
|
|
@@ -88,6 +135,8 @@ export type RunReport = {
|
|
|
88
135
|
appDir: string;
|
|
89
136
|
startedAt: string;
|
|
90
137
|
workDir: string;
|
|
138
|
+
/** Carried so the report can suggest sample params the build already knows. */
|
|
139
|
+
prerender?: PrerenderManifest;
|
|
91
140
|
environment: MeasurementEnvironment;
|
|
92
141
|
parameters: RunParameters;
|
|
93
142
|
routes: RouteReport[];
|
package/dist/target.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AppPathsManifest, type PagesManifest, type RoutesManifest } from "./manifests.js";
|
|
1
|
+
import { type AppPathsManifest, type PagesManifest, type PrerenderManifest, type RoutesManifest } from "./manifests.js";
|
|
2
2
|
export type TargetErrorCode = "NO_BUILD" | "NO_STANDALONE" | "BAD_MANIFEST";
|
|
3
3
|
export declare class TargetError extends Error {
|
|
4
4
|
readonly code: TargetErrorCode;
|
|
@@ -12,6 +12,11 @@ export type ValidatedTarget = {
|
|
|
12
12
|
pages: PagesManifest;
|
|
13
13
|
/** Absent on builds that do not emit it; nothing downstream reads it. */
|
|
14
14
|
routes: RoutesManifest | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Absent on builds with nothing prerendered. Carries which routes revalidate
|
|
17
|
+
* and the `previewModeId` needed to make them re-render under load.
|
|
18
|
+
*/
|
|
19
|
+
prerender: PrerenderManifest | undefined;
|
|
15
20
|
};
|
|
16
21
|
/**
|
|
17
22
|
* Validates that `appDir` contains a production build with
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { HeapSample } from "./control-server.js";
|
|
2
|
+
import type { TrendVerdict } from "./trend.js";
|
|
3
|
+
/**
|
|
4
|
+
* Memory classes compared between the two series, least to most specific.
|
|
5
|
+
*
|
|
6
|
+
* `arrayBuffers` is a subset of `external`, so when both are equally out of
|
|
7
|
+
* proportion the later one wins the tie below — naming the narrower class says
|
|
8
|
+
* more about the mechanism.
|
|
9
|
+
*/
|
|
10
|
+
declare const CLASSES: readonly ["heap", "external", "arrayBuffers"];
|
|
11
|
+
export type MemoryClass = (typeof CLASSES)[number];
|
|
12
|
+
export type UnreclaimedRetention = {
|
|
13
|
+
/** Where holding-versus-retaining was most out of proportion. */
|
|
14
|
+
class: MemoryClass;
|
|
15
|
+
/** Largest per-cycle gap between what was held and what survived a GC. */
|
|
16
|
+
largestGapBytes: number;
|
|
17
|
+
/** What the route retained after collection, in that class. */
|
|
18
|
+
retainedBytes: number;
|
|
19
|
+
/** The gap as a multiple of what is retained. */
|
|
20
|
+
ratio: number;
|
|
21
|
+
};
|
|
22
|
+
export type UnreclaimedRetentionInput = {
|
|
23
|
+
/** One reading per cycle, taken before any forced collection. */
|
|
24
|
+
unreclaimedSamples: readonly HeapSample[];
|
|
25
|
+
/** Post-GC samples: baseline first, then one per cycle. */
|
|
26
|
+
memorySamples: readonly HeapSample[];
|
|
27
|
+
/** The route's own verdict, computed from the post-GC series. */
|
|
28
|
+
verdict: TrendVerdict;
|
|
29
|
+
/**
|
|
30
|
+
* Whether that `leak` verdict is one the evidence plainly supports — the same
|
|
31
|
+
* bar an issue draft has to clear.
|
|
32
|
+
*
|
|
33
|
+
* A weak `leak` must not silence this note. Measured on the #96533
|
|
34
|
+
* reproduction: the post-GC verdict is `leak` at +0.15 MB/1000 req carrying
|
|
35
|
+
* two low-confidence warnings, which is noise grazing the threshold, while
|
|
36
|
+
* the gap is 3.96 MB of arrayBuffers at 4x what the route retains. Silencing
|
|
37
|
+
* on any `leak` at all let the weaker finding hide the stronger one.
|
|
38
|
+
*/
|
|
39
|
+
verdictIsWellSupported: boolean;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Whether a route holds materially more memory between collections than it
|
|
43
|
+
* retains after one.
|
|
44
|
+
*
|
|
45
|
+
* The shape of vercel/next.js#96533, where the reporter accumulated 1.16 GB of
|
|
46
|
+
* arrayBuffers over four days against a flat JS heap. Measured on that repro
|
|
47
|
+
* 2026-08-18, the signal is a *gap*, not a slope: arrayBuffers sat at 4–5 MB
|
|
48
|
+
* between collections against a flat 0.32 MB after one, while the series itself
|
|
49
|
+
* oscillated (+21.2, -3.0, -6.2, +14.2 MB) and classified as `inconclusive`. An
|
|
50
|
+
* earlier version keyed on that series climbing and reported nothing at all.
|
|
51
|
+
*
|
|
52
|
+
* Outside the verdict, like the peak-pressure note: those are statements about
|
|
53
|
+
* what survives collection, this is one about what precedes it. Silent when the
|
|
54
|
+
* route already leaks on evidence that supports it — but a marginal `leak` does
|
|
55
|
+
* not silence it, since on #96533 that weak verdict was hiding this finding.
|
|
56
|
+
*/
|
|
57
|
+
export declare function assessUnreclaimedRetention(input: UnreclaimedRetentionInput): UnreclaimedRetention | null;
|
|
58
|
+
/**
|
|
59
|
+
* One line, phrased so it never contradicts the verdict beside it, and so it
|
|
60
|
+
* states its own limit: the reading is taken seconds after load, not the hours
|
|
61
|
+
* a production process runs between full collections.
|
|
62
|
+
*/
|
|
63
|
+
export declare function describeUnreclaimedRetention(retention: UnreclaimedRetention): string;
|
|
64
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-leak",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Find out whether your Next.js app actually leaks memory — how much, on which route, and whose fault it is.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nextjs",
|
|
@@ -49,7 +49,11 @@
|
|
|
49
49
|
"overrides": {
|
|
50
50
|
"uuid": ">=11.1.1",
|
|
51
51
|
"esbuild": ">=0.28.1",
|
|
52
|
-
"qs": ">=6.15.2"
|
|
52
|
+
"qs": ">=6.15.2",
|
|
53
|
+
"js-yaml": ">=4.3.1 <5",
|
|
54
|
+
"fast-uri": ">=3.1.5 <4",
|
|
55
|
+
"ip-address": ">=10.3.1 <11",
|
|
56
|
+
"postcss": ">=8.5.23 <9"
|
|
53
57
|
}
|
|
54
58
|
},
|
|
55
59
|
"devDependencies": {
|