next-leak 0.5.0 → 0.6.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 +116 -18
- 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-2FZXZLZW.js → chunk-22PBLCGO.js} +29 -1
- package/dist/{chunk-NNDGXIGQ.js → chunk-6CJU5TVV.js} +1 -1
- package/dist/{chunk-IDSNTUKC.js → chunk-AMSDGT3Y.js} +818 -411
- package/dist/cli-args.d.ts +9 -0
- package/dist/cli.js +508 -6
- package/dist/confidence.d.ts +6 -1
- package/dist/{html-report-X2LUOZRH.js → html-report-2UAOOZQO.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 +22 -0
- package/dist/route-config.d.ts +19 -5
- package/dist/route-guidance.d.ts +21 -0
- package/dist/runner.d.ts +38 -0
- package/dist/target.d.ts +6 -1
- package/dist/unreclaimed-retention.d.ts +64 -0
- package/package.json +1 -1
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
|
@@ -89,6 +89,21 @@ export type RitualResult = {
|
|
|
89
89
|
memorySamples: HeapSample[];
|
|
90
90
|
/** Highest memory seen during each load cycle, sampled without collecting. */
|
|
91
91
|
peaks: PeakSample[];
|
|
92
|
+
/**
|
|
93
|
+
* One reading per cycle taken before any collection is forced — what the
|
|
94
|
+
* process is holding on its own.
|
|
95
|
+
*
|
|
96
|
+
* Every other number here is post-GC, which is what makes a verdict mean
|
|
97
|
+
* something and is also blind to a whole class of leak: memory a full GC
|
|
98
|
+
* would reclaim that a production process never runs one often enough to
|
|
99
|
+
* (vercel/next.js#96533). Absolute values overstate retention — this is
|
|
100
|
+
* taken seconds after load, not hours, so it includes garbage not yet
|
|
101
|
+
* collected — but a roughly constant offset across identical cycles cancels
|
|
102
|
+
* out of the deltas the trend is read from.
|
|
103
|
+
*/
|
|
104
|
+
unreclaimedSamples: HeapSample[];
|
|
105
|
+
/** Trend over `unreclaimedSamples`. Reported, never the verdict. */
|
|
106
|
+
unreclaimedTrend: TrendResult;
|
|
92
107
|
baselineSnapshot: string;
|
|
93
108
|
afterSnapshot: string;
|
|
94
109
|
trend: TrendResult;
|
|
@@ -110,6 +125,13 @@ export type RitualDeps = {
|
|
|
110
125
|
/** GC-free read, polled while the app is under load. */
|
|
111
126
|
readMemory: (port: number) => Promise<HeapSample>;
|
|
112
127
|
};
|
|
128
|
+
/**
|
|
129
|
+
* The pre-collection wait, taken *out of* the idle rather than added to it.
|
|
130
|
+
*
|
|
131
|
+
* Capped at a quarter of the idle so short profiles keep their settle budget:
|
|
132
|
+
* `--quick` idles for 8 s, and a flat 3 s would be nearly half of it.
|
|
133
|
+
*/
|
|
134
|
+
export declare function unreclaimedSettleFor(idleMs: number): number;
|
|
113
135
|
/**
|
|
114
136
|
* Single source of truth for ritual defaults — reports must echo them.
|
|
115
137
|
*
|
package/dist/route-config.d.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
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>>>;
|
|
@@ -18,12 +13,31 @@ export declare class RouteConfigError extends Error {
|
|
|
18
13
|
export declare const ROUTE_CONFIG_FILE = "next-leak.config.json";
|
|
19
14
|
/** Missing file → empty config. Present but invalid → loud failure. */
|
|
20
15
|
export declare function loadRouteConfig(appDir: string): Promise<RouteConfig>;
|
|
16
|
+
export type DynamicSegment = {
|
|
17
|
+
name: string;
|
|
18
|
+
/** `[...slug]` — the value may span several path segments. */
|
|
19
|
+
catchAll: boolean;
|
|
20
|
+
/** `[[...slug]]` — the segment can be omitted entirely. */
|
|
21
|
+
optional: boolean;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The dynamic segments of a route template, in order.
|
|
25
|
+
*
|
|
26
|
+
* Exported so guidance and resolution read the same grammar: a skeleton that
|
|
27
|
+
* named parameters the resolver does not recognise would be worse than none.
|
|
28
|
+
*/
|
|
29
|
+
export declare function dynamicSegmentsOf(routeTemplate: string): DynamicSegment[];
|
|
21
30
|
/**
|
|
22
31
|
* Sample values may contain `{n}`, which the load phase replaces with a
|
|
23
32
|
* per-request counter. Leaks keyed by URL (route caches, LRUs, bot traffic
|
|
24
33
|
* with unique tails) are invisible when every request hits the same path.
|
|
25
34
|
*/
|
|
26
35
|
export declare const UNIQUE_MARKER = "{n}";
|
|
36
|
+
/** The bound in a sample value, or null when it carries none. */
|
|
37
|
+
export declare function boundedMarkerOf(value: string): {
|
|
38
|
+
marker: string;
|
|
39
|
+
bound: number;
|
|
40
|
+
} | null;
|
|
27
41
|
/**
|
|
28
42
|
* Substitutes sample values into a dynamic route template and returns a
|
|
29
43
|
* 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
|
@@ -3,6 +3,7 @@ import { type ConfidenceReport } from "./confidence.js";
|
|
|
3
3
|
import type { HeapSample } from "./control-server.js";
|
|
4
4
|
import { type MeasurementEnvironment } from "./environment.js";
|
|
5
5
|
import { diffSnapshotFiles, type HeapDiff } from "./heap-diff.js";
|
|
6
|
+
import { type PrerenderManifest } from "./manifests.js";
|
|
6
7
|
import { extractModuleRegistry } from "./module-registry.js";
|
|
7
8
|
import { runRitual, type LoadOutcome, type PeakSample, type PhaseTiming, type SettleOutcome } from "./ritual.js";
|
|
8
9
|
import { readNextVersion, type MatchedSignature } from "./signatures.js";
|
|
@@ -15,6 +16,17 @@ export type RouteReport = {
|
|
|
15
16
|
route: string;
|
|
16
17
|
status: "failed";
|
|
17
18
|
reason: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The route was reachable, but the load could not have exercised the code
|
|
22
|
+
* path it represents — so no verdict is emitted. Measuring an ISR route
|
|
23
|
+
* without driving revalidation reports a flat curve about the static cache,
|
|
24
|
+
* which reads as health and is not.
|
|
25
|
+
*/
|
|
26
|
+
| {
|
|
27
|
+
route: string;
|
|
28
|
+
status: "not-exercised";
|
|
29
|
+
reason: string;
|
|
18
30
|
} | {
|
|
19
31
|
route: string;
|
|
20
32
|
status: "measured";
|
|
@@ -33,6 +45,30 @@ export type RouteReport = {
|
|
|
33
45
|
* here is post-GC; this is the one a container limit is judged against.
|
|
34
46
|
*/
|
|
35
47
|
peaks: PeakSample[];
|
|
48
|
+
/**
|
|
49
|
+
* One reading per cycle taken before any forced collection, and the
|
|
50
|
+
* trend over them. What a production process holds between full GCs is
|
|
51
|
+
* not what this tool's verdict measures — see `unreclaimed-retention`.
|
|
52
|
+
* Empty when a reading was lost: a hole would make every later delta
|
|
53
|
+
* span two cycles.
|
|
54
|
+
*/
|
|
55
|
+
unreclaimedSamples: HeapSample[];
|
|
56
|
+
unreclaimedTrend: TrendResult;
|
|
57
|
+
/** Requests each cycle served — what the growth rates normalize by. */
|
|
58
|
+
requestsPerCycle: number;
|
|
59
|
+
/**
|
|
60
|
+
* Distinct keys the load cycled through, when the route asked for a
|
|
61
|
+
* bounded set. A verdict about a cache depends on how many keys it saw,
|
|
62
|
+
* so the number belongs on the record with the rest of the regime.
|
|
63
|
+
*/
|
|
64
|
+
keyCardinality?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Seconds of the ISR revalidation period this route was driven through,
|
|
67
|
+
* when it has one. Absent on routes not served from the ISR cache.
|
|
68
|
+
* Recorded because a curve measured against a cache and one measured
|
|
69
|
+
* against a re-render are different experiments.
|
|
70
|
+
*/
|
|
71
|
+
revalidatedEverySeconds?: number;
|
|
36
72
|
/** RSS growth per 1000 requests, computed like the heap figure. */
|
|
37
73
|
rssPer1000Requests: number;
|
|
38
74
|
/** Wall-clock per phase — explains where a long run spent its time. */
|
|
@@ -88,6 +124,8 @@ export type RunReport = {
|
|
|
88
124
|
appDir: string;
|
|
89
125
|
startedAt: string;
|
|
90
126
|
workDir: string;
|
|
127
|
+
/** Carried so the report can suggest sample params the build already knows. */
|
|
128
|
+
prerender?: PrerenderManifest;
|
|
91
129
|
environment: MeasurementEnvironment;
|
|
92
130
|
parameters: RunParameters;
|
|
93
131
|
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 {};
|