next-leak 0.7.0 → 0.9.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 +22 -5
- 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-RS3AO7HB.js → chunk-T7LM4Z33.js} +8 -0
- package/dist/{chunk-BXDUPN6Z.js → chunk-UVA77PMC.js} +1544 -1434
- package/dist/cli-args.d.ts +3 -0
- package/dist/cli.js +335 -20
- package/dist/{html-report-TN7PYVU4.js → html-report-5XMR3EXE.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/launcher.d.ts +20 -1
- package/dist/load.d.ts +19 -0
- package/dist/ritual.d.ts +24 -0
- package/dist/route-guidance.d.ts +19 -0
- package/dist/runner.d.ts +22 -0
- package/package.json +1 -1
|
@@ -91969,1602 +91969,1650 @@ 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
|
-
|
|
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;
|
|
92236
92328
|
}
|
|
92237
|
-
|
|
92238
|
-
});
|
|
92239
|
-
const stderrWindow = () => stderrHead === stderrTailBuffer || stderrTailBuffer === "" ? stderrHead : `${stderrHead}
|
|
92240
|
-
[...]
|
|
92241
|
-
${stderrTailBuffer}`;
|
|
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;
|
|
92329
|
+
found = entry.sourceIndex;
|
|
92313
92330
|
}
|
|
92331
|
+
return found;
|
|
92314
92332
|
}
|
|
92315
92333
|
|
|
92316
|
-
// src/
|
|
92317
|
-
|
|
92318
|
-
|
|
92319
|
-
|
|
92320
|
-
|
|
92321
|
-
|
|
92322
|
-
|
|
92323
|
-
|
|
92324
|
-
|
|
92325
|
-
|
|
92326
|
-
|
|
92327
|
-
|
|
92328
|
-
|
|
92329
|
-
|
|
92330
|
-
|
|
92331
|
-
|
|
92332
|
-
|
|
92333
|
-
|
|
92334
|
-
|
|
92335
|
-
|
|
92336
|
-
|
|
92337
|
-
|
|
92338
|
-
return null;
|
|
92339
|
-
}
|
|
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
|
-
|
|
92413
|
-
Exit codes: 0 when the run completes (whatever the verdicts), 130 when
|
|
92414
|
-
interrupted, 1 on errors.
|
|
92415
|
-
`;
|
|
92416
|
-
}
|
|
92417
|
-
var LIMITS = {
|
|
92418
|
-
"--cycles": 100,
|
|
92419
|
-
"--requests": 1e6,
|
|
92420
|
-
"--connections": 1e4,
|
|
92421
|
-
"--idle": 3600,
|
|
92422
|
-
"--warmup": 1e6,
|
|
92423
|
-
"--max-old-space": 65536
|
|
92424
|
-
};
|
|
92425
|
-
var MIN_MAX_OLD_SPACE_MB = 128;
|
|
92426
|
-
function findSpec(argument) {
|
|
92427
|
-
return FLAGS.find((spec) => spec.flag === argument || spec.alias === argument);
|
|
92428
|
-
}
|
|
92429
|
-
var flagError = (message) => ({ kind: "error", message });
|
|
92430
|
-
var FLAG_OK = { kind: "ok" };
|
|
92431
|
-
function applyRoutesFlag(value, options) {
|
|
92432
|
-
const routes = value.split(",").map((route) => route.trim()).filter((route) => route !== "");
|
|
92433
|
-
if (routes.length === 0) {
|
|
92434
|
-
return flagError(`option "--routes" needs at least one route`);
|
|
92435
|
-
}
|
|
92436
|
-
options.routes = routes;
|
|
92437
|
-
return FLAG_OK;
|
|
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}"`);
|
|
92443
|
-
}
|
|
92444
|
-
const maximum = LIMITS[flag];
|
|
92445
|
-
if (maximum !== void 0 && parsed > maximum) {
|
|
92446
|
-
return flagError(
|
|
92447
|
-
`option "${flag}" is capped at ${maximum} (got ${parsed}) \u2014 a run that large would never finish`
|
|
92448
|
-
);
|
|
92449
|
-
}
|
|
92450
|
-
if (flag === "--cycles" && parsed < 3) {
|
|
92451
|
-
return flagError("the trend verdict needs at least 3 cycles (--cycles 3 or more)");
|
|
92452
|
-
}
|
|
92453
|
-
if (flag === "--max-old-space" && parsed < MIN_MAX_OLD_SPACE_MB) {
|
|
92454
|
-
return flagError(
|
|
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
|
-
);
|
|
92457
|
-
}
|
|
92458
|
-
if (flag === "--cycles") options.cycles = parsed;
|
|
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;
|
|
92465
|
-
}
|
|
92466
|
-
function applyFlag(spec, value, options) {
|
|
92467
|
-
switch (spec.flag) {
|
|
92468
|
-
case "--routes":
|
|
92469
|
-
return applyRoutesFlag(value, options);
|
|
92470
|
-
case "--cycles":
|
|
92471
|
-
case "--requests":
|
|
92472
|
-
case "--connections":
|
|
92473
|
-
case "--idle":
|
|
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;
|
|
92494
|
-
}
|
|
92495
|
-
}
|
|
92496
|
-
function readFlagValue(spec, argv, index) {
|
|
92497
|
-
if (spec.value === "none") {
|
|
92498
|
-
return { value: "" };
|
|
92499
|
-
}
|
|
92500
|
-
const value = argv[index + 1] ?? "";
|
|
92501
|
-
const looksNegativeNumber = /^-\d+(\.\d+)?$/.test(value);
|
|
92502
|
-
if (value === "" || value.startsWith("-") && !looksNegativeNumber) {
|
|
92503
|
-
return { error: `option "${spec.flag}" needs a value ${spec.argName ?? ""}` };
|
|
92504
|
-
}
|
|
92505
|
-
return { value };
|
|
92506
|
-
}
|
|
92507
|
-
function parseFlagAt(argv, index, options) {
|
|
92508
|
-
const argument = argv[index] ?? "";
|
|
92509
|
-
const spec = findSpec(argument);
|
|
92510
|
-
if (spec === void 0) {
|
|
92511
|
-
return { done: { kind: "error", message: `unknown option "${argument}" \u2014 see --help` } };
|
|
92512
|
-
}
|
|
92513
|
-
if (spec.flag === "--help") {
|
|
92514
|
-
return { done: { kind: "help" } };
|
|
92515
|
-
}
|
|
92516
|
-
if (spec.flag === "--version") {
|
|
92517
|
-
return { done: { kind: "version" } };
|
|
92518
|
-
}
|
|
92519
|
-
const read2 = readFlagValue(spec, argv, index);
|
|
92520
|
-
if ("error" in read2) {
|
|
92521
|
-
return { done: { kind: "error", message: read2.error } };
|
|
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) };
|
|
92522
92356
|
}
|
|
92523
|
-
const
|
|
92524
|
-
if (
|
|
92525
|
-
return
|
|
92357
|
+
const sectioned = sectionedMapSchema.safeParse(raw);
|
|
92358
|
+
if (!sectioned.success) {
|
|
92359
|
+
return null;
|
|
92526
92360
|
}
|
|
92527
|
-
|
|
92528
|
-
|
|
92529
|
-
|
|
92530
|
-
|
|
92531
|
-
|
|
92532
|
-
|
|
92533
|
-
|
|
92534
|
-
|
|
92535
|
-
|
|
92536
|
-
|
|
92537
|
-
|
|
92538
|
-
|
|
92539
|
-
|
|
92540
|
-
|
|
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` };
|
|
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
|
+
});
|
|
92552
92375
|
}
|
|
92553
|
-
options.appDir = argument;
|
|
92554
|
-
continue;
|
|
92555
|
-
}
|
|
92556
|
-
const step = parseFlagAt(rest, index, options);
|
|
92557
|
-
if ("done" in step) {
|
|
92558
|
-
return step.done;
|
|
92559
92376
|
}
|
|
92560
|
-
index += step.consumed;
|
|
92561
92377
|
}
|
|
92562
|
-
|
|
92563
|
-
|
|
92378
|
+
for (const line of lines) {
|
|
92379
|
+
line?.entries.sort((a, b) => a.column - b.column);
|
|
92564
92380
|
}
|
|
92565
|
-
|
|
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 } };
|
|
92381
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
92382
|
+
lines[i] ??= { entries: [] };
|
|
92574
92383
|
}
|
|
92575
|
-
return {
|
|
92576
|
-
}
|
|
92577
|
-
|
|
92578
|
-
// src/guards.ts
|
|
92579
|
-
function currentFacts() {
|
|
92580
|
-
return {
|
|
92581
|
-
nodeMajor: Number(process.versions.node.split(".")[0]),
|
|
92582
|
-
platform: process.platform
|
|
92583
|
-
};
|
|
92384
|
+
return { sources, lines };
|
|
92584
92385
|
}
|
|
92585
|
-
|
|
92586
|
-
|
|
92587
|
-
|
|
92588
|
-
|
|
92589
|
-
|
|
92590
|
-
|
|
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);
|
|
92591
92394
|
}
|
|
92592
|
-
return
|
|
92395
|
+
return last;
|
|
92593
92396
|
}
|
|
92594
|
-
|
|
92595
|
-
|
|
92596
|
-
|
|
92597
|
-
|
|
92598
|
-
|
|
92599
|
-
|
|
92600
|
-
|
|
92601
|
-
|
|
92602
|
-
|
|
92603
|
-
|
|
92604
|
-
|
|
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) {
|
|
92651
|
-
super(message);
|
|
92652
|
-
this.name = "RouteConfigError";
|
|
92653
|
-
}
|
|
92654
|
-
};
|
|
92655
|
-
var ROUTE_CONFIG_FILE = "next-leak.config.json";
|
|
92656
|
-
async function loadRouteConfig(appDir) {
|
|
92657
|
-
const file = path3.join(appDir, ROUTE_CONFIG_FILE);
|
|
92658
|
-
let raw;
|
|
92659
|
-
try {
|
|
92660
|
-
raw = await readFile3(file, "utf8");
|
|
92661
|
-
} catch {
|
|
92662
|
-
return {};
|
|
92663
|
-
}
|
|
92664
|
-
try {
|
|
92665
|
-
return routeConfigSchema.parse(JSON.parse(raw));
|
|
92666
|
-
} catch (cause) {
|
|
92667
|
-
throw new RouteConfigError(
|
|
92668
|
-
`${file} is invalid: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
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
|
|
92669
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
|
+
}
|
|
92670
92413
|
}
|
|
92671
92414
|
}
|
|
92672
|
-
|
|
92673
|
-
|
|
92674
|
-
|
|
92675
|
-
|
|
92676
|
-
const match = SEGMENT_PATTERN.exec(segment);
|
|
92677
|
-
if (match === null) {
|
|
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) {
|
|
92678
92419
|
continue;
|
|
92679
92420
|
}
|
|
92680
|
-
|
|
92681
|
-
|
|
92682
|
-
|
|
92683
|
-
|
|
92684
|
-
|
|
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
|
+
}
|
|
92685
92434
|
}
|
|
92686
|
-
return segments;
|
|
92687
92435
|
}
|
|
92688
|
-
|
|
92689
|
-
|
|
92690
|
-
|
|
92691
|
-
|
|
92692
|
-
const bound = Number(match?.[1]);
|
|
92693
|
-
if (match === null || !Number.isInteger(bound) || bound <= 0) {
|
|
92694
|
-
return null;
|
|
92436
|
+
function extractChunkModules(code, rawMap) {
|
|
92437
|
+
const result = /* @__PURE__ */ new Map();
|
|
92438
|
+
if (!code.includes("module.exports=[") && !code.includes("module.exports = [")) {
|
|
92439
|
+
return result;
|
|
92695
92440
|
}
|
|
92696
|
-
|
|
92441
|
+
const map = normalizeSourceMap(rawMap);
|
|
92442
|
+
if (map === null) {
|
|
92443
|
+
return result;
|
|
92444
|
+
}
|
|
92445
|
+
const codeLines = code.split("\n");
|
|
92446
|
+
collectFirstArrayIds(codeLines, map, result);
|
|
92447
|
+
collectTransitionIds(codeLines, map, result);
|
|
92448
|
+
return result;
|
|
92697
92449
|
}
|
|
92698
|
-
|
|
92699
|
-
|
|
92700
|
-
|
|
92701
|
-
|
|
92702
|
-
|
|
92703
|
-
|
|
92704
|
-
|
|
92705
|
-
|
|
92706
|
-
|
|
92707
|
-
if (
|
|
92708
|
-
|
|
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;
|
|
92457
|
+
}
|
|
92458
|
+
for (const entry of entries) {
|
|
92459
|
+
if (entry.isFile() && entry.name.endsWith(".js")) {
|
|
92460
|
+
out.push(path.join(entry.parentPath, entry.name));
|
|
92461
|
+
}
|
|
92462
|
+
}
|
|
92463
|
+
return out;
|
|
92464
|
+
}
|
|
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 {
|
|
92709
92474
|
continue;
|
|
92710
92475
|
}
|
|
92711
|
-
const
|
|
92712
|
-
|
|
92713
|
-
|
|
92714
|
-
|
|
92715
|
-
if (value !== void 0) {
|
|
92716
|
-
resolved.push(catchAll ? encodeCatchAll(value) : encodeSegment(value));
|
|
92717
|
-
} else if (!optional) {
|
|
92718
|
-
return null;
|
|
92476
|
+
for (const [id, source] of extractChunkModules(code, map)) {
|
|
92477
|
+
if (!registry.has(id)) {
|
|
92478
|
+
registry.set(id, source);
|
|
92479
|
+
}
|
|
92719
92480
|
}
|
|
92720
92481
|
}
|
|
92721
|
-
|
|
92722
|
-
const path7 = joined === "" ? "/" : joined;
|
|
92723
|
-
const query = config.query?.[routeTemplate];
|
|
92724
|
-
return query === void 0 || query === "" ? path7 : `${path7}?${query}`;
|
|
92482
|
+
return registry;
|
|
92725
92483
|
}
|
|
92726
92484
|
|
|
92727
|
-
// src/
|
|
92728
|
-
|
|
92729
|
-
|
|
92730
|
-
|
|
92731
|
-
|
|
92732
|
-
|
|
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";
|
|
92733
92516
|
}
|
|
92734
|
-
|
|
92735
|
-
|
|
92517
|
+
if (STATIC_ASSET_PATHS.has(routePath)) {
|
|
92518
|
+
return "static asset served by a generated handler \u2014 no user code to leak";
|
|
92519
|
+
}
|
|
92520
|
+
return void 0;
|
|
92521
|
+
}
|
|
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 {
|
|
92736
92532
|
return null;
|
|
92737
92533
|
}
|
|
92738
|
-
const
|
|
92739
|
-
|
|
92740
|
-
|
|
92741
|
-
|
|
92742
|
-
|
|
92743
|
-
|
|
92744
|
-
|
|
92534
|
+
const segments = withoutSuffix.split("/").filter((segment) => segment !== "").filter((segment) => !(segment.startsWith("(") && segment.endsWith(")"))).filter((segment) => !segment.startsWith("@"));
|
|
92535
|
+
return { path: `/${segments.join("/")}`, kind };
|
|
92536
|
+
}
|
|
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)) {
|
|
92745
92542
|
continue;
|
|
92746
92543
|
}
|
|
92747
|
-
|
|
92748
|
-
|
|
92749
|
-
|
|
92750
|
-
|
|
92751
|
-
|
|
92544
|
+
routes.push({
|
|
92545
|
+
path: key,
|
|
92546
|
+
kind: key === "/api" || key.startsWith("/api/") ? "route-handler" : "page",
|
|
92547
|
+
dynamic: key.includes("[")
|
|
92548
|
+
});
|
|
92752
92549
|
}
|
|
92753
|
-
return
|
|
92550
|
+
return routes.sort((a, b) => a.path.localeCompare(b.path));
|
|
92754
92551
|
}
|
|
92755
|
-
function
|
|
92756
|
-
const routes =
|
|
92757
|
-
for (const
|
|
92758
|
-
const
|
|
92759
|
-
if (
|
|
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)) {
|
|
92760
92557
|
continue;
|
|
92761
92558
|
}
|
|
92762
|
-
const
|
|
92763
|
-
|
|
92764
|
-
|
|
92765
|
-
|
|
92766
|
-
|
|
92767
|
-
|
|
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
|
+
});
|
|
92768
92566
|
}
|
|
92769
|
-
return
|
|
92770
|
-
}
|
|
92771
|
-
function hasPlaceholders(skeleton) {
|
|
92772
|
-
return skeleton.includes(PLACEHOLDER);
|
|
92567
|
+
return [...routes.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
92773
92568
|
}
|
|
92774
92569
|
|
|
92775
|
-
// src/
|
|
92776
|
-
|
|
92777
|
-
|
|
92778
|
-
var
|
|
92779
|
-
|
|
92780
|
-
|
|
92781
|
-
|
|
92782
|
-
|
|
92783
|
-
|
|
92784
|
-
}
|
|
92785
|
-
|
|
92786
|
-
|
|
92787
|
-
|
|
92788
|
-
|
|
92789
|
-
|
|
92790
|
-
|
|
92791
|
-
|
|
92792
|
-
gaps.push(read(held, memoryClass) - read(afterGc, memoryClass));
|
|
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) {
|
|
92576
|
+
super(message);
|
|
92577
|
+
this.name = "TargetError";
|
|
92578
|
+
this.code = code;
|
|
92579
|
+
}
|
|
92580
|
+
};
|
|
92581
|
+
async function exists(file) {
|
|
92582
|
+
try {
|
|
92583
|
+
await access(file);
|
|
92584
|
+
return true;
|
|
92585
|
+
} catch {
|
|
92586
|
+
return false;
|
|
92793
92587
|
}
|
|
92794
|
-
return gaps;
|
|
92795
92588
|
}
|
|
92796
|
-
function
|
|
92797
|
-
|
|
92798
|
-
|
|
92589
|
+
async function readManifest(file, parse2) {
|
|
92590
|
+
let raw;
|
|
92591
|
+
try {
|
|
92592
|
+
raw = JSON.parse(await readFile2(file, "utf8"));
|
|
92593
|
+
} catch (cause) {
|
|
92594
|
+
throw new TargetError(
|
|
92595
|
+
"BAD_MANIFEST",
|
|
92596
|
+
`Could not read ${file}: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
92597
|
+
);
|
|
92799
92598
|
}
|
|
92800
|
-
|
|
92801
|
-
|
|
92802
|
-
|
|
92803
|
-
|
|
92804
|
-
|
|
92805
|
-
|
|
92806
|
-
const largestGapBytes = largestGap(gaps);
|
|
92807
|
-
if (largestGapBytes < MIN_GAP_BYTES) {
|
|
92808
|
-
continue;
|
|
92809
|
-
}
|
|
92810
|
-
const retainedBytes = largestGap(
|
|
92811
|
-
input.memorySamples.slice(1).map((sample) => read(sample, memoryClass))
|
|
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.`
|
|
92812
92605
|
);
|
|
92813
|
-
const ratio = largestGapBytes / Math.max(retainedBytes, MIN_RETAINED_BYTES);
|
|
92814
|
-
if (ratio < MIN_GAP_RATIO) {
|
|
92815
|
-
continue;
|
|
92816
|
-
}
|
|
92817
|
-
if (worst === null || ratio >= worst.ratio) {
|
|
92818
|
-
worst = { class: memoryClass, largestGapBytes, retainedBytes, ratio };
|
|
92819
|
-
}
|
|
92820
92606
|
}
|
|
92821
|
-
return worst;
|
|
92822
92607
|
}
|
|
92823
|
-
|
|
92824
|
-
|
|
92825
|
-
|
|
92826
|
-
|
|
92827
|
-
|
|
92828
|
-
}
|
|
92829
|
-
|
|
92830
|
-
|
|
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
|
+
);
|
|
92615
|
+
}
|
|
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:
|
|
92622
|
+
|
|
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
|
+
);
|
|
92631
|
+
}
|
|
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
|
+
);
|
|
92641
|
+
}
|
|
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
|
+
};
|
|
92831
92654
|
}
|
|
92832
92655
|
|
|
92833
|
-
// src/
|
|
92834
|
-
|
|
92835
|
-
|
|
92836
|
-
|
|
92837
|
-
|
|
92838
|
-
|
|
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
|
+
}
|
|
92839
92669
|
};
|
|
92840
|
-
var
|
|
92841
|
-
var
|
|
92842
|
-
|
|
92843
|
-
|
|
92844
|
-
|
|
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;
|
|
92670
|
+
var activeChildren = /* @__PURE__ */ new Set();
|
|
92671
|
+
var STDERR_WINDOW = 4096;
|
|
92672
|
+
function killActiveChildren() {
|
|
92673
|
+
for (const child of activeChildren) {
|
|
92674
|
+
child.kill("SIGKILL");
|
|
92849
92675
|
}
|
|
92850
|
-
return classifyTrend(rss, { minGrowthPerCycle: RSS_MIN_GROWTH_PER_CYCLE }).verdict === "leak" && last - first >= RSS_MIN_TOTAL_GROWTH;
|
|
92851
92676
|
}
|
|
92852
|
-
function
|
|
92853
|
-
|
|
92854
|
-
|
|
92855
|
-
|
|
92856
|
-
|
|
92857
|
-
|
|
92858
|
-
|
|
92859
|
-
|
|
92860
|
-
|
|
92861
|
-
|
|
92677
|
+
function registerChild(child) {
|
|
92678
|
+
activeChildren.add(child);
|
|
92679
|
+
child.once("exit", () => activeChildren.delete(child));
|
|
92680
|
+
}
|
|
92681
|
+
function unregisterChild(child) {
|
|
92682
|
+
activeChildren.delete(child);
|
|
92683
|
+
}
|
|
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.`;
|
|
92862
92688
|
}
|
|
92689
|
+
if (/EADDRINUSE/.test(stderr)) {
|
|
92690
|
+
return "the port was taken by another process while starting.";
|
|
92691
|
+
}
|
|
92692
|
+
return `stderr:
|
|
92693
|
+
${stderr}`;
|
|
92863
92694
|
}
|
|
92864
|
-
function
|
|
92865
|
-
return
|
|
92866
|
-
` driven through ISR revalidation (revalidates every ${route.revalidatedEverySeconds}s; without it the load would serve the cache)`
|
|
92867
|
-
];
|
|
92695
|
+
function stderrShowsHeapExhaustion(stderr) {
|
|
92696
|
+
return /heap out of memory|Reached heap limit|Ineffective mark-compacts/i.test(stderr);
|
|
92868
92697
|
}
|
|
92869
|
-
function
|
|
92870
|
-
if (
|
|
92871
|
-
return
|
|
92698
|
+
function explainRuntimeFailure(stderr, maxOldSpaceMb) {
|
|
92699
|
+
if (stderrShowsHeapExhaustion(stderr)) {
|
|
92700
|
+
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.`;
|
|
92872
92701
|
}
|
|
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
|
-
];
|
|
92702
|
+
return `the measured process exited mid-run. ${explainStartupFailure(stderr)}`;
|
|
92876
92703
|
}
|
|
92877
|
-
|
|
92878
|
-
|
|
92879
|
-
|
|
92880
|
-
|
|
92881
|
-
|
|
92882
|
-
|
|
92704
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
92705
|
+
async function pollUntil(deadline, what, probe, failed) {
|
|
92706
|
+
for (; ; ) {
|
|
92707
|
+
const failure = failed();
|
|
92708
|
+
if (failure !== void 0) {
|
|
92709
|
+
throw new LaunchError(failure);
|
|
92710
|
+
}
|
|
92711
|
+
const result = await probe();
|
|
92712
|
+
if (result !== void 0) {
|
|
92713
|
+
return result;
|
|
92714
|
+
}
|
|
92715
|
+
if (Date.now() > deadline) {
|
|
92716
|
+
throw new LaunchError(`timed out waiting for ${what}`);
|
|
92717
|
+
}
|
|
92718
|
+
await sleep(100);
|
|
92883
92719
|
}
|
|
92884
|
-
|
|
92885
|
-
|
|
92720
|
+
}
|
|
92721
|
+
async function launchInstrumented(options) {
|
|
92722
|
+
const hostname = options.hostname ?? "127.0.0.1";
|
|
92723
|
+
const deadline = Date.now() + (options.readyTimeoutMs ?? 15e3);
|
|
92724
|
+
const child = spawn(
|
|
92725
|
+
process.execPath,
|
|
92726
|
+
[
|
|
92727
|
+
"--expose-gc",
|
|
92728
|
+
`--max-old-space-size=${options.maxOldSpaceMb ?? DEFAULT_MAX_OLD_SPACE_MB}`,
|
|
92729
|
+
"--import",
|
|
92730
|
+
pathToFileURL(options.bootstrapPath).href,
|
|
92731
|
+
options.serverPath
|
|
92732
|
+
],
|
|
92733
|
+
{
|
|
92734
|
+
cwd: path3.dirname(options.serverPath),
|
|
92735
|
+
env: {
|
|
92736
|
+
...process.env,
|
|
92737
|
+
...options.env,
|
|
92738
|
+
NODE_ENV: "production",
|
|
92739
|
+
PORT: String(options.appPort),
|
|
92740
|
+
HOSTNAME: hostname,
|
|
92741
|
+
NEXT_LEAK_DIR: options.workDir
|
|
92742
|
+
},
|
|
92743
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
92744
|
+
}
|
|
92745
|
+
);
|
|
92746
|
+
let stderrHead = "";
|
|
92747
|
+
let stderrTailBuffer = "";
|
|
92748
|
+
child.stderr?.on("data", (chunk) => {
|
|
92749
|
+
const text = chunk.toString();
|
|
92750
|
+
if (stderrHead.length < STDERR_WINDOW) {
|
|
92751
|
+
stderrHead = (stderrHead + text).slice(0, STDERR_WINDOW);
|
|
92752
|
+
}
|
|
92753
|
+
stderrTailBuffer = (stderrTailBuffer + text).slice(-STDERR_WINDOW);
|
|
92754
|
+
});
|
|
92755
|
+
const stderrWindow = () => stderrHead === stderrTailBuffer || stderrTailBuffer === "" ? stderrHead : `${stderrHead}
|
|
92756
|
+
[...]
|
|
92757
|
+
${stderrTailBuffer}`;
|
|
92758
|
+
let exited = false;
|
|
92759
|
+
activeChildren.add(child);
|
|
92760
|
+
child.once("exit", () => {
|
|
92761
|
+
exited = true;
|
|
92762
|
+
activeChildren.delete(child);
|
|
92763
|
+
});
|
|
92764
|
+
const failed = () => exited ? `server exited before becoming ready. ${explainStartupFailure(stderrWindow())}` : void 0;
|
|
92765
|
+
try {
|
|
92766
|
+
const controlPort = await pollUntil(
|
|
92767
|
+
deadline,
|
|
92768
|
+
`control channel in ${options.workDir}`,
|
|
92769
|
+
// Several processes may announce a channel (clustered servers); accept
|
|
92770
|
+
// the first one that actually answers instead of trusting a filename.
|
|
92771
|
+
async () => {
|
|
92772
|
+
let entries;
|
|
92773
|
+
try {
|
|
92774
|
+
entries = (await readdir2(options.workDir)).filter(
|
|
92775
|
+
(entry) => entry.startsWith("control-") && entry.endsWith(".json")
|
|
92776
|
+
);
|
|
92777
|
+
} catch {
|
|
92778
|
+
return void 0;
|
|
92779
|
+
}
|
|
92780
|
+
for (const entry of entries) {
|
|
92781
|
+
try {
|
|
92782
|
+
const parsed = controlFileSchema.parse(
|
|
92783
|
+
JSON.parse(await readFile3(path3.join(options.workDir, entry), "utf8"))
|
|
92784
|
+
);
|
|
92785
|
+
const response = await fetch(`http://127.0.0.1:${parsed.port}/gc`);
|
|
92786
|
+
if (response.ok) {
|
|
92787
|
+
return parsed.port;
|
|
92788
|
+
}
|
|
92789
|
+
} catch {
|
|
92790
|
+
continue;
|
|
92791
|
+
}
|
|
92792
|
+
}
|
|
92793
|
+
return void 0;
|
|
92794
|
+
},
|
|
92795
|
+
failed
|
|
92796
|
+
);
|
|
92797
|
+
await pollUntil(
|
|
92798
|
+
deadline,
|
|
92799
|
+
`app on ${hostname}:${options.appPort}`,
|
|
92800
|
+
async () => {
|
|
92801
|
+
try {
|
|
92802
|
+
await fetch(`http://${hostname}:${options.appPort}/`, { method: "HEAD" });
|
|
92803
|
+
return true;
|
|
92804
|
+
} catch {
|
|
92805
|
+
return void 0;
|
|
92806
|
+
}
|
|
92807
|
+
},
|
|
92808
|
+
failed
|
|
92809
|
+
);
|
|
92810
|
+
return {
|
|
92811
|
+
pid: child.pid ?? -1,
|
|
92812
|
+
appPort: options.appPort,
|
|
92813
|
+
controlPort,
|
|
92814
|
+
explainExit: () => {
|
|
92815
|
+
if (!exited) {
|
|
92816
|
+
return null;
|
|
92817
|
+
}
|
|
92818
|
+
const stderr = stderrWindow();
|
|
92819
|
+
return {
|
|
92820
|
+
reason: explainRuntimeFailure(
|
|
92821
|
+
stderr,
|
|
92822
|
+
options.maxOldSpaceMb ?? DEFAULT_MAX_OLD_SPACE_MB
|
|
92823
|
+
),
|
|
92824
|
+
heapExhausted: stderrShowsHeapExhaustion(stderr)
|
|
92825
|
+
};
|
|
92826
|
+
},
|
|
92827
|
+
close: async () => {
|
|
92828
|
+
if (exited) {
|
|
92829
|
+
return;
|
|
92830
|
+
}
|
|
92831
|
+
const gone = new Promise((resolve) => child.once("exit", () => resolve()));
|
|
92832
|
+
child.kill("SIGTERM");
|
|
92833
|
+
const killTimer = setTimeout(() => child.kill("SIGKILL"), 3e3);
|
|
92834
|
+
await gone;
|
|
92835
|
+
clearTimeout(killTimer);
|
|
92836
|
+
}
|
|
92837
|
+
};
|
|
92838
|
+
} catch (cause) {
|
|
92839
|
+
child.kill("SIGKILL");
|
|
92840
|
+
throw cause;
|
|
92886
92841
|
}
|
|
92887
|
-
return lines;
|
|
92888
92842
|
}
|
|
92889
|
-
|
|
92890
|
-
|
|
92891
|
-
|
|
92892
|
-
|
|
92893
|
-
|
|
92894
|
-
|
|
92895
|
-
|
|
92896
|
-
|
|
92897
|
-
|
|
92898
|
-
|
|
92899
|
-
|
|
92900
|
-
|
|
92901
|
-
|
|
92843
|
+
|
|
92844
|
+
// src/cli-args.ts
|
|
92845
|
+
var BUILD_COMMAND = "build";
|
|
92846
|
+
var RUN_ONLY_FLAGS = [
|
|
92847
|
+
["routes", "--routes"],
|
|
92848
|
+
["cycles", "--cycles"],
|
|
92849
|
+
["requests", "--requests"],
|
|
92850
|
+
["connections", "--connections"],
|
|
92851
|
+
["idleSeconds", "--idle"],
|
|
92852
|
+
["warmupRequests", "--warmup"],
|
|
92853
|
+
["maxOldSpaceMb", "--max-old-space"],
|
|
92854
|
+
["quick", "--quick"],
|
|
92855
|
+
["noResolve", "--no-resolve"],
|
|
92856
|
+
["diffAll", "--diff-all"],
|
|
92857
|
+
["writeConfig", "--write-config"]
|
|
92858
|
+
];
|
|
92859
|
+
function runOnlyFlagUsed(options) {
|
|
92860
|
+
for (const [field, flag] of RUN_ONLY_FLAGS) {
|
|
92861
|
+
const value = options[field];
|
|
92862
|
+
if (value !== null && value !== false) {
|
|
92863
|
+
return flag;
|
|
92864
|
+
}
|
|
92902
92865
|
}
|
|
92903
|
-
return
|
|
92866
|
+
return null;
|
|
92904
92867
|
}
|
|
92905
|
-
|
|
92906
|
-
|
|
92907
|
-
|
|
92908
|
-
|
|
92909
|
-
|
|
92910
|
-
|
|
92911
|
-
|
|
92912
|
-
|
|
92913
|
-
|
|
92914
|
-
|
|
92915
|
-
|
|
92916
|
-
|
|
92868
|
+
var FLAGS = [
|
|
92869
|
+
{
|
|
92870
|
+
flag: "--routes",
|
|
92871
|
+
value: "list",
|
|
92872
|
+
argName: "<list>",
|
|
92873
|
+
help: "Only measure these routes \u2014 comma-separated templates or prefixes (e.g. /api,/dashboard)"
|
|
92874
|
+
},
|
|
92875
|
+
{ flag: "--cycles", value: "int", argName: "<n>", help: "Load cycles per route (default 4, minimum 3)" },
|
|
92876
|
+
{ flag: "--requests", value: "int", argName: "<n>", help: "Requests per cycle (default 5000)" },
|
|
92877
|
+
{ flag: "--connections", value: "int", argName: "<n>", help: "Concurrent connections (default 100)" },
|
|
92878
|
+
{ flag: "--idle", value: "int", argName: "<seconds>", help: "Idle seconds before each sample (default 30)" },
|
|
92879
|
+
{
|
|
92880
|
+
flag: "--warmup",
|
|
92881
|
+
value: "int",
|
|
92882
|
+
argName: "<n>",
|
|
92883
|
+
help: "Requests before the baseline snapshot (default 200) \u2014 lower it on apps that cache per request"
|
|
92884
|
+
},
|
|
92885
|
+
{
|
|
92886
|
+
flag: "--max-old-space",
|
|
92887
|
+
value: "int",
|
|
92888
|
+
argName: "<mb>",
|
|
92889
|
+
help: "Heap cap of each measured process (default 512) \u2014 raise it for apps whose working set is larger"
|
|
92890
|
+
},
|
|
92891
|
+
{
|
|
92892
|
+
flag: "--quick",
|
|
92893
|
+
value: "none",
|
|
92894
|
+
help: "Fast preset: 2000 requests x 4 cycles, 8s idle \u2014 same cycle count as the default, less traffic per cycle"
|
|
92895
|
+
},
|
|
92896
|
+
{ flag: "--diff-all", value: "none", help: "Diff snapshots for stable routes too (slow)" },
|
|
92897
|
+
{
|
|
92898
|
+
flag: "--attribute",
|
|
92899
|
+
value: "none",
|
|
92900
|
+
help: "build only: also name what the worker retains \u2014 signals it for heap snapshots, which can stall the build"
|
|
92901
|
+
},
|
|
92902
|
+
{
|
|
92903
|
+
flag: "--no-resolve",
|
|
92904
|
+
value: "none",
|
|
92905
|
+
help: "Do not re-measure inconclusive routes with more cycles (default: re-measure once)"
|
|
92906
|
+
},
|
|
92907
|
+
{ flag: "--output", value: "string", argName: "<dir>", help: "Where to write runs (default <app-dir>/.next-leak)" },
|
|
92908
|
+
{
|
|
92909
|
+
flag: "--write-config",
|
|
92910
|
+
value: "none",
|
|
92911
|
+
help: "Write next-leak.config.json for the routes that need sample params, then exit (never overwrites)"
|
|
92912
|
+
},
|
|
92913
|
+
{ flag: "--help", alias: "-h", value: "none", help: "Show this help" },
|
|
92914
|
+
{ flag: "--version", alias: "-v", value: "none", help: "Print the version" }
|
|
92915
|
+
];
|
|
92916
|
+
function helpText(version2) {
|
|
92917
|
+
const rows = FLAGS.map((spec) => {
|
|
92918
|
+
const alias = spec.alias ? `${spec.alias}, ` : "";
|
|
92919
|
+
const argName = spec.argName ? ` ${spec.argName}` : "";
|
|
92920
|
+
const left = `${alias}${spec.flag}${argName}`;
|
|
92921
|
+
return ` ${left.padEnd(26)}${spec.help}`;
|
|
92922
|
+
}).join("\n");
|
|
92923
|
+
return `next-leak ${version2}
|
|
92924
|
+
|
|
92925
|
+
Find out whether your Next.js app actually leaks memory \u2014 how much, on which
|
|
92926
|
+
route, and whose fault it is.
|
|
92927
|
+
|
|
92928
|
+
Usage:
|
|
92929
|
+
next-leak <app-dir> [options] measure a built server under load
|
|
92930
|
+
next-leak build <app-dir> measure the memory of "next build"
|
|
92931
|
+
|
|
92932
|
+
The app must be built with output: "standalone" (next build). For each
|
|
92933
|
+
discovered route, next-leak boots a fresh instrumented process and runs:
|
|
92934
|
+
warm-up \u2192 GC \u2192 baseline snapshot \u2192 [load \u2192 idle \u2192 GC \u2192 sample] \xD7 cycles \u2192
|
|
92935
|
+
snapshot. Evidence (report.html, ISSUE drafts, raw snapshots, run.json) is
|
|
92936
|
+
written under the output directory.
|
|
92937
|
+
|
|
92938
|
+
The build command needs neither a previous build nor standalone output: it runs
|
|
92939
|
+
the build itself and samples the resident memory of each static-generation
|
|
92940
|
+
worker, which is where large sites run out of heap while prerendering. It takes
|
|
92941
|
+
--output only; the options below shape HTTP load and do not apply to it.
|
|
92942
|
+
|
|
92943
|
+
Options:
|
|
92944
|
+
${rows}
|
|
92945
|
+
|
|
92946
|
+
Exit codes: 0 when the run completes (whatever the verdicts), 130 when
|
|
92947
|
+
interrupted, 1 on errors.
|
|
92948
|
+
`;
|
|
92917
92949
|
}
|
|
92918
|
-
|
|
92919
|
-
|
|
92920
|
-
|
|
92921
|
-
|
|
92922
|
-
|
|
92923
|
-
|
|
92924
|
-
|
|
92925
|
-
|
|
92926
|
-
|
|
92927
|
-
|
|
92928
|
-
|
|
92929
|
-
return lines;
|
|
92950
|
+
var LIMITS = {
|
|
92951
|
+
"--cycles": 100,
|
|
92952
|
+
"--requests": 1e6,
|
|
92953
|
+
"--connections": 1e4,
|
|
92954
|
+
"--idle": 3600,
|
|
92955
|
+
"--warmup": 1e6,
|
|
92956
|
+
"--max-old-space": 65536
|
|
92957
|
+
};
|
|
92958
|
+
var MIN_MAX_OLD_SPACE_MB = 128;
|
|
92959
|
+
function findSpec(argument) {
|
|
92960
|
+
return FLAGS.find((spec) => spec.flag === argument || spec.alias === argument);
|
|
92930
92961
|
}
|
|
92931
|
-
|
|
92932
|
-
|
|
92933
|
-
|
|
92934
|
-
|
|
92962
|
+
var flagError = (message) => ({ kind: "error", message });
|
|
92963
|
+
var FLAG_OK = { kind: "ok" };
|
|
92964
|
+
function applyRoutesFlag(value, options) {
|
|
92965
|
+
const routes = value.split(",").map((route) => route.trim()).filter((route) => route !== "");
|
|
92966
|
+
if (routes.length === 0) {
|
|
92967
|
+
return flagError(`option "--routes" needs at least one route`);
|
|
92935
92968
|
}
|
|
92936
|
-
|
|
92937
|
-
|
|
92938
|
-
retainedHeapBytes: retained,
|
|
92939
|
-
maxOldSpaceMb: parameters.maxOldSpaceMb
|
|
92940
|
-
});
|
|
92941
|
-
return pressure === null ? [] : [` \u25B2 peak pressure: ${describePeakPressure(pressure)}`];
|
|
92942
|
-
}
|
|
92943
|
-
function unreclaimedLines(route, verdict) {
|
|
92944
|
-
const retention = assessUnreclaimedRetention({
|
|
92945
|
-
unreclaimedSamples: route.unreclaimedSamples,
|
|
92946
|
-
memorySamples: route.memorySamples,
|
|
92947
|
-
verdict,
|
|
92948
|
-
verdictIsWellSupported: warrantsIssueDraft(route)
|
|
92949
|
-
});
|
|
92950
|
-
return retention === null ? [] : [` \u25B2 unreclaimed: ${describeUnreclaimedRetention(retention)}`];
|
|
92969
|
+
options.routes = routes;
|
|
92970
|
+
return FLAG_OK;
|
|
92951
92971
|
}
|
|
92952
|
-
function
|
|
92953
|
-
|
|
92954
|
-
|
|
92972
|
+
function applyNumericFlag(flag, value, options) {
|
|
92973
|
+
const parsed = Number(value);
|
|
92974
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
92975
|
+
return flagError(`option "${flag}" needs a positive integer, got "${value}"`);
|
|
92955
92976
|
}
|
|
92956
|
-
|
|
92957
|
-
|
|
92977
|
+
const maximum = LIMITS[flag];
|
|
92978
|
+
if (maximum !== void 0 && parsed > maximum) {
|
|
92979
|
+
return flagError(
|
|
92980
|
+
`option "${flag}" is capped at ${maximum} (got ${parsed}) \u2014 a run that large would never finish`
|
|
92981
|
+
);
|
|
92958
92982
|
}
|
|
92959
|
-
if (
|
|
92960
|
-
return
|
|
92983
|
+
if (flag === "--cycles" && parsed < 3) {
|
|
92984
|
+
return flagError("the trend verdict needs at least 3 cycles (--cycles 3 or more)");
|
|
92961
92985
|
}
|
|
92962
|
-
|
|
92963
|
-
|
|
92964
|
-
|
|
92965
|
-
|
|
92966
|
-
|
|
92967
|
-
|
|
92968
|
-
|
|
92969
|
-
|
|
92970
|
-
|
|
92971
|
-
|
|
92972
|
-
|
|
92973
|
-
|
|
92974
|
-
...unreclaimedLines(route, verdict),
|
|
92975
|
-
...findingLines(route)
|
|
92976
|
-
];
|
|
92986
|
+
if (flag === "--max-old-space" && parsed < MIN_MAX_OLD_SPACE_MB) {
|
|
92987
|
+
return flagError(
|
|
92988
|
+
`option "--max-old-space" needs at least ${MIN_MAX_OLD_SPACE_MB} MB (got ${parsed}) \u2014 below that the measured app cannot start`
|
|
92989
|
+
);
|
|
92990
|
+
}
|
|
92991
|
+
if (flag === "--cycles") options.cycles = parsed;
|
|
92992
|
+
if (flag === "--requests") options.requests = parsed;
|
|
92993
|
+
if (flag === "--connections") options.connections = parsed;
|
|
92994
|
+
if (flag === "--idle") options.idleSeconds = parsed;
|
|
92995
|
+
if (flag === "--warmup") options.warmupRequests = parsed;
|
|
92996
|
+
if (flag === "--max-old-space") options.maxOldSpaceMb = parsed;
|
|
92997
|
+
return FLAG_OK;
|
|
92977
92998
|
}
|
|
92978
|
-
function
|
|
92979
|
-
|
|
92980
|
-
|
|
92981
|
-
|
|
92982
|
-
|
|
92999
|
+
function applyFlag(spec, value, options) {
|
|
93000
|
+
switch (spec.flag) {
|
|
93001
|
+
case "--routes":
|
|
93002
|
+
return applyRoutesFlag(value, options);
|
|
93003
|
+
case "--cycles":
|
|
93004
|
+
case "--requests":
|
|
93005
|
+
case "--connections":
|
|
93006
|
+
case "--idle":
|
|
93007
|
+
case "--warmup":
|
|
93008
|
+
case "--max-old-space":
|
|
93009
|
+
return applyNumericFlag(spec.flag, value, options);
|
|
93010
|
+
case "--quick":
|
|
93011
|
+
options.quick = true;
|
|
93012
|
+
return FLAG_OK;
|
|
93013
|
+
case "--diff-all":
|
|
93014
|
+
options.diffAll = true;
|
|
93015
|
+
return FLAG_OK;
|
|
93016
|
+
case "--attribute":
|
|
93017
|
+
options.attributeBuild = true;
|
|
93018
|
+
return FLAG_OK;
|
|
93019
|
+
case "--write-config":
|
|
93020
|
+
options.writeConfig = true;
|
|
93021
|
+
return FLAG_OK;
|
|
93022
|
+
case "--no-resolve":
|
|
93023
|
+
options.noResolve = true;
|
|
93024
|
+
return FLAG_OK;
|
|
93025
|
+
case "--output":
|
|
93026
|
+
options.output = value;
|
|
93027
|
+
return FLAG_OK;
|
|
93028
|
+
default:
|
|
93029
|
+
return FLAG_OK;
|
|
92983
93030
|
}
|
|
92984
|
-
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.`;
|
|
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
|
-
];
|
|
92990
93031
|
}
|
|
92991
|
-
function
|
|
92992
|
-
|
|
92993
|
-
|
|
92994
|
-
if (measured === total) {
|
|
92995
|
-
return `measured all ${total} discovered route(s)`;
|
|
93032
|
+
function readFlagValue(spec, argv, index) {
|
|
93033
|
+
if (spec.value === "none") {
|
|
93034
|
+
return { value: "" };
|
|
92996
93035
|
}
|
|
92997
|
-
const
|
|
92998
|
-
|
|
92999
|
-
|
|
93000
|
-
|
|
93001
|
-
|
|
93036
|
+
const value = argv[index + 1] ?? "";
|
|
93037
|
+
const looksNegativeNumber = /^-\d+(\.\d+)?$/.test(value);
|
|
93038
|
+
if (value === "" || value.startsWith("-") && !looksNegativeNumber) {
|
|
93039
|
+
return { error: `option "${spec.flag}" needs a value ${spec.argName ?? ""}` };
|
|
93040
|
+
}
|
|
93041
|
+
return { value };
|
|
93002
93042
|
}
|
|
93003
|
-
function
|
|
93004
|
-
const
|
|
93005
|
-
|
|
93006
|
-
|
|
93043
|
+
function parseFlagAt(argv, index, options) {
|
|
93044
|
+
const argument = argv[index] ?? "";
|
|
93045
|
+
const spec = findSpec(argument);
|
|
93046
|
+
if (spec === void 0) {
|
|
93047
|
+
return { done: { kind: "error", message: `unknown option "${argument}" \u2014 see --help` } };
|
|
93007
93048
|
}
|
|
93008
|
-
|
|
93009
|
-
|
|
93010
|
-
const resolvedCycles = [
|
|
93011
|
-
...new Set(
|
|
93012
|
-
report.routes.flatMap(
|
|
93013
|
-
(route) => route.status === "measured" && route.resolvedWithCycles !== void 0 ? [route.resolvedWithCycles] : []
|
|
93014
|
-
)
|
|
93015
|
-
)
|
|
93016
|
-
].sort((a, b) => a - b);
|
|
93017
|
-
const cyclesLabel = resolvedCycles.length === 0 ? `${cycles} cycles` : `${cycles} cycles (${resolvedCycles.join(", ")} where resolved)`;
|
|
93018
|
-
lines.push("", coverageLine(report));
|
|
93019
|
-
lines.push(
|
|
93020
|
-
`judged over ${cyclesLabel} \xD7 ${loadRequests} requests, growth gate ${(minGrowthPerCycle / 1024).toFixed(0)} KiB/cycle (${formatGrowth(minGrowthPerCycle / loadRequests * 1e3)}), heap cap ${maxOldSpaceMb} MB`,
|
|
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
|
-
);
|
|
93049
|
+
if (spec.flag === "--help") {
|
|
93050
|
+
return { done: { kind: "help" } };
|
|
93036
93051
|
}
|
|
93037
|
-
|
|
93052
|
+
if (spec.flag === "--version") {
|
|
93053
|
+
return { done: { kind: "version" } };
|
|
93054
|
+
}
|
|
93055
|
+
const read2 = readFlagValue(spec, argv, index);
|
|
93056
|
+
if ("error" in read2) {
|
|
93057
|
+
return { done: { kind: "error", message: read2.error } };
|
|
93058
|
+
}
|
|
93059
|
+
const outcome = applyFlag(spec, read2.value, options);
|
|
93060
|
+
if (outcome.kind === "error") {
|
|
93061
|
+
return { done: { kind: "error", message: outcome.message } };
|
|
93062
|
+
}
|
|
93063
|
+
return { consumed: spec.value === "none" ? 0 : 1 };
|
|
93038
93064
|
}
|
|
93039
|
-
|
|
93040
|
-
|
|
93041
|
-
|
|
93042
|
-
|
|
93043
|
-
|
|
93044
|
-
|
|
93045
|
-
|
|
93046
|
-
|
|
93065
|
+
function parseCliArgs(argv) {
|
|
93066
|
+
const isBuild = argv[0] === BUILD_COMMAND;
|
|
93067
|
+
const rest = isBuild ? argv.slice(1) : argv;
|
|
93068
|
+
const options = {
|
|
93069
|
+
appDir: "",
|
|
93070
|
+
routes: null,
|
|
93071
|
+
cycles: null,
|
|
93072
|
+
requests: null,
|
|
93073
|
+
connections: null,
|
|
93074
|
+
idleSeconds: null,
|
|
93075
|
+
warmupRequests: null,
|
|
93076
|
+
maxOldSpaceMb: null,
|
|
93077
|
+
quick: false,
|
|
93078
|
+
noResolve: false,
|
|
93079
|
+
diffAll: false,
|
|
93080
|
+
attributeBuild: false,
|
|
93081
|
+
writeConfig: false,
|
|
93082
|
+
output: null
|
|
93083
|
+
};
|
|
93084
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
93085
|
+
const argument = rest[index] ?? "";
|
|
93086
|
+
if (!argument.startsWith("-")) {
|
|
93087
|
+
if (options.appDir !== "") {
|
|
93088
|
+
return { kind: "error", message: `unexpected extra argument "${argument}" \u2014 one app directory only` };
|
|
93089
|
+
}
|
|
93090
|
+
options.appDir = argument;
|
|
93091
|
+
continue;
|
|
93092
|
+
}
|
|
93093
|
+
const step = parseFlagAt(rest, index, options);
|
|
93094
|
+
if ("done" in step) {
|
|
93095
|
+
return step.done;
|
|
93096
|
+
}
|
|
93097
|
+
index += step.consumed;
|
|
93047
93098
|
}
|
|
93048
|
-
|
|
93049
|
-
|
|
93050
|
-
|
|
93051
|
-
|
|
93052
|
-
const
|
|
93053
|
-
if (
|
|
93054
|
-
return {
|
|
93099
|
+
if (options.appDir === "") {
|
|
93100
|
+
return { kind: "help" };
|
|
93101
|
+
}
|
|
93102
|
+
if (isBuild) {
|
|
93103
|
+
const misplaced = runOnlyFlagUsed(options);
|
|
93104
|
+
if (misplaced !== null) {
|
|
93105
|
+
return {
|
|
93106
|
+
kind: "error",
|
|
93107
|
+
message: `option "${misplaced}" shapes HTTP load and does not apply to "next-leak build" \u2014 see --help`
|
|
93108
|
+
};
|
|
93055
93109
|
}
|
|
93056
|
-
return {
|
|
93110
|
+
return {
|
|
93111
|
+
kind: "build",
|
|
93112
|
+
options: {
|
|
93113
|
+
appDir: options.appDir,
|
|
93114
|
+
output: options.output,
|
|
93115
|
+
attributeBuild: options.attributeBuild
|
|
93116
|
+
}
|
|
93117
|
+
};
|
|
93057
93118
|
}
|
|
93058
|
-
|
|
93059
|
-
|
|
93119
|
+
return { kind: "run", options };
|
|
93120
|
+
}
|
|
93121
|
+
|
|
93122
|
+
// src/guards.ts
|
|
93123
|
+
function currentFacts() {
|
|
93124
|
+
return {
|
|
93125
|
+
nodeMajor: Number(process.versions.node.split(".")[0]),
|
|
93126
|
+
platform: process.platform
|
|
93127
|
+
};
|
|
93128
|
+
}
|
|
93129
|
+
function checkRuntime(facts = currentFacts()) {
|
|
93130
|
+
if (facts.platform === "win32") {
|
|
93131
|
+
return "next-leak does not support Windows (the measurement ritual relies on POSIX process control). Run it inside WSL2 or a Linux container.";
|
|
93060
93132
|
}
|
|
93061
|
-
|
|
93062
|
-
|
|
93063
|
-
return { owner: "app", source: relative, packageName: null };
|
|
93133
|
+
if (facts.nodeMajor < 22) {
|
|
93134
|
+
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.`;
|
|
93064
93135
|
}
|
|
93065
|
-
return
|
|
93136
|
+
return null;
|
|
93066
93137
|
}
|
|
93067
|
-
|
|
93068
|
-
|
|
93069
|
-
|
|
93070
|
-
|
|
93071
|
-
|
|
93138
|
+
|
|
93139
|
+
// src/route-config.ts
|
|
93140
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
93141
|
+
import path4 from "path";
|
|
93142
|
+
import { z as z4 } from "zod";
|
|
93143
|
+
var sampleValueSchema = z4.string().refine(
|
|
93144
|
+
(value) => !(value.includes("{n}") && /\{n%\d+\}/.test(value)),
|
|
93145
|
+
{ message: 'a sample value cannot carry both "{n}" and "{n%N}" \u2014 pick one' }
|
|
93146
|
+
);
|
|
93147
|
+
var routeConfigSchema = z4.object({
|
|
93148
|
+
params: z4.record(z4.string(), sampleValueSchema).optional(),
|
|
93149
|
+
routes: z4.record(z4.string(), z4.record(z4.string(), sampleValueSchema)).optional(),
|
|
93150
|
+
/**
|
|
93151
|
+
* Headers sent with every request. Real traffic is not header-less:
|
|
93152
|
+
* compression (`Accept-Encoding`), sessions and auth change which code
|
|
93153
|
+
* paths run, and some leaks only exist on those paths (zlib, per-session
|
|
93154
|
+
* caches). Measuring without them measures a different app.
|
|
93155
|
+
*/
|
|
93156
|
+
headers: z4.record(z4.string(), z4.string()).optional(),
|
|
93157
|
+
/** Query string appended per route template, e.g. `"weightKb=2048"`. */
|
|
93158
|
+
query: z4.record(z4.string(), z4.string()).optional(),
|
|
93159
|
+
/**
|
|
93160
|
+
* Simulates clients that give up before the response arrives (closed
|
|
93161
|
+
* tabs, load-balancer timeouts, bots). Several real leaks only exist on
|
|
93162
|
+
* that path — vercel/next.js#89091 traces `ServerResponse` retention to
|
|
93163
|
+
* an early disconnect — and a load generator that always waits politely
|
|
93164
|
+
* never reaches it.
|
|
93165
|
+
*
|
|
93166
|
+
* Counted **from the first byte of the response**, not from the request:
|
|
93167
|
+
* under load a server's first byte arrives long after any sane fixed
|
|
93168
|
+
* window, so a request-relative clock cuts before the stream starts and
|
|
93169
|
+
* tests the wrong path entirely.
|
|
93170
|
+
*/
|
|
93171
|
+
abandonAfterMs: z4.number().int().positive().optional(),
|
|
93172
|
+
/**
|
|
93173
|
+
* Where the abandon deadline starts. `first-byte` is the default and the
|
|
93174
|
+
* semantics everything else assumes.
|
|
93175
|
+
*
|
|
93176
|
+
* `request` starts it when the request is written, which is the only way
|
|
93177
|
+
* to reach leaks that need the client already gone before the server
|
|
93178
|
+
* produces anything: against a route whose time-to-first-byte exceeds the
|
|
93179
|
+
* deadline, a first-byte clock never cuts early. Measured on the
|
|
93180
|
+
* vercel/next.js#84648 repro, whose upstream answers in 400 ms while its
|
|
93181
|
+
* own load generator cuts at 60 ms — the first-byte clock reached 7% of
|
|
93182
|
+
* requests there, none of them on the path the issue describes.
|
|
93183
|
+
*
|
|
93184
|
+
* Opt-in per route, never a default: armed on connect, the deadline loses
|
|
93185
|
+
* the race to the first byte on fast routes, which is how the earlier
|
|
93186
|
+
* version of this measured 2 mid-stream cuts out of ~1500 on #94919.
|
|
93187
|
+
*/
|
|
93188
|
+
abandonFrom: z4.enum(["first-byte", "request"]).optional()
|
|
93189
|
+
}).strict().refine((config) => config.abandonFrom === void 0 || config.abandonAfterMs !== void 0, {
|
|
93190
|
+
message: '"abandonFrom" configures nothing without "abandonAfterMs"',
|
|
93191
|
+
path: ["abandonFrom"]
|
|
93192
|
+
});
|
|
93193
|
+
var RouteConfigError = class extends Error {
|
|
93194
|
+
constructor(message) {
|
|
93195
|
+
super(message);
|
|
93196
|
+
this.name = "RouteConfigError";
|
|
93197
|
+
}
|
|
93072
93198
|
};
|
|
93073
|
-
var
|
|
93074
|
-
|
|
93075
|
-
|
|
93076
|
-
|
|
93077
|
-
|
|
93078
|
-
|
|
93079
|
-
|
|
93080
|
-
|
|
93081
|
-
|
|
93082
|
-
|
|
93083
|
-
|
|
93084
|
-
|
|
93085
|
-
|
|
93086
|
-
|
|
93087
|
-
|
|
93088
|
-
return { owner: "framework", source: null, packageName: `next (${component})` };
|
|
93089
|
-
}
|
|
93199
|
+
var ROUTE_CONFIG_FILE = "next-leak.config.json";
|
|
93200
|
+
async function loadRouteConfig(appDir) {
|
|
93201
|
+
const file = path4.join(appDir, ROUTE_CONFIG_FILE);
|
|
93202
|
+
let raw;
|
|
93203
|
+
try {
|
|
93204
|
+
raw = await readFile4(file, "utf8");
|
|
93205
|
+
} catch {
|
|
93206
|
+
return {};
|
|
93207
|
+
}
|
|
93208
|
+
try {
|
|
93209
|
+
return routeConfigSchema.parse(JSON.parse(raw));
|
|
93210
|
+
} catch (cause) {
|
|
93211
|
+
throw new RouteConfigError(
|
|
93212
|
+
`${file} is invalid: ${cause instanceof Error ? cause.message : String(cause)}`
|
|
93213
|
+
);
|
|
93090
93214
|
}
|
|
93091
|
-
return null;
|
|
93092
93215
|
}
|
|
93093
|
-
|
|
93094
|
-
|
|
93095
|
-
|
|
93096
|
-
|
|
93097
|
-
|
|
93216
|
+
var SEGMENT_PATTERN = /^\[(\[)?(\.\.\.)?([^\]]+?)\]?\]$/;
|
|
93217
|
+
function dynamicSegmentsOf(routeTemplate) {
|
|
93218
|
+
const segments = [];
|
|
93219
|
+
for (const segment of routeTemplate.split("/")) {
|
|
93220
|
+
const match = SEGMENT_PATTERN.exec(segment);
|
|
93221
|
+
if (match === null) {
|
|
93098
93222
|
continue;
|
|
93099
93223
|
}
|
|
93100
|
-
|
|
93101
|
-
|
|
93102
|
-
|
|
93103
|
-
|
|
93224
|
+
segments.push({
|
|
93225
|
+
name: match[3] ?? "",
|
|
93226
|
+
catchAll: match[2] !== void 0,
|
|
93227
|
+
optional: match[1] !== void 0
|
|
93228
|
+
});
|
|
93104
93229
|
}
|
|
93105
|
-
|
|
93106
|
-
|
|
93230
|
+
return segments;
|
|
93231
|
+
}
|
|
93232
|
+
var UNIQUE_MARKER = "{n}";
|
|
93233
|
+
var BOUNDED_MARKER = /\{n%(\d+)\}/;
|
|
93234
|
+
function boundedMarkerOf(value) {
|
|
93235
|
+
const match = BOUNDED_MARKER.exec(value);
|
|
93236
|
+
const bound = Number(match?.[1]);
|
|
93237
|
+
if (match === null || !Number.isInteger(bound) || bound <= 0) {
|
|
93238
|
+
return null;
|
|
93107
93239
|
}
|
|
93108
|
-
return
|
|
93240
|
+
return { marker: match[0], bound };
|
|
93109
93241
|
}
|
|
93110
|
-
|
|
93111
|
-
|
|
93112
|
-
|
|
93113
|
-
|
|
93114
|
-
|
|
93115
|
-
|
|
93116
|
-
|
|
93242
|
+
var ENCODED_MARKER = encodeURIComponent(UNIQUE_MARKER);
|
|
93243
|
+
var encodeSegment = (value) => restoreMarkers(encodeURIComponent(value).split(ENCODED_MARKER).join(UNIQUE_MARKER));
|
|
93244
|
+
var ENCODED_BOUNDED = /%7Bn%25(\d+)%7D/gi;
|
|
93245
|
+
var restoreMarkers = (value) => value.replace(ENCODED_BOUNDED, (_match, bound) => `{n%${bound}}`);
|
|
93246
|
+
var encodeCatchAll = (value) => value.split("/").map(encodeSegment).join("/");
|
|
93247
|
+
function resolveRoutePath(routeTemplate, config) {
|
|
93248
|
+
const resolved = [];
|
|
93249
|
+
for (const segment of routeTemplate.split("/")) {
|
|
93250
|
+
const match = SEGMENT_PATTERN.exec(segment);
|
|
93251
|
+
if (match === null) {
|
|
93252
|
+
resolved.push(encodeSegment(segment));
|
|
93117
93253
|
continue;
|
|
93118
93254
|
}
|
|
93119
|
-
const
|
|
93120
|
-
|
|
93121
|
-
const
|
|
93122
|
-
const
|
|
93123
|
-
|
|
93124
|
-
|
|
93125
|
-
|
|
93126
|
-
|
|
93127
|
-
for (const group of byGroup.values()) {
|
|
93128
|
-
if (winner === null || group.bytes > winner.bytes) {
|
|
93129
|
-
winner = group;
|
|
93255
|
+
const optional = match[1] !== void 0;
|
|
93256
|
+
const catchAll = match[2] !== void 0;
|
|
93257
|
+
const name = match[3] ?? "";
|
|
93258
|
+
const value = config.routes?.[routeTemplate]?.[name] ?? config.params?.[name];
|
|
93259
|
+
if (value !== void 0) {
|
|
93260
|
+
resolved.push(catchAll ? encodeCatchAll(value) : encodeSegment(value));
|
|
93261
|
+
} else if (!optional) {
|
|
93262
|
+
return null;
|
|
93130
93263
|
}
|
|
93131
93264
|
}
|
|
93132
|
-
const
|
|
93133
|
-
|
|
93134
|
-
|
|
93135
|
-
|
|
93136
|
-
// src/environment.ts
|
|
93137
|
-
import { createRequire } from "module";
|
|
93138
|
-
import os from "os";
|
|
93139
|
-
var require2 = createRequire(import.meta.url);
|
|
93140
|
-
var { version: nextLeakVersion } = require2("../package.json");
|
|
93141
|
-
function captureEnvironment(nextVersion) {
|
|
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
|
-
};
|
|
93265
|
+
const joined = resolved.join("/");
|
|
93266
|
+
const path7 = joined === "" ? "/" : joined;
|
|
93267
|
+
const query = config.query?.[routeTemplate];
|
|
93268
|
+
return query === void 0 || query === "" ? path7 : `${path7}?${query}`;
|
|
93151
93269
|
}
|
|
93152
93270
|
|
|
93153
|
-
// src/
|
|
93154
|
-
var
|
|
93155
|
-
|
|
93156
|
-
|
|
93157
|
-
|
|
93158
|
-
|
|
93159
|
-
|
|
93160
|
-
|
|
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);
|
|
93271
|
+
// src/route-guidance.ts
|
|
93272
|
+
var PLACEHOLDER = "REPLACE-ME";
|
|
93273
|
+
function varyingValueFrom(prerendered) {
|
|
93274
|
+
const numbered = /^(.*?)(\d+)$/.exec(prerendered);
|
|
93275
|
+
if (numbered !== null && numbered[1] !== "") {
|
|
93276
|
+
return `${numbered[1]}{n}`;
|
|
93277
|
+
}
|
|
93278
|
+
return `${prerendered}-{n}`;
|
|
93185
93279
|
}
|
|
93186
|
-
function
|
|
93187
|
-
const
|
|
93188
|
-
|
|
93189
|
-
|
|
93190
|
-
|
|
93191
|
-
|
|
93192
|
-
|
|
93193
|
-
|
|
93194
|
-
|
|
93280
|
+
function sampleValuesFromManifest(manifest, routeTemplate) {
|
|
93281
|
+
const routes = manifest?.routes;
|
|
93282
|
+
if (routes === void 0) {
|
|
93283
|
+
return null;
|
|
93284
|
+
}
|
|
93285
|
+
const concrete = Object.entries(routes).find(([, entry]) => entry.srcRoute === routeTemplate)?.[0];
|
|
93286
|
+
if (concrete === void 0) {
|
|
93287
|
+
return null;
|
|
93288
|
+
}
|
|
93289
|
+
const templateSegments = routeTemplate.split("/");
|
|
93290
|
+
const concreteSegments = concrete.split("/");
|
|
93291
|
+
const values = {};
|
|
93292
|
+
for (const [index, segment] of templateSegments.entries()) {
|
|
93293
|
+
const dynamic = dynamicSegmentsOf(segment);
|
|
93294
|
+
const parameter = dynamic[0];
|
|
93295
|
+
if (parameter === void 0) {
|
|
93296
|
+
continue;
|
|
93195
93297
|
}
|
|
93196
|
-
const
|
|
93197
|
-
|
|
93198
|
-
|
|
93199
|
-
if (edge === void 0 || seen.has(edge.fromNode.id)) {
|
|
93200
|
-
break;
|
|
93298
|
+
const value = parameter.catchAll ? concreteSegments.slice(index).join("/") : concreteSegments[index];
|
|
93299
|
+
if (value === void 0 || value === "") {
|
|
93300
|
+
return null;
|
|
93201
93301
|
}
|
|
93202
|
-
|
|
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
|
-
);
|
|
93302
|
+
values[parameter.name] = value;
|
|
93211
93303
|
}
|
|
93212
|
-
return
|
|
93213
|
-
}
|
|
93214
|
-
function retainerChain(node, depth) {
|
|
93215
|
-
return walkChain(node, depth).parts.join(" <- ");
|
|
93304
|
+
return Object.keys(values).length === 0 ? null : values;
|
|
93216
93305
|
}
|
|
93217
|
-
function
|
|
93218
|
-
const
|
|
93219
|
-
for (const
|
|
93220
|
-
const
|
|
93221
|
-
|
|
93222
|
-
);
|
|
93223
|
-
if (!isModuleInstance) {
|
|
93306
|
+
function renderConfigSkeleton(routeTemplates, manifest = void 0) {
|
|
93307
|
+
const routes = {};
|
|
93308
|
+
for (const template of routeTemplates) {
|
|
93309
|
+
const segments = dynamicSegmentsOf(template);
|
|
93310
|
+
if (segments.length === 0) {
|
|
93224
93311
|
continue;
|
|
93225
93312
|
}
|
|
93226
|
-
|
|
93227
|
-
|
|
93228
|
-
|
|
93229
|
-
|
|
93230
|
-
|
|
93231
|
-
if (Number.isInteger(id) && !ids.includes(id)) {
|
|
93232
|
-
ids.push(id);
|
|
93233
|
-
}
|
|
93313
|
+
const sampled = sampleValuesFromManifest(manifest, template);
|
|
93314
|
+
const values = {};
|
|
93315
|
+
for (const segment of segments) {
|
|
93316
|
+
const prerendered = sampled?.[segment.name];
|
|
93317
|
+
values[segment.name] = prerendered === void 0 ? PLACEHOLDER : varyingValueFrom(prerendered);
|
|
93234
93318
|
}
|
|
93319
|
+
routes[template] = values;
|
|
93235
93320
|
}
|
|
93236
|
-
return
|
|
93321
|
+
return Object.keys(routes).length === 0 ? null : JSON.stringify({ routes }, null, 2);
|
|
93237
93322
|
}
|
|
93238
|
-
function
|
|
93239
|
-
|
|
93240
|
-
|
|
93241
|
-
|
|
93242
|
-
|
|
93243
|
-
|
|
93244
|
-
|
|
93245
|
-
|
|
93246
|
-
|
|
93247
|
-
|
|
93248
|
-
|
|
93249
|
-
|
|
93250
|
-
|
|
93251
|
-
|
|
93252
|
-
|
|
93253
|
-
|
|
93254
|
-
|
|
93255
|
-
|
|
93256
|
-
|
|
93257
|
-
|
|
93258
|
-
retainedBytes: node.retainedSize,
|
|
93259
|
-
retainerChain: chain.parts.join(" <- "),
|
|
93260
|
-
moduleIds: collectModuleIds(chain.nodes)
|
|
93261
|
-
});
|
|
93262
|
-
}
|
|
93263
|
-
return;
|
|
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 });
|
|
93323
|
+
function hasPlaceholders(skeleton) {
|
|
93324
|
+
return skeleton.includes(PLACEHOLDER);
|
|
93325
|
+
}
|
|
93326
|
+
|
|
93327
|
+
// src/unreclaimed-retention.ts
|
|
93328
|
+
var MB = 1024 * 1024;
|
|
93329
|
+
var CLASSES = ["heap", "external", "arrayBuffers"];
|
|
93330
|
+
var MIN_GAP_BYTES = 2 * MB;
|
|
93331
|
+
var MIN_GAP_RATIO = 0.35;
|
|
93332
|
+
var MIN_RETAINED_BYTES = MB;
|
|
93333
|
+
var read = (sample, memoryClass) => memoryClass === "heap" ? sample.heapUsed : memoryClass === "external" ? sample.external : sample.arrayBuffers;
|
|
93334
|
+
function largestGap(values) {
|
|
93335
|
+
return values.reduce((highest, value) => Math.max(highest, value), 0);
|
|
93336
|
+
}
|
|
93337
|
+
function gapsFor(input, memoryClass) {
|
|
93338
|
+
const gaps = [];
|
|
93339
|
+
for (const [index, held] of input.unreclaimedSamples.entries()) {
|
|
93340
|
+
const afterGc = input.memorySamples[index + 1];
|
|
93341
|
+
if (afterGc === void 0) {
|
|
93342
|
+
break;
|
|
93284
93343
|
}
|
|
93344
|
+
gaps.push(read(held, memoryClass) - read(afterGc, memoryClass));
|
|
93285
93345
|
}
|
|
93286
|
-
|
|
93287
|
-
grown.sort((a, b) => b.retainedBytes - a.retainedBytes);
|
|
93288
|
-
fresh.sort((a, b) => b.retainedBytes - a.retainedBytes);
|
|
93289
|
-
return {
|
|
93290
|
-
typeDeltas,
|
|
93291
|
-
grownNodes: grown.slice(0, resolved.maxFindings),
|
|
93292
|
-
newNodes: fresh.slice(0, resolved.maxFindings)
|
|
93293
|
-
};
|
|
93346
|
+
return gaps;
|
|
93294
93347
|
}
|
|
93295
|
-
|
|
93296
|
-
|
|
93297
|
-
|
|
93298
|
-
this.name = "SnapshotError";
|
|
93299
|
-
}
|
|
93300
|
-
};
|
|
93301
|
-
var SNAPSHOT_HEAD = '{"snapshot"';
|
|
93302
|
-
var PROBE_BYTES = 512;
|
|
93303
|
-
async function assertReadableSnapshot(file) {
|
|
93304
|
-
const { open, stat } = await import("fs/promises");
|
|
93305
|
-
const { constants } = await import("buffer");
|
|
93306
|
-
let size;
|
|
93307
|
-
try {
|
|
93308
|
-
size = (await stat(file)).size;
|
|
93309
|
-
} catch {
|
|
93310
|
-
throw new SnapshotError(`heap snapshot is missing: ${file}`);
|
|
93311
|
-
}
|
|
93312
|
-
if (size === 0) {
|
|
93313
|
-
throw new SnapshotError(`heap snapshot is empty: ${file}`);
|
|
93348
|
+
function assessUnreclaimedRetention(input) {
|
|
93349
|
+
if (input.verdict === "leak" && input.verdictIsWellSupported || input.unreclaimedSamples.length === 0) {
|
|
93350
|
+
return null;
|
|
93314
93351
|
}
|
|
93315
|
-
|
|
93316
|
-
|
|
93317
|
-
|
|
93352
|
+
let worst = null;
|
|
93353
|
+
for (const memoryClass of CLASSES) {
|
|
93354
|
+
const gaps = gapsFor(input, memoryClass);
|
|
93355
|
+
if (gaps.length === 0) {
|
|
93356
|
+
continue;
|
|
93357
|
+
}
|
|
93358
|
+
const largestGapBytes = largestGap(gaps);
|
|
93359
|
+
if (largestGapBytes < MIN_GAP_BYTES) {
|
|
93360
|
+
continue;
|
|
93361
|
+
}
|
|
93362
|
+
const retainedBytes = largestGap(
|
|
93363
|
+
input.memorySamples.slice(1).map((sample) => read(sample, memoryClass))
|
|
93318
93364
|
);
|
|
93319
|
-
|
|
93320
|
-
|
|
93321
|
-
|
|
93322
|
-
const head = Buffer.alloc(Math.min(PROBE_BYTES, size));
|
|
93323
|
-
await handle.read(head, 0, head.length, 0);
|
|
93324
|
-
if (!head.toString("utf8").trimStart().startsWith(SNAPSHOT_HEAD)) {
|
|
93325
|
-
throw new SnapshotError(
|
|
93326
|
-
`not a V8 heap snapshot (missing "snapshot" header): ${file}`
|
|
93327
|
-
);
|
|
93365
|
+
const ratio = largestGapBytes / Math.max(retainedBytes, MIN_RETAINED_BYTES);
|
|
93366
|
+
if (ratio < MIN_GAP_RATIO) {
|
|
93367
|
+
continue;
|
|
93328
93368
|
}
|
|
93329
|
-
|
|
93330
|
-
|
|
93331
|
-
if (!tail.toString("utf8").trimEnd().endsWith("}")) {
|
|
93332
|
-
throw new SnapshotError(
|
|
93333
|
-
`heap snapshot looks truncated (JSON does not close): ${file} \u2014 the measured process may have been killed while writing it`
|
|
93334
|
-
);
|
|
93369
|
+
if (worst === null || ratio >= worst.ratio) {
|
|
93370
|
+
worst = { class: memoryClass, largestGapBytes, retainedBytes, ratio };
|
|
93335
93371
|
}
|
|
93336
|
-
} finally {
|
|
93337
|
-
await handle.close();
|
|
93338
93372
|
}
|
|
93373
|
+
return worst;
|
|
93339
93374
|
}
|
|
93340
|
-
var
|
|
93341
|
-
|
|
93342
|
-
|
|
93343
|
-
|
|
93375
|
+
var mb = (bytes) => `${(bytes / MB).toFixed(2)} MB`;
|
|
93376
|
+
var CLASS_LABEL = {
|
|
93377
|
+
heap: "heap",
|
|
93378
|
+
external: "external memory",
|
|
93379
|
+
arrayBuffers: "arrayBuffers"
|
|
93344
93380
|
};
|
|
93345
|
-
|
|
93346
|
-
|
|
93347
|
-
const after = await loadHeap(afterFile);
|
|
93348
|
-
return diffAgainstBaseline(summary, after, options);
|
|
93381
|
+
function describeUnreclaimedRetention(retention) {
|
|
93382
|
+
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`;
|
|
93349
93383
|
}
|
|
93350
93384
|
|
|
93351
|
-
// src/
|
|
93352
|
-
var
|
|
93353
|
-
var
|
|
93354
|
-
|
|
93355
|
-
const
|
|
93356
|
-
|
|
93357
|
-
|
|
93358
|
-
|
|
93359
|
-
|
|
93360
|
-
|
|
93361
|
-
|
|
93362
|
-
|
|
93363
|
-
|
|
93364
|
-
|
|
93365
|
-
|
|
93366
|
-
|
|
93367
|
-
values.push((value & 1) === 1 ? -(value >>> 1) : value >>> 1);
|
|
93368
|
-
value = 0;
|
|
93369
|
-
shift = 0;
|
|
93370
|
-
}
|
|
93385
|
+
// src/report.ts
|
|
93386
|
+
var MB2 = 1024 * 1024;
|
|
93387
|
+
var formatMb = (bytes) => `${(bytes / MB2).toFixed(1)} MB`;
|
|
93388
|
+
var formatGrowth = (bytes) => {
|
|
93389
|
+
const sign = bytes >= 0 ? "+" : "";
|
|
93390
|
+
return `${sign}${(bytes / MB2).toFixed(2)} MB/1000 req`;
|
|
93391
|
+
};
|
|
93392
|
+
var VERDICT_ICON = { leak: "\u2716", stable: "\u2714", inconclusive: "?" };
|
|
93393
|
+
var RSS_MIN_GROWTH_PER_CYCLE = 16 * MB2;
|
|
93394
|
+
var RSS_MIN_TOTAL_GROWTH = 64 * MB2;
|
|
93395
|
+
function hasSustainedRssGrowth(memorySamples) {
|
|
93396
|
+
const rss = memorySamples.map((sample) => sample.rss);
|
|
93397
|
+
const first = rss[1];
|
|
93398
|
+
const last = rss.at(-1);
|
|
93399
|
+
if (first === void 0 || last === void 0 || rss.length < 4) {
|
|
93400
|
+
return false;
|
|
93371
93401
|
}
|
|
93372
|
-
return
|
|
93402
|
+
return classifyTrend(rss, { minGrowthPerCycle: RSS_MIN_GROWTH_PER_CYCLE }).verdict === "leak" && last - first >= RSS_MIN_TOTAL_GROWTH;
|
|
93373
93403
|
}
|
|
93374
|
-
function
|
|
93375
|
-
|
|
93376
|
-
|
|
93377
|
-
|
|
93378
|
-
|
|
93379
|
-
|
|
93404
|
+
function ownerLabel(attribution) {
|
|
93405
|
+
switch (attribution.owner) {
|
|
93406
|
+
case "app":
|
|
93407
|
+
return `${attribution.source ?? "your code"} (your code)`;
|
|
93408
|
+
case "dependency":
|
|
93409
|
+
return `${attribution.packageName ?? "a dependency"} (dependency)`;
|
|
93410
|
+
case "framework":
|
|
93411
|
+
return attribution.packageName === null ? "Next.js/runtime internals" : `${attribution.packageName} (framework)`;
|
|
93412
|
+
case "unattributed":
|
|
93413
|
+
return null;
|
|
93380
93414
|
}
|
|
93381
|
-
return segments;
|
|
93382
93415
|
}
|
|
93383
|
-
function
|
|
93384
|
-
|
|
93385
|
-
|
|
93386
|
-
|
|
93387
|
-
|
|
93388
|
-
|
|
93389
|
-
|
|
93390
|
-
|
|
93391
|
-
if (columnDelta === void 0) {
|
|
93392
|
-
continue;
|
|
93393
|
-
}
|
|
93394
|
-
column += columnDelta;
|
|
93395
|
-
if (segment.length >= 4) {
|
|
93396
|
-
sourceIndex += segment[1] ?? 0;
|
|
93397
|
-
entries.push({ column, sourceIndex });
|
|
93398
|
-
}
|
|
93399
|
-
}
|
|
93400
|
-
lines.push({ entries });
|
|
93416
|
+
function revalidationLines(route) {
|
|
93417
|
+
return route.revalidatedEverySeconds === void 0 ? [] : [
|
|
93418
|
+
` driven through ISR revalidation (revalidates every ${route.revalidatedEverySeconds}s; without it the load would serve the cache)`
|
|
93419
|
+
];
|
|
93420
|
+
}
|
|
93421
|
+
function abandonLines(route) {
|
|
93422
|
+
if (route.abandon === void 0) {
|
|
93423
|
+
return [];
|
|
93401
93424
|
}
|
|
93402
|
-
return
|
|
93425
|
+
return [
|
|
93426
|
+
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`
|
|
93427
|
+
];
|
|
93403
93428
|
}
|
|
93404
|
-
function
|
|
93405
|
-
const
|
|
93406
|
-
if (
|
|
93407
|
-
|
|
93429
|
+
function confidenceLines(route) {
|
|
93430
|
+
const lines = [];
|
|
93431
|
+
if (route.confidence.supersededVerdict !== void 0) {
|
|
93432
|
+
lines.push(
|
|
93433
|
+
` measured ${route.trend.verdict}, withdrawn: the run did not observe what that verdict needs`
|
|
93434
|
+
);
|
|
93408
93435
|
}
|
|
93409
|
-
|
|
93410
|
-
|
|
93411
|
-
if (entry.column > column) {
|
|
93412
|
-
break;
|
|
93413
|
-
}
|
|
93414
|
-
found = entry.sourceIndex;
|
|
93436
|
+
for (const warning of route.confidence.warnings) {
|
|
93437
|
+
lines.push(` \u26A0 low confidence: ${warning.detail}`);
|
|
93415
93438
|
}
|
|
93416
|
-
return
|
|
93439
|
+
return lines;
|
|
93417
93440
|
}
|
|
93418
|
-
|
|
93419
|
-
|
|
93420
|
-
|
|
93421
|
-
|
|
93422
|
-
|
|
93423
|
-
|
|
93424
|
-
|
|
93425
|
-
sources: z4.array(z4.string()),
|
|
93426
|
-
mappings: z4.string()
|
|
93427
|
-
});
|
|
93428
|
-
var sectionedMapSchema = z4.looseObject({
|
|
93429
|
-
version: z4.number(),
|
|
93430
|
-
sections: z4.array(
|
|
93431
|
-
z4.looseObject({
|
|
93432
|
-
offset: z4.looseObject({ line: z4.number(), column: z4.number() }),
|
|
93433
|
-
map: z4.looseObject({ sources: z4.array(z4.string()), mappings: z4.string() })
|
|
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) };
|
|
93441
|
+
function memorySourceLines(route, verdict) {
|
|
93442
|
+
const lines = [];
|
|
93443
|
+
if (route.trend.source === "external" && verdict !== "stable") {
|
|
93444
|
+
const externalCurve = route.memorySamples.map((sample) => formatMb(sample.external)).join(" \u2192 ");
|
|
93445
|
+
lines.push(
|
|
93446
|
+
` verdict comes from EXTERNAL memory (buffers, streams, fetch bodies), not the JS heap: external ${externalCurve}`
|
|
93447
|
+
);
|
|
93441
93448
|
}
|
|
93442
|
-
|
|
93443
|
-
|
|
93444
|
-
|
|
93449
|
+
if (verdict === "stable" && hasSustainedRssGrowth(route.memorySamples)) {
|
|
93450
|
+
const rssCurve = route.memorySamples.map((sample) => formatMb(sample.rss)).join(" \u2192 ");
|
|
93451
|
+
lines.push(
|
|
93452
|
+
` note: heap is flat but RSS grows ${formatGrowth(route.rssPer1000Requests)} \u2014 not a JS-heap leak (allocator, external buffers or fragmentation): RSS ${rssCurve}`
|
|
93453
|
+
);
|
|
93445
93454
|
}
|
|
93446
|
-
|
|
93447
|
-
|
|
93448
|
-
|
|
93449
|
-
|
|
93450
|
-
|
|
93451
|
-
for (const [lineIndex, mapping] of decodeMappings(section.map.mappings).entries()) {
|
|
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
|
-
}
|
|
93455
|
+
return lines;
|
|
93456
|
+
}
|
|
93457
|
+
function diffFindingLines(route) {
|
|
93458
|
+
if (route.diff === null) {
|
|
93459
|
+
return [];
|
|
93462
93460
|
}
|
|
93463
|
-
|
|
93464
|
-
|
|
93461
|
+
const findings = [...route.diff.grownNodes, ...route.diff.newNodes];
|
|
93462
|
+
return findings.slice(0, 3).map((finding, index) => {
|
|
93463
|
+
const size = formatMb(finding.retainedBytes);
|
|
93464
|
+
const attribution = route.attribution?.findings[index];
|
|
93465
|
+
const owner = attribution === void 0 ? null : ownerLabel(attribution);
|
|
93466
|
+
const detail = owner ?? (finding.retainerChain === "" ? "" : finding.retainerChain);
|
|
93467
|
+
return ` \u21B3 ${finding.kind} [${finding.nodeType}] ${finding.name} ${size}${detail === "" ? "" : ` \u2014 ${detail}`}`;
|
|
93468
|
+
});
|
|
93469
|
+
}
|
|
93470
|
+
function findingLines(route) {
|
|
93471
|
+
const lines = [];
|
|
93472
|
+
const culprit = route.attribution === null ? null : ownerLabel(route.attribution.route);
|
|
93473
|
+
if (culprit !== null) {
|
|
93474
|
+
lines.push(` culprit: ${culprit}`);
|
|
93465
93475
|
}
|
|
93466
|
-
|
|
93467
|
-
|
|
93476
|
+
lines.push(...diffFindingLines(route));
|
|
93477
|
+
for (const signature of route.signatures) {
|
|
93478
|
+
const flag = signature.historical ? " (historical)" : "";
|
|
93479
|
+
lines.push(` \u26A0 known cause${flag}: ${signature.title} \u2014 ${signature.issue}`);
|
|
93468
93480
|
}
|
|
93469
|
-
return
|
|
93481
|
+
return lines;
|
|
93470
93482
|
}
|
|
93471
|
-
|
|
93472
|
-
|
|
93473
|
-
|
|
93474
|
-
|
|
93475
|
-
let match = ID_IN_GAP.exec(gap);
|
|
93476
|
-
while (match !== null) {
|
|
93477
|
-
last = Number(match[1]);
|
|
93478
|
-
match = ID_IN_GAP.exec(gap);
|
|
93483
|
+
function peakPressureLines(route, parameters) {
|
|
93484
|
+
const retained = route.memorySamples.at(-1)?.heapUsed;
|
|
93485
|
+
if (retained === void 0) {
|
|
93486
|
+
return [];
|
|
93479
93487
|
}
|
|
93480
|
-
|
|
93488
|
+
const pressure = assessPeakPressure({
|
|
93489
|
+
peaks: route.peaks,
|
|
93490
|
+
retainedHeapBytes: retained,
|
|
93491
|
+
maxOldSpaceMb: parameters.maxOldSpaceMb
|
|
93492
|
+
});
|
|
93493
|
+
return pressure === null ? [] : [` \u25B2 peak pressure: ${describePeakPressure(pressure)}`];
|
|
93481
93494
|
}
|
|
93482
|
-
function
|
|
93483
|
-
|
|
93484
|
-
|
|
93485
|
-
|
|
93486
|
-
|
|
93487
|
-
|
|
93488
|
-
|
|
93489
|
-
|
|
93490
|
-
const spanEnd = nextId === null ? lineText.length : idEnd + nextId.index;
|
|
93491
|
-
const entry = map.lines[lineIndex]?.entries.find(
|
|
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
|
-
}
|
|
93498
|
-
}
|
|
93495
|
+
function unreclaimedLines(route, verdict) {
|
|
93496
|
+
const retention = assessUnreclaimedRetention({
|
|
93497
|
+
unreclaimedSamples: route.unreclaimedSamples,
|
|
93498
|
+
memorySamples: route.memorySamples,
|
|
93499
|
+
verdict,
|
|
93500
|
+
verdictIsWellSupported: warrantsIssueDraft(route)
|
|
93501
|
+
});
|
|
93502
|
+
return retention === null ? [] : [` \u25B2 unreclaimed: ${describeUnreclaimedRetention(retention)}`];
|
|
93499
93503
|
}
|
|
93500
|
-
function
|
|
93501
|
-
|
|
93502
|
-
|
|
93503
|
-
if (lineText === void 0) {
|
|
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
|
-
}
|
|
93504
|
+
function routeLines(route, parameters) {
|
|
93505
|
+
if (route.status === "skipped") {
|
|
93506
|
+
return [` \u2013 ${route.route} skipped: ${route.reason}`];
|
|
93519
93507
|
}
|
|
93520
|
-
|
|
93521
|
-
|
|
93522
|
-
const result = /* @__PURE__ */ new Map();
|
|
93523
|
-
if (!code.includes("module.exports=[") && !code.includes("module.exports = [")) {
|
|
93524
|
-
return result;
|
|
93508
|
+
if (route.status === "failed") {
|
|
93509
|
+
return [` \u2716 ${route.route} failed: ${route.reason}`];
|
|
93525
93510
|
}
|
|
93526
|
-
|
|
93527
|
-
|
|
93528
|
-
return result;
|
|
93511
|
+
if (route.status === "not-exercised") {
|
|
93512
|
+
return [` \u2013 ${route.route} not exercised: ${route.reason}`];
|
|
93529
93513
|
}
|
|
93530
|
-
|
|
93531
|
-
|
|
93532
|
-
|
|
93533
|
-
|
|
93514
|
+
if (route.status === "died-of-heap") {
|
|
93515
|
+
const curve2 = route.memorySamples.map((sample) => formatMb(sample.heapUsed)).join(" \u2192 ");
|
|
93516
|
+
return [
|
|
93517
|
+
` ${VERDICT_ICON.leak} ${route.route} leak (ran out of heap after ${route.cyclesCompleted} of ${route.cyclesRequested} cycles)` + (curve2 === "" ? "" : ` heap ${curve2}`),
|
|
93518
|
+
` ${route.reason}`
|
|
93519
|
+
];
|
|
93520
|
+
}
|
|
93521
|
+
const verdict = effectiveVerdict(route);
|
|
93522
|
+
const curve = route.samples.map(formatMb).join(" \u2192 ");
|
|
93523
|
+
const resolved = route.resolvedWithCycles === void 0 ? "" : ` (resolved at ${route.resolvedWithCycles} cycles)`;
|
|
93524
|
+
return [
|
|
93525
|
+
` ${VERDICT_ICON[verdict]} ${route.route} ${verdict} (${formatGrowth(
|
|
93526
|
+
route.growthPer1000Requests
|
|
93527
|
+
)}) heap ${curve}${resolved}`,
|
|
93528
|
+
...revalidationLines(route),
|
|
93529
|
+
...abandonLines(route),
|
|
93530
|
+
...confidenceLines(route),
|
|
93531
|
+
...memorySourceLines(route, verdict),
|
|
93532
|
+
...peakPressureLines(route, parameters),
|
|
93533
|
+
...unreclaimedLines(route, verdict),
|
|
93534
|
+
...findingLines(route)
|
|
93535
|
+
];
|
|
93534
93536
|
}
|
|
93535
|
-
|
|
93536
|
-
const
|
|
93537
|
-
|
|
93538
|
-
|
|
93539
|
-
|
|
93540
|
-
} catch {
|
|
93541
|
-
return out;
|
|
93537
|
+
function skippedGuidanceLines(report) {
|
|
93538
|
+
const needConfig = report.routes.filter((route) => route.status === "skipped" && route.reason.includes("sample params")).map((route) => route.route);
|
|
93539
|
+
const skeleton = renderConfigSkeleton(needConfig, report.prerender);
|
|
93540
|
+
if (skeleton === null) {
|
|
93541
|
+
return [];
|
|
93542
93542
|
}
|
|
93543
|
-
|
|
93544
|
-
|
|
93545
|
-
|
|
93546
|
-
}
|
|
93543
|
+
const editing = hasPlaceholders(skeleton) ? ` Replace each ${"REPLACE-ME"} with a value that exists in your app.` : ` The shapes come from paths your build already prerendered; \`{n}\` makes every request use a different one, because reusing a prerendered value serves the warm cache and reads as flat whatever the route retains. Use \`{n%200}\` instead to revisit a fixed set of 200 keys, and drop the marker only if the app 404s on params it never prerendered.`;
|
|
93544
|
+
return [
|
|
93545
|
+
"",
|
|
93546
|
+
`${needConfig.length} route(s) need sample params. Write this to next-leak.config.json in the app directory:${editing}`,
|
|
93547
|
+
...skeleton.split("\n").map((line) => ` ${line}`)
|
|
93548
|
+
];
|
|
93549
|
+
}
|
|
93550
|
+
function coverageLine(report) {
|
|
93551
|
+
const total = report.routes.length;
|
|
93552
|
+
const measured = report.routes.filter(
|
|
93553
|
+
(route) => route.status === "measured" || route.status === "died-of-heap"
|
|
93554
|
+
).length;
|
|
93555
|
+
if (measured === total) {
|
|
93556
|
+
return `measured all ${total} discovered route(s)`;
|
|
93547
93557
|
}
|
|
93548
|
-
|
|
93558
|
+
const breakdown = ["skipped", "not-exercised", "failed"].map((status) => ({
|
|
93559
|
+
status,
|
|
93560
|
+
count: report.routes.filter((route) => route.status === status).length
|
|
93561
|
+
})).filter((entry) => entry.count > 0).map((entry) => `${entry.count} ${entry.status.replace("-", " ")}`).join(", ");
|
|
93562
|
+
return `measured ${measured} of ${total} discovered route(s) \u2014 ${breakdown}; the rest is not a verdict about your app`;
|
|
93549
93563
|
}
|
|
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
|
-
}
|
|
93564
|
+
function formatReport(report) {
|
|
93565
|
+
const lines = [`next-leak \u2014 ${report.appDir}`, ""];
|
|
93566
|
+
for (const route of report.routes) {
|
|
93567
|
+
lines.push(...routeLines(route, report.parameters));
|
|
93566
93568
|
}
|
|
93567
|
-
|
|
93569
|
+
lines.push(...skippedGuidanceLines(report));
|
|
93570
|
+
const { minGrowthPerCycle, loadRequests, cycles, maxOldSpaceMb } = report.parameters;
|
|
93571
|
+
const resolvedCycles = [
|
|
93572
|
+
...new Set(
|
|
93573
|
+
report.routes.flatMap(
|
|
93574
|
+
(route) => route.status === "measured" && route.resolvedWithCycles !== void 0 ? [route.resolvedWithCycles] : []
|
|
93575
|
+
)
|
|
93576
|
+
)
|
|
93577
|
+
].sort((a, b) => a - b);
|
|
93578
|
+
const cyclesLabel = resolvedCycles.length === 0 ? `${cycles} cycles` : `${cycles} cycles (${resolvedCycles.join(", ")} where resolved)`;
|
|
93579
|
+
lines.push("", coverageLine(report));
|
|
93580
|
+
lines.push(
|
|
93581
|
+
`judged over ${cyclesLabel} \xD7 ${loadRequests} requests, growth gate ${(minGrowthPerCycle / 1024).toFixed(0)} KiB/cycle (${formatGrowth(minGrowthPerCycle / loadRequests * 1e3)}), heap cap ${maxOldSpaceMb} MB`,
|
|
93582
|
+
`snapshots and run.json: ${report.workDir}`,
|
|
93583
|
+
`report: ${report.bundle.htmlReport}`,
|
|
93584
|
+
...report.bundle.issues.map((issue) => `issue draft (${issue.route}): ${issue.file}`)
|
|
93585
|
+
);
|
|
93586
|
+
const inconclusive = report.routes.filter(
|
|
93587
|
+
(route) => route.status === "measured" && effectiveVerdict(route) === "inconclusive"
|
|
93588
|
+
);
|
|
93589
|
+
if (inconclusive.length > 0) {
|
|
93590
|
+
const routeList = inconclusive.map((route) => route.route).join(",");
|
|
93591
|
+
const moreCycles = resolveCycles(report.parameters.cycles);
|
|
93592
|
+
lines.push(
|
|
93593
|
+
"",
|
|
93594
|
+
"hint: inconclusive means sustained sub-threshold growth \u2014 measure longer to resolve it:",
|
|
93595
|
+
` next-leak ${report.appDir} --routes ${routeList} --cycles ${moreCycles}`
|
|
93596
|
+
);
|
|
93597
|
+
}
|
|
93598
|
+
return lines.join("\n");
|
|
93599
|
+
}
|
|
93600
|
+
|
|
93601
|
+
// src/environment.ts
|
|
93602
|
+
import { createRequire } from "module";
|
|
93603
|
+
import os from "os";
|
|
93604
|
+
var require2 = createRequire(import.meta.url);
|
|
93605
|
+
var { version: nextLeakVersion } = require2("../package.json");
|
|
93606
|
+
function captureEnvironment(nextVersion) {
|
|
93607
|
+
return {
|
|
93608
|
+
nodeVersion: process.version,
|
|
93609
|
+
platform: os.platform(),
|
|
93610
|
+
arch: os.arch(),
|
|
93611
|
+
cpuModel: os.cpus()[0]?.model ?? null,
|
|
93612
|
+
totalMemoryBytes: os.totalmem(),
|
|
93613
|
+
nextVersion,
|
|
93614
|
+
nextLeakVersion
|
|
93615
|
+
};
|
|
93568
93616
|
}
|
|
93569
93617
|
|
|
93570
93618
|
// src/load.ts
|
|
@@ -93589,6 +93637,29 @@ async function describeRedirect(url) {
|
|
|
93589
93637
|
return null;
|
|
93590
93638
|
}
|
|
93591
93639
|
}
|
|
93640
|
+
async function describeUnprerenderedParams(url) {
|
|
93641
|
+
const bounded = boundedMarkerOf(url);
|
|
93642
|
+
const marker = bounded === null ? UNIQUE_MARKER : bounded.marker;
|
|
93643
|
+
if (!url.includes(marker)) {
|
|
93644
|
+
return null;
|
|
93645
|
+
}
|
|
93646
|
+
const probe = async (value) => {
|
|
93647
|
+
try {
|
|
93648
|
+
const response = await fetch(url.split(marker).join(value), { redirect: "manual" });
|
|
93649
|
+
return response.status;
|
|
93650
|
+
} catch {
|
|
93651
|
+
return null;
|
|
93652
|
+
}
|
|
93653
|
+
};
|
|
93654
|
+
const [novel, familiar] = await Promise.all([probe("999999"), probe("0")]);
|
|
93655
|
+
if (novel === null || familiar === null) {
|
|
93656
|
+
return null;
|
|
93657
|
+
}
|
|
93658
|
+
if (novel === 404 && familiar >= 200 && familiar < 300) {
|
|
93659
|
+
return `the sample value varies per request (\`${marker}\`) and this app answers 404 for params it never prerendered \u2014 the route itself is fine, the value is the problem. Bound it to the params your build did prerender (\`{n%N}\` with N of them), or drop the marker and accept that every request serves the same cached entry`;
|
|
93660
|
+
}
|
|
93661
|
+
return null;
|
|
93662
|
+
}
|
|
93592
93663
|
function boundedRequestSequence(fullUrl, bounded) {
|
|
93593
93664
|
const parsed = new URL(fullUrl);
|
|
93594
93665
|
const template = fullUrl.slice(parsed.origin.length);
|
|
@@ -93640,9 +93711,9 @@ async function runLoadPhase(options) {
|
|
|
93640
93711
|
const ratio = options.amount === 0 ? 0 : failures / options.amount;
|
|
93641
93712
|
if (ratio > (options.maxErrorRatio ?? 0.01)) {
|
|
93642
93713
|
const unanswered = failures - result.non2xx - result.errors - result.timeouts;
|
|
93643
|
-
const
|
|
93714
|
+
const explained = result.non2xx > 0 ? await describeUnprerenderedParams(options.url) ?? await describeRedirect(options.url) : null;
|
|
93644
93715
|
throw new LoadError(
|
|
93645
|
-
`${failures} of ${options.amount} requests failed against ${options.url} (${result.non2xx} non-2xx, ${result.errors} errors, ${result.timeouts} timeouts` + (unanswered > 0 ? `, ${unanswered} with no recorded response` : "") + ")" + (
|
|
93716
|
+
`${failures} of ${options.amount} requests failed against ${options.url} (${result.non2xx} non-2xx, ${result.errors} errors, ${result.timeouts} timeouts` + (unanswered > 0 ? `, ${unanswered} with no recorded response` : "") + ")" + (explained === null ? diagnoseFailure(result, options.connections) : ` \u2014 ${explained}`),
|
|
93646
93717
|
result
|
|
93647
93718
|
);
|
|
93648
93719
|
}
|
|
@@ -93854,6 +93925,14 @@ Host: ${target.host}\r
|
|
|
93854
93925
|
|
|
93855
93926
|
// src/ritual.ts
|
|
93856
93927
|
var MIN_POLLS_TO_JUDGE = 2;
|
|
93928
|
+
var HeapExhaustedError = class extends Error {
|
|
93929
|
+
evidence;
|
|
93930
|
+
constructor(message, evidence) {
|
|
93931
|
+
super(message);
|
|
93932
|
+
this.name = "HeapExhaustedError";
|
|
93933
|
+
this.evidence = evidence;
|
|
93934
|
+
}
|
|
93935
|
+
};
|
|
93857
93936
|
var SETTLE_POLL_MS = 2e3;
|
|
93858
93937
|
var SETTLE_TOLERANCE = 0.01;
|
|
93859
93938
|
var PEAK_POLL_MS = 250;
|
|
@@ -94006,6 +94085,13 @@ async function runRitual(options, deps = defaultDeps) {
|
|
|
94006
94085
|
timings.push({ phase, seconds: Math.round((Date.now() - started) / 100) / 10 });
|
|
94007
94086
|
}
|
|
94008
94087
|
};
|
|
94088
|
+
const memorySamples = [];
|
|
94089
|
+
const unreclaimedSamples = [];
|
|
94090
|
+
let unreclaimedLost = false;
|
|
94091
|
+
const peaks = [];
|
|
94092
|
+
let afterSnapshot = "";
|
|
94093
|
+
let baselineSnapshot = "";
|
|
94094
|
+
let cyclesCompleted = 0;
|
|
94009
94095
|
try {
|
|
94010
94096
|
const routeUrl = `http://127.0.0.1:${app.appPort}${options.route}`;
|
|
94011
94097
|
await timed(
|
|
@@ -94021,11 +94107,8 @@ async function runRitual(options, deps = defaultDeps) {
|
|
|
94021
94107
|
"baseline snapshot",
|
|
94022
94108
|
() => requestSnapshot(app.controlPort, "baseline")
|
|
94023
94109
|
);
|
|
94024
|
-
|
|
94025
|
-
|
|
94026
|
-
let unreclaimedLost = false;
|
|
94027
|
-
const peaks = [];
|
|
94028
|
-
let afterSnapshot = "";
|
|
94110
|
+
memorySamples.push(baseline.sample);
|
|
94111
|
+
baselineSnapshot = baseline.file;
|
|
94029
94112
|
for (let cycle = 1; cycle <= cycles; cycle += 1) {
|
|
94030
94113
|
await timed(`cycle ${cycle} load`, async () => {
|
|
94031
94114
|
const poller = pollPeak(app.controlPort, `cycle ${cycle}`, deps);
|
|
@@ -94059,6 +94142,7 @@ async function runRitual(options, deps = defaultDeps) {
|
|
|
94059
94142
|
} else {
|
|
94060
94143
|
memorySamples.push(await requestGc(app.controlPort));
|
|
94061
94144
|
}
|
|
94145
|
+
cyclesCompleted = cycle;
|
|
94062
94146
|
}
|
|
94063
94147
|
const samples = memorySamples.map((sample) => sample.heapUsed);
|
|
94064
94148
|
const externalSamples = memorySamples.map((sample) => sample.external);
|
|
@@ -94079,7 +94163,7 @@ async function runRitual(options, deps = defaultDeps) {
|
|
|
94079
94163
|
unreclaimedSamples.map((sample) => sample.external),
|
|
94080
94164
|
{ minGrowthPerCycle }
|
|
94081
94165
|
),
|
|
94082
|
-
baselineSnapshot
|
|
94166
|
+
baselineSnapshot,
|
|
94083
94167
|
afterSnapshot,
|
|
94084
94168
|
trend: classifyMemoryTrend(samples, externalSamples, { minGrowthPerCycle }),
|
|
94085
94169
|
requestsPerCycle: loadRequests,
|
|
@@ -94088,7 +94172,18 @@ async function runRitual(options, deps = defaultDeps) {
|
|
|
94088
94172
|
} catch (cause) {
|
|
94089
94173
|
const death = app.explainExit();
|
|
94090
94174
|
if (death !== null) {
|
|
94091
|
-
|
|
94175
|
+
if (death.heapExhausted) {
|
|
94176
|
+
throw new HeapExhaustedError(death.reason, {
|
|
94177
|
+
memorySamples,
|
|
94178
|
+
peaks,
|
|
94179
|
+
unreclaimedSamples: unreclaimedLost ? [] : unreclaimedSamples,
|
|
94180
|
+
cyclesCompleted,
|
|
94181
|
+
cyclesRequested: cycles,
|
|
94182
|
+
requestsPerCycle: loadRequests,
|
|
94183
|
+
baselineSnapshot
|
|
94184
|
+
});
|
|
94185
|
+
}
|
|
94186
|
+
throw new Error(death.reason);
|
|
94092
94187
|
}
|
|
94093
94188
|
throw cause;
|
|
94094
94189
|
} finally {
|
|
@@ -94438,7 +94533,7 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
94438
94533
|
};
|
|
94439
94534
|
}
|
|
94440
94535
|
async function writeEvidenceBundle(report, workDir) {
|
|
94441
|
-
const { renderHtmlReport } = await import("./html-report-
|
|
94536
|
+
const { renderHtmlReport } = await import("./html-report-5XMR3EXE.js");
|
|
94442
94537
|
const { renderIssueMarkdown } = await import("./issue-report-HKA26WNV.js");
|
|
94443
94538
|
for (const route of report.routes) {
|
|
94444
94539
|
if (route.status === "measured" && warrantsIssueDraft(route)) {
|
|
@@ -94487,6 +94582,21 @@ async function routeReportFor(context, route, index, total) {
|
|
|
94487
94582
|
return first;
|
|
94488
94583
|
}
|
|
94489
94584
|
} catch (cause) {
|
|
94585
|
+
if (cause instanceof HeapExhaustedError) {
|
|
94586
|
+
progress(`leak ${label}: ${cause.message}`);
|
|
94587
|
+
const { evidence } = cause;
|
|
94588
|
+
return {
|
|
94589
|
+
route: route.path,
|
|
94590
|
+
status: "died-of-heap",
|
|
94591
|
+
requestPath,
|
|
94592
|
+
reason: cause.message,
|
|
94593
|
+
memorySamples: evidence.memorySamples,
|
|
94594
|
+
peaks: evidence.peaks,
|
|
94595
|
+
cyclesCompleted: evidence.cyclesCompleted,
|
|
94596
|
+
cyclesRequested: evidence.cyclesRequested,
|
|
94597
|
+
requestsPerCycle: evidence.requestsPerCycle
|
|
94598
|
+
};
|
|
94599
|
+
}
|
|
94490
94600
|
const failure = cause instanceof Error ? cause.message : String(cause);
|
|
94491
94601
|
progress(`failed ${label}: ${failure}`);
|
|
94492
94602
|
return { route: route.path, status: "failed", reason: failure };
|
|
@@ -94583,6 +94693,20 @@ async function runMeasurement(options, deps = defaultDeps2) {
|
|
|
94583
94693
|
}
|
|
94584
94694
|
|
|
94585
94695
|
export {
|
|
94696
|
+
classifySource,
|
|
94697
|
+
attributeFinding,
|
|
94698
|
+
attributeDiff,
|
|
94699
|
+
summarizeBaseline,
|
|
94700
|
+
retainerChain,
|
|
94701
|
+
diffAgainstBaseline,
|
|
94702
|
+
SnapshotError,
|
|
94703
|
+
assertReadableSnapshot,
|
|
94704
|
+
diffSnapshotFiles,
|
|
94705
|
+
decodeVlqLine,
|
|
94706
|
+
decodeMappings,
|
|
94707
|
+
sourceIndexAt,
|
|
94708
|
+
extractChunkModules,
|
|
94709
|
+
extractModuleRegistry,
|
|
94586
94710
|
appPathsManifestSchema,
|
|
94587
94711
|
routesManifestSchema,
|
|
94588
94712
|
discoverPagesRoutes,
|
|
@@ -94604,21 +94728,7 @@ export {
|
|
|
94604
94728
|
renderConfigSkeleton,
|
|
94605
94729
|
hasPlaceholders,
|
|
94606
94730
|
formatReport,
|
|
94607
|
-
classifySource,
|
|
94608
|
-
attributeFinding,
|
|
94609
|
-
attributeDiff,
|
|
94610
94731
|
captureEnvironment,
|
|
94611
|
-
summarizeBaseline,
|
|
94612
|
-
retainerChain,
|
|
94613
|
-
diffAgainstBaseline,
|
|
94614
|
-
SnapshotError,
|
|
94615
|
-
assertReadableSnapshot,
|
|
94616
|
-
diffSnapshotFiles,
|
|
94617
|
-
decodeVlqLine,
|
|
94618
|
-
decodeMappings,
|
|
94619
|
-
sourceIndexAt,
|
|
94620
|
-
extractChunkModules,
|
|
94621
|
-
extractModuleRegistry,
|
|
94622
94732
|
LoadError,
|
|
94623
94733
|
runLoadPhase,
|
|
94624
94734
|
runRitual,
|