next-leak 0.1.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/LICENSE +21 -0
- package/README.md +199 -0
- package/dist/abandon-load.d.ts +33 -0
- package/dist/attribution.d.ts +40 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +100 -0
- package/dist/chunk-4BMPC45G.js +81 -0
- package/dist/chunk-5K4WEMIS.js +88 -0
- package/dist/chunk-K5PZFVJH.js +103 -0
- package/dist/chunk-OSZ6ND6E.js +38 -0
- package/dist/chunk-UZTZWHKH.js +67803 -0
- package/dist/cli-args.d.ts +24 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +106 -0
- package/dist/confidence.d.ts +78 -0
- package/dist/control-client.d.ts +11 -0
- package/dist/control-server.d.ts +33 -0
- package/dist/environment.d.ts +11 -0
- package/dist/guards.d.ts +9 -0
- package/dist/heap-diff.d.ts +96 -0
- package/dist/html-report-3JIRVRBF.js +9 -0
- package/dist/html-report.d.ts +3 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +95 -0
- package/dist/issue-report-AB62FRNM.js +8 -0
- package/dist/issue-report.d.ts +10 -0
- package/dist/launcher.d.ts +39 -0
- package/dist/load.d.ts +34 -0
- package/dist/manifests.d.ts +44 -0
- package/dist/module-registry.d.ts +24 -0
- package/dist/report.d.ts +3 -0
- package/dist/ritual.d.ts +95 -0
- package/dist/route-config.d.ts +36 -0
- package/dist/run-report.fixture.d.ts +3 -0
- package/dist/runner.d.ts +127 -0
- package/dist/signatures.d.ts +28 -0
- package/dist/target.d.ts +21 -0
- package/dist/trend.d.ts +38 -0
- package/dist/vlq.d.ts +16 -0
- package/package.json +75 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type LineMapping } from "./vlq.js";
|
|
2
|
+
/** Bundler module id → source path (e.g. `[project]/src/app/leaky/page.tsx`). */
|
|
3
|
+
export type ModuleRegistry = ReadonlyMap<number, string>;
|
|
4
|
+
export type NormalizedSourceMap = {
|
|
5
|
+
sources: string[];
|
|
6
|
+
lines: LineMapping[];
|
|
7
|
+
};
|
|
8
|
+
/** Accepts flat or sectioned sourcemaps; null when it is neither. */
|
|
9
|
+
export declare function normalizeSourceMap(raw: unknown): NormalizedSourceMap | null;
|
|
10
|
+
/**
|
|
11
|
+
* Extracts `[id, factory, ...]` pairs from a Turbopack CJS chunk by walking
|
|
12
|
+
* the sourcemap instead of lexing JavaScript: each factory is a contiguous
|
|
13
|
+
* region mapped to one source, so a change of source index marks a factory
|
|
14
|
+
* boundary, and the module id is the last integer literal in the unmapped
|
|
15
|
+
* gap right before it. Non-Turbopack chunks contribute nothing.
|
|
16
|
+
*/
|
|
17
|
+
export declare function extractChunkModules(code: string, rawMap: unknown): Map<number, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Builds the module registry by scanning every server chunk that has an
|
|
20
|
+
* adjacent sourcemap. Missing maps, unparseable chunks, or non-Turbopack
|
|
21
|
+
* builds simply contribute nothing: an empty registry means every finding
|
|
22
|
+
* degrades to `unattributed`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function extractModuleRegistry(nextServerDir: string): Promise<ModuleRegistry>;
|
package/dist/report.d.ts
ADDED
package/dist/ritual.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { HeapSample } from "./control-server.js";
|
|
2
|
+
import { launchInstrumented } from "./launcher.js";
|
|
3
|
+
import { runLoadPhase } from "./load.js";
|
|
4
|
+
import { type TrendResult } from "./trend.js";
|
|
5
|
+
export type RitualOptions = {
|
|
6
|
+
/** Absolute path to the standalone server.js. */
|
|
7
|
+
serverPath: string;
|
|
8
|
+
/** Concrete request path (dynamic params already resolved), e.g. "/products/42". */
|
|
9
|
+
route: string;
|
|
10
|
+
/** Directory for this route's snapshots and control file. */
|
|
11
|
+
workDir: string;
|
|
12
|
+
/** Built bootstrap module for `--import`. */
|
|
13
|
+
bootstrapPath: string;
|
|
14
|
+
appPort: number;
|
|
15
|
+
warmupRequests?: number;
|
|
16
|
+
loadRequests?: number;
|
|
17
|
+
connections?: number;
|
|
18
|
+
cycles?: number;
|
|
19
|
+
idleMs?: number;
|
|
20
|
+
/** Headers sent with every request during warm-up and load. */
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
/** Emulate clients that disconnect before the response arrives. */
|
|
23
|
+
abandonAfterMs?: number;
|
|
24
|
+
};
|
|
25
|
+
export type PhaseTiming = {
|
|
26
|
+
phase: string;
|
|
27
|
+
seconds: number;
|
|
28
|
+
};
|
|
29
|
+
/** What each load phase actually did — auditable after the fact. */
|
|
30
|
+
export type LoadOutcome = {
|
|
31
|
+
phase: string;
|
|
32
|
+
sent: number;
|
|
33
|
+
ok2xx?: number;
|
|
34
|
+
non2xx?: number;
|
|
35
|
+
errors?: number;
|
|
36
|
+
timeouts?: number;
|
|
37
|
+
abandoned?: number;
|
|
38
|
+
/** Abandonments where the response had already started — the mid-stream path. */
|
|
39
|
+
abandonedMidStream?: number;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Whether the heap actually held still before each sample was taken.
|
|
43
|
+
*
|
|
44
|
+
* `unknown` is not a softer `moving`: with fewer than two GC readings there is
|
|
45
|
+
* nothing to compare, so the run never learned whether the heap was steady.
|
|
46
|
+
* Conflating the two made every short-idle run look like a moving heap.
|
|
47
|
+
*/
|
|
48
|
+
export type SettleStatus = "settled" | "moving" | "unknown";
|
|
49
|
+
export type SettleOutcome = {
|
|
50
|
+
phase: string;
|
|
51
|
+
status: SettleStatus;
|
|
52
|
+
/** GC polls taken before converging or giving up. */
|
|
53
|
+
polls: number;
|
|
54
|
+
};
|
|
55
|
+
export type RitualResult = {
|
|
56
|
+
route: string;
|
|
57
|
+
/** Wall-clock per phase, so slow runs can be explained instead of guessed. */
|
|
58
|
+
timings: PhaseTiming[];
|
|
59
|
+
/** Per-phase request outcomes; without these a run cannot be audited. */
|
|
60
|
+
loadOutcomes: LoadOutcome[];
|
|
61
|
+
/** Per-cycle settle results: a sample taken while the heap moved is suspect. */
|
|
62
|
+
settleOutcomes: SettleOutcome[];
|
|
63
|
+
/** Post-GC heapUsed per phase: baseline first, then one per cycle. */
|
|
64
|
+
samples: number[];
|
|
65
|
+
/** Full memory samples in the same order. */
|
|
66
|
+
memorySamples: HeapSample[];
|
|
67
|
+
baselineSnapshot: string;
|
|
68
|
+
afterSnapshot: string;
|
|
69
|
+
trend: TrendResult;
|
|
70
|
+
requestsPerCycle: number;
|
|
71
|
+
};
|
|
72
|
+
/** Injectable seams for unit tests; production uses the real implementations. */
|
|
73
|
+
export type RitualDeps = {
|
|
74
|
+
launch: typeof launchInstrumented;
|
|
75
|
+
load: typeof runLoadPhase;
|
|
76
|
+
sleep: (ms: number) => Promise<void>;
|
|
77
|
+
};
|
|
78
|
+
/** Single source of truth for ritual defaults — reports must echo them. */
|
|
79
|
+
export declare const RITUAL_DEFAULTS: {
|
|
80
|
+
readonly warmupRequests: 200;
|
|
81
|
+
readonly loadRequests: 5000;
|
|
82
|
+
readonly connections: 100;
|
|
83
|
+
readonly cycles: 3;
|
|
84
|
+
readonly idleMs: 30000;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Runs the validated phase-0 ritual against one route in a fresh process:
|
|
88
|
+
*
|
|
89
|
+
* warm-up → GC → baseline snapshot
|
|
90
|
+
* → [load → idle → GC → sample] × cycles (last cycle snapshots)
|
|
91
|
+
*
|
|
92
|
+
* Warm-up before the baseline and idle+GC before every sample are what
|
|
93
|
+
* separate a real measurement from the classic false positive.
|
|
94
|
+
*/
|
|
95
|
+
export declare function runRitual(options: RitualOptions, deps?: RitualDeps): Promise<RitualResult>;
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
export declare const routeConfigSchema: z.ZodObject<{
|
|
8
|
+
params: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
9
|
+
routes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>>;
|
|
10
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
11
|
+
query: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
12
|
+
abandonAfterMs: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
}, z.core.$strict>;
|
|
14
|
+
export type RouteConfig = z.infer<typeof routeConfigSchema>;
|
|
15
|
+
export declare class RouteConfigError extends Error {
|
|
16
|
+
constructor(message: string);
|
|
17
|
+
}
|
|
18
|
+
export declare const ROUTE_CONFIG_FILE = "next-leak.config.json";
|
|
19
|
+
/** Missing file → empty config. Present but invalid → loud failure. */
|
|
20
|
+
export declare function loadRouteConfig(appDir: string): Promise<RouteConfig>;
|
|
21
|
+
/**
|
|
22
|
+
* Sample values may contain `{n}`, which the load phase replaces with a
|
|
23
|
+
* per-request counter. Leaks keyed by URL (route caches, LRUs, bot traffic
|
|
24
|
+
* with unique tails) are invisible when every request hits the same path.
|
|
25
|
+
*/
|
|
26
|
+
export declare const UNIQUE_MARKER = "{n}";
|
|
27
|
+
/**
|
|
28
|
+
* Substitutes sample values into a dynamic route template and returns a
|
|
29
|
+
* URL-safe request path (or null when a param has no configured value;
|
|
30
|
+
* optional catch-alls without a value are dropped instead).
|
|
31
|
+
*
|
|
32
|
+
* Percent-encoding is not cosmetic: an unencoded `#` silently truncated the
|
|
33
|
+
* path (a different route got measured) and unencoded non-ASCII produced
|
|
34
|
+
* requests that never completed — both verified against a real server.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveRoutePath(routeTemplate: string, config: RouteConfig): string | null;
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { type AttributedDiff } from "./attribution.js";
|
|
2
|
+
import { type ConfidenceReport } from "./confidence.js";
|
|
3
|
+
import type { HeapSample } from "./control-server.js";
|
|
4
|
+
import { type MeasurementEnvironment } from "./environment.js";
|
|
5
|
+
import { diffSnapshotFiles, type HeapDiff } from "./heap-diff.js";
|
|
6
|
+
import { extractModuleRegistry } from "./module-registry.js";
|
|
7
|
+
import { runRitual, type LoadOutcome, type PhaseTiming, type SettleOutcome } from "./ritual.js";
|
|
8
|
+
import { readNextVersion, type MatchedSignature } from "./signatures.js";
|
|
9
|
+
import type { TrendResult } from "./trend.js";
|
|
10
|
+
export type RouteReport = {
|
|
11
|
+
route: string;
|
|
12
|
+
status: "skipped";
|
|
13
|
+
reason: string;
|
|
14
|
+
} | {
|
|
15
|
+
route: string;
|
|
16
|
+
status: "failed";
|
|
17
|
+
reason: string;
|
|
18
|
+
} | {
|
|
19
|
+
route: string;
|
|
20
|
+
status: "measured";
|
|
21
|
+
/** Concrete path requested (differs from `route` for dynamic templates). */
|
|
22
|
+
requestPath: string;
|
|
23
|
+
samples: number[];
|
|
24
|
+
/**
|
|
25
|
+
* Full post-GC memory samples. RSS matters as much as the heap: a
|
|
26
|
+
* process can hold gigabytes of RSS with a flat JS heap (allocator
|
|
27
|
+
* behaviour, external buffers), which is a different diagnosis and a
|
|
28
|
+
* different fix than a heap leak.
|
|
29
|
+
*/
|
|
30
|
+
memorySamples: HeapSample[];
|
|
31
|
+
/** RSS growth per 1000 requests, computed like the heap figure. */
|
|
32
|
+
rssPer1000Requests: number;
|
|
33
|
+
/** Wall-clock per phase — explains where a long run spent its time. */
|
|
34
|
+
timings: PhaseTiming[];
|
|
35
|
+
/** What each load phase actually did (sent, 2xx, abandoned…). */
|
|
36
|
+
loadOutcomes: LoadOutcome[];
|
|
37
|
+
/** Whether the heap held still before each sample. */
|
|
38
|
+
settleOutcomes: SettleOutcome[];
|
|
39
|
+
/**
|
|
40
|
+
* Audit of the measurement against its own evidence. `trend` stays as
|
|
41
|
+
* measured; when the evidence does not support it, `confidence`
|
|
42
|
+
* carries the verdict that does — see `effectiveVerdict`.
|
|
43
|
+
*/
|
|
44
|
+
confidence: ConfidenceReport;
|
|
45
|
+
trend: TrendResult;
|
|
46
|
+
growthPer1000Requests: number;
|
|
47
|
+
baselineSnapshot: string;
|
|
48
|
+
afterSnapshot: string;
|
|
49
|
+
/** Null when the verdict is stable and diffAll was not requested. */
|
|
50
|
+
diff: HeapDiff | null;
|
|
51
|
+
/** Null when there is no diff or no module registry. */
|
|
52
|
+
attribution: AttributedDiff | null;
|
|
53
|
+
signatures: MatchedSignature[];
|
|
54
|
+
};
|
|
55
|
+
export type MeasuredRoute = Extract<RouteReport, {
|
|
56
|
+
status: "measured";
|
|
57
|
+
}>;
|
|
58
|
+
export type RunParameters = {
|
|
59
|
+
warmupRequests: number;
|
|
60
|
+
loadRequests: number;
|
|
61
|
+
connections: number;
|
|
62
|
+
cycles: number;
|
|
63
|
+
idleMs: number;
|
|
64
|
+
};
|
|
65
|
+
export type RunReport = {
|
|
66
|
+
appDir: string;
|
|
67
|
+
startedAt: string;
|
|
68
|
+
workDir: string;
|
|
69
|
+
environment: MeasurementEnvironment;
|
|
70
|
+
parameters: RunParameters;
|
|
71
|
+
routes: RouteReport[];
|
|
72
|
+
bundle: {
|
|
73
|
+
htmlReport: string;
|
|
74
|
+
issues: Array<{
|
|
75
|
+
route: string;
|
|
76
|
+
file: string;
|
|
77
|
+
}>;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
export type RunOptions = {
|
|
81
|
+
appDir: string;
|
|
82
|
+
/** Built bootstrap module for `--import` into measured processes. */
|
|
83
|
+
bootstrapPath: string;
|
|
84
|
+
/** Parent output directory. Default: `<appDir>/.next-leak`. */
|
|
85
|
+
outputDir?: string;
|
|
86
|
+
warmupRequests?: number;
|
|
87
|
+
loadRequests?: number;
|
|
88
|
+
connections?: number;
|
|
89
|
+
cycles?: number;
|
|
90
|
+
idleMs?: number;
|
|
91
|
+
/** Also diff routes with a stable verdict. Default false: diffs are slow. */
|
|
92
|
+
diffAll?: boolean;
|
|
93
|
+
/** Only measure routes matching these templates or prefixes. */
|
|
94
|
+
routeFilter?: string[];
|
|
95
|
+
/** Abort between phases; remaining routes are reported as interrupted. */
|
|
96
|
+
signal?: AbortSignal;
|
|
97
|
+
onProgress?: (message: string) => void;
|
|
98
|
+
};
|
|
99
|
+
export declare function estimateRunSeconds(routeCount: number, parameters: RunParameters): number;
|
|
100
|
+
export declare function formatDuration(seconds: number): string;
|
|
101
|
+
export type RunnerDeps = {
|
|
102
|
+
ritual: typeof runRitual;
|
|
103
|
+
diff: typeof diffSnapshotFiles;
|
|
104
|
+
freePort: () => Promise<number>;
|
|
105
|
+
registry: typeof extractModuleRegistry;
|
|
106
|
+
nextVersion: typeof readNextVersion;
|
|
107
|
+
};
|
|
108
|
+
export declare function freePort(): Promise<number>;
|
|
109
|
+
/**
|
|
110
|
+
* Filesystem-safe label for a route. Distinct routes MUST get distinct slugs:
|
|
111
|
+
* `/a/b` and `/a_b` used to collapse onto the same `ISSUE-a_b.md`, and any
|
|
112
|
+
* all-non-ASCII path (`/ñ`) became "root", colliding with `/`. A short digest
|
|
113
|
+
* disambiguates whenever the sanitized form loses information.
|
|
114
|
+
*/
|
|
115
|
+
/**
|
|
116
|
+
* Mean RSS growth per cycle, excluding the warm-up cycle exactly like the
|
|
117
|
+
* heap verdict does. Reported alongside the heap so a flat heap with growing
|
|
118
|
+
* RSS is visible instead of invisible.
|
|
119
|
+
*/
|
|
120
|
+
export declare function rssTrend(memorySamples: readonly HeapSample[]): number;
|
|
121
|
+
export declare function routeSlug(route: string): string;
|
|
122
|
+
/**
|
|
123
|
+
* Full measurement run: validate the target, discover routes, run the ritual
|
|
124
|
+
* per route in a fresh process, diff snapshots for non-stable verdicts, and
|
|
125
|
+
* persist `run.json` plus raw snapshots under the work directory.
|
|
126
|
+
*/
|
|
127
|
+
export declare function runMeasurement(options: RunOptions, deps?: RunnerDeps): Promise<RunReport>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { HeapDiff } from "./heap-diff.js";
|
|
3
|
+
declare const signatureSchema: z.ZodObject<{
|
|
4
|
+
id: z.ZodString;
|
|
5
|
+
title: z.ZodString;
|
|
6
|
+
nextRange: z.ZodString;
|
|
7
|
+
cause: z.ZodString;
|
|
8
|
+
issue: z.ZodURL;
|
|
9
|
+
historical: z.ZodBoolean;
|
|
10
|
+
match: z.ZodObject<{
|
|
11
|
+
chainIncludes: z.ZodOptional<z.ZodString>;
|
|
12
|
+
typeDeltaAbove: z.ZodOptional<z.ZodObject<{
|
|
13
|
+
type: z.ZodString;
|
|
14
|
+
bytes: z.ZodNumber;
|
|
15
|
+
}, z.core.$strip>>;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
}, z.core.$strict>;
|
|
18
|
+
export type Signature = z.infer<typeof signatureSchema>;
|
|
19
|
+
export declare function loadSignatures(raw?: unknown[]): Signature[];
|
|
20
|
+
export type MatchedSignature = Pick<Signature, "id" | "title" | "cause" | "issue" | "historical">;
|
|
21
|
+
/**
|
|
22
|
+
* Matches signatures against a diff, gated by the measured app's Next.js
|
|
23
|
+
* version. Unknown version or no matching range → no annotations, no errors.
|
|
24
|
+
*/
|
|
25
|
+
export declare function matchSignatures(diff: HeapDiff, nextVersion: string | null, signatures?: Signature[]): MatchedSignature[];
|
|
26
|
+
/** Reads the measured app's Next.js version from its standalone bundle. */
|
|
27
|
+
export declare function readNextVersion(appDir: string): Promise<string | null>;
|
|
28
|
+
export {};
|
package/dist/target.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type AppPathsManifest, type PagesManifest, type RoutesManifest } from "./manifests.js";
|
|
2
|
+
export type TargetErrorCode = "NO_BUILD" | "NO_STANDALONE" | "BAD_MANIFEST";
|
|
3
|
+
export declare class TargetError extends Error {
|
|
4
|
+
readonly code: TargetErrorCode;
|
|
5
|
+
constructor(code: TargetErrorCode, message: string);
|
|
6
|
+
}
|
|
7
|
+
export type ValidatedTarget = {
|
|
8
|
+
appDir: string;
|
|
9
|
+
/** Absolute path to `.next/standalone/server.js`. */
|
|
10
|
+
standaloneServer: string;
|
|
11
|
+
appPaths: AppPathsManifest;
|
|
12
|
+
pages: PagesManifest;
|
|
13
|
+
/** Absent on builds that do not emit it; nothing downstream reads it. */
|
|
14
|
+
routes: RoutesManifest | undefined;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Validates that `appDir` contains a production build with
|
|
18
|
+
* `output: "standalone"` and readable route manifests. Fails fast with an
|
|
19
|
+
* actionable message otherwise.
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateTarget(appDir: string): Promise<ValidatedTarget>;
|
package/dist/trend.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type TrendVerdict = "leak" | "stable" | "inconclusive";
|
|
2
|
+
export type TrendResult = {
|
|
3
|
+
verdict: TrendVerdict;
|
|
4
|
+
/** Mean retained-heap growth per cycle (bytes) over the analyzed window. */
|
|
5
|
+
growthPerCycle: number;
|
|
6
|
+
/** Per-cycle deltas (bytes) after dropping the warm-up cycle. */
|
|
7
|
+
deltas: number[];
|
|
8
|
+
/**
|
|
9
|
+
* Which memory produced the verdict. A Node process can leak in three
|
|
10
|
+
* places and only one of them is the JS heap: `external`/`arrayBuffers`
|
|
11
|
+
* hold fetch bodies, streams and Buffers, and can OOM a process while the
|
|
12
|
+
* heap stays flat (vercel/next.js#92287 reports 4.3 GB of arrayBuffers
|
|
13
|
+
* against a healthy heap). Reporting the heap alone would call that
|
|
14
|
+
* "stable".
|
|
15
|
+
*/
|
|
16
|
+
source?: "heap" | "external";
|
|
17
|
+
};
|
|
18
|
+
export type TrendOptions = {
|
|
19
|
+
/** Minimum per-cycle growth (bytes) considered leak-like. Default: 256 KiB. */
|
|
20
|
+
minGrowthPerCycle?: number;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Classifies a series of post-GC retained-heap samples — baseline first, then
|
|
24
|
+
* one sample per load cycle — as leaking or stable.
|
|
25
|
+
*
|
|
26
|
+
* The baseline→cycle-1 delta is excluded from the verdict: measurements on
|
|
27
|
+
* healthy routes show it is dominated by one-time engine warm-up (JIT code,
|
|
28
|
+
* lazy caches) even after an HTTP-level warm-up phase. A leak must keep
|
|
29
|
+
* growing across the remaining cycles; warm-up flattens out.
|
|
30
|
+
*/
|
|
31
|
+
export declare function classifyTrend(samples: readonly number[], options?: TrendOptions): TrendResult;
|
|
32
|
+
/**
|
|
33
|
+
* Verdict over both the JS heap and external memory, taking the worse of the
|
|
34
|
+
* two. External memory (`external`, which includes `arrayBuffers`) holds
|
|
35
|
+
* fetch bodies, streams and Buffers; a process can be killed by OOM with a
|
|
36
|
+
* perfectly flat heap, so judging the heap alone answers the wrong question.
|
|
37
|
+
*/
|
|
38
|
+
export declare function classifyMemoryTrend(heapSamples: readonly number[], externalSamples: readonly number[], options?: TrendOptions): TrendResult;
|
package/dist/vlq.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Decodes one line of base64-VLQ segments from a sourcemap `mappings` string. */
|
|
2
|
+
export declare function decodeVlqLine(line: string): number[][];
|
|
3
|
+
export type LineMapping = {
|
|
4
|
+
/** Generated column → source index, sorted by column. */
|
|
5
|
+
entries: Array<{
|
|
6
|
+
column: number;
|
|
7
|
+
sourceIndex: number;
|
|
8
|
+
}>;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Decodes a sourcemap `mappings` string into per-line column → source-index
|
|
12
|
+
* tables. Only the fields attribution needs; name/line positions are dropped.
|
|
13
|
+
*/
|
|
14
|
+
export declare function decodeMappings(mappings: string): LineMapping[];
|
|
15
|
+
/** Returns the source index active at (line, column), or undefined. */
|
|
16
|
+
export declare function sourceIndexAt(lines: LineMapping[], line: number, column: number): number | undefined;
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "next-leak",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Find out whether your Next.js app actually leaks memory — how much, on which route, and whose fault it is.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nextjs",
|
|
7
|
+
"memory-leak",
|
|
8
|
+
"heap-snapshot",
|
|
9
|
+
"diagnostics",
|
|
10
|
+
"cli"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Xabier Lameiro <xabier.lameiro@gmail.com>",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"next-leak": "dist/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22"
|
|
25
|
+
},
|
|
26
|
+
"packageManager": "pnpm@10.20.0",
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup && tsc -p tsconfig.build.json",
|
|
29
|
+
"dev": "tsup --watch",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"prepublishOnly": "npm run typecheck && npm run test && npm run build",
|
|
34
|
+
"pack:smoke": "node scripts/pack-smoke.mjs",
|
|
35
|
+
"test:mutation": "stryker run",
|
|
36
|
+
"prepare": "husky >/dev/null 2>&1 || true",
|
|
37
|
+
"release": "release-it"
|
|
38
|
+
},
|
|
39
|
+
"pnpm": {
|
|
40
|
+
"overrides": {
|
|
41
|
+
"uuid": ">=11.1.1"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@commitlint/cli": "^21.2.1",
|
|
46
|
+
"@commitlint/config-conventional": "^21.2.0",
|
|
47
|
+
"@memlab/core": "^2.0.4",
|
|
48
|
+
"@memlab/heap-analysis": "^2.0.4",
|
|
49
|
+
"@release-it/conventional-changelog": "^11.0.1",
|
|
50
|
+
"@stryker-mutator/core": "^9.6.1",
|
|
51
|
+
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
52
|
+
"@types/autocannon": "^7.12.7",
|
|
53
|
+
"@types/node": "^26.1.1",
|
|
54
|
+
"@types/semver": "^7.7.1",
|
|
55
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
56
|
+
"husky": "^9.1.7",
|
|
57
|
+
"release-it": "^20.2.1",
|
|
58
|
+
"tsup": "^8.5.1",
|
|
59
|
+
"typescript": "^7.0.2",
|
|
60
|
+
"vitest": "^4.1.10"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"autocannon": "^8.0.0",
|
|
64
|
+
"semver": "^7.8.5",
|
|
65
|
+
"zod": "^4.4.3"
|
|
66
|
+
},
|
|
67
|
+
"repository": {
|
|
68
|
+
"type": "git",
|
|
69
|
+
"url": "git+https://github.com/xabierlameiro/next-leak.git"
|
|
70
|
+
},
|
|
71
|
+
"homepage": "https://github.com/xabierlameiro/next-leak#readme",
|
|
72
|
+
"bugs": {
|
|
73
|
+
"url": "https://github.com/xabierlameiro/next-leak/issues"
|
|
74
|
+
}
|
|
75
|
+
}
|