next-leak 0.7.0 → 0.8.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 +11 -0
- package/dist/build-attribution.d.ts +33 -0
- package/dist/build-report.d.ts +2 -1
- package/dist/build-run.d.ts +23 -0
- package/dist/build-snapshot.d.ts +55 -0
- package/dist/{chunk-BXDUPN6Z.js → chunk-LFGWGM5Z.js} +1442 -1426
- package/dist/cli-args.d.ts +3 -0
- package/dist/cli.js +312 -19
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -91969,1602 +91969,1618 @@ var require_autocannon = __commonJS({
|
|
|
91969
91969
|
}
|
|
91970
91970
|
});
|
|
91971
91971
|
|
|
91972
|
-
// src/
|
|
91973
|
-
|
|
91974
|
-
|
|
91975
|
-
|
|
91976
|
-
|
|
91977
|
-
|
|
91978
|
-
|
|
91979
|
-
});
|
|
91980
|
-
var routesManifestSchema = z.looseObject({
|
|
91981
|
-
version: z.number(),
|
|
91982
|
-
basePath: z.string(),
|
|
91983
|
-
staticRoutes: z.array(routeEntrySchema),
|
|
91984
|
-
dynamicRoutes: z.array(routeEntrySchema)
|
|
91985
|
-
});
|
|
91986
|
-
var prerenderManifestSchema = z.looseObject({
|
|
91987
|
-
routes: z.record(
|
|
91988
|
-
z.string(),
|
|
91989
|
-
z.looseObject({
|
|
91990
|
-
// `false` on a route that is prerendered but never revalidates.
|
|
91991
|
-
initialRevalidateSeconds: z.union([z.number(), z.literal(false)]).optional(),
|
|
91992
|
-
srcRoute: z.string().nullable().optional()
|
|
91993
|
-
})
|
|
91994
|
-
).optional(),
|
|
91995
|
-
preview: z.looseObject({ previewModeId: z.string() }).optional()
|
|
91996
|
-
});
|
|
91997
|
-
var INTERNAL_PATHS = /* @__PURE__ */ new Set(["/_global-error", "/_not-found"]);
|
|
91998
|
-
var INTERCEPTING_SEGMENT = /^(\(\.{1,3}\))+/;
|
|
91999
|
-
var STATIC_ASSET_PATHS = /* @__PURE__ */ new Set(["/favicon.ico"]);
|
|
92000
|
-
function unaddressableReason(routePath) {
|
|
92001
|
-
if (routePath.split("/").some((segment) => INTERCEPTING_SEGMENT.test(segment))) {
|
|
92002
|
-
return "intercepting route \u2014 only reachable via client navigation";
|
|
91972
|
+
// src/attribution.ts
|
|
91973
|
+
var UNATTRIBUTED = { owner: "unattributed", source: null, packageName: null };
|
|
91974
|
+
function classifySource(rawSource) {
|
|
91975
|
+
let source = rawSource.replace(/^turbopack:\/+(?=\[)/, "");
|
|
91976
|
+
try {
|
|
91977
|
+
source = decodeURIComponent(source);
|
|
91978
|
+
} catch {
|
|
92003
91979
|
}
|
|
92004
|
-
|
|
92005
|
-
|
|
91980
|
+
const nodeModulesSplit = source.split("/node_modules/");
|
|
91981
|
+
if (nodeModulesSplit.length > 1) {
|
|
91982
|
+
const tail = nodeModulesSplit.at(-1) ?? "";
|
|
91983
|
+
const segments = tail.split("/");
|
|
91984
|
+
const packageName = (segments[0]?.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0]) ?? null;
|
|
91985
|
+
if (packageName === "next" || packageName === "react" || packageName === "react-dom") {
|
|
91986
|
+
return { owner: "framework", source: null, packageName };
|
|
91987
|
+
}
|
|
91988
|
+
return { owner: "dependency", source: null, packageName };
|
|
92006
91989
|
}
|
|
92007
|
-
|
|
91990
|
+
if (source.startsWith("[project]/")) {
|
|
91991
|
+
return { owner: "app", source: source.slice("[project]/".length), packageName: null };
|
|
91992
|
+
}
|
|
91993
|
+
const relative = source.replace(/^(\.\.\/)+/, "");
|
|
91994
|
+
if (relative !== source && !relative.startsWith("[")) {
|
|
91995
|
+
return { owner: "app", source: relative, packageName: null };
|
|
91996
|
+
}
|
|
91997
|
+
return { owner: "framework", source: null, packageName: null };
|
|
92008
91998
|
}
|
|
92009
|
-
|
|
92010
|
-
|
|
92011
|
-
|
|
92012
|
-
|
|
92013
|
-
|
|
92014
|
-
|
|
92015
|
-
|
|
92016
|
-
|
|
92017
|
-
|
|
92018
|
-
|
|
92019
|
-
|
|
91999
|
+
var OWNER_PRIORITY = {
|
|
92000
|
+
app: 0,
|
|
92001
|
+
dependency: 1,
|
|
92002
|
+
framework: 2,
|
|
92003
|
+
unattributed: 3
|
|
92004
|
+
};
|
|
92005
|
+
var FRAMEWORK_CHAIN_MARKERS = [
|
|
92006
|
+
["fsChecker", "route filesystem checker"],
|
|
92007
|
+
["getDynamicRoutes", "dynamic route matcher"],
|
|
92008
|
+
["NextNodeServer", "Next server"],
|
|
92009
|
+
["AppPageRouteModule", "App Router page module"],
|
|
92010
|
+
["AppRouteRouteModule", "App Router route handler module"],
|
|
92011
|
+
["incrementalCache", "incremental cache"],
|
|
92012
|
+
["IncrementalCache", "incremental cache"],
|
|
92013
|
+
["ReactServer", "React Server Components renderer"],
|
|
92014
|
+
["loadManifests", "manifest loader"],
|
|
92015
|
+
["renderToHTML", "HTML renderer"]
|
|
92016
|
+
];
|
|
92017
|
+
function classifyByChain(retainerChain2) {
|
|
92018
|
+
for (const [marker, component] of FRAMEWORK_CHAIN_MARKERS) {
|
|
92019
|
+
if (retainerChain2.includes(marker)) {
|
|
92020
|
+
return { owner: "framework", source: null, packageName: `next (${component})` };
|
|
92021
|
+
}
|
|
92020
92022
|
}
|
|
92021
|
-
|
|
92022
|
-
return { path: `/${segments.join("/")}`, kind };
|
|
92023
|
+
return null;
|
|
92023
92024
|
}
|
|
92024
|
-
|
|
92025
|
-
|
|
92026
|
-
const
|
|
92027
|
-
|
|
92028
|
-
if (
|
|
92025
|
+
function attributeFinding(finding, registry) {
|
|
92026
|
+
let best = UNATTRIBUTED;
|
|
92027
|
+
for (const id of finding.moduleIds) {
|
|
92028
|
+
const source = registry.get(id);
|
|
92029
|
+
if (source === void 0) {
|
|
92029
92030
|
continue;
|
|
92030
92031
|
}
|
|
92031
|
-
|
|
92032
|
-
|
|
92033
|
-
|
|
92034
|
-
|
|
92035
|
-
});
|
|
92032
|
+
const candidate = classifySource(source);
|
|
92033
|
+
if (OWNER_PRIORITY[candidate.owner] < OWNER_PRIORITY[best.owner]) {
|
|
92034
|
+
best = candidate;
|
|
92035
|
+
}
|
|
92036
92036
|
}
|
|
92037
|
-
|
|
92037
|
+
if (best.owner === "unattributed") {
|
|
92038
|
+
return classifyByChain(finding.retainerChain ?? "") ?? UNATTRIBUTED;
|
|
92039
|
+
}
|
|
92040
|
+
return best;
|
|
92038
92041
|
}
|
|
92039
|
-
function
|
|
92040
|
-
const
|
|
92041
|
-
|
|
92042
|
-
|
|
92043
|
-
|
|
92042
|
+
function attributeDiff(diff, registry) {
|
|
92043
|
+
const all = [...diff.grownNodes, ...diff.newNodes];
|
|
92044
|
+
const findings = all.map((finding) => attributeFinding(finding, registry));
|
|
92045
|
+
const byGroup = /* @__PURE__ */ new Map();
|
|
92046
|
+
let attributedBytes = 0;
|
|
92047
|
+
for (const [index, attribution] of findings.entries()) {
|
|
92048
|
+
if (attribution.owner === "unattributed") {
|
|
92044
92049
|
continue;
|
|
92045
92050
|
}
|
|
92046
|
-
const
|
|
92047
|
-
|
|
92048
|
-
|
|
92049
|
-
|
|
92050
|
-
|
|
92051
|
-
|
|
92052
|
-
});
|
|
92051
|
+
const bytes = all[index]?.retainedBytes ?? 0;
|
|
92052
|
+
attributedBytes += bytes;
|
|
92053
|
+
const key = `${attribution.owner}|${attribution.source ?? ""}|${attribution.packageName ?? ""}`;
|
|
92054
|
+
const group = byGroup.get(key) ?? { attribution, bytes: 0 };
|
|
92055
|
+
group.bytes += bytes;
|
|
92056
|
+
byGroup.set(key, group);
|
|
92053
92057
|
}
|
|
92054
|
-
|
|
92058
|
+
let winner = null;
|
|
92059
|
+
for (const group of byGroup.values()) {
|
|
92060
|
+
if (winner === null || group.bytes > winner.bytes) {
|
|
92061
|
+
winner = group;
|
|
92062
|
+
}
|
|
92063
|
+
}
|
|
92064
|
+
const route = winner === null || attributedBytes === 0 ? { ...UNATTRIBUTED, dominance: 0 } : { ...winner.attribution, dominance: winner.bytes / attributedBytes };
|
|
92065
|
+
return { findings, route };
|
|
92055
92066
|
}
|
|
92056
92067
|
|
|
92057
|
-
// src/
|
|
92058
|
-
|
|
92059
|
-
|
|
92060
|
-
var
|
|
92061
|
-
|
|
92062
|
-
|
|
92063
|
-
|
|
92064
|
-
|
|
92065
|
-
|
|
92066
|
-
|
|
92068
|
+
// src/heap-diff.ts
|
|
92069
|
+
var import_core = __toESM(require_dist(), 1);
|
|
92070
|
+
var import_heap_analysis = __toESM(require_dist4(), 1);
|
|
92071
|
+
var DEFAULTS = {
|
|
92072
|
+
minTypeDeltaBytes: 20 * 1024,
|
|
92073
|
+
grownThresholdBytes: 100 * 1024,
|
|
92074
|
+
newThresholdBytes: 2 * 1024,
|
|
92075
|
+
bigRetainedFloorBytes: 50 * 1024,
|
|
92076
|
+
maxFindings: 10,
|
|
92077
|
+
chainDepth: 7
|
|
92067
92078
|
};
|
|
92068
|
-
|
|
92069
|
-
|
|
92070
|
-
|
|
92071
|
-
|
|
92072
|
-
|
|
92073
|
-
|
|
92074
|
-
}
|
|
92075
|
-
|
|
92076
|
-
|
|
92077
|
-
|
|
92078
|
-
|
|
92079
|
-
|
|
92080
|
-
|
|
92081
|
-
|
|
92082
|
-
|
|
92083
|
-
`Could not read ${file}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
92079
|
+
function summarizeBaseline(heap, options = {}) {
|
|
92080
|
+
const resolved = { ...DEFAULTS, ...options };
|
|
92081
|
+
const summary = {
|
|
92082
|
+
nodeIds: /* @__PURE__ */ new Set(),
|
|
92083
|
+
bigRetained: /* @__PURE__ */ new Map(),
|
|
92084
|
+
typeSelfSizes: /* @__PURE__ */ new Map()
|
|
92085
|
+
};
|
|
92086
|
+
heap.nodes.forEach((node) => {
|
|
92087
|
+
summary.nodeIds.add(node.id);
|
|
92088
|
+
if (node.retainedSize >= resolved.bigRetainedFloorBytes) {
|
|
92089
|
+
summary.bigRetained.set(node.id, node.retainedSize);
|
|
92090
|
+
}
|
|
92091
|
+
summary.typeSelfSizes.set(
|
|
92092
|
+
node.type,
|
|
92093
|
+
(summary.typeSelfSizes.get(node.type) ?? 0) + node.self_size
|
|
92084
92094
|
);
|
|
92085
|
-
}
|
|
92086
|
-
|
|
92087
|
-
|
|
92088
|
-
|
|
92089
|
-
|
|
92090
|
-
|
|
92091
|
-
|
|
92095
|
+
});
|
|
92096
|
+
return summary;
|
|
92097
|
+
}
|
|
92098
|
+
function truncateLabel(value, max2 = 60) {
|
|
92099
|
+
return value.replaceAll("\n", " ").slice(0, max2);
|
|
92100
|
+
}
|
|
92101
|
+
function walkChain(node, depth) {
|
|
92102
|
+
const parts = [];
|
|
92103
|
+
const nodes = [node];
|
|
92104
|
+
let current = node;
|
|
92105
|
+
const seen = /* @__PURE__ */ new Set([node.id]);
|
|
92106
|
+
for (let i = 0; i < depth; i += 1) {
|
|
92107
|
+
const referrers = current.referrers;
|
|
92108
|
+
if (referrers.length === 0) {
|
|
92109
|
+
break;
|
|
92110
|
+
}
|
|
92111
|
+
const edge = referrers.find(
|
|
92112
|
+
(candidate) => !seen.has(candidate.fromNode.id) && candidate.fromNode.type !== "synthetic" && candidate.type !== "weak"
|
|
92113
|
+
) ?? referrers[0];
|
|
92114
|
+
if (edge === void 0 || seen.has(edge.fromNode.id)) {
|
|
92115
|
+
break;
|
|
92116
|
+
}
|
|
92117
|
+
current = edge.fromNode;
|
|
92118
|
+
seen.add(current.id);
|
|
92119
|
+
nodes.push(current);
|
|
92120
|
+
parts.push(
|
|
92121
|
+
`${truncateLabel(current.name)}#${current.type}[.${truncateLabel(
|
|
92122
|
+
String(edge.name_or_index),
|
|
92123
|
+
40
|
|
92124
|
+
)}]`
|
|
92092
92125
|
);
|
|
92093
92126
|
}
|
|
92127
|
+
return { parts, nodes };
|
|
92094
92128
|
}
|
|
92095
|
-
|
|
92096
|
-
|
|
92097
|
-
|
|
92098
|
-
|
|
92099
|
-
|
|
92100
|
-
|
|
92129
|
+
function retainerChain(node, depth) {
|
|
92130
|
+
return walkChain(node, depth).parts.join(" <- ");
|
|
92131
|
+
}
|
|
92132
|
+
function collectModuleIds(chainNodes) {
|
|
92133
|
+
const ids = [];
|
|
92134
|
+
for (const node of chainNodes) {
|
|
92135
|
+
const isModuleInstance = (node.references ?? []).some(
|
|
92136
|
+
(edge) => edge.name_or_index === "namespaceObject" || edge.name_or_index === "exports"
|
|
92101
92137
|
);
|
|
92138
|
+
if (!isModuleInstance) {
|
|
92139
|
+
continue;
|
|
92140
|
+
}
|
|
92141
|
+
for (const edge of node.referrers) {
|
|
92142
|
+
if (edge.type !== "element") {
|
|
92143
|
+
continue;
|
|
92144
|
+
}
|
|
92145
|
+
const id = Number(edge.name_or_index);
|
|
92146
|
+
if (Number.isInteger(id) && !ids.includes(id)) {
|
|
92147
|
+
ids.push(id);
|
|
92148
|
+
}
|
|
92149
|
+
}
|
|
92102
92150
|
}
|
|
92103
|
-
|
|
92104
|
-
|
|
92105
|
-
|
|
92106
|
-
|
|
92107
|
-
|
|
92108
|
-
|
|
92109
|
-
|
|
92110
|
-
|
|
92111
|
-
|
|
92112
|
-
|
|
92113
|
-
|
|
92114
|
-
|
|
92115
|
-
then rebuild: next build
|
|
92116
|
-
This only changes how the build is packaged \u2014 not how your app behaves.`
|
|
92117
|
-
);
|
|
92118
|
-
}
|
|
92119
|
-
const appPathsFile = path.join(nextDir, "server", "app-paths-manifest.json");
|
|
92120
|
-
const pagesFile = path.join(nextDir, "server", "pages-manifest.json");
|
|
92121
|
-
const appPaths = await exists(appPathsFile) ? await readManifest(appPathsFile, (raw) => appPathsManifestSchema.parse(raw)) : {};
|
|
92122
|
-
const pages = await exists(pagesFile) ? await readManifest(pagesFile, (raw) => pagesManifestSchema.parse(raw)) : {};
|
|
92123
|
-
if (Object.keys(appPaths).length === 0 && Object.keys(pages).length === 0) {
|
|
92124
|
-
throw new TargetError(
|
|
92125
|
-
"BAD_MANIFEST",
|
|
92126
|
-
`No routes found in ${nextDir}/server: neither app-paths-manifest.json nor pages-manifest.json is present. Rebuild the app with "next build".`
|
|
92151
|
+
return ids;
|
|
92152
|
+
}
|
|
92153
|
+
function diffAgainstBaseline(baseline, after, options = {}) {
|
|
92154
|
+
const resolved = { ...DEFAULTS, ...options };
|
|
92155
|
+
const afterTypeSelfSizes = /* @__PURE__ */ new Map();
|
|
92156
|
+
const grown = [];
|
|
92157
|
+
const fresh = [];
|
|
92158
|
+
after.nodes.forEach((node) => {
|
|
92159
|
+
afterTypeSelfSizes.set(
|
|
92160
|
+
node.type,
|
|
92161
|
+
(afterTypeSelfSizes.get(node.type) ?? 0) + node.self_size
|
|
92127
92162
|
);
|
|
92163
|
+
if (node.type === "synthetic") {
|
|
92164
|
+
return;
|
|
92165
|
+
}
|
|
92166
|
+
if (!baseline.nodeIds.has(node.id)) {
|
|
92167
|
+
if (node.retainedSize >= resolved.newThresholdBytes) {
|
|
92168
|
+
const chain = walkChain(node, resolved.chainDepth);
|
|
92169
|
+
fresh.push({
|
|
92170
|
+
kind: "new",
|
|
92171
|
+
nodeType: node.type,
|
|
92172
|
+
name: truncateLabel(node.name),
|
|
92173
|
+
retainedBytes: node.retainedSize,
|
|
92174
|
+
retainerChain: chain.parts.join(" <- "),
|
|
92175
|
+
moduleIds: collectModuleIds(chain.nodes)
|
|
92176
|
+
});
|
|
92177
|
+
}
|
|
92178
|
+
return;
|
|
92179
|
+
}
|
|
92180
|
+
const before = baseline.bigRetained.get(node.id);
|
|
92181
|
+
if (before !== void 0 && node.retainedSize - before >= resolved.grownThresholdBytes) {
|
|
92182
|
+
const chain = walkChain(node, resolved.chainDepth);
|
|
92183
|
+
grown.push({
|
|
92184
|
+
kind: "grown",
|
|
92185
|
+
nodeType: node.type,
|
|
92186
|
+
name: truncateLabel(node.name),
|
|
92187
|
+
retainedBytes: node.retainedSize - before,
|
|
92188
|
+
retainerChain: chain.parts.join(" <- "),
|
|
92189
|
+
moduleIds: collectModuleIds(chain.nodes)
|
|
92190
|
+
});
|
|
92191
|
+
}
|
|
92192
|
+
});
|
|
92193
|
+
const typeDeltas = [];
|
|
92194
|
+
const allTypes = /* @__PURE__ */ new Set([...baseline.typeSelfSizes.keys(), ...afterTypeSelfSizes.keys()]);
|
|
92195
|
+
for (const type of allTypes) {
|
|
92196
|
+
const delta = (afterTypeSelfSizes.get(type) ?? 0) - (baseline.typeSelfSizes.get(type) ?? 0);
|
|
92197
|
+
if (Math.abs(delta) >= resolved.minTypeDeltaBytes) {
|
|
92198
|
+
typeDeltas.push({ type, deltaBytes: delta });
|
|
92199
|
+
}
|
|
92128
92200
|
}
|
|
92129
|
-
|
|
92130
|
-
|
|
92131
|
-
|
|
92132
|
-
const prerender = await exists(prerenderFile) ? await readManifest(prerenderFile, (raw) => prerenderManifestSchema.parse(raw)) : void 0;
|
|
92201
|
+
typeDeltas.sort((a, b) => b.deltaBytes - a.deltaBytes);
|
|
92202
|
+
grown.sort((a, b) => b.retainedBytes - a.retainedBytes);
|
|
92203
|
+
fresh.sort((a, b) => b.retainedBytes - a.retainedBytes);
|
|
92133
92204
|
return {
|
|
92134
|
-
|
|
92135
|
-
|
|
92136
|
-
|
|
92137
|
-
pages,
|
|
92138
|
-
routes,
|
|
92139
|
-
prerender
|
|
92205
|
+
typeDeltas,
|
|
92206
|
+
grownNodes: grown.slice(0, resolved.maxFindings),
|
|
92207
|
+
newNodes: fresh.slice(0, resolved.maxFindings)
|
|
92140
92208
|
};
|
|
92141
92209
|
}
|
|
92142
|
-
|
|
92143
|
-
// src/launcher.ts
|
|
92144
|
-
import { spawn } from "child_process";
|
|
92145
|
-
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
92146
|
-
import path2 from "path";
|
|
92147
|
-
import { pathToFileURL } from "url";
|
|
92148
|
-
import { z as z2 } from "zod";
|
|
92149
|
-
var controlFileSchema = z2.object({ port: z2.number(), pid: z2.number() });
|
|
92150
|
-
var DEFAULT_MAX_OLD_SPACE_MB = 512;
|
|
92151
|
-
var LaunchError = class extends Error {
|
|
92210
|
+
var SnapshotError = class extends Error {
|
|
92152
92211
|
constructor(message) {
|
|
92153
92212
|
super(message);
|
|
92154
|
-
this.name = "
|
|
92213
|
+
this.name = "SnapshotError";
|
|
92155
92214
|
}
|
|
92156
92215
|
};
|
|
92157
|
-
var
|
|
92158
|
-
var
|
|
92159
|
-
function
|
|
92160
|
-
|
|
92161
|
-
|
|
92216
|
+
var SNAPSHOT_HEAD = '{"snapshot"';
|
|
92217
|
+
var PROBE_BYTES = 512;
|
|
92218
|
+
async function assertReadableSnapshot(file) {
|
|
92219
|
+
const { open, stat } = await import("fs/promises");
|
|
92220
|
+
const { constants } = await import("buffer");
|
|
92221
|
+
let size;
|
|
92222
|
+
try {
|
|
92223
|
+
size = (await stat(file)).size;
|
|
92224
|
+
} catch {
|
|
92225
|
+
throw new SnapshotError(`heap snapshot is missing: ${file}`);
|
|
92162
92226
|
}
|
|
92163
|
-
|
|
92164
|
-
|
|
92165
|
-
activeChildren.add(child);
|
|
92166
|
-
child.once("exit", () => activeChildren.delete(child));
|
|
92167
|
-
}
|
|
92168
|
-
function unregisterChild(child) {
|
|
92169
|
-
activeChildren.delete(child);
|
|
92170
|
-
}
|
|
92171
|
-
function explainStartupFailure(stderr) {
|
|
92172
|
-
const missingModule = /Cannot find module '([^']+)'/.exec(stderr);
|
|
92173
|
-
if (missingModule !== null) {
|
|
92174
|
-
return `the standalone build is missing a dependency (${missingModule[1]}). This is a build problem, not a measurement one: \`node .next/standalone/server.js\` fails the same way on its own. Rebuild, or copy the missing package into .next/standalone/node_modules.`;
|
|
92227
|
+
if (size === 0) {
|
|
92228
|
+
throw new SnapshotError(`heap snapshot is empty: ${file}`);
|
|
92175
92229
|
}
|
|
92176
|
-
if (
|
|
92177
|
-
|
|
92230
|
+
if (size > constants.MAX_STRING_LENGTH) {
|
|
92231
|
+
throw new SnapshotError(
|
|
92232
|
+
`heap snapshot is ${(size / (1024 * 1024)).toFixed(0)} MB, past the ${(constants.MAX_STRING_LENGTH / (1024 * 1024)).toFixed(0)} MB a single string can hold, so it cannot be parsed: ${file}. The measurement itself is unaffected \u2014 only the snapshot diff, which names the retaining object, is unavailable. Lower --requests or --cycles to keep the heap smaller if you need it`
|
|
92233
|
+
);
|
|
92178
92234
|
}
|
|
92179
|
-
|
|
92180
|
-
|
|
92181
|
-
|
|
92182
|
-
|
|
92183
|
-
|
|
92184
|
-
|
|
92235
|
+
const handle = await open(file, "r");
|
|
92236
|
+
try {
|
|
92237
|
+
const head = Buffer.alloc(Math.min(PROBE_BYTES, size));
|
|
92238
|
+
await handle.read(head, 0, head.length, 0);
|
|
92239
|
+
if (!head.toString("utf8").trimStart().startsWith(SNAPSHOT_HEAD)) {
|
|
92240
|
+
throw new SnapshotError(
|
|
92241
|
+
`not a V8 heap snapshot (missing "snapshot" header): ${file}`
|
|
92242
|
+
);
|
|
92243
|
+
}
|
|
92244
|
+
const tail = Buffer.alloc(Math.min(PROBE_BYTES, size));
|
|
92245
|
+
await handle.read(tail, 0, tail.length, size - tail.length);
|
|
92246
|
+
if (!tail.toString("utf8").trimEnd().endsWith("}")) {
|
|
92247
|
+
throw new SnapshotError(
|
|
92248
|
+
`heap snapshot looks truncated (JSON does not close): ${file} \u2014 the measured process may have been killed while writing it`
|
|
92249
|
+
);
|
|
92250
|
+
}
|
|
92251
|
+
} finally {
|
|
92252
|
+
await handle.close();
|
|
92185
92253
|
}
|
|
92186
|
-
return `the measured process exited mid-run. ${explainStartupFailure(stderr)}`;
|
|
92187
92254
|
}
|
|
92188
|
-
var
|
|
92189
|
-
|
|
92190
|
-
|
|
92191
|
-
|
|
92192
|
-
|
|
92193
|
-
|
|
92255
|
+
var defaultLoader = async (file) => {
|
|
92256
|
+
await assertReadableSnapshot(file);
|
|
92257
|
+
import_core.config.muteConsole = true;
|
|
92258
|
+
return (0, import_heap_analysis.getFullHeapFromFile)(file);
|
|
92259
|
+
};
|
|
92260
|
+
async function diffSnapshotFiles(baselineFile, afterFile, options = {}, loadHeap = defaultLoader) {
|
|
92261
|
+
const summary = summarizeBaseline(await loadHeap(baselineFile), options);
|
|
92262
|
+
const after = await loadHeap(afterFile);
|
|
92263
|
+
return diffAgainstBaseline(summary, after, options);
|
|
92264
|
+
}
|
|
92265
|
+
|
|
92266
|
+
// src/vlq.ts
|
|
92267
|
+
var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
92268
|
+
var CHAR_TO_VALUE = new Map([...BASE64].map((char, index) => [char, index]));
|
|
92269
|
+
function decodeVlqSegment(raw) {
|
|
92270
|
+
const values = [];
|
|
92271
|
+
let value = 0;
|
|
92272
|
+
let shift = 0;
|
|
92273
|
+
for (const char of raw) {
|
|
92274
|
+
const digit = CHAR_TO_VALUE.get(char);
|
|
92275
|
+
if (digit === void 0) {
|
|
92276
|
+
throw new Error(`invalid VLQ character: ${char}`);
|
|
92194
92277
|
}
|
|
92195
|
-
|
|
92196
|
-
if (
|
|
92197
|
-
|
|
92278
|
+
value += (digit & 31) << shift;
|
|
92279
|
+
if ((digit & 32) !== 0) {
|
|
92280
|
+
shift += 5;
|
|
92281
|
+
} else {
|
|
92282
|
+
values.push((value & 1) === 1 ? -(value >>> 1) : value >>> 1);
|
|
92283
|
+
value = 0;
|
|
92284
|
+
shift = 0;
|
|
92198
92285
|
}
|
|
92199
|
-
|
|
92200
|
-
|
|
92286
|
+
}
|
|
92287
|
+
return values;
|
|
92288
|
+
}
|
|
92289
|
+
function decodeVlqLine(line) {
|
|
92290
|
+
const segments = [];
|
|
92291
|
+
for (const raw of line.split(",")) {
|
|
92292
|
+
if (raw !== "") {
|
|
92293
|
+
segments.push(decodeVlqSegment(raw));
|
|
92201
92294
|
}
|
|
92202
|
-
await sleep(100);
|
|
92203
92295
|
}
|
|
92296
|
+
return segments;
|
|
92204
92297
|
}
|
|
92205
|
-
|
|
92206
|
-
const
|
|
92207
|
-
|
|
92208
|
-
const
|
|
92209
|
-
|
|
92210
|
-
|
|
92211
|
-
|
|
92212
|
-
|
|
92213
|
-
|
|
92214
|
-
|
|
92215
|
-
|
|
92216
|
-
|
|
92217
|
-
|
|
92218
|
-
|
|
92219
|
-
|
|
92220
|
-
|
|
92221
|
-
...options.env,
|
|
92222
|
-
NODE_ENV: "production",
|
|
92223
|
-
PORT: String(options.appPort),
|
|
92224
|
-
HOSTNAME: hostname,
|
|
92225
|
-
NEXT_LEAK_DIR: options.workDir
|
|
92226
|
-
},
|
|
92227
|
-
stdio: ["ignore", "ignore", "pipe"]
|
|
92298
|
+
function decodeMappings(mappings) {
|
|
92299
|
+
const lines = [];
|
|
92300
|
+
let sourceIndex = 0;
|
|
92301
|
+
for (const line of mappings.split(";")) {
|
|
92302
|
+
const entries = [];
|
|
92303
|
+
let column = 0;
|
|
92304
|
+
for (const segment of decodeVlqLine(line)) {
|
|
92305
|
+
const columnDelta = segment[0];
|
|
92306
|
+
if (columnDelta === void 0) {
|
|
92307
|
+
continue;
|
|
92308
|
+
}
|
|
92309
|
+
column += columnDelta;
|
|
92310
|
+
if (segment.length >= 4) {
|
|
92311
|
+
sourceIndex += segment[1] ?? 0;
|
|
92312
|
+
entries.push({ column, sourceIndex });
|
|
92313
|
+
}
|
|
92228
92314
|
}
|
|
92229
|
-
|
|
92230
|
-
|
|
92231
|
-
|
|
92232
|
-
|
|
92233
|
-
|
|
92234
|
-
|
|
92235
|
-
|
|
92236
|
-
|
|
92237
|
-
|
|
92238
|
-
|
|
92239
|
-
const
|
|
92240
|
-
|
|
92241
|
-
|
|
92242
|
-
let exited = false;
|
|
92243
|
-
activeChildren.add(child);
|
|
92244
|
-
child.once("exit", () => {
|
|
92245
|
-
exited = true;
|
|
92246
|
-
activeChildren.delete(child);
|
|
92247
|
-
});
|
|
92248
|
-
const failed = () => exited ? `server exited before becoming ready. ${explainStartupFailure(stderrWindow())}` : void 0;
|
|
92249
|
-
try {
|
|
92250
|
-
const controlPort = await pollUntil(
|
|
92251
|
-
deadline,
|
|
92252
|
-
`control channel in ${options.workDir}`,
|
|
92253
|
-
// Several processes may announce a channel (clustered servers); accept
|
|
92254
|
-
// the first one that actually answers instead of trusting a filename.
|
|
92255
|
-
async () => {
|
|
92256
|
-
let entries;
|
|
92257
|
-
try {
|
|
92258
|
-
entries = (await readdir(options.workDir)).filter(
|
|
92259
|
-
(entry) => entry.startsWith("control-") && entry.endsWith(".json")
|
|
92260
|
-
);
|
|
92261
|
-
} catch {
|
|
92262
|
-
return void 0;
|
|
92263
|
-
}
|
|
92264
|
-
for (const entry of entries) {
|
|
92265
|
-
try {
|
|
92266
|
-
const parsed = controlFileSchema.parse(
|
|
92267
|
-
JSON.parse(await readFile2(path2.join(options.workDir, entry), "utf8"))
|
|
92268
|
-
);
|
|
92269
|
-
const response = await fetch(`http://127.0.0.1:${parsed.port}/gc`);
|
|
92270
|
-
if (response.ok) {
|
|
92271
|
-
return parsed.port;
|
|
92272
|
-
}
|
|
92273
|
-
} catch {
|
|
92274
|
-
continue;
|
|
92275
|
-
}
|
|
92276
|
-
}
|
|
92277
|
-
return void 0;
|
|
92278
|
-
},
|
|
92279
|
-
failed
|
|
92280
|
-
);
|
|
92281
|
-
await pollUntil(
|
|
92282
|
-
deadline,
|
|
92283
|
-
`app on ${hostname}:${options.appPort}`,
|
|
92284
|
-
async () => {
|
|
92285
|
-
try {
|
|
92286
|
-
await fetch(`http://${hostname}:${options.appPort}/`, { method: "HEAD" });
|
|
92287
|
-
return true;
|
|
92288
|
-
} catch {
|
|
92289
|
-
return void 0;
|
|
92290
|
-
}
|
|
92291
|
-
},
|
|
92292
|
-
failed
|
|
92293
|
-
);
|
|
92294
|
-
return {
|
|
92295
|
-
pid: child.pid ?? -1,
|
|
92296
|
-
appPort: options.appPort,
|
|
92297
|
-
controlPort,
|
|
92298
|
-
explainExit: () => exited ? explainRuntimeFailure(stderrWindow(), options.maxOldSpaceMb ?? DEFAULT_MAX_OLD_SPACE_MB) : null,
|
|
92299
|
-
close: async () => {
|
|
92300
|
-
if (exited) {
|
|
92301
|
-
return;
|
|
92302
|
-
}
|
|
92303
|
-
const gone = new Promise((resolve) => child.once("exit", () => resolve()));
|
|
92304
|
-
child.kill("SIGTERM");
|
|
92305
|
-
const killTimer = setTimeout(() => child.kill("SIGKILL"), 3e3);
|
|
92306
|
-
await gone;
|
|
92307
|
-
clearTimeout(killTimer);
|
|
92308
|
-
}
|
|
92309
|
-
};
|
|
92310
|
-
} catch (cause) {
|
|
92311
|
-
child.kill("SIGKILL");
|
|
92312
|
-
throw cause;
|
|
92313
|
-
}
|
|
92314
|
-
}
|
|
92315
|
-
|
|
92316
|
-
// src/cli-args.ts
|
|
92317
|
-
var BUILD_COMMAND = "build";
|
|
92318
|
-
var RUN_ONLY_FLAGS = [
|
|
92319
|
-
["routes", "--routes"],
|
|
92320
|
-
["cycles", "--cycles"],
|
|
92321
|
-
["requests", "--requests"],
|
|
92322
|
-
["connections", "--connections"],
|
|
92323
|
-
["idleSeconds", "--idle"],
|
|
92324
|
-
["warmupRequests", "--warmup"],
|
|
92325
|
-
["maxOldSpaceMb", "--max-old-space"],
|
|
92326
|
-
["quick", "--quick"],
|
|
92327
|
-
["noResolve", "--no-resolve"],
|
|
92328
|
-
["diffAll", "--diff-all"],
|
|
92329
|
-
["writeConfig", "--write-config"]
|
|
92330
|
-
];
|
|
92331
|
-
function runOnlyFlagUsed(options) {
|
|
92332
|
-
for (const [field, flag] of RUN_ONLY_FLAGS) {
|
|
92333
|
-
const value = options[field];
|
|
92334
|
-
if (value !== null && value !== false) {
|
|
92335
|
-
return flag;
|
|
92315
|
+
lines.push({ entries });
|
|
92316
|
+
}
|
|
92317
|
+
return lines;
|
|
92318
|
+
}
|
|
92319
|
+
function sourceIndexAt(lines, line, column) {
|
|
92320
|
+
const mapping = lines[line];
|
|
92321
|
+
if (mapping === void 0) {
|
|
92322
|
+
return void 0;
|
|
92323
|
+
}
|
|
92324
|
+
let found;
|
|
92325
|
+
for (const entry of mapping.entries) {
|
|
92326
|
+
if (entry.column > column) {
|
|
92327
|
+
break;
|
|
92336
92328
|
}
|
|
92329
|
+
found = entry.sourceIndex;
|
|
92337
92330
|
}
|
|
92338
|
-
return
|
|
92331
|
+
return found;
|
|
92339
92332
|
}
|
|
92340
|
-
var FLAGS = [
|
|
92341
|
-
{
|
|
92342
|
-
flag: "--routes",
|
|
92343
|
-
value: "list",
|
|
92344
|
-
argName: "<list>",
|
|
92345
|
-
help: "Only measure these routes \u2014 comma-separated templates or prefixes (e.g. /api,/dashboard)"
|
|
92346
|
-
},
|
|
92347
|
-
{ flag: "--cycles", value: "int", argName: "<n>", help: "Load cycles per route (default 4, minimum 3)" },
|
|
92348
|
-
{ flag: "--requests", value: "int", argName: "<n>", help: "Requests per cycle (default 5000)" },
|
|
92349
|
-
{ flag: "--connections", value: "int", argName: "<n>", help: "Concurrent connections (default 100)" },
|
|
92350
|
-
{ flag: "--idle", value: "int", argName: "<seconds>", help: "Idle seconds before each sample (default 30)" },
|
|
92351
|
-
{
|
|
92352
|
-
flag: "--warmup",
|
|
92353
|
-
value: "int",
|
|
92354
|
-
argName: "<n>",
|
|
92355
|
-
help: "Requests before the baseline snapshot (default 200) \u2014 lower it on apps that cache per request"
|
|
92356
|
-
},
|
|
92357
|
-
{
|
|
92358
|
-
flag: "--max-old-space",
|
|
92359
|
-
value: "int",
|
|
92360
|
-
argName: "<mb>",
|
|
92361
|
-
help: "Heap cap of each measured process (default 512) \u2014 raise it for apps whose working set is larger"
|
|
92362
|
-
},
|
|
92363
|
-
{
|
|
92364
|
-
flag: "--quick",
|
|
92365
|
-
value: "none",
|
|
92366
|
-
help: "Fast preset: 2000 requests x 4 cycles, 8s idle \u2014 same cycle count as the default, less traffic per cycle"
|
|
92367
|
-
},
|
|
92368
|
-
{ flag: "--diff-all", value: "none", help: "Diff snapshots for stable routes too (slow)" },
|
|
92369
|
-
{
|
|
92370
|
-
flag: "--no-resolve",
|
|
92371
|
-
value: "none",
|
|
92372
|
-
help: "Do not re-measure inconclusive routes with more cycles (default: re-measure once)"
|
|
92373
|
-
},
|
|
92374
|
-
{ flag: "--output", value: "string", argName: "<dir>", help: "Where to write runs (default <app-dir>/.next-leak)" },
|
|
92375
|
-
{
|
|
92376
|
-
flag: "--write-config",
|
|
92377
|
-
value: "none",
|
|
92378
|
-
help: "Write next-leak.config.json for the routes that need sample params, then exit (never overwrites)"
|
|
92379
|
-
},
|
|
92380
|
-
{ flag: "--help", alias: "-h", value: "none", help: "Show this help" },
|
|
92381
|
-
{ flag: "--version", alias: "-v", value: "none", help: "Print the version" }
|
|
92382
|
-
];
|
|
92383
|
-
function helpText(version2) {
|
|
92384
|
-
const rows = FLAGS.map((spec) => {
|
|
92385
|
-
const alias = spec.alias ? `${spec.alias}, ` : "";
|
|
92386
|
-
const argName = spec.argName ? ` ${spec.argName}` : "";
|
|
92387
|
-
const left = `${alias}${spec.flag}${argName}`;
|
|
92388
|
-
return ` ${left.padEnd(26)}${spec.help}`;
|
|
92389
|
-
}).join("\n");
|
|
92390
|
-
return `next-leak ${version2}
|
|
92391
|
-
|
|
92392
|
-
Find out whether your Next.js app actually leaks memory \u2014 how much, on which
|
|
92393
|
-
route, and whose fault it is.
|
|
92394
|
-
|
|
92395
|
-
Usage:
|
|
92396
|
-
next-leak <app-dir> [options] measure a built server under load
|
|
92397
|
-
next-leak build <app-dir> measure the memory of "next build"
|
|
92398
|
-
|
|
92399
|
-
The app must be built with output: "standalone" (next build). For each
|
|
92400
|
-
discovered route, next-leak boots a fresh instrumented process and runs:
|
|
92401
|
-
warm-up \u2192 GC \u2192 baseline snapshot \u2192 [load \u2192 idle \u2192 GC \u2192 sample] \xD7 cycles \u2192
|
|
92402
|
-
snapshot. Evidence (report.html, ISSUE drafts, raw snapshots, run.json) is
|
|
92403
|
-
written under the output directory.
|
|
92404
|
-
|
|
92405
|
-
The build command needs neither a previous build nor standalone output: it runs
|
|
92406
|
-
the build itself and samples the resident memory of each static-generation
|
|
92407
|
-
worker, which is where large sites run out of heap while prerendering. It takes
|
|
92408
|
-
--output only; the options below shape HTTP load and do not apply to it.
|
|
92409
|
-
|
|
92410
|
-
Options:
|
|
92411
|
-
${rows}
|
|
92412
92333
|
|
|
92413
|
-
|
|
92414
|
-
|
|
92415
|
-
|
|
92416
|
-
}
|
|
92417
|
-
var
|
|
92418
|
-
|
|
92419
|
-
|
|
92420
|
-
|
|
92421
|
-
|
|
92422
|
-
|
|
92423
|
-
|
|
92424
|
-
|
|
92425
|
-
|
|
92426
|
-
|
|
92427
|
-
|
|
92428
|
-
}
|
|
92429
|
-
|
|
92430
|
-
|
|
92431
|
-
function
|
|
92432
|
-
const
|
|
92433
|
-
if (
|
|
92434
|
-
return
|
|
92334
|
+
// src/module-registry.ts
|
|
92335
|
+
import { readdir, readFile } from "fs/promises";
|
|
92336
|
+
import path from "path";
|
|
92337
|
+
import { z } from "zod";
|
|
92338
|
+
var flatMapSchema = z.looseObject({
|
|
92339
|
+
version: z.number(),
|
|
92340
|
+
sources: z.array(z.string()),
|
|
92341
|
+
mappings: z.string()
|
|
92342
|
+
});
|
|
92343
|
+
var sectionedMapSchema = z.looseObject({
|
|
92344
|
+
version: z.number(),
|
|
92345
|
+
sections: z.array(
|
|
92346
|
+
z.looseObject({
|
|
92347
|
+
offset: z.looseObject({ line: z.number(), column: z.number() }),
|
|
92348
|
+
map: z.looseObject({ sources: z.array(z.string()), mappings: z.string() })
|
|
92349
|
+
})
|
|
92350
|
+
)
|
|
92351
|
+
});
|
|
92352
|
+
function normalizeSourceMap(raw) {
|
|
92353
|
+
const flat = flatMapSchema.safeParse(raw);
|
|
92354
|
+
if (flat.success) {
|
|
92355
|
+
return { sources: flat.data.sources, lines: decodeMappings(flat.data.mappings) };
|
|
92435
92356
|
}
|
|
92436
|
-
|
|
92437
|
-
|
|
92438
|
-
|
|
92439
|
-
function applyNumericFlag(flag, value, options) {
|
|
92440
|
-
const parsed = Number(value);
|
|
92441
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
92442
|
-
return flagError(`option "${flag}" needs a positive integer, got "${value}"`);
|
|
92357
|
+
const sectioned = sectionedMapSchema.safeParse(raw);
|
|
92358
|
+
if (!sectioned.success) {
|
|
92359
|
+
return null;
|
|
92443
92360
|
}
|
|
92444
|
-
const
|
|
92445
|
-
|
|
92446
|
-
|
|
92447
|
-
|
|
92448
|
-
);
|
|
92361
|
+
const sources = [];
|
|
92362
|
+
const lines = [];
|
|
92363
|
+
for (const section of sectioned.data.sections) {
|
|
92364
|
+
const sourceBase = sources.length;
|
|
92365
|
+
sources.push(...section.map.sources);
|
|
92366
|
+
for (const [lineIndex, mapping] of decodeMappings(section.map.mappings).entries()) {
|
|
92367
|
+
const targetLine = section.offset.line + lineIndex;
|
|
92368
|
+
const columnShift = lineIndex === 0 ? section.offset.column : 0;
|
|
92369
|
+
const target = lines[targetLine] ??= { entries: [] };
|
|
92370
|
+
for (const entry of mapping.entries) {
|
|
92371
|
+
target.entries.push({
|
|
92372
|
+
column: entry.column + columnShift,
|
|
92373
|
+
sourceIndex: entry.sourceIndex + sourceBase
|
|
92374
|
+
});
|
|
92375
|
+
}
|
|
92376
|
+
}
|
|
92449
92377
|
}
|
|
92450
|
-
|
|
92451
|
-
|
|
92378
|
+
for (const line of lines) {
|
|
92379
|
+
line?.entries.sort((a, b) => a.column - b.column);
|
|
92452
92380
|
}
|
|
92453
|
-
|
|
92454
|
-
|
|
92455
|
-
`option "--max-old-space" needs at least ${MIN_MAX_OLD_SPACE_MB} MB (got ${parsed}) \u2014 below that the measured app cannot start`
|
|
92456
|
-
);
|
|
92381
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
92382
|
+
lines[i] ??= { entries: [] };
|
|
92457
92383
|
}
|
|
92458
|
-
|
|
92459
|
-
if (flag === "--requests") options.requests = parsed;
|
|
92460
|
-
if (flag === "--connections") options.connections = parsed;
|
|
92461
|
-
if (flag === "--idle") options.idleSeconds = parsed;
|
|
92462
|
-
if (flag === "--warmup") options.warmupRequests = parsed;
|
|
92463
|
-
if (flag === "--max-old-space") options.maxOldSpaceMb = parsed;
|
|
92464
|
-
return FLAG_OK;
|
|
92384
|
+
return { sources, lines };
|
|
92465
92385
|
}
|
|
92466
|
-
|
|
92467
|
-
|
|
92468
|
-
|
|
92469
|
-
|
|
92470
|
-
|
|
92471
|
-
|
|
92472
|
-
|
|
92473
|
-
|
|
92474
|
-
case "--warmup":
|
|
92475
|
-
case "--max-old-space":
|
|
92476
|
-
return applyNumericFlag(spec.flag, value, options);
|
|
92477
|
-
case "--quick":
|
|
92478
|
-
options.quick = true;
|
|
92479
|
-
return FLAG_OK;
|
|
92480
|
-
case "--diff-all":
|
|
92481
|
-
options.diffAll = true;
|
|
92482
|
-
return FLAG_OK;
|
|
92483
|
-
case "--write-config":
|
|
92484
|
-
options.writeConfig = true;
|
|
92485
|
-
return FLAG_OK;
|
|
92486
|
-
case "--no-resolve":
|
|
92487
|
-
options.noResolve = true;
|
|
92488
|
-
return FLAG_OK;
|
|
92489
|
-
case "--output":
|
|
92490
|
-
options.output = value;
|
|
92491
|
-
return FLAG_OK;
|
|
92492
|
-
default:
|
|
92493
|
-
return FLAG_OK;
|
|
92386
|
+
var ID_IN_GAP = /[,[](\d{1,9}),/g;
|
|
92387
|
+
function lastModuleIdIn(gap) {
|
|
92388
|
+
ID_IN_GAP.lastIndex = 0;
|
|
92389
|
+
let last = null;
|
|
92390
|
+
let match = ID_IN_GAP.exec(gap);
|
|
92391
|
+
while (match !== null) {
|
|
92392
|
+
last = Number(match[1]);
|
|
92393
|
+
match = ID_IN_GAP.exec(gap);
|
|
92494
92394
|
}
|
|
92395
|
+
return last;
|
|
92495
92396
|
}
|
|
92496
|
-
function
|
|
92497
|
-
|
|
92498
|
-
|
|
92499
|
-
|
|
92500
|
-
|
|
92501
|
-
|
|
92502
|
-
|
|
92503
|
-
|
|
92397
|
+
function collectFirstArrayIds(codeLines, map, result) {
|
|
92398
|
+
for (const [lineIndex, lineText] of codeLines.entries()) {
|
|
92399
|
+
const first = /module\.exports ?= ?\[(\d+),/.exec(lineText);
|
|
92400
|
+
if (first?.[1] === void 0) {
|
|
92401
|
+
continue;
|
|
92402
|
+
}
|
|
92403
|
+
const idEnd = first.index + first[0].length;
|
|
92404
|
+
const nextId = /[,[]\d{1,9},/.exec(lineText.slice(idEnd));
|
|
92405
|
+
const spanEnd = nextId === null ? lineText.length : idEnd + nextId.index;
|
|
92406
|
+
const entry = map.lines[lineIndex]?.entries.find(
|
|
92407
|
+
(candidate) => candidate.column >= idEnd && candidate.column < spanEnd
|
|
92408
|
+
);
|
|
92409
|
+
const source = entry === void 0 ? void 0 : map.sources[entry.sourceIndex];
|
|
92410
|
+
if (source !== void 0) {
|
|
92411
|
+
result.set(Number(first[1]), source);
|
|
92412
|
+
}
|
|
92504
92413
|
}
|
|
92505
|
-
return { value };
|
|
92506
92414
|
}
|
|
92507
|
-
function
|
|
92508
|
-
const
|
|
92509
|
-
|
|
92510
|
-
|
|
92511
|
-
|
|
92415
|
+
function collectTransitionIds(codeLines, map, result) {
|
|
92416
|
+
for (const [lineIndex, mapping] of map.lines.entries()) {
|
|
92417
|
+
const lineText = codeLines[lineIndex];
|
|
92418
|
+
if (lineText === void 0) {
|
|
92419
|
+
continue;
|
|
92420
|
+
}
|
|
92421
|
+
let previousColumn = 0;
|
|
92422
|
+
let previousSource = null;
|
|
92423
|
+
for (const entry of mapping.entries) {
|
|
92424
|
+
if (entry.sourceIndex !== previousSource) {
|
|
92425
|
+
const id = lastModuleIdIn(lineText.slice(previousColumn, entry.column));
|
|
92426
|
+
const source = map.sources[entry.sourceIndex];
|
|
92427
|
+
if (id !== null && source !== void 0 && !result.has(id)) {
|
|
92428
|
+
result.set(id, source);
|
|
92429
|
+
}
|
|
92430
|
+
previousSource = entry.sourceIndex;
|
|
92431
|
+
}
|
|
92432
|
+
previousColumn = entry.column;
|
|
92433
|
+
}
|
|
92512
92434
|
}
|
|
92513
|
-
|
|
92514
|
-
|
|
92435
|
+
}
|
|
92436
|
+
function extractChunkModules(code, rawMap) {
|
|
92437
|
+
const result = /* @__PURE__ */ new Map();
|
|
92438
|
+
if (!code.includes("module.exports=[") && !code.includes("module.exports = [")) {
|
|
92439
|
+
return result;
|
|
92515
92440
|
}
|
|
92516
|
-
|
|
92517
|
-
|
|
92441
|
+
const map = normalizeSourceMap(rawMap);
|
|
92442
|
+
if (map === null) {
|
|
92443
|
+
return result;
|
|
92518
92444
|
}
|
|
92519
|
-
const
|
|
92520
|
-
|
|
92521
|
-
|
|
92445
|
+
const codeLines = code.split("\n");
|
|
92446
|
+
collectFirstArrayIds(codeLines, map, result);
|
|
92447
|
+
collectTransitionIds(codeLines, map, result);
|
|
92448
|
+
return result;
|
|
92449
|
+
}
|
|
92450
|
+
async function jsFilesUnder(dir) {
|
|
92451
|
+
const out = [];
|
|
92452
|
+
let entries;
|
|
92453
|
+
try {
|
|
92454
|
+
entries = await readdir(dir, { withFileTypes: true, recursive: true });
|
|
92455
|
+
} catch {
|
|
92456
|
+
return out;
|
|
92522
92457
|
}
|
|
92523
|
-
const
|
|
92524
|
-
|
|
92525
|
-
|
|
92458
|
+
for (const entry of entries) {
|
|
92459
|
+
if (entry.isFile() && entry.name.endsWith(".js")) {
|
|
92460
|
+
out.push(path.join(entry.parentPath, entry.name));
|
|
92461
|
+
}
|
|
92526
92462
|
}
|
|
92527
|
-
return
|
|
92463
|
+
return out;
|
|
92528
92464
|
}
|
|
92529
|
-
function
|
|
92530
|
-
const
|
|
92531
|
-
const
|
|
92532
|
-
|
|
92533
|
-
|
|
92534
|
-
|
|
92535
|
-
|
|
92536
|
-
|
|
92537
|
-
|
|
92538
|
-
idleSeconds: null,
|
|
92539
|
-
warmupRequests: null,
|
|
92540
|
-
maxOldSpaceMb: null,
|
|
92541
|
-
quick: false,
|
|
92542
|
-
noResolve: false,
|
|
92543
|
-
diffAll: false,
|
|
92544
|
-
writeConfig: false,
|
|
92545
|
-
output: null
|
|
92546
|
-
};
|
|
92547
|
-
for (let index = 0; index < rest.length; index += 1) {
|
|
92548
|
-
const argument = rest[index] ?? "";
|
|
92549
|
-
if (!argument.startsWith("-")) {
|
|
92550
|
-
if (options.appDir !== "") {
|
|
92551
|
-
return { kind: "error", message: `unexpected extra argument "${argument}" \u2014 one app directory only` };
|
|
92552
|
-
}
|
|
92553
|
-
options.appDir = argument;
|
|
92465
|
+
async function extractModuleRegistry(nextServerDir) {
|
|
92466
|
+
const registry = /* @__PURE__ */ new Map();
|
|
92467
|
+
for (const file of await jsFilesUnder(nextServerDir)) {
|
|
92468
|
+
let code;
|
|
92469
|
+
let map;
|
|
92470
|
+
try {
|
|
92471
|
+
code = await readFile(file, "utf8");
|
|
92472
|
+
map = JSON.parse(await readFile(`${file}.map`, "utf8"));
|
|
92473
|
+
} catch {
|
|
92554
92474
|
continue;
|
|
92555
92475
|
}
|
|
92556
|
-
const
|
|
92557
|
-
|
|
92558
|
-
|
|
92476
|
+
for (const [id, source] of extractChunkModules(code, map)) {
|
|
92477
|
+
if (!registry.has(id)) {
|
|
92478
|
+
registry.set(id, source);
|
|
92479
|
+
}
|
|
92559
92480
|
}
|
|
92560
|
-
index += step.consumed;
|
|
92561
92481
|
}
|
|
92562
|
-
|
|
92563
|
-
|
|
92482
|
+
return registry;
|
|
92483
|
+
}
|
|
92484
|
+
|
|
92485
|
+
// src/manifests.ts
|
|
92486
|
+
import { z as z2 } from "zod";
|
|
92487
|
+
var appPathsManifestSchema = z2.record(z2.string(), z2.string());
|
|
92488
|
+
var pagesManifestSchema = z2.record(z2.string(), z2.string());
|
|
92489
|
+
var routeEntrySchema = z2.looseObject({
|
|
92490
|
+
page: z2.string(),
|
|
92491
|
+
regex: z2.string()
|
|
92492
|
+
});
|
|
92493
|
+
var routesManifestSchema = z2.looseObject({
|
|
92494
|
+
version: z2.number(),
|
|
92495
|
+
basePath: z2.string(),
|
|
92496
|
+
staticRoutes: z2.array(routeEntrySchema),
|
|
92497
|
+
dynamicRoutes: z2.array(routeEntrySchema)
|
|
92498
|
+
});
|
|
92499
|
+
var prerenderManifestSchema = z2.looseObject({
|
|
92500
|
+
routes: z2.record(
|
|
92501
|
+
z2.string(),
|
|
92502
|
+
z2.looseObject({
|
|
92503
|
+
// `false` on a route that is prerendered but never revalidates.
|
|
92504
|
+
initialRevalidateSeconds: z2.union([z2.number(), z2.literal(false)]).optional(),
|
|
92505
|
+
srcRoute: z2.string().nullable().optional()
|
|
92506
|
+
})
|
|
92507
|
+
).optional(),
|
|
92508
|
+
preview: z2.looseObject({ previewModeId: z2.string() }).optional()
|
|
92509
|
+
});
|
|
92510
|
+
var INTERNAL_PATHS = /* @__PURE__ */ new Set(["/_global-error", "/_not-found"]);
|
|
92511
|
+
var INTERCEPTING_SEGMENT = /^(\(\.{1,3}\))+/;
|
|
92512
|
+
var STATIC_ASSET_PATHS = /* @__PURE__ */ new Set(["/favicon.ico"]);
|
|
92513
|
+
function unaddressableReason(routePath) {
|
|
92514
|
+
if (routePath.split("/").some((segment) => INTERCEPTING_SEGMENT.test(segment))) {
|
|
92515
|
+
return "intercepting route \u2014 only reachable via client navigation";
|
|
92564
92516
|
}
|
|
92565
|
-
if (
|
|
92566
|
-
|
|
92567
|
-
if (misplaced !== null) {
|
|
92568
|
-
return {
|
|
92569
|
-
kind: "error",
|
|
92570
|
-
message: `option "${misplaced}" shapes HTTP load and does not apply to "next-leak build" \u2014 see --help`
|
|
92571
|
-
};
|
|
92572
|
-
}
|
|
92573
|
-
return { kind: "build", options: { appDir: options.appDir, output: options.output } };
|
|
92517
|
+
if (STATIC_ASSET_PATHS.has(routePath)) {
|
|
92518
|
+
return "static asset served by a generated handler \u2014 no user code to leak";
|
|
92574
92519
|
}
|
|
92575
|
-
return
|
|
92520
|
+
return void 0;
|
|
92576
92521
|
}
|
|
92577
|
-
|
|
92578
|
-
|
|
92579
|
-
|
|
92580
|
-
|
|
92581
|
-
|
|
92582
|
-
|
|
92583
|
-
}
|
|
92522
|
+
function toRequestPath(manifestKey) {
|
|
92523
|
+
let kind;
|
|
92524
|
+
let withoutSuffix;
|
|
92525
|
+
if (manifestKey.endsWith("/page")) {
|
|
92526
|
+
kind = "page";
|
|
92527
|
+
withoutSuffix = manifestKey.slice(0, -"/page".length);
|
|
92528
|
+
} else if (manifestKey.endsWith("/route")) {
|
|
92529
|
+
kind = "route-handler";
|
|
92530
|
+
withoutSuffix = manifestKey.slice(0, -"/route".length);
|
|
92531
|
+
} else {
|
|
92532
|
+
return null;
|
|
92533
|
+
}
|
|
92534
|
+
const segments = withoutSuffix.split("/").filter((segment) => segment !== "").filter((segment) => !(segment.startsWith("(") && segment.endsWith(")"))).filter((segment) => !segment.startsWith("@"));
|
|
92535
|
+
return { path: `/${segments.join("/")}`, kind };
|
|
92584
92536
|
}
|
|
92585
|
-
|
|
92586
|
-
|
|
92587
|
-
|
|
92537
|
+
var PAGES_INTERNAL = /* @__PURE__ */ new Set(["/_app", "/_document", "/_error", "/404", "/500"]);
|
|
92538
|
+
function discoverPagesRoutes(pages) {
|
|
92539
|
+
const routes = [];
|
|
92540
|
+
for (const key of Object.keys(pages)) {
|
|
92541
|
+
if (PAGES_INTERNAL.has(key)) {
|
|
92542
|
+
continue;
|
|
92543
|
+
}
|
|
92544
|
+
routes.push({
|
|
92545
|
+
path: key,
|
|
92546
|
+
kind: key === "/api" || key.startsWith("/api/") ? "route-handler" : "page",
|
|
92547
|
+
dynamic: key.includes("[")
|
|
92548
|
+
});
|
|
92588
92549
|
}
|
|
92589
|
-
|
|
92590
|
-
|
|
92550
|
+
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
92551
|
+
}
|
|
92552
|
+
function discoverRoutes(appPaths) {
|
|
92553
|
+
const routes = /* @__PURE__ */ new Map();
|
|
92554
|
+
for (const key of Object.keys(appPaths)) {
|
|
92555
|
+
const parsed = toRequestPath(key);
|
|
92556
|
+
if (parsed === null || INTERNAL_PATHS.has(parsed.path) || routes.has(parsed.path)) {
|
|
92557
|
+
continue;
|
|
92558
|
+
}
|
|
92559
|
+
const reason = unaddressableReason(parsed.path);
|
|
92560
|
+
routes.set(parsed.path, {
|
|
92561
|
+
path: parsed.path,
|
|
92562
|
+
kind: parsed.kind,
|
|
92563
|
+
dynamic: parsed.path.includes("["),
|
|
92564
|
+
...reason !== void 0 && { unaddressableReason: reason }
|
|
92565
|
+
});
|
|
92591
92566
|
}
|
|
92592
|
-
return
|
|
92567
|
+
return [...routes.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
92593
92568
|
}
|
|
92594
92569
|
|
|
92595
|
-
// src/
|
|
92596
|
-
import { readFile as
|
|
92597
|
-
import
|
|
92598
|
-
|
|
92599
|
-
|
|
92600
|
-
(
|
|
92601
|
-
{ message: 'a sample value cannot carry both "{n}" and "{n%N}" \u2014 pick one' }
|
|
92602
|
-
);
|
|
92603
|
-
var routeConfigSchema = z3.object({
|
|
92604
|
-
params: z3.record(z3.string(), sampleValueSchema).optional(),
|
|
92605
|
-
routes: z3.record(z3.string(), z3.record(z3.string(), sampleValueSchema)).optional(),
|
|
92606
|
-
/**
|
|
92607
|
-
* Headers sent with every request. Real traffic is not header-less:
|
|
92608
|
-
* compression (`Accept-Encoding`), sessions and auth change which code
|
|
92609
|
-
* paths run, and some leaks only exist on those paths (zlib, per-session
|
|
92610
|
-
* caches). Measuring without them measures a different app.
|
|
92611
|
-
*/
|
|
92612
|
-
headers: z3.record(z3.string(), z3.string()).optional(),
|
|
92613
|
-
/** Query string appended per route template, e.g. `"weightKb=2048"`. */
|
|
92614
|
-
query: z3.record(z3.string(), z3.string()).optional(),
|
|
92615
|
-
/**
|
|
92616
|
-
* Simulates clients that give up before the response arrives (closed
|
|
92617
|
-
* tabs, load-balancer timeouts, bots). Several real leaks only exist on
|
|
92618
|
-
* that path — vercel/next.js#89091 traces `ServerResponse` retention to
|
|
92619
|
-
* an early disconnect — and a load generator that always waits politely
|
|
92620
|
-
* never reaches it.
|
|
92621
|
-
*
|
|
92622
|
-
* Counted **from the first byte of the response**, not from the request:
|
|
92623
|
-
* under load a server's first byte arrives long after any sane fixed
|
|
92624
|
-
* window, so a request-relative clock cuts before the stream starts and
|
|
92625
|
-
* tests the wrong path entirely.
|
|
92626
|
-
*/
|
|
92627
|
-
abandonAfterMs: z3.number().int().positive().optional(),
|
|
92628
|
-
/**
|
|
92629
|
-
* Where the abandon deadline starts. `first-byte` is the default and the
|
|
92630
|
-
* semantics everything else assumes.
|
|
92631
|
-
*
|
|
92632
|
-
* `request` starts it when the request is written, which is the only way
|
|
92633
|
-
* to reach leaks that need the client already gone before the server
|
|
92634
|
-
* produces anything: against a route whose time-to-first-byte exceeds the
|
|
92635
|
-
* deadline, a first-byte clock never cuts early. Measured on the
|
|
92636
|
-
* vercel/next.js#84648 repro, whose upstream answers in 400 ms while its
|
|
92637
|
-
* own load generator cuts at 60 ms — the first-byte clock reached 7% of
|
|
92638
|
-
* requests there, none of them on the path the issue describes.
|
|
92639
|
-
*
|
|
92640
|
-
* Opt-in per route, never a default: armed on connect, the deadline loses
|
|
92641
|
-
* the race to the first byte on fast routes, which is how the earlier
|
|
92642
|
-
* version of this measured 2 mid-stream cuts out of ~1500 on #94919.
|
|
92643
|
-
*/
|
|
92644
|
-
abandonFrom: z3.enum(["first-byte", "request"]).optional()
|
|
92645
|
-
}).strict().refine((config) => config.abandonFrom === void 0 || config.abandonAfterMs !== void 0, {
|
|
92646
|
-
message: '"abandonFrom" configures nothing without "abandonAfterMs"',
|
|
92647
|
-
path: ["abandonFrom"]
|
|
92648
|
-
});
|
|
92649
|
-
var RouteConfigError = class extends Error {
|
|
92650
|
-
constructor(message) {
|
|
92570
|
+
// src/target.ts
|
|
92571
|
+
import { access, readFile as readFile2 } from "fs/promises";
|
|
92572
|
+
import path2 from "path";
|
|
92573
|
+
var TargetError = class extends Error {
|
|
92574
|
+
code;
|
|
92575
|
+
constructor(code, message) {
|
|
92651
92576
|
super(message);
|
|
92652
|
-
this.name = "
|
|
92577
|
+
this.name = "TargetError";
|
|
92578
|
+
this.code = code;
|
|
92653
92579
|
}
|
|
92654
92580
|
};
|
|
92655
|
-
|
|
92656
|
-
async function loadRouteConfig(appDir) {
|
|
92657
|
-
const file = path3.join(appDir, ROUTE_CONFIG_FILE);
|
|
92658
|
-
let raw;
|
|
92581
|
+
async function exists(file) {
|
|
92659
92582
|
try {
|
|
92660
|
-
|
|
92583
|
+
await access(file);
|
|
92584
|
+
return true;
|
|
92661
92585
|
} catch {
|
|
92662
|
-
return
|
|
92586
|
+
return false;
|
|
92663
92587
|
}
|
|
92588
|
+
}
|
|
92589
|
+
async function readManifest(file, parse2) {
|
|
92590
|
+
let raw;
|
|
92664
92591
|
try {
|
|
92665
|
-
|
|
92592
|
+
raw = JSON.parse(await readFile2(file, "utf8"));
|
|
92666
92593
|
} catch (cause) {
|
|
92667
|
-
throw new
|
|
92668
|
-
|
|
92594
|
+
throw new TargetError(
|
|
92595
|
+
"BAD_MANIFEST",
|
|
92596
|
+
`Could not read ${file}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
92669
92597
|
);
|
|
92670
92598
|
}
|
|
92671
|
-
|
|
92672
|
-
|
|
92673
|
-
|
|
92674
|
-
|
|
92675
|
-
|
|
92676
|
-
|
|
92677
|
-
|
|
92678
|
-
continue;
|
|
92679
|
-
}
|
|
92680
|
-
segments.push({
|
|
92681
|
-
name: match[3] ?? "",
|
|
92682
|
-
catchAll: match[2] !== void 0,
|
|
92683
|
-
optional: match[1] !== void 0
|
|
92684
|
-
});
|
|
92685
|
-
}
|
|
92686
|
-
return segments;
|
|
92687
|
-
}
|
|
92688
|
-
var UNIQUE_MARKER = "{n}";
|
|
92689
|
-
var BOUNDED_MARKER = /\{n%(\d+)\}/;
|
|
92690
|
-
function boundedMarkerOf(value) {
|
|
92691
|
-
const match = BOUNDED_MARKER.exec(value);
|
|
92692
|
-
const bound = Number(match?.[1]);
|
|
92693
|
-
if (match === null || !Number.isInteger(bound) || bound <= 0) {
|
|
92694
|
-
return null;
|
|
92599
|
+
try {
|
|
92600
|
+
return parse2(raw);
|
|
92601
|
+
} catch {
|
|
92602
|
+
throw new TargetError(
|
|
92603
|
+
"BAD_MANIFEST",
|
|
92604
|
+
`${file} does not match the expected Next.js manifest shape. This Next.js version may not be supported yet.`
|
|
92605
|
+
);
|
|
92695
92606
|
}
|
|
92696
|
-
return { marker: match[0], bound };
|
|
92697
92607
|
}
|
|
92698
|
-
|
|
92699
|
-
|
|
92700
|
-
|
|
92701
|
-
|
|
92702
|
-
|
|
92703
|
-
|
|
92704
|
-
|
|
92705
|
-
for (const segment of routeTemplate.split("/")) {
|
|
92706
|
-
const match = SEGMENT_PATTERN.exec(segment);
|
|
92707
|
-
if (match === null) {
|
|
92708
|
-
resolved.push(encodeSegment(segment));
|
|
92709
|
-
continue;
|
|
92710
|
-
}
|
|
92711
|
-
const optional = match[1] !== void 0;
|
|
92712
|
-
const catchAll = match[2] !== void 0;
|
|
92713
|
-
const name = match[3] ?? "";
|
|
92714
|
-
const value = config.routes?.[routeTemplate]?.[name] ?? config.params?.[name];
|
|
92715
|
-
if (value !== void 0) {
|
|
92716
|
-
resolved.push(catchAll ? encodeCatchAll(value) : encodeSegment(value));
|
|
92717
|
-
} else if (!optional) {
|
|
92718
|
-
return null;
|
|
92719
|
-
}
|
|
92608
|
+
async function validateTarget(appDir) {
|
|
92609
|
+
const nextDir = path2.resolve(appDir, ".next");
|
|
92610
|
+
if (!await exists(nextDir)) {
|
|
92611
|
+
throw new TargetError(
|
|
92612
|
+
"NO_BUILD",
|
|
92613
|
+
`No .next directory in ${appDir}. Run "next build" first.`
|
|
92614
|
+
);
|
|
92720
92615
|
}
|
|
92721
|
-
const
|
|
92722
|
-
|
|
92723
|
-
|
|
92724
|
-
|
|
92725
|
-
}
|
|
92616
|
+
const standaloneServer = path2.join(nextDir, "standalone", "server.js");
|
|
92617
|
+
if (!await exists(standaloneServer)) {
|
|
92618
|
+
throw new TargetError(
|
|
92619
|
+
"NO_STANDALONE",
|
|
92620
|
+
`No ${standaloneServer}.
|
|
92621
|
+
next-leak measures the standalone server bundle. Enable it once in next.config:
|
|
92726
92622
|
|
|
92727
|
-
|
|
92728
|
-
|
|
92729
|
-
|
|
92730
|
-
|
|
92731
|
-
|
|
92732
|
-
|
|
92733
|
-
|
|
92734
|
-
|
|
92735
|
-
if (concrete === void 0) {
|
|
92736
|
-
return null;
|
|
92623
|
+
const nextConfig = {
|
|
92624
|
+
output: "standalone",
|
|
92625
|
+
// ...your existing config
|
|
92626
|
+
};
|
|
92627
|
+
|
|
92628
|
+
then rebuild: next build
|
|
92629
|
+
This only changes how the build is packaged \u2014 not how your app behaves.`
|
|
92630
|
+
);
|
|
92737
92631
|
}
|
|
92738
|
-
const
|
|
92739
|
-
const
|
|
92740
|
-
const
|
|
92741
|
-
|
|
92742
|
-
|
|
92743
|
-
|
|
92744
|
-
|
|
92745
|
-
|
|
92746
|
-
|
|
92747
|
-
const value = parameter.catchAll ? concreteSegments.slice(index).join("/") : concreteSegments[index];
|
|
92748
|
-
if (value === void 0 || value === "") {
|
|
92749
|
-
return null;
|
|
92750
|
-
}
|
|
92751
|
-
values[parameter.name] = value;
|
|
92632
|
+
const appPathsFile = path2.join(nextDir, "server", "app-paths-manifest.json");
|
|
92633
|
+
const pagesFile = path2.join(nextDir, "server", "pages-manifest.json");
|
|
92634
|
+
const appPaths = await exists(appPathsFile) ? await readManifest(appPathsFile, (raw) => appPathsManifestSchema.parse(raw)) : {};
|
|
92635
|
+
const pages = await exists(pagesFile) ? await readManifest(pagesFile, (raw) => pagesManifestSchema.parse(raw)) : {};
|
|
92636
|
+
if (Object.keys(appPaths).length === 0 && Object.keys(pages).length === 0) {
|
|
92637
|
+
throw new TargetError(
|
|
92638
|
+
"BAD_MANIFEST",
|
|
92639
|
+
`No routes found in ${nextDir}/server: neither app-paths-manifest.json nor pages-manifest.json is present. Rebuild the app with "next build".`
|
|
92640
|
+
);
|
|
92752
92641
|
}
|
|
92753
|
-
|
|
92642
|
+
const routesFile = path2.join(nextDir, "routes-manifest.json");
|
|
92643
|
+
const routes = await exists(routesFile) ? await readManifest(routesFile, (raw) => routesManifestSchema.parse(raw)) : void 0;
|
|
92644
|
+
const prerenderFile = path2.join(nextDir, "prerender-manifest.json");
|
|
92645
|
+
const prerender = await exists(prerenderFile) ? await readManifest(prerenderFile, (raw) => prerenderManifestSchema.parse(raw)) : void 0;
|
|
92646
|
+
return {
|
|
92647
|
+
appDir: path2.resolve(appDir),
|
|
92648
|
+
standaloneServer,
|
|
92649
|
+
appPaths,
|
|
92650
|
+
pages,
|
|
92651
|
+
routes,
|
|
92652
|
+
prerender
|
|
92653
|
+
};
|
|
92754
92654
|
}
|
|
92755
|
-
|
|
92756
|
-
|
|
92757
|
-
|
|
92758
|
-
|
|
92759
|
-
|
|
92760
|
-
|
|
92761
|
-
|
|
92762
|
-
|
|
92763
|
-
|
|
92764
|
-
|
|
92765
|
-
|
|
92766
|
-
|
|
92767
|
-
|
|
92655
|
+
|
|
92656
|
+
// src/launcher.ts
|
|
92657
|
+
import { spawn } from "child_process";
|
|
92658
|
+
import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
|
|
92659
|
+
import path3 from "path";
|
|
92660
|
+
import { pathToFileURL } from "url";
|
|
92661
|
+
import { z as z3 } from "zod";
|
|
92662
|
+
var controlFileSchema = z3.object({ port: z3.number(), pid: z3.number() });
|
|
92663
|
+
var DEFAULT_MAX_OLD_SPACE_MB = 512;
|
|
92664
|
+
var LaunchError = class extends Error {
|
|
92665
|
+
constructor(message) {
|
|
92666
|
+
super(message);
|
|
92667
|
+
this.name = "LaunchError";
|
|
92668
|
+
}
|
|
92669
|
+
};
|
|
92670
|
+
var activeChildren = /* @__PURE__ */ new Set();
|
|
92671
|
+
var STDERR_WINDOW = 4096;
|
|
92672
|
+
function killActiveChildren() {
|
|
92673
|
+
for (const child of activeChildren) {
|
|
92674
|
+
child.kill("SIGKILL");
|
|
92768
92675
|
}
|
|
92769
|
-
return Object.keys(routes).length === 0 ? null : JSON.stringify({ routes }, null, 2);
|
|
92770
92676
|
}
|
|
92771
|
-
function
|
|
92772
|
-
|
|
92677
|
+
function registerChild(child) {
|
|
92678
|
+
activeChildren.add(child);
|
|
92679
|
+
child.once("exit", () => activeChildren.delete(child));
|
|
92773
92680
|
}
|
|
92774
|
-
|
|
92775
|
-
|
|
92776
|
-
var MB = 1024 * 1024;
|
|
92777
|
-
var CLASSES = ["heap", "external", "arrayBuffers"];
|
|
92778
|
-
var MIN_GAP_BYTES = 2 * MB;
|
|
92779
|
-
var MIN_GAP_RATIO = 0.35;
|
|
92780
|
-
var MIN_RETAINED_BYTES = MB;
|
|
92781
|
-
var read = (sample, memoryClass) => memoryClass === "heap" ? sample.heapUsed : memoryClass === "external" ? sample.external : sample.arrayBuffers;
|
|
92782
|
-
function largestGap(values) {
|
|
92783
|
-
return values.reduce((highest, value) => Math.max(highest, value), 0);
|
|
92681
|
+
function unregisterChild(child) {
|
|
92682
|
+
activeChildren.delete(child);
|
|
92784
92683
|
}
|
|
92785
|
-
function
|
|
92786
|
-
const
|
|
92787
|
-
|
|
92788
|
-
|
|
92789
|
-
if (afterGc === void 0) {
|
|
92790
|
-
break;
|
|
92791
|
-
}
|
|
92792
|
-
gaps.push(read(held, memoryClass) - read(afterGc, memoryClass));
|
|
92684
|
+
function explainStartupFailure(stderr) {
|
|
92685
|
+
const missingModule = /Cannot find module '([^']+)'/.exec(stderr);
|
|
92686
|
+
if (missingModule !== null) {
|
|
92687
|
+
return `the standalone build is missing a dependency (${missingModule[1]}). This is a build problem, not a measurement one: \`node .next/standalone/server.js\` fails the same way on its own. Rebuild, or copy the missing package into .next/standalone/node_modules.`;
|
|
92793
92688
|
}
|
|
92794
|
-
|
|
92689
|
+
if (/EADDRINUSE/.test(stderr)) {
|
|
92690
|
+
return "the port was taken by another process while starting.";
|
|
92691
|
+
}
|
|
92692
|
+
return `stderr:
|
|
92693
|
+
${stderr}`;
|
|
92795
92694
|
}
|
|
92796
|
-
function
|
|
92797
|
-
if (
|
|
92798
|
-
return
|
|
92695
|
+
function explainRuntimeFailure(stderr, maxOldSpaceMb) {
|
|
92696
|
+
if (/heap out of memory|Reached heap limit|Ineffective mark-compacts/i.test(stderr)) {
|
|
92697
|
+
return `the measured process ran out of heap and was killed by V8 mid-run (limit in force: --max-old-space-size=${maxOldSpaceMb} MB). That is the measurement: this route does not fit in ${maxOldSpaceMb} MB under this load. Raise it with --max-old-space <mb> to match your deployment, or lower --requests/--connections to measure a lighter regime.`;
|
|
92799
92698
|
}
|
|
92800
|
-
|
|
92801
|
-
|
|
92802
|
-
|
|
92803
|
-
|
|
92804
|
-
|
|
92805
|
-
|
|
92806
|
-
|
|
92807
|
-
|
|
92808
|
-
continue;
|
|
92699
|
+
return `the measured process exited mid-run. ${explainStartupFailure(stderr)}`;
|
|
92700
|
+
}
|
|
92701
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
92702
|
+
async function pollUntil(deadline, what, probe, failed) {
|
|
92703
|
+
for (; ; ) {
|
|
92704
|
+
const failure = failed();
|
|
92705
|
+
if (failure !== void 0) {
|
|
92706
|
+
throw new LaunchError(failure);
|
|
92809
92707
|
}
|
|
92810
|
-
const
|
|
92811
|
-
|
|
92812
|
-
|
|
92813
|
-
const ratio = largestGapBytes / Math.max(retainedBytes, MIN_RETAINED_BYTES);
|
|
92814
|
-
if (ratio < MIN_GAP_RATIO) {
|
|
92815
|
-
continue;
|
|
92708
|
+
const result = await probe();
|
|
92709
|
+
if (result !== void 0) {
|
|
92710
|
+
return result;
|
|
92816
92711
|
}
|
|
92817
|
-
if (
|
|
92818
|
-
|
|
92712
|
+
if (Date.now() > deadline) {
|
|
92713
|
+
throw new LaunchError(`timed out waiting for ${what}`);
|
|
92819
92714
|
}
|
|
92715
|
+
await sleep(100);
|
|
92820
92716
|
}
|
|
92821
|
-
return worst;
|
|
92822
|
-
}
|
|
92823
|
-
var mb = (bytes) => `${(bytes / MB).toFixed(2)} MB`;
|
|
92824
|
-
var CLASS_LABEL = {
|
|
92825
|
-
heap: "heap",
|
|
92826
|
-
external: "external memory",
|
|
92827
|
-
arrayBuffers: "arrayBuffers"
|
|
92828
|
-
};
|
|
92829
|
-
function describeUnreclaimedRetention(retention) {
|
|
92830
|
-
return `held ${mb(retention.largestGapBytes)} of ${CLASS_LABEL[retention.class]} between collections that a GC took back (${retention.ratio.toFixed(1)}x what it retains) \u2014 a forced GC reclaims this, a long-running process may not run one often enough to; read seconds after load, so it also includes memory not yet collected`;
|
|
92831
|
-
}
|
|
92832
|
-
|
|
92833
|
-
// src/report.ts
|
|
92834
|
-
var MB2 = 1024 * 1024;
|
|
92835
|
-
var formatMb = (bytes) => `${(bytes / MB2).toFixed(1)} MB`;
|
|
92836
|
-
var formatGrowth = (bytes) => {
|
|
92837
|
-
const sign = bytes >= 0 ? "+" : "";
|
|
92838
|
-
return `${sign}${(bytes / MB2).toFixed(2)} MB/1000 req`;
|
|
92839
|
-
};
|
|
92840
|
-
var VERDICT_ICON = { leak: "\u2716", stable: "\u2714", inconclusive: "?" };
|
|
92841
|
-
var RSS_MIN_GROWTH_PER_CYCLE = 16 * MB2;
|
|
92842
|
-
var RSS_MIN_TOTAL_GROWTH = 64 * MB2;
|
|
92843
|
-
function hasSustainedRssGrowth(memorySamples) {
|
|
92844
|
-
const rss = memorySamples.map((sample) => sample.rss);
|
|
92845
|
-
const first = rss[1];
|
|
92846
|
-
const last = rss.at(-1);
|
|
92847
|
-
if (first === void 0 || last === void 0 || rss.length < 4) {
|
|
92848
|
-
return false;
|
|
92849
|
-
}
|
|
92850
|
-
return classifyTrend(rss, { minGrowthPerCycle: RSS_MIN_GROWTH_PER_CYCLE }).verdict === "leak" && last - first >= RSS_MIN_TOTAL_GROWTH;
|
|
92851
|
-
}
|
|
92852
|
-
function ownerLabel(attribution) {
|
|
92853
|
-
switch (attribution.owner) {
|
|
92854
|
-
case "app":
|
|
92855
|
-
return `${attribution.source ?? "your code"} (your code)`;
|
|
92856
|
-
case "dependency":
|
|
92857
|
-
return `${attribution.packageName ?? "a dependency"} (dependency)`;
|
|
92858
|
-
case "framework":
|
|
92859
|
-
return attribution.packageName === null ? "Next.js/runtime internals" : `${attribution.packageName} (framework)`;
|
|
92860
|
-
case "unattributed":
|
|
92861
|
-
return null;
|
|
92862
|
-
}
|
|
92863
|
-
}
|
|
92864
|
-
function revalidationLines(route) {
|
|
92865
|
-
return route.revalidatedEverySeconds === void 0 ? [] : [
|
|
92866
|
-
` driven through ISR revalidation (revalidates every ${route.revalidatedEverySeconds}s; without it the load would serve the cache)`
|
|
92867
|
-
];
|
|
92868
|
-
}
|
|
92869
|
-
function abandonLines(route) {
|
|
92870
|
-
if (route.abandon === void 0) {
|
|
92871
|
-
return [];
|
|
92872
|
-
}
|
|
92873
|
-
return [
|
|
92874
|
-
route.abandon.from === "request" ? ` cut ${route.abandon.afterMs}ms after the request was sent, so the client is gone before the response starts` : ` cut ${route.abandon.afterMs}ms after the first byte, so the cut lands mid-stream`
|
|
92875
|
-
];
|
|
92876
|
-
}
|
|
92877
|
-
function confidenceLines(route) {
|
|
92878
|
-
const lines = [];
|
|
92879
|
-
if (route.confidence.supersededVerdict !== void 0) {
|
|
92880
|
-
lines.push(
|
|
92881
|
-
` measured ${route.trend.verdict}, withdrawn: the run did not observe what that verdict needs`
|
|
92882
|
-
);
|
|
92883
|
-
}
|
|
92884
|
-
for (const warning of route.confidence.warnings) {
|
|
92885
|
-
lines.push(` \u26A0 low confidence: ${warning.detail}`);
|
|
92886
|
-
}
|
|
92887
|
-
return lines;
|
|
92888
|
-
}
|
|
92889
|
-
function memorySourceLines(route, verdict) {
|
|
92890
|
-
const lines = [];
|
|
92891
|
-
if (route.trend.source === "external" && verdict !== "stable") {
|
|
92892
|
-
const externalCurve = route.memorySamples.map((sample) => formatMb(sample.external)).join(" \u2192 ");
|
|
92893
|
-
lines.push(
|
|
92894
|
-
` verdict comes from EXTERNAL memory (buffers, streams, fetch bodies), not the JS heap: external ${externalCurve}`
|
|
92895
|
-
);
|
|
92896
|
-
}
|
|
92897
|
-
if (verdict === "stable" && hasSustainedRssGrowth(route.memorySamples)) {
|
|
92898
|
-
const rssCurve = route.memorySamples.map((sample) => formatMb(sample.rss)).join(" \u2192 ");
|
|
92899
|
-
lines.push(
|
|
92900
|
-
` note: heap is flat but RSS grows ${formatGrowth(route.rssPer1000Requests)} \u2014 not a JS-heap leak (allocator, external buffers or fragmentation): RSS ${rssCurve}`
|
|
92901
|
-
);
|
|
92902
|
-
}
|
|
92903
|
-
return lines;
|
|
92904
|
-
}
|
|
92905
|
-
function diffFindingLines(route) {
|
|
92906
|
-
if (route.diff === null) {
|
|
92907
|
-
return [];
|
|
92908
|
-
}
|
|
92909
|
-
const findings = [...route.diff.grownNodes, ...route.diff.newNodes];
|
|
92910
|
-
return findings.slice(0, 3).map((finding, index) => {
|
|
92911
|
-
const size = formatMb(finding.retainedBytes);
|
|
92912
|
-
const attribution = route.attribution?.findings[index];
|
|
92913
|
-
const owner = attribution === void 0 ? null : ownerLabel(attribution);
|
|
92914
|
-
const detail = owner ?? (finding.retainerChain === "" ? "" : finding.retainerChain);
|
|
92915
|
-
return ` \u21B3 ${finding.kind} [${finding.nodeType}] ${finding.name} ${size}${detail === "" ? "" : ` \u2014 ${detail}`}`;
|
|
92916
|
-
});
|
|
92917
|
-
}
|
|
92918
|
-
function findingLines(route) {
|
|
92919
|
-
const lines = [];
|
|
92920
|
-
const culprit = route.attribution === null ? null : ownerLabel(route.attribution.route);
|
|
92921
|
-
if (culprit !== null) {
|
|
92922
|
-
lines.push(` culprit: ${culprit}`);
|
|
92923
|
-
}
|
|
92924
|
-
lines.push(...diffFindingLines(route));
|
|
92925
|
-
for (const signature of route.signatures) {
|
|
92926
|
-
const flag = signature.historical ? " (historical)" : "";
|
|
92927
|
-
lines.push(` \u26A0 known cause${flag}: ${signature.title} \u2014 ${signature.issue}`);
|
|
92928
|
-
}
|
|
92929
|
-
return lines;
|
|
92930
92717
|
}
|
|
92931
|
-
function
|
|
92932
|
-
const
|
|
92933
|
-
|
|
92934
|
-
|
|
92935
|
-
|
|
92936
|
-
|
|
92937
|
-
|
|
92938
|
-
|
|
92939
|
-
|
|
92718
|
+
async function launchInstrumented(options) {
|
|
92719
|
+
const hostname = options.hostname ?? "127.0.0.1";
|
|
92720
|
+
const deadline = Date.now() + (options.readyTimeoutMs ?? 15e3);
|
|
92721
|
+
const child = spawn(
|
|
92722
|
+
process.execPath,
|
|
92723
|
+
[
|
|
92724
|
+
"--expose-gc",
|
|
92725
|
+
`--max-old-space-size=${options.maxOldSpaceMb ?? DEFAULT_MAX_OLD_SPACE_MB}`,
|
|
92726
|
+
"--import",
|
|
92727
|
+
pathToFileURL(options.bootstrapPath).href,
|
|
92728
|
+
options.serverPath
|
|
92729
|
+
],
|
|
92730
|
+
{
|
|
92731
|
+
cwd: path3.dirname(options.serverPath),
|
|
92732
|
+
env: {
|
|
92733
|
+
...process.env,
|
|
92734
|
+
...options.env,
|
|
92735
|
+
NODE_ENV: "production",
|
|
92736
|
+
PORT: String(options.appPort),
|
|
92737
|
+
HOSTNAME: hostname,
|
|
92738
|
+
NEXT_LEAK_DIR: options.workDir
|
|
92739
|
+
},
|
|
92740
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
92741
|
+
}
|
|
92742
|
+
);
|
|
92743
|
+
let stderrHead = "";
|
|
92744
|
+
let stderrTailBuffer = "";
|
|
92745
|
+
child.stderr?.on("data", (chunk) => {
|
|
92746
|
+
const text = chunk.toString();
|
|
92747
|
+
if (stderrHead.length < STDERR_WINDOW) {
|
|
92748
|
+
stderrHead = (stderrHead + text).slice(0, STDERR_WINDOW);
|
|
92749
|
+
}
|
|
92750
|
+
stderrTailBuffer = (stderrTailBuffer + text).slice(-STDERR_WINDOW);
|
|
92940
92751
|
});
|
|
92941
|
-
|
|
92942
|
-
|
|
92943
|
-
|
|
92944
|
-
|
|
92945
|
-
|
|
92946
|
-
|
|
92947
|
-
|
|
92948
|
-
|
|
92752
|
+
const stderrWindow = () => stderrHead === stderrTailBuffer || stderrTailBuffer === "" ? stderrHead : `${stderrHead}
|
|
92753
|
+
[...]
|
|
92754
|
+
${stderrTailBuffer}`;
|
|
92755
|
+
let exited = false;
|
|
92756
|
+
activeChildren.add(child);
|
|
92757
|
+
child.once("exit", () => {
|
|
92758
|
+
exited = true;
|
|
92759
|
+
activeChildren.delete(child);
|
|
92949
92760
|
});
|
|
92950
|
-
|
|
92951
|
-
|
|
92952
|
-
|
|
92953
|
-
|
|
92954
|
-
|
|
92955
|
-
|
|
92956
|
-
|
|
92957
|
-
|
|
92958
|
-
|
|
92959
|
-
|
|
92960
|
-
|
|
92761
|
+
const failed = () => exited ? `server exited before becoming ready. ${explainStartupFailure(stderrWindow())}` : void 0;
|
|
92762
|
+
try {
|
|
92763
|
+
const controlPort = await pollUntil(
|
|
92764
|
+
deadline,
|
|
92765
|
+
`control channel in ${options.workDir}`,
|
|
92766
|
+
// Several processes may announce a channel (clustered servers); accept
|
|
92767
|
+
// the first one that actually answers instead of trusting a filename.
|
|
92768
|
+
async () => {
|
|
92769
|
+
let entries;
|
|
92770
|
+
try {
|
|
92771
|
+
entries = (await readdir2(options.workDir)).filter(
|
|
92772
|
+
(entry) => entry.startsWith("control-") && entry.endsWith(".json")
|
|
92773
|
+
);
|
|
92774
|
+
} catch {
|
|
92775
|
+
return void 0;
|
|
92776
|
+
}
|
|
92777
|
+
for (const entry of entries) {
|
|
92778
|
+
try {
|
|
92779
|
+
const parsed = controlFileSchema.parse(
|
|
92780
|
+
JSON.parse(await readFile3(path3.join(options.workDir, entry), "utf8"))
|
|
92781
|
+
);
|
|
92782
|
+
const response = await fetch(`http://127.0.0.1:${parsed.port}/gc`);
|
|
92783
|
+
if (response.ok) {
|
|
92784
|
+
return parsed.port;
|
|
92785
|
+
}
|
|
92786
|
+
} catch {
|
|
92787
|
+
continue;
|
|
92788
|
+
}
|
|
92789
|
+
}
|
|
92790
|
+
return void 0;
|
|
92791
|
+
},
|
|
92792
|
+
failed
|
|
92793
|
+
);
|
|
92794
|
+
await pollUntil(
|
|
92795
|
+
deadline,
|
|
92796
|
+
`app on ${hostname}:${options.appPort}`,
|
|
92797
|
+
async () => {
|
|
92798
|
+
try {
|
|
92799
|
+
await fetch(`http://${hostname}:${options.appPort}/`, { method: "HEAD" });
|
|
92800
|
+
return true;
|
|
92801
|
+
} catch {
|
|
92802
|
+
return void 0;
|
|
92803
|
+
}
|
|
92804
|
+
},
|
|
92805
|
+
failed
|
|
92806
|
+
);
|
|
92807
|
+
return {
|
|
92808
|
+
pid: child.pid ?? -1,
|
|
92809
|
+
appPort: options.appPort,
|
|
92810
|
+
controlPort,
|
|
92811
|
+
explainExit: () => exited ? explainRuntimeFailure(stderrWindow(), options.maxOldSpaceMb ?? DEFAULT_MAX_OLD_SPACE_MB) : null,
|
|
92812
|
+
close: async () => {
|
|
92813
|
+
if (exited) {
|
|
92814
|
+
return;
|
|
92815
|
+
}
|
|
92816
|
+
const gone = new Promise((resolve) => child.once("exit", () => resolve()));
|
|
92817
|
+
child.kill("SIGTERM");
|
|
92818
|
+
const killTimer = setTimeout(() => child.kill("SIGKILL"), 3e3);
|
|
92819
|
+
await gone;
|
|
92820
|
+
clearTimeout(killTimer);
|
|
92821
|
+
}
|
|
92822
|
+
};
|
|
92823
|
+
} catch (cause) {
|
|
92824
|
+
child.kill("SIGKILL");
|
|
92825
|
+
throw cause;
|
|
92961
92826
|
}
|
|
92962
|
-
const verdict = effectiveVerdict(route);
|
|
92963
|
-
const curve = route.samples.map(formatMb).join(" \u2192 ");
|
|
92964
|
-
const resolved = route.resolvedWithCycles === void 0 ? "" : ` (resolved at ${route.resolvedWithCycles} cycles)`;
|
|
92965
|
-
return [
|
|
92966
|
-
` ${VERDICT_ICON[verdict]} ${route.route} ${verdict} (${formatGrowth(
|
|
92967
|
-
route.growthPer1000Requests
|
|
92968
|
-
)}) heap ${curve}${resolved}`,
|
|
92969
|
-
...revalidationLines(route),
|
|
92970
|
-
...abandonLines(route),
|
|
92971
|
-
...confidenceLines(route),
|
|
92972
|
-
...memorySourceLines(route, verdict),
|
|
92973
|
-
...peakPressureLines(route, parameters),
|
|
92974
|
-
...unreclaimedLines(route, verdict),
|
|
92975
|
-
...findingLines(route)
|
|
92976
|
-
];
|
|
92977
92827
|
}
|
|
92978
|
-
|
|
92979
|
-
|
|
92980
|
-
|
|
92981
|
-
|
|
92982
|
-
|
|
92828
|
+
|
|
92829
|
+
// src/cli-args.ts
|
|
92830
|
+
var BUILD_COMMAND = "build";
|
|
92831
|
+
var RUN_ONLY_FLAGS = [
|
|
92832
|
+
["routes", "--routes"],
|
|
92833
|
+
["cycles", "--cycles"],
|
|
92834
|
+
["requests", "--requests"],
|
|
92835
|
+
["connections", "--connections"],
|
|
92836
|
+
["idleSeconds", "--idle"],
|
|
92837
|
+
["warmupRequests", "--warmup"],
|
|
92838
|
+
["maxOldSpaceMb", "--max-old-space"],
|
|
92839
|
+
["quick", "--quick"],
|
|
92840
|
+
["noResolve", "--no-resolve"],
|
|
92841
|
+
["diffAll", "--diff-all"],
|
|
92842
|
+
["writeConfig", "--write-config"]
|
|
92843
|
+
];
|
|
92844
|
+
function runOnlyFlagUsed(options) {
|
|
92845
|
+
for (const [field, flag] of RUN_ONLY_FLAGS) {
|
|
92846
|
+
const value = options[field];
|
|
92847
|
+
if (value !== null && value !== false) {
|
|
92848
|
+
return flag;
|
|
92849
|
+
}
|
|
92983
92850
|
}
|
|
92984
|
-
|
|
92985
|
-
return [
|
|
92986
|
-
"",
|
|
92987
|
-
`${needConfig.length} route(s) need sample params. Write this to next-leak.config.json in the app directory:${editing}`,
|
|
92988
|
-
...skeleton.split("\n").map((line) => ` ${line}`)
|
|
92989
|
-
];
|
|
92851
|
+
return null;
|
|
92990
92852
|
}
|
|
92991
|
-
|
|
92992
|
-
|
|
92993
|
-
|
|
92994
|
-
|
|
92995
|
-
|
|
92996
|
-
|
|
92997
|
-
|
|
92998
|
-
|
|
92999
|
-
|
|
93000
|
-
|
|
93001
|
-
|
|
92853
|
+
var FLAGS = [
|
|
92854
|
+
{
|
|
92855
|
+
flag: "--routes",
|
|
92856
|
+
value: "list",
|
|
92857
|
+
argName: "<list>",
|
|
92858
|
+
help: "Only measure these routes \u2014 comma-separated templates or prefixes (e.g. /api,/dashboard)"
|
|
92859
|
+
},
|
|
92860
|
+
{ flag: "--cycles", value: "int", argName: "<n>", help: "Load cycles per route (default 4, minimum 3)" },
|
|
92861
|
+
{ flag: "--requests", value: "int", argName: "<n>", help: "Requests per cycle (default 5000)" },
|
|
92862
|
+
{ flag: "--connections", value: "int", argName: "<n>", help: "Concurrent connections (default 100)" },
|
|
92863
|
+
{ flag: "--idle", value: "int", argName: "<seconds>", help: "Idle seconds before each sample (default 30)" },
|
|
92864
|
+
{
|
|
92865
|
+
flag: "--warmup",
|
|
92866
|
+
value: "int",
|
|
92867
|
+
argName: "<n>",
|
|
92868
|
+
help: "Requests before the baseline snapshot (default 200) \u2014 lower it on apps that cache per request"
|
|
92869
|
+
},
|
|
92870
|
+
{
|
|
92871
|
+
flag: "--max-old-space",
|
|
92872
|
+
value: "int",
|
|
92873
|
+
argName: "<mb>",
|
|
92874
|
+
help: "Heap cap of each measured process (default 512) \u2014 raise it for apps whose working set is larger"
|
|
92875
|
+
},
|
|
92876
|
+
{
|
|
92877
|
+
flag: "--quick",
|
|
92878
|
+
value: "none",
|
|
92879
|
+
help: "Fast preset: 2000 requests x 4 cycles, 8s idle \u2014 same cycle count as the default, less traffic per cycle"
|
|
92880
|
+
},
|
|
92881
|
+
{ flag: "--diff-all", value: "none", help: "Diff snapshots for stable routes too (slow)" },
|
|
92882
|
+
{
|
|
92883
|
+
flag: "--attribute",
|
|
92884
|
+
value: "none",
|
|
92885
|
+
help: "build only: also name what the worker retains \u2014 signals it for heap snapshots, which can stall the build"
|
|
92886
|
+
},
|
|
92887
|
+
{
|
|
92888
|
+
flag: "--no-resolve",
|
|
92889
|
+
value: "none",
|
|
92890
|
+
help: "Do not re-measure inconclusive routes with more cycles (default: re-measure once)"
|
|
92891
|
+
},
|
|
92892
|
+
{ flag: "--output", value: "string", argName: "<dir>", help: "Where to write runs (default <app-dir>/.next-leak)" },
|
|
92893
|
+
{
|
|
92894
|
+
flag: "--write-config",
|
|
92895
|
+
value: "none",
|
|
92896
|
+
help: "Write next-leak.config.json for the routes that need sample params, then exit (never overwrites)"
|
|
92897
|
+
},
|
|
92898
|
+
{ flag: "--help", alias: "-h", value: "none", help: "Show this help" },
|
|
92899
|
+
{ flag: "--version", alias: "-v", value: "none", help: "Print the version" }
|
|
92900
|
+
];
|
|
92901
|
+
function helpText(version2) {
|
|
92902
|
+
const rows = FLAGS.map((spec) => {
|
|
92903
|
+
const alias = spec.alias ? `${spec.alias}, ` : "";
|
|
92904
|
+
const argName = spec.argName ? ` ${spec.argName}` : "";
|
|
92905
|
+
const left = `${alias}${spec.flag}${argName}`;
|
|
92906
|
+
return ` ${left.padEnd(26)}${spec.help}`;
|
|
92907
|
+
}).join("\n");
|
|
92908
|
+
return `next-leak ${version2}
|
|
92909
|
+
|
|
92910
|
+
Find out whether your Next.js app actually leaks memory \u2014 how much, on which
|
|
92911
|
+
route, and whose fault it is.
|
|
92912
|
+
|
|
92913
|
+
Usage:
|
|
92914
|
+
next-leak <app-dir> [options] measure a built server under load
|
|
92915
|
+
next-leak build <app-dir> measure the memory of "next build"
|
|
92916
|
+
|
|
92917
|
+
The app must be built with output: "standalone" (next build). For each
|
|
92918
|
+
discovered route, next-leak boots a fresh instrumented process and runs:
|
|
92919
|
+
warm-up \u2192 GC \u2192 baseline snapshot \u2192 [load \u2192 idle \u2192 GC \u2192 sample] \xD7 cycles \u2192
|
|
92920
|
+
snapshot. Evidence (report.html, ISSUE drafts, raw snapshots, run.json) is
|
|
92921
|
+
written under the output directory.
|
|
92922
|
+
|
|
92923
|
+
The build command needs neither a previous build nor standalone output: it runs
|
|
92924
|
+
the build itself and samples the resident memory of each static-generation
|
|
92925
|
+
worker, which is where large sites run out of heap while prerendering. It takes
|
|
92926
|
+
--output only; the options below shape HTTP load and do not apply to it.
|
|
92927
|
+
|
|
92928
|
+
Options:
|
|
92929
|
+
${rows}
|
|
92930
|
+
|
|
92931
|
+
Exit codes: 0 when the run completes (whatever the verdicts), 130 when
|
|
92932
|
+
interrupted, 1 on errors.
|
|
92933
|
+
`;
|
|
93002
92934
|
}
|
|
93003
|
-
|
|
93004
|
-
|
|
93005
|
-
|
|
93006
|
-
|
|
93007
|
-
|
|
93008
|
-
|
|
93009
|
-
|
|
93010
|
-
|
|
93011
|
-
|
|
93012
|
-
|
|
93013
|
-
|
|
93014
|
-
|
|
93015
|
-
|
|
93016
|
-
|
|
93017
|
-
|
|
93018
|
-
|
|
93019
|
-
|
|
93020
|
-
|
|
93021
|
-
`snapshots and run.json: ${report.workDir}`,
|
|
93022
|
-
`report: ${report.bundle.htmlReport}`,
|
|
93023
|
-
...report.bundle.issues.map((issue) => `issue draft (${issue.route}): ${issue.file}`)
|
|
93024
|
-
);
|
|
93025
|
-
const inconclusive = report.routes.filter(
|
|
93026
|
-
(route) => route.status === "measured" && effectiveVerdict(route) === "inconclusive"
|
|
93027
|
-
);
|
|
93028
|
-
if (inconclusive.length > 0) {
|
|
93029
|
-
const routeList = inconclusive.map((route) => route.route).join(",");
|
|
93030
|
-
const moreCycles = resolveCycles(report.parameters.cycles);
|
|
93031
|
-
lines.push(
|
|
93032
|
-
"",
|
|
93033
|
-
"hint: inconclusive means sustained sub-threshold growth \u2014 measure longer to resolve it:",
|
|
93034
|
-
` next-leak ${report.appDir} --routes ${routeList} --cycles ${moreCycles}`
|
|
93035
|
-
);
|
|
92935
|
+
var LIMITS = {
|
|
92936
|
+
"--cycles": 100,
|
|
92937
|
+
"--requests": 1e6,
|
|
92938
|
+
"--connections": 1e4,
|
|
92939
|
+
"--idle": 3600,
|
|
92940
|
+
"--warmup": 1e6,
|
|
92941
|
+
"--max-old-space": 65536
|
|
92942
|
+
};
|
|
92943
|
+
var MIN_MAX_OLD_SPACE_MB = 128;
|
|
92944
|
+
function findSpec(argument) {
|
|
92945
|
+
return FLAGS.find((spec) => spec.flag === argument || spec.alias === argument);
|
|
92946
|
+
}
|
|
92947
|
+
var flagError = (message) => ({ kind: "error", message });
|
|
92948
|
+
var FLAG_OK = { kind: "ok" };
|
|
92949
|
+
function applyRoutesFlag(value, options) {
|
|
92950
|
+
const routes = value.split(",").map((route) => route.trim()).filter((route) => route !== "");
|
|
92951
|
+
if (routes.length === 0) {
|
|
92952
|
+
return flagError(`option "--routes" needs at least one route`);
|
|
93036
92953
|
}
|
|
93037
|
-
|
|
92954
|
+
options.routes = routes;
|
|
92955
|
+
return FLAG_OK;
|
|
93038
92956
|
}
|
|
93039
|
-
|
|
93040
|
-
|
|
93041
|
-
|
|
93042
|
-
|
|
93043
|
-
let source = rawSource.replace(/^turbopack:\/+(?=\[)/, "");
|
|
93044
|
-
try {
|
|
93045
|
-
source = decodeURIComponent(source);
|
|
93046
|
-
} catch {
|
|
92957
|
+
function applyNumericFlag(flag, value, options) {
|
|
92958
|
+
const parsed = Number(value);
|
|
92959
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
92960
|
+
return flagError(`option "${flag}" needs a positive integer, got "${value}"`);
|
|
93047
92961
|
}
|
|
93048
|
-
const
|
|
93049
|
-
if (
|
|
93050
|
-
|
|
93051
|
-
|
|
93052
|
-
|
|
93053
|
-
if (packageName === "next" || packageName === "react" || packageName === "react-dom") {
|
|
93054
|
-
return { owner: "framework", source: null, packageName };
|
|
93055
|
-
}
|
|
93056
|
-
return { owner: "dependency", source: null, packageName };
|
|
92962
|
+
const maximum = LIMITS[flag];
|
|
92963
|
+
if (maximum !== void 0 && parsed > maximum) {
|
|
92964
|
+
return flagError(
|
|
92965
|
+
`option "${flag}" is capped at ${maximum} (got ${parsed}) \u2014 a run that large would never finish`
|
|
92966
|
+
);
|
|
93057
92967
|
}
|
|
93058
|
-
if (
|
|
93059
|
-
return
|
|
92968
|
+
if (flag === "--cycles" && parsed < 3) {
|
|
92969
|
+
return flagError("the trend verdict needs at least 3 cycles (--cycles 3 or more)");
|
|
93060
92970
|
}
|
|
93061
|
-
|
|
93062
|
-
|
|
93063
|
-
|
|
92971
|
+
if (flag === "--max-old-space" && parsed < MIN_MAX_OLD_SPACE_MB) {
|
|
92972
|
+
return flagError(
|
|
92973
|
+
`option "--max-old-space" needs at least ${MIN_MAX_OLD_SPACE_MB} MB (got ${parsed}) \u2014 below that the measured app cannot start`
|
|
92974
|
+
);
|
|
93064
92975
|
}
|
|
93065
|
-
|
|
92976
|
+
if (flag === "--cycles") options.cycles = parsed;
|
|
92977
|
+
if (flag === "--requests") options.requests = parsed;
|
|
92978
|
+
if (flag === "--connections") options.connections = parsed;
|
|
92979
|
+
if (flag === "--idle") options.idleSeconds = parsed;
|
|
92980
|
+
if (flag === "--warmup") options.warmupRequests = parsed;
|
|
92981
|
+
if (flag === "--max-old-space") options.maxOldSpaceMb = parsed;
|
|
92982
|
+
return FLAG_OK;
|
|
93066
92983
|
}
|
|
93067
|
-
|
|
93068
|
-
|
|
93069
|
-
|
|
93070
|
-
|
|
93071
|
-
|
|
93072
|
-
|
|
93073
|
-
|
|
93074
|
-
|
|
93075
|
-
|
|
93076
|
-
|
|
93077
|
-
|
|
93078
|
-
|
|
93079
|
-
|
|
93080
|
-
|
|
93081
|
-
|
|
93082
|
-
|
|
93083
|
-
|
|
93084
|
-
|
|
93085
|
-
|
|
93086
|
-
|
|
93087
|
-
|
|
93088
|
-
|
|
93089
|
-
|
|
92984
|
+
function applyFlag(spec, value, options) {
|
|
92985
|
+
switch (spec.flag) {
|
|
92986
|
+
case "--routes":
|
|
92987
|
+
return applyRoutesFlag(value, options);
|
|
92988
|
+
case "--cycles":
|
|
92989
|
+
case "--requests":
|
|
92990
|
+
case "--connections":
|
|
92991
|
+
case "--idle":
|
|
92992
|
+
case "--warmup":
|
|
92993
|
+
case "--max-old-space":
|
|
92994
|
+
return applyNumericFlag(spec.flag, value, options);
|
|
92995
|
+
case "--quick":
|
|
92996
|
+
options.quick = true;
|
|
92997
|
+
return FLAG_OK;
|
|
92998
|
+
case "--diff-all":
|
|
92999
|
+
options.diffAll = true;
|
|
93000
|
+
return FLAG_OK;
|
|
93001
|
+
case "--attribute":
|
|
93002
|
+
options.attributeBuild = true;
|
|
93003
|
+
return FLAG_OK;
|
|
93004
|
+
case "--write-config":
|
|
93005
|
+
options.writeConfig = true;
|
|
93006
|
+
return FLAG_OK;
|
|
93007
|
+
case "--no-resolve":
|
|
93008
|
+
options.noResolve = true;
|
|
93009
|
+
return FLAG_OK;
|
|
93010
|
+
case "--output":
|
|
93011
|
+
options.output = value;
|
|
93012
|
+
return FLAG_OK;
|
|
93013
|
+
default:
|
|
93014
|
+
return FLAG_OK;
|
|
93090
93015
|
}
|
|
93091
|
-
return null;
|
|
93092
93016
|
}
|
|
93093
|
-
function
|
|
93094
|
-
|
|
93095
|
-
|
|
93096
|
-
const source = registry.get(id);
|
|
93097
|
-
if (source === void 0) {
|
|
93098
|
-
continue;
|
|
93099
|
-
}
|
|
93100
|
-
const candidate = classifySource(source);
|
|
93101
|
-
if (OWNER_PRIORITY[candidate.owner] < OWNER_PRIORITY[best.owner]) {
|
|
93102
|
-
best = candidate;
|
|
93103
|
-
}
|
|
93017
|
+
function readFlagValue(spec, argv, index) {
|
|
93018
|
+
if (spec.value === "none") {
|
|
93019
|
+
return { value: "" };
|
|
93104
93020
|
}
|
|
93105
|
-
|
|
93106
|
-
|
|
93021
|
+
const value = argv[index + 1] ?? "";
|
|
93022
|
+
const looksNegativeNumber = /^-\d+(\.\d+)?$/.test(value);
|
|
93023
|
+
if (value === "" || value.startsWith("-") && !looksNegativeNumber) {
|
|
93024
|
+
return { error: `option "${spec.flag}" needs a value ${spec.argName ?? ""}` };
|
|
93107
93025
|
}
|
|
93108
|
-
return
|
|
93026
|
+
return { value };
|
|
93109
93027
|
}
|
|
93110
|
-
function
|
|
93111
|
-
const
|
|
93112
|
-
const
|
|
93113
|
-
|
|
93114
|
-
|
|
93115
|
-
for (const [index, attribution] of findings.entries()) {
|
|
93116
|
-
if (attribution.owner === "unattributed") {
|
|
93117
|
-
continue;
|
|
93118
|
-
}
|
|
93119
|
-
const bytes = all[index]?.retainedBytes ?? 0;
|
|
93120
|
-
attributedBytes += bytes;
|
|
93121
|
-
const key = `${attribution.owner}|${attribution.source ?? ""}|${attribution.packageName ?? ""}`;
|
|
93122
|
-
const group = byGroup.get(key) ?? { attribution, bytes: 0 };
|
|
93123
|
-
group.bytes += bytes;
|
|
93124
|
-
byGroup.set(key, group);
|
|
93028
|
+
function parseFlagAt(argv, index, options) {
|
|
93029
|
+
const argument = argv[index] ?? "";
|
|
93030
|
+
const spec = findSpec(argument);
|
|
93031
|
+
if (spec === void 0) {
|
|
93032
|
+
return { done: { kind: "error", message: `unknown option "${argument}" \u2014 see --help` } };
|
|
93125
93033
|
}
|
|
93126
|
-
|
|
93127
|
-
|
|
93128
|
-
if (winner === null || group.bytes > winner.bytes) {
|
|
93129
|
-
winner = group;
|
|
93130
|
-
}
|
|
93034
|
+
if (spec.flag === "--help") {
|
|
93035
|
+
return { done: { kind: "help" } };
|
|
93131
93036
|
}
|
|
93132
|
-
|
|
93133
|
-
|
|
93134
|
-
}
|
|
93135
|
-
|
|
93136
|
-
|
|
93137
|
-
|
|
93138
|
-
|
|
93139
|
-
|
|
93140
|
-
|
|
93141
|
-
|
|
93142
|
-
return {
|
|
93143
|
-
nodeVersion: process.version,
|
|
93144
|
-
platform: os.platform(),
|
|
93145
|
-
arch: os.arch(),
|
|
93146
|
-
cpuModel: os.cpus()[0]?.model ?? null,
|
|
93147
|
-
totalMemoryBytes: os.totalmem(),
|
|
93148
|
-
nextVersion,
|
|
93149
|
-
nextLeakVersion
|
|
93150
|
-
};
|
|
93151
|
-
}
|
|
93152
|
-
|
|
93153
|
-
// src/heap-diff.ts
|
|
93154
|
-
var import_core = __toESM(require_dist(), 1);
|
|
93155
|
-
var import_heap_analysis = __toESM(require_dist4(), 1);
|
|
93156
|
-
var DEFAULTS = {
|
|
93157
|
-
minTypeDeltaBytes: 20 * 1024,
|
|
93158
|
-
grownThresholdBytes: 100 * 1024,
|
|
93159
|
-
newThresholdBytes: 2 * 1024,
|
|
93160
|
-
bigRetainedFloorBytes: 50 * 1024,
|
|
93161
|
-
maxFindings: 10,
|
|
93162
|
-
chainDepth: 7
|
|
93163
|
-
};
|
|
93164
|
-
function summarizeBaseline(heap, options = {}) {
|
|
93165
|
-
const resolved = { ...DEFAULTS, ...options };
|
|
93166
|
-
const summary = {
|
|
93167
|
-
nodeIds: /* @__PURE__ */ new Set(),
|
|
93168
|
-
bigRetained: /* @__PURE__ */ new Map(),
|
|
93169
|
-
typeSelfSizes: /* @__PURE__ */ new Map()
|
|
93170
|
-
};
|
|
93171
|
-
heap.nodes.forEach((node) => {
|
|
93172
|
-
summary.nodeIds.add(node.id);
|
|
93173
|
-
if (node.retainedSize >= resolved.bigRetainedFloorBytes) {
|
|
93174
|
-
summary.bigRetained.set(node.id, node.retainedSize);
|
|
93175
|
-
}
|
|
93176
|
-
summary.typeSelfSizes.set(
|
|
93177
|
-
node.type,
|
|
93178
|
-
(summary.typeSelfSizes.get(node.type) ?? 0) + node.self_size
|
|
93179
|
-
);
|
|
93180
|
-
});
|
|
93181
|
-
return summary;
|
|
93182
|
-
}
|
|
93183
|
-
function truncateLabel(value, max2 = 60) {
|
|
93184
|
-
return value.replaceAll("\n", " ").slice(0, max2);
|
|
93185
|
-
}
|
|
93186
|
-
function walkChain(node, depth) {
|
|
93187
|
-
const parts = [];
|
|
93188
|
-
const nodes = [node];
|
|
93189
|
-
let current = node;
|
|
93190
|
-
const seen = /* @__PURE__ */ new Set([node.id]);
|
|
93191
|
-
for (let i = 0; i < depth; i += 1) {
|
|
93192
|
-
const referrers = current.referrers;
|
|
93193
|
-
if (referrers.length === 0) {
|
|
93194
|
-
break;
|
|
93195
|
-
}
|
|
93196
|
-
const edge = referrers.find(
|
|
93197
|
-
(candidate) => !seen.has(candidate.fromNode.id) && candidate.fromNode.type !== "synthetic" && candidate.type !== "weak"
|
|
93198
|
-
) ?? referrers[0];
|
|
93199
|
-
if (edge === void 0 || seen.has(edge.fromNode.id)) {
|
|
93200
|
-
break;
|
|
93201
|
-
}
|
|
93202
|
-
current = edge.fromNode;
|
|
93203
|
-
seen.add(current.id);
|
|
93204
|
-
nodes.push(current);
|
|
93205
|
-
parts.push(
|
|
93206
|
-
`${truncateLabel(current.name)}#${current.type}[.${truncateLabel(
|
|
93207
|
-
String(edge.name_or_index),
|
|
93208
|
-
40
|
|
93209
|
-
)}]`
|
|
93210
|
-
);
|
|
93037
|
+
if (spec.flag === "--version") {
|
|
93038
|
+
return { done: { kind: "version" } };
|
|
93039
|
+
}
|
|
93040
|
+
const read2 = readFlagValue(spec, argv, index);
|
|
93041
|
+
if ("error" in read2) {
|
|
93042
|
+
return { done: { kind: "error", message: read2.error } };
|
|
93043
|
+
}
|
|
93044
|
+
const outcome = applyFlag(spec, read2.value, options);
|
|
93045
|
+
if (outcome.kind === "error") {
|
|
93046
|
+
return { done: { kind: "error", message: outcome.message } };
|
|
93211
93047
|
}
|
|
93212
|
-
return {
|
|
93213
|
-
}
|
|
93214
|
-
function retainerChain(node, depth) {
|
|
93215
|
-
return walkChain(node, depth).parts.join(" <- ");
|
|
93048
|
+
return { consumed: spec.value === "none" ? 0 : 1 };
|
|
93216
93049
|
}
|
|
93217
|
-
function
|
|
93218
|
-
const
|
|
93219
|
-
|
|
93220
|
-
|
|
93221
|
-
|
|
93222
|
-
|
|
93223
|
-
|
|
93050
|
+
function parseCliArgs(argv) {
|
|
93051
|
+
const isBuild = argv[0] === BUILD_COMMAND;
|
|
93052
|
+
const rest = isBuild ? argv.slice(1) : argv;
|
|
93053
|
+
const options = {
|
|
93054
|
+
appDir: "",
|
|
93055
|
+
routes: null,
|
|
93056
|
+
cycles: null,
|
|
93057
|
+
requests: null,
|
|
93058
|
+
connections: null,
|
|
93059
|
+
idleSeconds: null,
|
|
93060
|
+
warmupRequests: null,
|
|
93061
|
+
maxOldSpaceMb: null,
|
|
93062
|
+
quick: false,
|
|
93063
|
+
noResolve: false,
|
|
93064
|
+
diffAll: false,
|
|
93065
|
+
attributeBuild: false,
|
|
93066
|
+
writeConfig: false,
|
|
93067
|
+
output: null
|
|
93068
|
+
};
|
|
93069
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
93070
|
+
const argument = rest[index] ?? "";
|
|
93071
|
+
if (!argument.startsWith("-")) {
|
|
93072
|
+
if (options.appDir !== "") {
|
|
93073
|
+
return { kind: "error", message: `unexpected extra argument "${argument}" \u2014 one app directory only` };
|
|
93074
|
+
}
|
|
93075
|
+
options.appDir = argument;
|
|
93224
93076
|
continue;
|
|
93225
93077
|
}
|
|
93226
|
-
|
|
93227
|
-
|
|
93228
|
-
|
|
93229
|
-
}
|
|
93230
|
-
const id = Number(edge.name_or_index);
|
|
93231
|
-
if (Number.isInteger(id) && !ids.includes(id)) {
|
|
93232
|
-
ids.push(id);
|
|
93233
|
-
}
|
|
93078
|
+
const step = parseFlagAt(rest, index, options);
|
|
93079
|
+
if ("done" in step) {
|
|
93080
|
+
return step.done;
|
|
93234
93081
|
}
|
|
93082
|
+
index += step.consumed;
|
|
93235
93083
|
}
|
|
93236
|
-
|
|
93237
|
-
}
|
|
93238
|
-
|
|
93239
|
-
|
|
93240
|
-
|
|
93241
|
-
|
|
93242
|
-
|
|
93243
|
-
|
|
93244
|
-
|
|
93245
|
-
|
|
93246
|
-
(afterTypeSelfSizes.get(node.type) ?? 0) + node.self_size
|
|
93247
|
-
);
|
|
93248
|
-
if (node.type === "synthetic") {
|
|
93249
|
-
return;
|
|
93084
|
+
if (options.appDir === "") {
|
|
93085
|
+
return { kind: "help" };
|
|
93086
|
+
}
|
|
93087
|
+
if (isBuild) {
|
|
93088
|
+
const misplaced = runOnlyFlagUsed(options);
|
|
93089
|
+
if (misplaced !== null) {
|
|
93090
|
+
return {
|
|
93091
|
+
kind: "error",
|
|
93092
|
+
message: `option "${misplaced}" shapes HTTP load and does not apply to "next-leak build" \u2014 see --help`
|
|
93093
|
+
};
|
|
93250
93094
|
}
|
|
93251
|
-
|
|
93252
|
-
|
|
93253
|
-
|
|
93254
|
-
|
|
93255
|
-
|
|
93256
|
-
|
|
93257
|
-
name: truncateLabel(node.name),
|
|
93258
|
-
retainedBytes: node.retainedSize,
|
|
93259
|
-
retainerChain: chain.parts.join(" <- "),
|
|
93260
|
-
moduleIds: collectModuleIds(chain.nodes)
|
|
93261
|
-
});
|
|
93095
|
+
return {
|
|
93096
|
+
kind: "build",
|
|
93097
|
+
options: {
|
|
93098
|
+
appDir: options.appDir,
|
|
93099
|
+
output: options.output,
|
|
93100
|
+
attributeBuild: options.attributeBuild
|
|
93262
93101
|
}
|
|
93263
|
-
|
|
93264
|
-
}
|
|
93265
|
-
const before = baseline.bigRetained.get(node.id);
|
|
93266
|
-
if (before !== void 0 && node.retainedSize - before >= resolved.grownThresholdBytes) {
|
|
93267
|
-
const chain = walkChain(node, resolved.chainDepth);
|
|
93268
|
-
grown.push({
|
|
93269
|
-
kind: "grown",
|
|
93270
|
-
nodeType: node.type,
|
|
93271
|
-
name: truncateLabel(node.name),
|
|
93272
|
-
retainedBytes: node.retainedSize - before,
|
|
93273
|
-
retainerChain: chain.parts.join(" <- "),
|
|
93274
|
-
moduleIds: collectModuleIds(chain.nodes)
|
|
93275
|
-
});
|
|
93276
|
-
}
|
|
93277
|
-
});
|
|
93278
|
-
const typeDeltas = [];
|
|
93279
|
-
const allTypes = /* @__PURE__ */ new Set([...baseline.typeSelfSizes.keys(), ...afterTypeSelfSizes.keys()]);
|
|
93280
|
-
for (const type of allTypes) {
|
|
93281
|
-
const delta = (afterTypeSelfSizes.get(type) ?? 0) - (baseline.typeSelfSizes.get(type) ?? 0);
|
|
93282
|
-
if (Math.abs(delta) >= resolved.minTypeDeltaBytes) {
|
|
93283
|
-
typeDeltas.push({ type, deltaBytes: delta });
|
|
93284
|
-
}
|
|
93102
|
+
};
|
|
93285
93103
|
}
|
|
93286
|
-
|
|
93287
|
-
|
|
93288
|
-
|
|
93104
|
+
return { kind: "run", options };
|
|
93105
|
+
}
|
|
93106
|
+
|
|
93107
|
+
// src/guards.ts
|
|
93108
|
+
function currentFacts() {
|
|
93289
93109
|
return {
|
|
93290
|
-
|
|
93291
|
-
|
|
93292
|
-
newNodes: fresh.slice(0, resolved.maxFindings)
|
|
93110
|
+
nodeMajor: Number(process.versions.node.split(".")[0]),
|
|
93111
|
+
platform: process.platform
|
|
93293
93112
|
};
|
|
93294
93113
|
}
|
|
93295
|
-
|
|
93114
|
+
function checkRuntime(facts = currentFacts()) {
|
|
93115
|
+
if (facts.platform === "win32") {
|
|
93116
|
+
return "next-leak does not support Windows (the measurement ritual relies on POSIX process control). Run it inside WSL2 or a Linux container.";
|
|
93117
|
+
}
|
|
93118
|
+
if (facts.nodeMajor < 22) {
|
|
93119
|
+
return `next-leak needs Node.js >= 22 (you are on ${process.versions.node}). It relies on --expose-gc semantics and heap snapshot behavior verified on modern V8. Upgrade Node and retry.`;
|
|
93120
|
+
}
|
|
93121
|
+
return null;
|
|
93122
|
+
}
|
|
93123
|
+
|
|
93124
|
+
// src/route-config.ts
|
|
93125
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
93126
|
+
import path4 from "path";
|
|
93127
|
+
import { z as z4 } from "zod";
|
|
93128
|
+
var sampleValueSchema = z4.string().refine(
|
|
93129
|
+
(value) => !(value.includes("{n}") && /\{n%\d+\}/.test(value)),
|
|
93130
|
+
{ message: 'a sample value cannot carry both "{n}" and "{n%N}" \u2014 pick one' }
|
|
93131
|
+
);
|
|
93132
|
+
var routeConfigSchema = z4.object({
|
|
93133
|
+
params: z4.record(z4.string(), sampleValueSchema).optional(),
|
|
93134
|
+
routes: z4.record(z4.string(), z4.record(z4.string(), sampleValueSchema)).optional(),
|
|
93135
|
+
/**
|
|
93136
|
+
* Headers sent with every request. Real traffic is not header-less:
|
|
93137
|
+
* compression (`Accept-Encoding`), sessions and auth change which code
|
|
93138
|
+
* paths run, and some leaks only exist on those paths (zlib, per-session
|
|
93139
|
+
* caches). Measuring without them measures a different app.
|
|
93140
|
+
*/
|
|
93141
|
+
headers: z4.record(z4.string(), z4.string()).optional(),
|
|
93142
|
+
/** Query string appended per route template, e.g. `"weightKb=2048"`. */
|
|
93143
|
+
query: z4.record(z4.string(), z4.string()).optional(),
|
|
93144
|
+
/**
|
|
93145
|
+
* Simulates clients that give up before the response arrives (closed
|
|
93146
|
+
* tabs, load-balancer timeouts, bots). Several real leaks only exist on
|
|
93147
|
+
* that path — vercel/next.js#89091 traces `ServerResponse` retention to
|
|
93148
|
+
* an early disconnect — and a load generator that always waits politely
|
|
93149
|
+
* never reaches it.
|
|
93150
|
+
*
|
|
93151
|
+
* Counted **from the first byte of the response**, not from the request:
|
|
93152
|
+
* under load a server's first byte arrives long after any sane fixed
|
|
93153
|
+
* window, so a request-relative clock cuts before the stream starts and
|
|
93154
|
+
* tests the wrong path entirely.
|
|
93155
|
+
*/
|
|
93156
|
+
abandonAfterMs: z4.number().int().positive().optional(),
|
|
93157
|
+
/**
|
|
93158
|
+
* Where the abandon deadline starts. `first-byte` is the default and the
|
|
93159
|
+
* semantics everything else assumes.
|
|
93160
|
+
*
|
|
93161
|
+
* `request` starts it when the request is written, which is the only way
|
|
93162
|
+
* to reach leaks that need the client already gone before the server
|
|
93163
|
+
* produces anything: against a route whose time-to-first-byte exceeds the
|
|
93164
|
+
* deadline, a first-byte clock never cuts early. Measured on the
|
|
93165
|
+
* vercel/next.js#84648 repro, whose upstream answers in 400 ms while its
|
|
93166
|
+
* own load generator cuts at 60 ms — the first-byte clock reached 7% of
|
|
93167
|
+
* requests there, none of them on the path the issue describes.
|
|
93168
|
+
*
|
|
93169
|
+
* Opt-in per route, never a default: armed on connect, the deadline loses
|
|
93170
|
+
* the race to the first byte on fast routes, which is how the earlier
|
|
93171
|
+
* version of this measured 2 mid-stream cuts out of ~1500 on #94919.
|
|
93172
|
+
*/
|
|
93173
|
+
abandonFrom: z4.enum(["first-byte", "request"]).optional()
|
|
93174
|
+
}).strict().refine((config) => config.abandonFrom === void 0 || config.abandonAfterMs !== void 0, {
|
|
93175
|
+
message: '"abandonFrom" configures nothing without "abandonAfterMs"',
|
|
93176
|
+
path: ["abandonFrom"]
|
|
93177
|
+
});
|
|
93178
|
+
var RouteConfigError = class extends Error {
|
|
93296
93179
|
constructor(message) {
|
|
93297
93180
|
super(message);
|
|
93298
|
-
this.name = "
|
|
93181
|
+
this.name = "RouteConfigError";
|
|
93299
93182
|
}
|
|
93300
93183
|
};
|
|
93301
|
-
var
|
|
93302
|
-
|
|
93303
|
-
|
|
93304
|
-
|
|
93305
|
-
const { constants } = await import("buffer");
|
|
93306
|
-
let size;
|
|
93184
|
+
var ROUTE_CONFIG_FILE = "next-leak.config.json";
|
|
93185
|
+
async function loadRouteConfig(appDir) {
|
|
93186
|
+
const file = path4.join(appDir, ROUTE_CONFIG_FILE);
|
|
93187
|
+
let raw;
|
|
93307
93188
|
try {
|
|
93308
|
-
|
|
93189
|
+
raw = await readFile4(file, "utf8");
|
|
93309
93190
|
} catch {
|
|
93310
|
-
|
|
93311
|
-
}
|
|
93312
|
-
if (size === 0) {
|
|
93313
|
-
throw new SnapshotError(`heap snapshot is empty: ${file}`);
|
|
93191
|
+
return {};
|
|
93314
93192
|
}
|
|
93315
|
-
|
|
93316
|
-
|
|
93317
|
-
|
|
93193
|
+
try {
|
|
93194
|
+
return routeConfigSchema.parse(JSON.parse(raw));
|
|
93195
|
+
} catch (cause) {
|
|
93196
|
+
throw new RouteConfigError(
|
|
93197
|
+
`${file} is invalid: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
93318
93198
|
);
|
|
93319
93199
|
}
|
|
93320
|
-
|
|
93321
|
-
|
|
93322
|
-
|
|
93323
|
-
|
|
93324
|
-
|
|
93325
|
-
|
|
93326
|
-
|
|
93327
|
-
|
|
93200
|
+
}
|
|
93201
|
+
var SEGMENT_PATTERN = /^\[(\[)?(\.\.\.)?([^\]]+?)\]?\]$/;
|
|
93202
|
+
function dynamicSegmentsOf(routeTemplate) {
|
|
93203
|
+
const segments = [];
|
|
93204
|
+
for (const segment of routeTemplate.split("/")) {
|
|
93205
|
+
const match = SEGMENT_PATTERN.exec(segment);
|
|
93206
|
+
if (match === null) {
|
|
93207
|
+
continue;
|
|
93328
93208
|
}
|
|
93329
|
-
|
|
93330
|
-
|
|
93331
|
-
|
|
93332
|
-
|
|
93333
|
-
|
|
93334
|
-
|
|
93209
|
+
segments.push({
|
|
93210
|
+
name: match[3] ?? "",
|
|
93211
|
+
catchAll: match[2] !== void 0,
|
|
93212
|
+
optional: match[1] !== void 0
|
|
93213
|
+
});
|
|
93214
|
+
}
|
|
93215
|
+
return segments;
|
|
93216
|
+
}
|
|
93217
|
+
var UNIQUE_MARKER = "{n}";
|
|
93218
|
+
var BOUNDED_MARKER = /\{n%(\d+)\}/;
|
|
93219
|
+
function boundedMarkerOf(value) {
|
|
93220
|
+
const match = BOUNDED_MARKER.exec(value);
|
|
93221
|
+
const bound = Number(match?.[1]);
|
|
93222
|
+
if (match === null || !Number.isInteger(bound) || bound <= 0) {
|
|
93223
|
+
return null;
|
|
93224
|
+
}
|
|
93225
|
+
return { marker: match[0], bound };
|
|
93226
|
+
}
|
|
93227
|
+
var ENCODED_MARKER = encodeURIComponent(UNIQUE_MARKER);
|
|
93228
|
+
var encodeSegment = (value) => restoreMarkers(encodeURIComponent(value).split(ENCODED_MARKER).join(UNIQUE_MARKER));
|
|
93229
|
+
var ENCODED_BOUNDED = /%7Bn%25(\d+)%7D/gi;
|
|
93230
|
+
var restoreMarkers = (value) => value.replace(ENCODED_BOUNDED, (_match, bound) => `{n%${bound}}`);
|
|
93231
|
+
var encodeCatchAll = (value) => value.split("/").map(encodeSegment).join("/");
|
|
93232
|
+
function resolveRoutePath(routeTemplate, config) {
|
|
93233
|
+
const resolved = [];
|
|
93234
|
+
for (const segment of routeTemplate.split("/")) {
|
|
93235
|
+
const match = SEGMENT_PATTERN.exec(segment);
|
|
93236
|
+
if (match === null) {
|
|
93237
|
+
resolved.push(encodeSegment(segment));
|
|
93238
|
+
continue;
|
|
93239
|
+
}
|
|
93240
|
+
const optional = match[1] !== void 0;
|
|
93241
|
+
const catchAll = match[2] !== void 0;
|
|
93242
|
+
const name = match[3] ?? "";
|
|
93243
|
+
const value = config.routes?.[routeTemplate]?.[name] ?? config.params?.[name];
|
|
93244
|
+
if (value !== void 0) {
|
|
93245
|
+
resolved.push(catchAll ? encodeCatchAll(value) : encodeSegment(value));
|
|
93246
|
+
} else if (!optional) {
|
|
93247
|
+
return null;
|
|
93335
93248
|
}
|
|
93336
|
-
} finally {
|
|
93337
|
-
await handle.close();
|
|
93338
93249
|
}
|
|
93339
|
-
|
|
93340
|
-
|
|
93341
|
-
|
|
93342
|
-
|
|
93343
|
-
return (0, import_heap_analysis.getFullHeapFromFile)(file);
|
|
93344
|
-
};
|
|
93345
|
-
async function diffSnapshotFiles(baselineFile, afterFile, options = {}, loadHeap = defaultLoader) {
|
|
93346
|
-
const summary = summarizeBaseline(await loadHeap(baselineFile), options);
|
|
93347
|
-
const after = await loadHeap(afterFile);
|
|
93348
|
-
return diffAgainstBaseline(summary, after, options);
|
|
93250
|
+
const joined = resolved.join("/");
|
|
93251
|
+
const path7 = joined === "" ? "/" : joined;
|
|
93252
|
+
const query = config.query?.[routeTemplate];
|
|
93253
|
+
return query === void 0 || query === "" ? path7 : `${path7}?${query}`;
|
|
93349
93254
|
}
|
|
93350
93255
|
|
|
93351
|
-
// src/
|
|
93352
|
-
var
|
|
93353
|
-
|
|
93354
|
-
|
|
93355
|
-
|
|
93356
|
-
|
|
93357
|
-
|
|
93358
|
-
|
|
93359
|
-
|
|
93360
|
-
|
|
93361
|
-
|
|
93256
|
+
// src/route-guidance.ts
|
|
93257
|
+
var PLACEHOLDER = "REPLACE-ME";
|
|
93258
|
+
function sampleValuesFromManifest(manifest, routeTemplate) {
|
|
93259
|
+
const routes = manifest?.routes;
|
|
93260
|
+
if (routes === void 0) {
|
|
93261
|
+
return null;
|
|
93262
|
+
}
|
|
93263
|
+
const concrete = Object.entries(routes).find(([, entry]) => entry.srcRoute === routeTemplate)?.[0];
|
|
93264
|
+
if (concrete === void 0) {
|
|
93265
|
+
return null;
|
|
93266
|
+
}
|
|
93267
|
+
const templateSegments = routeTemplate.split("/");
|
|
93268
|
+
const concreteSegments = concrete.split("/");
|
|
93269
|
+
const values = {};
|
|
93270
|
+
for (const [index, segment] of templateSegments.entries()) {
|
|
93271
|
+
const dynamic = dynamicSegmentsOf(segment);
|
|
93272
|
+
const parameter = dynamic[0];
|
|
93273
|
+
if (parameter === void 0) {
|
|
93274
|
+
continue;
|
|
93362
93275
|
}
|
|
93363
|
-
value
|
|
93364
|
-
if (
|
|
93365
|
-
|
|
93366
|
-
} else {
|
|
93367
|
-
values.push((value & 1) === 1 ? -(value >>> 1) : value >>> 1);
|
|
93368
|
-
value = 0;
|
|
93369
|
-
shift = 0;
|
|
93276
|
+
const value = parameter.catchAll ? concreteSegments.slice(index).join("/") : concreteSegments[index];
|
|
93277
|
+
if (value === void 0 || value === "") {
|
|
93278
|
+
return null;
|
|
93370
93279
|
}
|
|
93280
|
+
values[parameter.name] = value;
|
|
93371
93281
|
}
|
|
93372
|
-
return values;
|
|
93282
|
+
return Object.keys(values).length === 0 ? null : values;
|
|
93373
93283
|
}
|
|
93374
|
-
function
|
|
93375
|
-
const
|
|
93376
|
-
for (const
|
|
93377
|
-
|
|
93378
|
-
|
|
93284
|
+
function renderConfigSkeleton(routeTemplates, manifest = void 0) {
|
|
93285
|
+
const routes = {};
|
|
93286
|
+
for (const template of routeTemplates) {
|
|
93287
|
+
const segments = dynamicSegmentsOf(template);
|
|
93288
|
+
if (segments.length === 0) {
|
|
93289
|
+
continue;
|
|
93290
|
+
}
|
|
93291
|
+
const sampled = sampleValuesFromManifest(manifest, template);
|
|
93292
|
+
const values = {};
|
|
93293
|
+
for (const segment of segments) {
|
|
93294
|
+
values[segment.name] = sampled?.[segment.name] ?? PLACEHOLDER;
|
|
93379
93295
|
}
|
|
93296
|
+
routes[template] = values;
|
|
93380
93297
|
}
|
|
93381
|
-
return
|
|
93298
|
+
return Object.keys(routes).length === 0 ? null : JSON.stringify({ routes }, null, 2);
|
|
93382
93299
|
}
|
|
93383
|
-
function
|
|
93384
|
-
|
|
93385
|
-
|
|
93386
|
-
|
|
93387
|
-
|
|
93388
|
-
|
|
93389
|
-
|
|
93390
|
-
|
|
93391
|
-
|
|
93392
|
-
|
|
93393
|
-
|
|
93394
|
-
|
|
93395
|
-
|
|
93396
|
-
|
|
93397
|
-
|
|
93398
|
-
|
|
93300
|
+
function hasPlaceholders(skeleton) {
|
|
93301
|
+
return skeleton.includes(PLACEHOLDER);
|
|
93302
|
+
}
|
|
93303
|
+
|
|
93304
|
+
// src/unreclaimed-retention.ts
|
|
93305
|
+
var MB = 1024 * 1024;
|
|
93306
|
+
var CLASSES = ["heap", "external", "arrayBuffers"];
|
|
93307
|
+
var MIN_GAP_BYTES = 2 * MB;
|
|
93308
|
+
var MIN_GAP_RATIO = 0.35;
|
|
93309
|
+
var MIN_RETAINED_BYTES = MB;
|
|
93310
|
+
var read = (sample, memoryClass) => memoryClass === "heap" ? sample.heapUsed : memoryClass === "external" ? sample.external : sample.arrayBuffers;
|
|
93311
|
+
function largestGap(values) {
|
|
93312
|
+
return values.reduce((highest, value) => Math.max(highest, value), 0);
|
|
93313
|
+
}
|
|
93314
|
+
function gapsFor(input, memoryClass) {
|
|
93315
|
+
const gaps = [];
|
|
93316
|
+
for (const [index, held] of input.unreclaimedSamples.entries()) {
|
|
93317
|
+
const afterGc = input.memorySamples[index + 1];
|
|
93318
|
+
if (afterGc === void 0) {
|
|
93319
|
+
break;
|
|
93399
93320
|
}
|
|
93400
|
-
|
|
93321
|
+
gaps.push(read(held, memoryClass) - read(afterGc, memoryClass));
|
|
93401
93322
|
}
|
|
93402
|
-
return
|
|
93323
|
+
return gaps;
|
|
93403
93324
|
}
|
|
93404
|
-
function
|
|
93405
|
-
|
|
93406
|
-
|
|
93407
|
-
return void 0;
|
|
93325
|
+
function assessUnreclaimedRetention(input) {
|
|
93326
|
+
if (input.verdict === "leak" && input.verdictIsWellSupported || input.unreclaimedSamples.length === 0) {
|
|
93327
|
+
return null;
|
|
93408
93328
|
}
|
|
93409
|
-
let
|
|
93410
|
-
for (const
|
|
93411
|
-
|
|
93412
|
-
|
|
93329
|
+
let worst = null;
|
|
93330
|
+
for (const memoryClass of CLASSES) {
|
|
93331
|
+
const gaps = gapsFor(input, memoryClass);
|
|
93332
|
+
if (gaps.length === 0) {
|
|
93333
|
+
continue;
|
|
93334
|
+
}
|
|
93335
|
+
const largestGapBytes = largestGap(gaps);
|
|
93336
|
+
if (largestGapBytes < MIN_GAP_BYTES) {
|
|
93337
|
+
continue;
|
|
93338
|
+
}
|
|
93339
|
+
const retainedBytes = largestGap(
|
|
93340
|
+
input.memorySamples.slice(1).map((sample) => read(sample, memoryClass))
|
|
93341
|
+
);
|
|
93342
|
+
const ratio = largestGapBytes / Math.max(retainedBytes, MIN_RETAINED_BYTES);
|
|
93343
|
+
if (ratio < MIN_GAP_RATIO) {
|
|
93344
|
+
continue;
|
|
93345
|
+
}
|
|
93346
|
+
if (worst === null || ratio >= worst.ratio) {
|
|
93347
|
+
worst = { class: memoryClass, largestGapBytes, retainedBytes, ratio };
|
|
93413
93348
|
}
|
|
93414
|
-
found = entry.sourceIndex;
|
|
93415
93349
|
}
|
|
93416
|
-
return
|
|
93350
|
+
return worst;
|
|
93351
|
+
}
|
|
93352
|
+
var mb = (bytes) => `${(bytes / MB).toFixed(2)} MB`;
|
|
93353
|
+
var CLASS_LABEL = {
|
|
93354
|
+
heap: "heap",
|
|
93355
|
+
external: "external memory",
|
|
93356
|
+
arrayBuffers: "arrayBuffers"
|
|
93357
|
+
};
|
|
93358
|
+
function describeUnreclaimedRetention(retention) {
|
|
93359
|
+
return `held ${mb(retention.largestGapBytes)} of ${CLASS_LABEL[retention.class]} between collections that a GC took back (${retention.ratio.toFixed(1)}x what it retains) \u2014 a forced GC reclaims this, a long-running process may not run one often enough to; read seconds after load, so it also includes memory not yet collected`;
|
|
93417
93360
|
}
|
|
93418
93361
|
|
|
93419
|
-
// src/
|
|
93420
|
-
|
|
93421
|
-
|
|
93422
|
-
|
|
93423
|
-
|
|
93424
|
-
|
|
93425
|
-
|
|
93426
|
-
|
|
93427
|
-
|
|
93428
|
-
var
|
|
93429
|
-
|
|
93430
|
-
|
|
93431
|
-
|
|
93432
|
-
|
|
93433
|
-
|
|
93434
|
-
|
|
93435
|
-
)
|
|
93436
|
-
});
|
|
93437
|
-
function normalizeSourceMap(raw) {
|
|
93438
|
-
const flat = flatMapSchema.safeParse(raw);
|
|
93439
|
-
if (flat.success) {
|
|
93440
|
-
return { sources: flat.data.sources, lines: decodeMappings(flat.data.mappings) };
|
|
93362
|
+
// src/report.ts
|
|
93363
|
+
var MB2 = 1024 * 1024;
|
|
93364
|
+
var formatMb = (bytes) => `${(bytes / MB2).toFixed(1)} MB`;
|
|
93365
|
+
var formatGrowth = (bytes) => {
|
|
93366
|
+
const sign = bytes >= 0 ? "+" : "";
|
|
93367
|
+
return `${sign}${(bytes / MB2).toFixed(2)} MB/1000 req`;
|
|
93368
|
+
};
|
|
93369
|
+
var VERDICT_ICON = { leak: "\u2716", stable: "\u2714", inconclusive: "?" };
|
|
93370
|
+
var RSS_MIN_GROWTH_PER_CYCLE = 16 * MB2;
|
|
93371
|
+
var RSS_MIN_TOTAL_GROWTH = 64 * MB2;
|
|
93372
|
+
function hasSustainedRssGrowth(memorySamples) {
|
|
93373
|
+
const rss = memorySamples.map((sample) => sample.rss);
|
|
93374
|
+
const first = rss[1];
|
|
93375
|
+
const last = rss.at(-1);
|
|
93376
|
+
if (first === void 0 || last === void 0 || rss.length < 4) {
|
|
93377
|
+
return false;
|
|
93441
93378
|
}
|
|
93442
|
-
|
|
93443
|
-
|
|
93444
|
-
|
|
93379
|
+
return classifyTrend(rss, { minGrowthPerCycle: RSS_MIN_GROWTH_PER_CYCLE }).verdict === "leak" && last - first >= RSS_MIN_TOTAL_GROWTH;
|
|
93380
|
+
}
|
|
93381
|
+
function ownerLabel(attribution) {
|
|
93382
|
+
switch (attribution.owner) {
|
|
93383
|
+
case "app":
|
|
93384
|
+
return `${attribution.source ?? "your code"} (your code)`;
|
|
93385
|
+
case "dependency":
|
|
93386
|
+
return `${attribution.packageName ?? "a dependency"} (dependency)`;
|
|
93387
|
+
case "framework":
|
|
93388
|
+
return attribution.packageName === null ? "Next.js/runtime internals" : `${attribution.packageName} (framework)`;
|
|
93389
|
+
case "unattributed":
|
|
93390
|
+
return null;
|
|
93445
93391
|
}
|
|
93446
|
-
|
|
93392
|
+
}
|
|
93393
|
+
function revalidationLines(route) {
|
|
93394
|
+
return route.revalidatedEverySeconds === void 0 ? [] : [
|
|
93395
|
+
` driven through ISR revalidation (revalidates every ${route.revalidatedEverySeconds}s; without it the load would serve the cache)`
|
|
93396
|
+
];
|
|
93397
|
+
}
|
|
93398
|
+
function abandonLines(route) {
|
|
93399
|
+
if (route.abandon === void 0) {
|
|
93400
|
+
return [];
|
|
93401
|
+
}
|
|
93402
|
+
return [
|
|
93403
|
+
route.abandon.from === "request" ? ` cut ${route.abandon.afterMs}ms after the request was sent, so the client is gone before the response starts` : ` cut ${route.abandon.afterMs}ms after the first byte, so the cut lands mid-stream`
|
|
93404
|
+
];
|
|
93405
|
+
}
|
|
93406
|
+
function confidenceLines(route) {
|
|
93447
93407
|
const lines = [];
|
|
93448
|
-
|
|
93449
|
-
|
|
93450
|
-
|
|
93451
|
-
|
|
93452
|
-
const targetLine = section.offset.line + lineIndex;
|
|
93453
|
-
const columnShift = lineIndex === 0 ? section.offset.column : 0;
|
|
93454
|
-
const target = lines[targetLine] ??= { entries: [] };
|
|
93455
|
-
for (const entry of mapping.entries) {
|
|
93456
|
-
target.entries.push({
|
|
93457
|
-
column: entry.column + columnShift,
|
|
93458
|
-
sourceIndex: entry.sourceIndex + sourceBase
|
|
93459
|
-
});
|
|
93460
|
-
}
|
|
93461
|
-
}
|
|
93408
|
+
if (route.confidence.supersededVerdict !== void 0) {
|
|
93409
|
+
lines.push(
|
|
93410
|
+
` measured ${route.trend.verdict}, withdrawn: the run did not observe what that verdict needs`
|
|
93411
|
+
);
|
|
93462
93412
|
}
|
|
93463
|
-
for (const
|
|
93464
|
-
|
|
93413
|
+
for (const warning of route.confidence.warnings) {
|
|
93414
|
+
lines.push(` \u26A0 low confidence: ${warning.detail}`);
|
|
93415
|
+
}
|
|
93416
|
+
return lines;
|
|
93417
|
+
}
|
|
93418
|
+
function memorySourceLines(route, verdict) {
|
|
93419
|
+
const lines = [];
|
|
93420
|
+
if (route.trend.source === "external" && verdict !== "stable") {
|
|
93421
|
+
const externalCurve = route.memorySamples.map((sample) => formatMb(sample.external)).join(" \u2192 ");
|
|
93422
|
+
lines.push(
|
|
93423
|
+
` verdict comes from EXTERNAL memory (buffers, streams, fetch bodies), not the JS heap: external ${externalCurve}`
|
|
93424
|
+
);
|
|
93465
93425
|
}
|
|
93466
|
-
|
|
93467
|
-
|
|
93426
|
+
if (verdict === "stable" && hasSustainedRssGrowth(route.memorySamples)) {
|
|
93427
|
+
const rssCurve = route.memorySamples.map((sample) => formatMb(sample.rss)).join(" \u2192 ");
|
|
93428
|
+
lines.push(
|
|
93429
|
+
` note: heap is flat but RSS grows ${formatGrowth(route.rssPer1000Requests)} \u2014 not a JS-heap leak (allocator, external buffers or fragmentation): RSS ${rssCurve}`
|
|
93430
|
+
);
|
|
93468
93431
|
}
|
|
93469
|
-
return
|
|
93432
|
+
return lines;
|
|
93470
93433
|
}
|
|
93471
|
-
|
|
93472
|
-
|
|
93473
|
-
|
|
93474
|
-
let last = null;
|
|
93475
|
-
let match = ID_IN_GAP.exec(gap);
|
|
93476
|
-
while (match !== null) {
|
|
93477
|
-
last = Number(match[1]);
|
|
93478
|
-
match = ID_IN_GAP.exec(gap);
|
|
93434
|
+
function diffFindingLines(route) {
|
|
93435
|
+
if (route.diff === null) {
|
|
93436
|
+
return [];
|
|
93479
93437
|
}
|
|
93480
|
-
|
|
93438
|
+
const findings = [...route.diff.grownNodes, ...route.diff.newNodes];
|
|
93439
|
+
return findings.slice(0, 3).map((finding, index) => {
|
|
93440
|
+
const size = formatMb(finding.retainedBytes);
|
|
93441
|
+
const attribution = route.attribution?.findings[index];
|
|
93442
|
+
const owner = attribution === void 0 ? null : ownerLabel(attribution);
|
|
93443
|
+
const detail = owner ?? (finding.retainerChain === "" ? "" : finding.retainerChain);
|
|
93444
|
+
return ` \u21B3 ${finding.kind} [${finding.nodeType}] ${finding.name} ${size}${detail === "" ? "" : ` \u2014 ${detail}`}`;
|
|
93445
|
+
});
|
|
93481
93446
|
}
|
|
93482
|
-
function
|
|
93483
|
-
|
|
93484
|
-
|
|
93485
|
-
|
|
93486
|
-
|
|
93487
|
-
|
|
93488
|
-
|
|
93489
|
-
|
|
93490
|
-
const
|
|
93491
|
-
|
|
93492
|
-
(candidate) => candidate.column >= idEnd && candidate.column < spanEnd
|
|
93493
|
-
);
|
|
93494
|
-
const source = entry === void 0 ? void 0 : map.sources[entry.sourceIndex];
|
|
93495
|
-
if (source !== void 0) {
|
|
93496
|
-
result.set(Number(first[1]), source);
|
|
93497
|
-
}
|
|
93447
|
+
function findingLines(route) {
|
|
93448
|
+
const lines = [];
|
|
93449
|
+
const culprit = route.attribution === null ? null : ownerLabel(route.attribution.route);
|
|
93450
|
+
if (culprit !== null) {
|
|
93451
|
+
lines.push(` culprit: ${culprit}`);
|
|
93452
|
+
}
|
|
93453
|
+
lines.push(...diffFindingLines(route));
|
|
93454
|
+
for (const signature of route.signatures) {
|
|
93455
|
+
const flag = signature.historical ? " (historical)" : "";
|
|
93456
|
+
lines.push(` \u26A0 known cause${flag}: ${signature.title} \u2014 ${signature.issue}`);
|
|
93498
93457
|
}
|
|
93458
|
+
return lines;
|
|
93499
93459
|
}
|
|
93500
|
-
function
|
|
93501
|
-
|
|
93502
|
-
|
|
93503
|
-
|
|
93504
|
-
continue;
|
|
93505
|
-
}
|
|
93506
|
-
let previousColumn = 0;
|
|
93507
|
-
let previousSource = null;
|
|
93508
|
-
for (const entry of mapping.entries) {
|
|
93509
|
-
if (entry.sourceIndex !== previousSource) {
|
|
93510
|
-
const id = lastModuleIdIn(lineText.slice(previousColumn, entry.column));
|
|
93511
|
-
const source = map.sources[entry.sourceIndex];
|
|
93512
|
-
if (id !== null && source !== void 0 && !result.has(id)) {
|
|
93513
|
-
result.set(id, source);
|
|
93514
|
-
}
|
|
93515
|
-
previousSource = entry.sourceIndex;
|
|
93516
|
-
}
|
|
93517
|
-
previousColumn = entry.column;
|
|
93518
|
-
}
|
|
93460
|
+
function peakPressureLines(route, parameters) {
|
|
93461
|
+
const retained = route.memorySamples.at(-1)?.heapUsed;
|
|
93462
|
+
if (retained === void 0) {
|
|
93463
|
+
return [];
|
|
93519
93464
|
}
|
|
93465
|
+
const pressure = assessPeakPressure({
|
|
93466
|
+
peaks: route.peaks,
|
|
93467
|
+
retainedHeapBytes: retained,
|
|
93468
|
+
maxOldSpaceMb: parameters.maxOldSpaceMb
|
|
93469
|
+
});
|
|
93470
|
+
return pressure === null ? [] : [` \u25B2 peak pressure: ${describePeakPressure(pressure)}`];
|
|
93520
93471
|
}
|
|
93521
|
-
function
|
|
93522
|
-
const
|
|
93523
|
-
|
|
93524
|
-
|
|
93472
|
+
function unreclaimedLines(route, verdict) {
|
|
93473
|
+
const retention = assessUnreclaimedRetention({
|
|
93474
|
+
unreclaimedSamples: route.unreclaimedSamples,
|
|
93475
|
+
memorySamples: route.memorySamples,
|
|
93476
|
+
verdict,
|
|
93477
|
+
verdictIsWellSupported: warrantsIssueDraft(route)
|
|
93478
|
+
});
|
|
93479
|
+
return retention === null ? [] : [` \u25B2 unreclaimed: ${describeUnreclaimedRetention(retention)}`];
|
|
93480
|
+
}
|
|
93481
|
+
function routeLines(route, parameters) {
|
|
93482
|
+
if (route.status === "skipped") {
|
|
93483
|
+
return [` \u2013 ${route.route} skipped: ${route.reason}`];
|
|
93525
93484
|
}
|
|
93526
|
-
|
|
93527
|
-
|
|
93528
|
-
return result;
|
|
93485
|
+
if (route.status === "failed") {
|
|
93486
|
+
return [` \u2716 ${route.route} failed: ${route.reason}`];
|
|
93529
93487
|
}
|
|
93530
|
-
|
|
93531
|
-
|
|
93532
|
-
|
|
93533
|
-
|
|
93488
|
+
if (route.status === "not-exercised") {
|
|
93489
|
+
return [` \u2013 ${route.route} not exercised: ${route.reason}`];
|
|
93490
|
+
}
|
|
93491
|
+
const verdict = effectiveVerdict(route);
|
|
93492
|
+
const curve = route.samples.map(formatMb).join(" \u2192 ");
|
|
93493
|
+
const resolved = route.resolvedWithCycles === void 0 ? "" : ` (resolved at ${route.resolvedWithCycles} cycles)`;
|
|
93494
|
+
return [
|
|
93495
|
+
` ${VERDICT_ICON[verdict]} ${route.route} ${verdict} (${formatGrowth(
|
|
93496
|
+
route.growthPer1000Requests
|
|
93497
|
+
)}) heap ${curve}${resolved}`,
|
|
93498
|
+
...revalidationLines(route),
|
|
93499
|
+
...abandonLines(route),
|
|
93500
|
+
...confidenceLines(route),
|
|
93501
|
+
...memorySourceLines(route, verdict),
|
|
93502
|
+
...peakPressureLines(route, parameters),
|
|
93503
|
+
...unreclaimedLines(route, verdict),
|
|
93504
|
+
...findingLines(route)
|
|
93505
|
+
];
|
|
93534
93506
|
}
|
|
93535
|
-
|
|
93536
|
-
const
|
|
93537
|
-
|
|
93538
|
-
|
|
93539
|
-
|
|
93540
|
-
} catch {
|
|
93541
|
-
return out;
|
|
93507
|
+
function skippedGuidanceLines(report) {
|
|
93508
|
+
const needConfig = report.routes.filter((route) => route.status === "skipped" && route.reason.includes("sample params")).map((route) => route.route);
|
|
93509
|
+
const skeleton = renderConfigSkeleton(needConfig, report.prerender);
|
|
93510
|
+
if (skeleton === null) {
|
|
93511
|
+
return [];
|
|
93542
93512
|
}
|
|
93543
|
-
|
|
93544
|
-
|
|
93545
|
-
|
|
93546
|
-
}
|
|
93513
|
+
const editing = hasPlaceholders(skeleton) ? ` Replace each ${"REPLACE-ME"} with a value that exists in your app.` : ` The values come from paths your build already prerendered.`;
|
|
93514
|
+
return [
|
|
93515
|
+
"",
|
|
93516
|
+
`${needConfig.length} route(s) need sample params. Write this to next-leak.config.json in the app directory:${editing}`,
|
|
93517
|
+
...skeleton.split("\n").map((line) => ` ${line}`)
|
|
93518
|
+
];
|
|
93519
|
+
}
|
|
93520
|
+
function coverageLine(report) {
|
|
93521
|
+
const total = report.routes.length;
|
|
93522
|
+
const measured = report.routes.filter((route) => route.status === "measured").length;
|
|
93523
|
+
if (measured === total) {
|
|
93524
|
+
return `measured all ${total} discovered route(s)`;
|
|
93547
93525
|
}
|
|
93548
|
-
|
|
93526
|
+
const breakdown = ["skipped", "not-exercised", "failed"].map((status) => ({
|
|
93527
|
+
status,
|
|
93528
|
+
count: report.routes.filter((route) => route.status === status).length
|
|
93529
|
+
})).filter((entry) => entry.count > 0).map((entry) => `${entry.count} ${entry.status.replace("-", " ")}`).join(", ");
|
|
93530
|
+
return `measured ${measured} of ${total} discovered route(s) \u2014 ${breakdown}; the rest is not a verdict about your app`;
|
|
93549
93531
|
}
|
|
93550
|
-
|
|
93551
|
-
const
|
|
93552
|
-
for (const
|
|
93553
|
-
|
|
93554
|
-
let map;
|
|
93555
|
-
try {
|
|
93556
|
-
code = await readFile4(file, "utf8");
|
|
93557
|
-
map = JSON.parse(await readFile4(`${file}.map`, "utf8"));
|
|
93558
|
-
} catch {
|
|
93559
|
-
continue;
|
|
93560
|
-
}
|
|
93561
|
-
for (const [id, source] of extractChunkModules(code, map)) {
|
|
93562
|
-
if (!registry.has(id)) {
|
|
93563
|
-
registry.set(id, source);
|
|
93564
|
-
}
|
|
93565
|
-
}
|
|
93532
|
+
function formatReport(report) {
|
|
93533
|
+
const lines = [`next-leak \u2014 ${report.appDir}`, ""];
|
|
93534
|
+
for (const route of report.routes) {
|
|
93535
|
+
lines.push(...routeLines(route, report.parameters));
|
|
93566
93536
|
}
|
|
93567
|
-
|
|
93537
|
+
lines.push(...skippedGuidanceLines(report));
|
|
93538
|
+
const { minGrowthPerCycle, loadRequests, cycles, maxOldSpaceMb } = report.parameters;
|
|
93539
|
+
const resolvedCycles = [
|
|
93540
|
+
...new Set(
|
|
93541
|
+
report.routes.flatMap(
|
|
93542
|
+
(route) => route.status === "measured" && route.resolvedWithCycles !== void 0 ? [route.resolvedWithCycles] : []
|
|
93543
|
+
)
|
|
93544
|
+
)
|
|
93545
|
+
].sort((a, b) => a - b);
|
|
93546
|
+
const cyclesLabel = resolvedCycles.length === 0 ? `${cycles} cycles` : `${cycles} cycles (${resolvedCycles.join(", ")} where resolved)`;
|
|
93547
|
+
lines.push("", coverageLine(report));
|
|
93548
|
+
lines.push(
|
|
93549
|
+
`judged over ${cyclesLabel} \xD7 ${loadRequests} requests, growth gate ${(minGrowthPerCycle / 1024).toFixed(0)} KiB/cycle (${formatGrowth(minGrowthPerCycle / loadRequests * 1e3)}), heap cap ${maxOldSpaceMb} MB`,
|
|
93550
|
+
`snapshots and run.json: ${report.workDir}`,
|
|
93551
|
+
`report: ${report.bundle.htmlReport}`,
|
|
93552
|
+
...report.bundle.issues.map((issue) => `issue draft (${issue.route}): ${issue.file}`)
|
|
93553
|
+
);
|
|
93554
|
+
const inconclusive = report.routes.filter(
|
|
93555
|
+
(route) => route.status === "measured" && effectiveVerdict(route) === "inconclusive"
|
|
93556
|
+
);
|
|
93557
|
+
if (inconclusive.length > 0) {
|
|
93558
|
+
const routeList = inconclusive.map((route) => route.route).join(",");
|
|
93559
|
+
const moreCycles = resolveCycles(report.parameters.cycles);
|
|
93560
|
+
lines.push(
|
|
93561
|
+
"",
|
|
93562
|
+
"hint: inconclusive means sustained sub-threshold growth \u2014 measure longer to resolve it:",
|
|
93563
|
+
` next-leak ${report.appDir} --routes ${routeList} --cycles ${moreCycles}`
|
|
93564
|
+
);
|
|
93565
|
+
}
|
|
93566
|
+
return lines.join("\n");
|
|
93567
|
+
}
|
|
93568
|
+
|
|
93569
|
+
// src/environment.ts
|
|
93570
|
+
import { createRequire } from "module";
|
|
93571
|
+
import os from "os";
|
|
93572
|
+
var require2 = createRequire(import.meta.url);
|
|
93573
|
+
var { version: nextLeakVersion } = require2("../package.json");
|
|
93574
|
+
function captureEnvironment(nextVersion) {
|
|
93575
|
+
return {
|
|
93576
|
+
nodeVersion: process.version,
|
|
93577
|
+
platform: os.platform(),
|
|
93578
|
+
arch: os.arch(),
|
|
93579
|
+
cpuModel: os.cpus()[0]?.model ?? null,
|
|
93580
|
+
totalMemoryBytes: os.totalmem(),
|
|
93581
|
+
nextVersion,
|
|
93582
|
+
nextLeakVersion
|
|
93583
|
+
};
|
|
93568
93584
|
}
|
|
93569
93585
|
|
|
93570
93586
|
// src/load.ts
|
|
@@ -94583,6 +94599,20 @@ async function runMeasurement(options, deps = defaultDeps2) {
|
|
|
94583
94599
|
}
|
|
94584
94600
|
|
|
94585
94601
|
export {
|
|
94602
|
+
classifySource,
|
|
94603
|
+
attributeFinding,
|
|
94604
|
+
attributeDiff,
|
|
94605
|
+
summarizeBaseline,
|
|
94606
|
+
retainerChain,
|
|
94607
|
+
diffAgainstBaseline,
|
|
94608
|
+
SnapshotError,
|
|
94609
|
+
assertReadableSnapshot,
|
|
94610
|
+
diffSnapshotFiles,
|
|
94611
|
+
decodeVlqLine,
|
|
94612
|
+
decodeMappings,
|
|
94613
|
+
sourceIndexAt,
|
|
94614
|
+
extractChunkModules,
|
|
94615
|
+
extractModuleRegistry,
|
|
94586
94616
|
appPathsManifestSchema,
|
|
94587
94617
|
routesManifestSchema,
|
|
94588
94618
|
discoverPagesRoutes,
|
|
@@ -94604,21 +94634,7 @@ export {
|
|
|
94604
94634
|
renderConfigSkeleton,
|
|
94605
94635
|
hasPlaceholders,
|
|
94606
94636
|
formatReport,
|
|
94607
|
-
classifySource,
|
|
94608
|
-
attributeFinding,
|
|
94609
|
-
attributeDiff,
|
|
94610
94637
|
captureEnvironment,
|
|
94611
|
-
summarizeBaseline,
|
|
94612
|
-
retainerChain,
|
|
94613
|
-
diffAgainstBaseline,
|
|
94614
|
-
SnapshotError,
|
|
94615
|
-
assertReadableSnapshot,
|
|
94616
|
-
diffSnapshotFiles,
|
|
94617
|
-
decodeVlqLine,
|
|
94618
|
-
decodeMappings,
|
|
94619
|
-
sourceIndexAt,
|
|
94620
|
-
extractChunkModules,
|
|
94621
|
-
extractModuleRegistry,
|
|
94622
94638
|
LoadError,
|
|
94623
94639
|
runLoadPhase,
|
|
94624
94640
|
runRitual,
|