next-leak 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -4
- package/dist/abandon-load.d.ts +12 -2
- 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-22PBLCGO.js → chunk-EJ26OBZI.js} +7 -4
- package/dist/{chunk-AMSDGT3Y.js → chunk-LFGWGM5Z.js} +890 -825
- package/dist/{chunk-6CJU5TVV.js → chunk-RS3AO7HB.js} +1 -1
- package/dist/cli-args.d.ts +3 -0
- package/dist/cli.js +313 -20
- package/dist/confidence.d.ts +3 -0
- package/dist/{html-report-2UAOOZQO.js → html-report-TN7PYVU4.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/ritual.d.ts +3 -1
- package/dist/route-config.d.ts +4 -0
- package/dist/runner.d.ts +11 -0
- package/package.json +6 -2
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
minGrowthFor,
|
|
8
8
|
resolveCycles,
|
|
9
9
|
warrantsIssueDraft
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-EJ26OBZI.js";
|
|
11
11
|
import {
|
|
12
12
|
assessPeakPressure,
|
|
13
13
|
describePeakPressure
|
|
@@ -91969,30 +91969,543 @@ var require_autocannon = __commonJS({
|
|
|
91969
91969
|
}
|
|
91970
91970
|
});
|
|
91971
91971
|
|
|
91972
|
-
// src/
|
|
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 {
|
|
91979
|
+
}
|
|
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 };
|
|
91989
|
+
}
|
|
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 };
|
|
91998
|
+
}
|
|
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
|
+
}
|
|
92022
|
+
}
|
|
92023
|
+
return null;
|
|
92024
|
+
}
|
|
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) {
|
|
92030
|
+
continue;
|
|
92031
|
+
}
|
|
92032
|
+
const candidate = classifySource(source);
|
|
92033
|
+
if (OWNER_PRIORITY[candidate.owner] < OWNER_PRIORITY[best.owner]) {
|
|
92034
|
+
best = candidate;
|
|
92035
|
+
}
|
|
92036
|
+
}
|
|
92037
|
+
if (best.owner === "unattributed") {
|
|
92038
|
+
return classifyByChain(finding.retainerChain ?? "") ?? UNATTRIBUTED;
|
|
92039
|
+
}
|
|
92040
|
+
return best;
|
|
92041
|
+
}
|
|
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") {
|
|
92049
|
+
continue;
|
|
92050
|
+
}
|
|
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);
|
|
92057
|
+
}
|
|
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 };
|
|
92066
|
+
}
|
|
92067
|
+
|
|
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
|
|
92078
|
+
};
|
|
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
|
|
92094
|
+
);
|
|
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
|
+
)}]`
|
|
92125
|
+
);
|
|
92126
|
+
}
|
|
92127
|
+
return { parts, nodes };
|
|
92128
|
+
}
|
|
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"
|
|
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
|
+
}
|
|
92150
|
+
}
|
|
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
|
|
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
|
+
}
|
|
92200
|
+
}
|
|
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);
|
|
92204
|
+
return {
|
|
92205
|
+
typeDeltas,
|
|
92206
|
+
grownNodes: grown.slice(0, resolved.maxFindings),
|
|
92207
|
+
newNodes: fresh.slice(0, resolved.maxFindings)
|
|
92208
|
+
};
|
|
92209
|
+
}
|
|
92210
|
+
var SnapshotError = class extends Error {
|
|
92211
|
+
constructor(message) {
|
|
92212
|
+
super(message);
|
|
92213
|
+
this.name = "SnapshotError";
|
|
92214
|
+
}
|
|
92215
|
+
};
|
|
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}`);
|
|
92226
|
+
}
|
|
92227
|
+
if (size === 0) {
|
|
92228
|
+
throw new SnapshotError(`heap snapshot is empty: ${file}`);
|
|
92229
|
+
}
|
|
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
|
+
);
|
|
92234
|
+
}
|
|
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();
|
|
92253
|
+
}
|
|
92254
|
+
}
|
|
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}`);
|
|
92277
|
+
}
|
|
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;
|
|
92285
|
+
}
|
|
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));
|
|
92294
|
+
}
|
|
92295
|
+
}
|
|
92296
|
+
return segments;
|
|
92297
|
+
}
|
|
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
|
+
}
|
|
92314
|
+
}
|
|
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;
|
|
92328
|
+
}
|
|
92329
|
+
found = entry.sourceIndex;
|
|
92330
|
+
}
|
|
92331
|
+
return found;
|
|
92332
|
+
}
|
|
92333
|
+
|
|
92334
|
+
// src/module-registry.ts
|
|
92335
|
+
import { readdir, readFile } from "fs/promises";
|
|
92336
|
+
import path from "path";
|
|
91973
92337
|
import { z } from "zod";
|
|
91974
|
-
var
|
|
91975
|
-
var pagesManifestSchema = z.record(z.string(), z.string());
|
|
91976
|
-
var routeEntrySchema = z.looseObject({
|
|
91977
|
-
page: z.string(),
|
|
91978
|
-
regex: z.string()
|
|
91979
|
-
});
|
|
91980
|
-
var routesManifestSchema = z.looseObject({
|
|
92338
|
+
var flatMapSchema = z.looseObject({
|
|
91981
92339
|
version: z.number(),
|
|
91982
|
-
|
|
91983
|
-
|
|
91984
|
-
dynamicRoutes: z.array(routeEntrySchema)
|
|
92340
|
+
sources: z.array(z.string()),
|
|
92341
|
+
mappings: z.string()
|
|
91985
92342
|
});
|
|
91986
|
-
var
|
|
91987
|
-
|
|
91988
|
-
|
|
92343
|
+
var sectionedMapSchema = z.looseObject({
|
|
92344
|
+
version: z.number(),
|
|
92345
|
+
sections: z.array(
|
|
91989
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) };
|
|
92356
|
+
}
|
|
92357
|
+
const sectioned = sectionedMapSchema.safeParse(raw);
|
|
92358
|
+
if (!sectioned.success) {
|
|
92359
|
+
return null;
|
|
92360
|
+
}
|
|
92361
|
+
const sources = [];
|
|
92362
|
+
const lines = [];
|
|
92363
|
+
for (const section of sectioned.data.sections) {
|
|
92364
|
+
const sourceBase = sources.length;
|
|
92365
|
+
sources.push(...section.map.sources);
|
|
92366
|
+
for (const [lineIndex, mapping] of decodeMappings(section.map.mappings).entries()) {
|
|
92367
|
+
const targetLine = section.offset.line + lineIndex;
|
|
92368
|
+
const columnShift = lineIndex === 0 ? section.offset.column : 0;
|
|
92369
|
+
const target = lines[targetLine] ??= { entries: [] };
|
|
92370
|
+
for (const entry of mapping.entries) {
|
|
92371
|
+
target.entries.push({
|
|
92372
|
+
column: entry.column + columnShift,
|
|
92373
|
+
sourceIndex: entry.sourceIndex + sourceBase
|
|
92374
|
+
});
|
|
92375
|
+
}
|
|
92376
|
+
}
|
|
92377
|
+
}
|
|
92378
|
+
for (const line of lines) {
|
|
92379
|
+
line?.entries.sort((a, b) => a.column - b.column);
|
|
92380
|
+
}
|
|
92381
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
92382
|
+
lines[i] ??= { entries: [] };
|
|
92383
|
+
}
|
|
92384
|
+
return { sources, lines };
|
|
92385
|
+
}
|
|
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);
|
|
92394
|
+
}
|
|
92395
|
+
return last;
|
|
92396
|
+
}
|
|
92397
|
+
function collectFirstArrayIds(codeLines, map, result) {
|
|
92398
|
+
for (const [lineIndex, lineText] of codeLines.entries()) {
|
|
92399
|
+
const first = /module\.exports ?= ?\[(\d+),/.exec(lineText);
|
|
92400
|
+
if (first?.[1] === void 0) {
|
|
92401
|
+
continue;
|
|
92402
|
+
}
|
|
92403
|
+
const idEnd = first.index + first[0].length;
|
|
92404
|
+
const nextId = /[,[]\d{1,9},/.exec(lineText.slice(idEnd));
|
|
92405
|
+
const spanEnd = nextId === null ? lineText.length : idEnd + nextId.index;
|
|
92406
|
+
const entry = map.lines[lineIndex]?.entries.find(
|
|
92407
|
+
(candidate) => candidate.column >= idEnd && candidate.column < spanEnd
|
|
92408
|
+
);
|
|
92409
|
+
const source = entry === void 0 ? void 0 : map.sources[entry.sourceIndex];
|
|
92410
|
+
if (source !== void 0) {
|
|
92411
|
+
result.set(Number(first[1]), source);
|
|
92412
|
+
}
|
|
92413
|
+
}
|
|
92414
|
+
}
|
|
92415
|
+
function collectTransitionIds(codeLines, map, result) {
|
|
92416
|
+
for (const [lineIndex, mapping] of map.lines.entries()) {
|
|
92417
|
+
const lineText = codeLines[lineIndex];
|
|
92418
|
+
if (lineText === void 0) {
|
|
92419
|
+
continue;
|
|
92420
|
+
}
|
|
92421
|
+
let previousColumn = 0;
|
|
92422
|
+
let previousSource = null;
|
|
92423
|
+
for (const entry of mapping.entries) {
|
|
92424
|
+
if (entry.sourceIndex !== previousSource) {
|
|
92425
|
+
const id = lastModuleIdIn(lineText.slice(previousColumn, entry.column));
|
|
92426
|
+
const source = map.sources[entry.sourceIndex];
|
|
92427
|
+
if (id !== null && source !== void 0 && !result.has(id)) {
|
|
92428
|
+
result.set(id, source);
|
|
92429
|
+
}
|
|
92430
|
+
previousSource = entry.sourceIndex;
|
|
92431
|
+
}
|
|
92432
|
+
previousColumn = entry.column;
|
|
92433
|
+
}
|
|
92434
|
+
}
|
|
92435
|
+
}
|
|
92436
|
+
function extractChunkModules(code, rawMap) {
|
|
92437
|
+
const result = /* @__PURE__ */ new Map();
|
|
92438
|
+
if (!code.includes("module.exports=[") && !code.includes("module.exports = [")) {
|
|
92439
|
+
return result;
|
|
92440
|
+
}
|
|
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;
|
|
92449
|
+
}
|
|
92450
|
+
async function jsFilesUnder(dir) {
|
|
92451
|
+
const out = [];
|
|
92452
|
+
let entries;
|
|
92453
|
+
try {
|
|
92454
|
+
entries = await readdir(dir, { withFileTypes: true, recursive: true });
|
|
92455
|
+
} catch {
|
|
92456
|
+
return out;
|
|
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 {
|
|
92474
|
+
continue;
|
|
92475
|
+
}
|
|
92476
|
+
for (const [id, source] of extractChunkModules(code, map)) {
|
|
92477
|
+
if (!registry.has(id)) {
|
|
92478
|
+
registry.set(id, source);
|
|
92479
|
+
}
|
|
92480
|
+
}
|
|
92481
|
+
}
|
|
92482
|
+
return registry;
|
|
92483
|
+
}
|
|
92484
|
+
|
|
92485
|
+
// src/manifests.ts
|
|
92486
|
+
import { z as z2 } from "zod";
|
|
92487
|
+
var appPathsManifestSchema = z2.record(z2.string(), z2.string());
|
|
92488
|
+
var pagesManifestSchema = z2.record(z2.string(), z2.string());
|
|
92489
|
+
var routeEntrySchema = z2.looseObject({
|
|
92490
|
+
page: z2.string(),
|
|
92491
|
+
regex: z2.string()
|
|
92492
|
+
});
|
|
92493
|
+
var routesManifestSchema = z2.looseObject({
|
|
92494
|
+
version: z2.number(),
|
|
92495
|
+
basePath: z2.string(),
|
|
92496
|
+
staticRoutes: z2.array(routeEntrySchema),
|
|
92497
|
+
dynamicRoutes: z2.array(routeEntrySchema)
|
|
92498
|
+
});
|
|
92499
|
+
var prerenderManifestSchema = z2.looseObject({
|
|
92500
|
+
routes: z2.record(
|
|
92501
|
+
z2.string(),
|
|
92502
|
+
z2.looseObject({
|
|
91990
92503
|
// `false` on a route that is prerendered but never revalidates.
|
|
91991
|
-
initialRevalidateSeconds:
|
|
91992
|
-
srcRoute:
|
|
92504
|
+
initialRevalidateSeconds: z2.union([z2.number(), z2.literal(false)]).optional(),
|
|
92505
|
+
srcRoute: z2.string().nullable().optional()
|
|
91993
92506
|
})
|
|
91994
92507
|
).optional(),
|
|
91995
|
-
preview:
|
|
92508
|
+
preview: z2.looseObject({ previewModeId: z2.string() }).optional()
|
|
91996
92509
|
});
|
|
91997
92510
|
var INTERNAL_PATHS = /* @__PURE__ */ new Set(["/_global-error", "/_not-found"]);
|
|
91998
92511
|
var INTERCEPTING_SEGMENT = /^(\(\.{1,3}\))+/;
|
|
@@ -92055,8 +92568,8 @@ function discoverRoutes(appPaths) {
|
|
|
92055
92568
|
}
|
|
92056
92569
|
|
|
92057
92570
|
// src/target.ts
|
|
92058
|
-
import { access, readFile } from "fs/promises";
|
|
92059
|
-
import
|
|
92571
|
+
import { access, readFile as readFile2 } from "fs/promises";
|
|
92572
|
+
import path2 from "path";
|
|
92060
92573
|
var TargetError = class extends Error {
|
|
92061
92574
|
code;
|
|
92062
92575
|
constructor(code, message) {
|
|
@@ -92076,7 +92589,7 @@ async function exists(file) {
|
|
|
92076
92589
|
async function readManifest(file, parse2) {
|
|
92077
92590
|
let raw;
|
|
92078
92591
|
try {
|
|
92079
|
-
raw = JSON.parse(await
|
|
92592
|
+
raw = JSON.parse(await readFile2(file, "utf8"));
|
|
92080
92593
|
} catch (cause) {
|
|
92081
92594
|
throw new TargetError(
|
|
92082
92595
|
"BAD_MANIFEST",
|
|
@@ -92093,14 +92606,14 @@ async function readManifest(file, parse2) {
|
|
|
92093
92606
|
}
|
|
92094
92607
|
}
|
|
92095
92608
|
async function validateTarget(appDir) {
|
|
92096
|
-
const nextDir =
|
|
92609
|
+
const nextDir = path2.resolve(appDir, ".next");
|
|
92097
92610
|
if (!await exists(nextDir)) {
|
|
92098
92611
|
throw new TargetError(
|
|
92099
92612
|
"NO_BUILD",
|
|
92100
92613
|
`No .next directory in ${appDir}. Run "next build" first.`
|
|
92101
92614
|
);
|
|
92102
92615
|
}
|
|
92103
|
-
const standaloneServer =
|
|
92616
|
+
const standaloneServer = path2.join(nextDir, "standalone", "server.js");
|
|
92104
92617
|
if (!await exists(standaloneServer)) {
|
|
92105
92618
|
throw new TargetError(
|
|
92106
92619
|
"NO_STANDALONE",
|
|
@@ -92116,8 +92629,8 @@ then rebuild: next build
|
|
|
92116
92629
|
This only changes how the build is packaged \u2014 not how your app behaves.`
|
|
92117
92630
|
);
|
|
92118
92631
|
}
|
|
92119
|
-
const appPathsFile =
|
|
92120
|
-
const pagesFile =
|
|
92632
|
+
const appPathsFile = path2.join(nextDir, "server", "app-paths-manifest.json");
|
|
92633
|
+
const pagesFile = path2.join(nextDir, "server", "pages-manifest.json");
|
|
92121
92634
|
const appPaths = await exists(appPathsFile) ? await readManifest(appPathsFile, (raw) => appPathsManifestSchema.parse(raw)) : {};
|
|
92122
92635
|
const pages = await exists(pagesFile) ? await readManifest(pagesFile, (raw) => pagesManifestSchema.parse(raw)) : {};
|
|
92123
92636
|
if (Object.keys(appPaths).length === 0 && Object.keys(pages).length === 0) {
|
|
@@ -92126,12 +92639,12 @@ This only changes how the build is packaged \u2014 not how your app behaves.`
|
|
|
92126
92639
|
`No routes found in ${nextDir}/server: neither app-paths-manifest.json nor pages-manifest.json is present. Rebuild the app with "next build".`
|
|
92127
92640
|
);
|
|
92128
92641
|
}
|
|
92129
|
-
const routesFile =
|
|
92642
|
+
const routesFile = path2.join(nextDir, "routes-manifest.json");
|
|
92130
92643
|
const routes = await exists(routesFile) ? await readManifest(routesFile, (raw) => routesManifestSchema.parse(raw)) : void 0;
|
|
92131
|
-
const prerenderFile =
|
|
92644
|
+
const prerenderFile = path2.join(nextDir, "prerender-manifest.json");
|
|
92132
92645
|
const prerender = await exists(prerenderFile) ? await readManifest(prerenderFile, (raw) => prerenderManifestSchema.parse(raw)) : void 0;
|
|
92133
92646
|
return {
|
|
92134
|
-
appDir:
|
|
92647
|
+
appDir: path2.resolve(appDir),
|
|
92135
92648
|
standaloneServer,
|
|
92136
92649
|
appPaths,
|
|
92137
92650
|
pages,
|
|
@@ -92142,11 +92655,11 @@ This only changes how the build is packaged \u2014 not how your app behaves.`
|
|
|
92142
92655
|
|
|
92143
92656
|
// src/launcher.ts
|
|
92144
92657
|
import { spawn } from "child_process";
|
|
92145
|
-
import { readdir, readFile as
|
|
92146
|
-
import
|
|
92658
|
+
import { readdir as readdir2, readFile as readFile3 } from "fs/promises";
|
|
92659
|
+
import path3 from "path";
|
|
92147
92660
|
import { pathToFileURL } from "url";
|
|
92148
|
-
import { z as
|
|
92149
|
-
var controlFileSchema =
|
|
92661
|
+
import { z as z3 } from "zod";
|
|
92662
|
+
var controlFileSchema = z3.object({ port: z3.number(), pid: z3.number() });
|
|
92150
92663
|
var DEFAULT_MAX_OLD_SPACE_MB = 512;
|
|
92151
92664
|
var LaunchError = class extends Error {
|
|
92152
92665
|
constructor(message) {
|
|
@@ -92215,7 +92728,7 @@ async function launchInstrumented(options) {
|
|
|
92215
92728
|
options.serverPath
|
|
92216
92729
|
],
|
|
92217
92730
|
{
|
|
92218
|
-
cwd:
|
|
92731
|
+
cwd: path3.dirname(options.serverPath),
|
|
92219
92732
|
env: {
|
|
92220
92733
|
...process.env,
|
|
92221
92734
|
...options.env,
|
|
@@ -92255,7 +92768,7 @@ ${stderrTailBuffer}`;
|
|
|
92255
92768
|
async () => {
|
|
92256
92769
|
let entries;
|
|
92257
92770
|
try {
|
|
92258
|
-
entries = (await
|
|
92771
|
+
entries = (await readdir2(options.workDir)).filter(
|
|
92259
92772
|
(entry) => entry.startsWith("control-") && entry.endsWith(".json")
|
|
92260
92773
|
);
|
|
92261
92774
|
} catch {
|
|
@@ -92264,7 +92777,7 @@ ${stderrTailBuffer}`;
|
|
|
92264
92777
|
for (const entry of entries) {
|
|
92265
92778
|
try {
|
|
92266
92779
|
const parsed = controlFileSchema.parse(
|
|
92267
|
-
JSON.parse(await
|
|
92780
|
+
JSON.parse(await readFile3(path3.join(options.workDir, entry), "utf8"))
|
|
92268
92781
|
);
|
|
92269
92782
|
const response = await fetch(`http://127.0.0.1:${parsed.port}/gc`);
|
|
92270
92783
|
if (response.ok) {
|
|
@@ -92366,6 +92879,11 @@ var FLAGS = [
|
|
|
92366
92879
|
help: "Fast preset: 2000 requests x 4 cycles, 8s idle \u2014 same cycle count as the default, less traffic per cycle"
|
|
92367
92880
|
},
|
|
92368
92881
|
{ flag: "--diff-all", value: "none", help: "Diff snapshots for stable routes too (slow)" },
|
|
92882
|
+
{
|
|
92883
|
+
flag: "--attribute",
|
|
92884
|
+
value: "none",
|
|
92885
|
+
help: "build only: also name what the worker retains \u2014 signals it for heap snapshots, which can stall the build"
|
|
92886
|
+
},
|
|
92369
92887
|
{
|
|
92370
92888
|
flag: "--no-resolve",
|
|
92371
92889
|
value: "none",
|
|
@@ -92480,6 +92998,9 @@ function applyFlag(spec, value, options) {
|
|
|
92480
92998
|
case "--diff-all":
|
|
92481
92999
|
options.diffAll = true;
|
|
92482
93000
|
return FLAG_OK;
|
|
93001
|
+
case "--attribute":
|
|
93002
|
+
options.attributeBuild = true;
|
|
93003
|
+
return FLAG_OK;
|
|
92483
93004
|
case "--write-config":
|
|
92484
93005
|
options.writeConfig = true;
|
|
92485
93006
|
return FLAG_OK;
|
|
@@ -92541,6 +93062,7 @@ function parseCliArgs(argv) {
|
|
|
92541
93062
|
quick: false,
|
|
92542
93063
|
noResolve: false,
|
|
92543
93064
|
diffAll: false,
|
|
93065
|
+
attributeBuild: false,
|
|
92544
93066
|
writeConfig: false,
|
|
92545
93067
|
output: null
|
|
92546
93068
|
};
|
|
@@ -92570,8 +93092,15 @@ function parseCliArgs(argv) {
|
|
|
92570
93092
|
message: `option "${misplaced}" shapes HTTP load and does not apply to "next-leak build" \u2014 see --help`
|
|
92571
93093
|
};
|
|
92572
93094
|
}
|
|
92573
|
-
return {
|
|
92574
|
-
|
|
93095
|
+
return {
|
|
93096
|
+
kind: "build",
|
|
93097
|
+
options: {
|
|
93098
|
+
appDir: options.appDir,
|
|
93099
|
+
output: options.output,
|
|
93100
|
+
attributeBuild: options.attributeBuild
|
|
93101
|
+
}
|
|
93102
|
+
};
|
|
93103
|
+
}
|
|
92575
93104
|
return { kind: "run", options };
|
|
92576
93105
|
}
|
|
92577
93106
|
|
|
@@ -92593,25 +93122,25 @@ function checkRuntime(facts = currentFacts()) {
|
|
|
92593
93122
|
}
|
|
92594
93123
|
|
|
92595
93124
|
// src/route-config.ts
|
|
92596
|
-
import { readFile as
|
|
92597
|
-
import
|
|
92598
|
-
import { z as
|
|
92599
|
-
var sampleValueSchema =
|
|
93125
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
93126
|
+
import path4 from "path";
|
|
93127
|
+
import { z as z4 } from "zod";
|
|
93128
|
+
var sampleValueSchema = z4.string().refine(
|
|
92600
93129
|
(value) => !(value.includes("{n}") && /\{n%\d+\}/.test(value)),
|
|
92601
93130
|
{ message: 'a sample value cannot carry both "{n}" and "{n%N}" \u2014 pick one' }
|
|
92602
93131
|
);
|
|
92603
|
-
var routeConfigSchema =
|
|
92604
|
-
params:
|
|
92605
|
-
routes:
|
|
93132
|
+
var routeConfigSchema = z4.object({
|
|
93133
|
+
params: z4.record(z4.string(), sampleValueSchema).optional(),
|
|
93134
|
+
routes: z4.record(z4.string(), z4.record(z4.string(), sampleValueSchema)).optional(),
|
|
92606
93135
|
/**
|
|
92607
93136
|
* Headers sent with every request. Real traffic is not header-less:
|
|
92608
93137
|
* compression (`Accept-Encoding`), sessions and auth change which code
|
|
92609
93138
|
* paths run, and some leaks only exist on those paths (zlib, per-session
|
|
92610
93139
|
* caches). Measuring without them measures a different app.
|
|
92611
93140
|
*/
|
|
92612
|
-
headers:
|
|
93141
|
+
headers: z4.record(z4.string(), z4.string()).optional(),
|
|
92613
93142
|
/** Query string appended per route template, e.g. `"weightKb=2048"`. */
|
|
92614
|
-
query:
|
|
93143
|
+
query: z4.record(z4.string(), z4.string()).optional(),
|
|
92615
93144
|
/**
|
|
92616
93145
|
* Simulates clients that give up before the response arrives (closed
|
|
92617
93146
|
* tabs, load-balancer timeouts, bots). Several real leaks only exist on
|
|
@@ -92624,8 +93153,28 @@ var routeConfigSchema = z3.object({
|
|
|
92624
93153
|
* window, so a request-relative clock cuts before the stream starts and
|
|
92625
93154
|
* tests the wrong path entirely.
|
|
92626
93155
|
*/
|
|
92627
|
-
abandonAfterMs:
|
|
92628
|
-
|
|
93156
|
+
abandonAfterMs: z4.number().int().positive().optional(),
|
|
93157
|
+
/**
|
|
93158
|
+
* Where the abandon deadline starts. `first-byte` is the default and the
|
|
93159
|
+
* semantics everything else assumes.
|
|
93160
|
+
*
|
|
93161
|
+
* `request` starts it when the request is written, which is the only way
|
|
93162
|
+
* to reach leaks that need the client already gone before the server
|
|
93163
|
+
* produces anything: against a route whose time-to-first-byte exceeds the
|
|
93164
|
+
* deadline, a first-byte clock never cuts early. Measured on the
|
|
93165
|
+
* vercel/next.js#84648 repro, whose upstream answers in 400 ms while its
|
|
93166
|
+
* own load generator cuts at 60 ms — the first-byte clock reached 7% of
|
|
93167
|
+
* requests there, none of them on the path the issue describes.
|
|
93168
|
+
*
|
|
93169
|
+
* Opt-in per route, never a default: armed on connect, the deadline loses
|
|
93170
|
+
* the race to the first byte on fast routes, which is how the earlier
|
|
93171
|
+
* version of this measured 2 mid-stream cuts out of ~1500 on #94919.
|
|
93172
|
+
*/
|
|
93173
|
+
abandonFrom: z4.enum(["first-byte", "request"]).optional()
|
|
93174
|
+
}).strict().refine((config) => config.abandonFrom === void 0 || config.abandonAfterMs !== void 0, {
|
|
93175
|
+
message: '"abandonFrom" configures nothing without "abandonAfterMs"',
|
|
93176
|
+
path: ["abandonFrom"]
|
|
93177
|
+
});
|
|
92629
93178
|
var RouteConfigError = class extends Error {
|
|
92630
93179
|
constructor(message) {
|
|
92631
93180
|
super(message);
|
|
@@ -92634,10 +93183,10 @@ var RouteConfigError = class extends Error {
|
|
|
92634
93183
|
};
|
|
92635
93184
|
var ROUTE_CONFIG_FILE = "next-leak.config.json";
|
|
92636
93185
|
async function loadRouteConfig(appDir) {
|
|
92637
|
-
const file =
|
|
93186
|
+
const file = path4.join(appDir, ROUTE_CONFIG_FILE);
|
|
92638
93187
|
let raw;
|
|
92639
93188
|
try {
|
|
92640
|
-
raw = await
|
|
93189
|
+
raw = await readFile4(file, "utf8");
|
|
92641
93190
|
} catch {
|
|
92642
93191
|
return {};
|
|
92643
93192
|
}
|
|
@@ -92737,805 +93286,301 @@ function renderConfigSkeleton(routeTemplates, manifest = void 0) {
|
|
|
92737
93286
|
for (const template of routeTemplates) {
|
|
92738
93287
|
const segments = dynamicSegmentsOf(template);
|
|
92739
93288
|
if (segments.length === 0) {
|
|
92740
|
-
continue;
|
|
92741
|
-
}
|
|
92742
|
-
const sampled = sampleValuesFromManifest(manifest, template);
|
|
92743
|
-
const values = {};
|
|
92744
|
-
for (const segment of segments) {
|
|
92745
|
-
values[segment.name] = sampled?.[segment.name] ?? PLACEHOLDER;
|
|
92746
|
-
}
|
|
92747
|
-
routes[template] = values;
|
|
92748
|
-
}
|
|
92749
|
-
return Object.keys(routes).length === 0 ? null : JSON.stringify({ routes }, null, 2);
|
|
92750
|
-
}
|
|
92751
|
-
function hasPlaceholders(skeleton) {
|
|
92752
|
-
return skeleton.includes(PLACEHOLDER);
|
|
92753
|
-
}
|
|
92754
|
-
|
|
92755
|
-
// src/unreclaimed-retention.ts
|
|
92756
|
-
var MB = 1024 * 1024;
|
|
92757
|
-
var CLASSES = ["heap", "external", "arrayBuffers"];
|
|
92758
|
-
var MIN_GAP_BYTES = 2 * MB;
|
|
92759
|
-
var MIN_GAP_RATIO = 0.35;
|
|
92760
|
-
var MIN_RETAINED_BYTES = MB;
|
|
92761
|
-
var read = (sample, memoryClass) => memoryClass === "heap" ? sample.heapUsed : memoryClass === "external" ? sample.external : sample.arrayBuffers;
|
|
92762
|
-
function largestGap(values) {
|
|
92763
|
-
return values.reduce((highest, value) => Math.max(highest, value), 0);
|
|
92764
|
-
}
|
|
92765
|
-
function gapsFor(input, memoryClass) {
|
|
92766
|
-
const gaps = [];
|
|
92767
|
-
for (const [index, held] of input.unreclaimedSamples.entries()) {
|
|
92768
|
-
const afterGc = input.memorySamples[index + 1];
|
|
92769
|
-
if (afterGc === void 0) {
|
|
92770
|
-
break;
|
|
92771
|
-
}
|
|
92772
|
-
gaps.push(read(held, memoryClass) - read(afterGc, memoryClass));
|
|
92773
|
-
}
|
|
92774
|
-
return gaps;
|
|
92775
|
-
}
|
|
92776
|
-
function assessUnreclaimedRetention(input) {
|
|
92777
|
-
if (input.verdict === "leak" && input.verdictIsWellSupported || input.unreclaimedSamples.length === 0) {
|
|
92778
|
-
return null;
|
|
92779
|
-
}
|
|
92780
|
-
let worst = null;
|
|
92781
|
-
for (const memoryClass of CLASSES) {
|
|
92782
|
-
const gaps = gapsFor(input, memoryClass);
|
|
92783
|
-
if (gaps.length === 0) {
|
|
92784
|
-
continue;
|
|
92785
|
-
}
|
|
92786
|
-
const largestGapBytes = largestGap(gaps);
|
|
92787
|
-
if (largestGapBytes < MIN_GAP_BYTES) {
|
|
92788
|
-
continue;
|
|
92789
|
-
}
|
|
92790
|
-
const retainedBytes = largestGap(
|
|
92791
|
-
input.memorySamples.slice(1).map((sample) => read(sample, memoryClass))
|
|
92792
|
-
);
|
|
92793
|
-
const ratio = largestGapBytes / Math.max(retainedBytes, MIN_RETAINED_BYTES);
|
|
92794
|
-
if (ratio < MIN_GAP_RATIO) {
|
|
92795
|
-
continue;
|
|
92796
|
-
}
|
|
92797
|
-
if (worst === null || ratio >= worst.ratio) {
|
|
92798
|
-
worst = { class: memoryClass, largestGapBytes, retainedBytes, ratio };
|
|
92799
|
-
}
|
|
92800
|
-
}
|
|
92801
|
-
return worst;
|
|
92802
|
-
}
|
|
92803
|
-
var mb = (bytes) => `${(bytes / MB).toFixed(2)} MB`;
|
|
92804
|
-
var CLASS_LABEL = {
|
|
92805
|
-
heap: "heap",
|
|
92806
|
-
external: "external memory",
|
|
92807
|
-
arrayBuffers: "arrayBuffers"
|
|
92808
|
-
};
|
|
92809
|
-
function describeUnreclaimedRetention(retention) {
|
|
92810
|
-
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`;
|
|
92811
|
-
}
|
|
92812
|
-
|
|
92813
|
-
// src/report.ts
|
|
92814
|
-
var MB2 = 1024 * 1024;
|
|
92815
|
-
var formatMb = (bytes) => `${(bytes / MB2).toFixed(1)} MB`;
|
|
92816
|
-
var formatGrowth = (bytes) => {
|
|
92817
|
-
const sign = bytes >= 0 ? "+" : "";
|
|
92818
|
-
return `${sign}${(bytes / MB2).toFixed(2)} MB/1000 req`;
|
|
92819
|
-
};
|
|
92820
|
-
var VERDICT_ICON = { leak: "\u2716", stable: "\u2714", inconclusive: "?" };
|
|
92821
|
-
var RSS_MIN_GROWTH_PER_CYCLE = 16 * MB2;
|
|
92822
|
-
var RSS_MIN_TOTAL_GROWTH = 64 * MB2;
|
|
92823
|
-
function hasSustainedRssGrowth(memorySamples) {
|
|
92824
|
-
const rss = memorySamples.map((sample) => sample.rss);
|
|
92825
|
-
const first = rss[1];
|
|
92826
|
-
const last = rss.at(-1);
|
|
92827
|
-
if (first === void 0 || last === void 0 || rss.length < 4) {
|
|
92828
|
-
return false;
|
|
92829
|
-
}
|
|
92830
|
-
return classifyTrend(rss, { minGrowthPerCycle: RSS_MIN_GROWTH_PER_CYCLE }).verdict === "leak" && last - first >= RSS_MIN_TOTAL_GROWTH;
|
|
92831
|
-
}
|
|
92832
|
-
function ownerLabel(attribution) {
|
|
92833
|
-
switch (attribution.owner) {
|
|
92834
|
-
case "app":
|
|
92835
|
-
return `${attribution.source ?? "your code"} (your code)`;
|
|
92836
|
-
case "dependency":
|
|
92837
|
-
return `${attribution.packageName ?? "a dependency"} (dependency)`;
|
|
92838
|
-
case "framework":
|
|
92839
|
-
return attribution.packageName === null ? "Next.js/runtime internals" : `${attribution.packageName} (framework)`;
|
|
92840
|
-
case "unattributed":
|
|
92841
|
-
return null;
|
|
92842
|
-
}
|
|
92843
|
-
}
|
|
92844
|
-
function revalidationLines(route) {
|
|
92845
|
-
return route.revalidatedEverySeconds === void 0 ? [] : [
|
|
92846
|
-
` driven through ISR revalidation (revalidates every ${route.revalidatedEverySeconds}s; without it the load would serve the cache)`
|
|
92847
|
-
];
|
|
92848
|
-
}
|
|
92849
|
-
function confidenceLines(route) {
|
|
92850
|
-
const lines = [];
|
|
92851
|
-
if (route.confidence.supersededVerdict !== void 0) {
|
|
92852
|
-
lines.push(
|
|
92853
|
-
` measured ${route.trend.verdict}, withdrawn: the run did not observe what that verdict needs`
|
|
92854
|
-
);
|
|
92855
|
-
}
|
|
92856
|
-
for (const warning of route.confidence.warnings) {
|
|
92857
|
-
lines.push(` \u26A0 low confidence: ${warning.detail}`);
|
|
92858
|
-
}
|
|
92859
|
-
return lines;
|
|
92860
|
-
}
|
|
92861
|
-
function memorySourceLines(route, verdict) {
|
|
92862
|
-
const lines = [];
|
|
92863
|
-
if (route.trend.source === "external" && verdict !== "stable") {
|
|
92864
|
-
const externalCurve = route.memorySamples.map((sample) => formatMb(sample.external)).join(" \u2192 ");
|
|
92865
|
-
lines.push(
|
|
92866
|
-
` verdict comes from EXTERNAL memory (buffers, streams, fetch bodies), not the JS heap: external ${externalCurve}`
|
|
92867
|
-
);
|
|
92868
|
-
}
|
|
92869
|
-
if (verdict === "stable" && hasSustainedRssGrowth(route.memorySamples)) {
|
|
92870
|
-
const rssCurve = route.memorySamples.map((sample) => formatMb(sample.rss)).join(" \u2192 ");
|
|
92871
|
-
lines.push(
|
|
92872
|
-
` note: heap is flat but RSS grows ${formatGrowth(route.rssPer1000Requests)} \u2014 not a JS-heap leak (allocator, external buffers or fragmentation): RSS ${rssCurve}`
|
|
92873
|
-
);
|
|
92874
|
-
}
|
|
92875
|
-
return lines;
|
|
92876
|
-
}
|
|
92877
|
-
function diffFindingLines(route) {
|
|
92878
|
-
if (route.diff === null) {
|
|
92879
|
-
return [];
|
|
92880
|
-
}
|
|
92881
|
-
const findings = [...route.diff.grownNodes, ...route.diff.newNodes];
|
|
92882
|
-
return findings.slice(0, 3).map((finding, index) => {
|
|
92883
|
-
const size = formatMb(finding.retainedBytes);
|
|
92884
|
-
const attribution = route.attribution?.findings[index];
|
|
92885
|
-
const owner = attribution === void 0 ? null : ownerLabel(attribution);
|
|
92886
|
-
const detail = owner ?? (finding.retainerChain === "" ? "" : finding.retainerChain);
|
|
92887
|
-
return ` \u21B3 ${finding.kind} [${finding.nodeType}] ${finding.name} ${size}${detail === "" ? "" : ` \u2014 ${detail}`}`;
|
|
92888
|
-
});
|
|
92889
|
-
}
|
|
92890
|
-
function findingLines(route) {
|
|
92891
|
-
const lines = [];
|
|
92892
|
-
const culprit = route.attribution === null ? null : ownerLabel(route.attribution.route);
|
|
92893
|
-
if (culprit !== null) {
|
|
92894
|
-
lines.push(` culprit: ${culprit}`);
|
|
92895
|
-
}
|
|
92896
|
-
lines.push(...diffFindingLines(route));
|
|
92897
|
-
for (const signature of route.signatures) {
|
|
92898
|
-
const flag = signature.historical ? " (historical)" : "";
|
|
92899
|
-
lines.push(` \u26A0 known cause${flag}: ${signature.title} \u2014 ${signature.issue}`);
|
|
92900
|
-
}
|
|
92901
|
-
return lines;
|
|
92902
|
-
}
|
|
92903
|
-
function peakPressureLines(route, parameters) {
|
|
92904
|
-
const retained = route.memorySamples.at(-1)?.heapUsed;
|
|
92905
|
-
if (retained === void 0) {
|
|
92906
|
-
return [];
|
|
92907
|
-
}
|
|
92908
|
-
const pressure = assessPeakPressure({
|
|
92909
|
-
peaks: route.peaks,
|
|
92910
|
-
retainedHeapBytes: retained,
|
|
92911
|
-
maxOldSpaceMb: parameters.maxOldSpaceMb
|
|
92912
|
-
});
|
|
92913
|
-
return pressure === null ? [] : [` \u25B2 peak pressure: ${describePeakPressure(pressure)}`];
|
|
92914
|
-
}
|
|
92915
|
-
function unreclaimedLines(route, verdict) {
|
|
92916
|
-
const retention = assessUnreclaimedRetention({
|
|
92917
|
-
unreclaimedSamples: route.unreclaimedSamples,
|
|
92918
|
-
memorySamples: route.memorySamples,
|
|
92919
|
-
verdict,
|
|
92920
|
-
verdictIsWellSupported: warrantsIssueDraft(route)
|
|
92921
|
-
});
|
|
92922
|
-
return retention === null ? [] : [` \u25B2 unreclaimed: ${describeUnreclaimedRetention(retention)}`];
|
|
92923
|
-
}
|
|
92924
|
-
function routeLines(route, parameters) {
|
|
92925
|
-
if (route.status === "skipped") {
|
|
92926
|
-
return [` \u2013 ${route.route} skipped: ${route.reason}`];
|
|
92927
|
-
}
|
|
92928
|
-
if (route.status === "failed") {
|
|
92929
|
-
return [` \u2716 ${route.route} failed: ${route.reason}`];
|
|
92930
|
-
}
|
|
92931
|
-
if (route.status === "not-exercised") {
|
|
92932
|
-
return [` \u2013 ${route.route} not exercised: ${route.reason}`];
|
|
92933
|
-
}
|
|
92934
|
-
const verdict = effectiveVerdict(route);
|
|
92935
|
-
const curve = route.samples.map(formatMb).join(" \u2192 ");
|
|
92936
|
-
const resolved = route.resolvedWithCycles === void 0 ? "" : ` (resolved at ${route.resolvedWithCycles} cycles)`;
|
|
92937
|
-
return [
|
|
92938
|
-
` ${VERDICT_ICON[verdict]} ${route.route} ${verdict} (${formatGrowth(
|
|
92939
|
-
route.growthPer1000Requests
|
|
92940
|
-
)}) heap ${curve}${resolved}`,
|
|
92941
|
-
...revalidationLines(route),
|
|
92942
|
-
...confidenceLines(route),
|
|
92943
|
-
...memorySourceLines(route, verdict),
|
|
92944
|
-
...peakPressureLines(route, parameters),
|
|
92945
|
-
...unreclaimedLines(route, verdict),
|
|
92946
|
-
...findingLines(route)
|
|
92947
|
-
];
|
|
92948
|
-
}
|
|
92949
|
-
function skippedGuidanceLines(report) {
|
|
92950
|
-
const needConfig = report.routes.filter((route) => route.status === "skipped" && route.reason.includes("sample params")).map((route) => route.route);
|
|
92951
|
-
const skeleton = renderConfigSkeleton(needConfig, report.prerender);
|
|
92952
|
-
if (skeleton === null) {
|
|
92953
|
-
return [];
|
|
92954
|
-
}
|
|
92955
|
-
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.`;
|
|
92956
|
-
return [
|
|
92957
|
-
"",
|
|
92958
|
-
`${needConfig.length} route(s) need sample params. Write this to next-leak.config.json in the app directory:${editing}`,
|
|
92959
|
-
...skeleton.split("\n").map((line) => ` ${line}`)
|
|
92960
|
-
];
|
|
92961
|
-
}
|
|
92962
|
-
function coverageLine(report) {
|
|
92963
|
-
const total = report.routes.length;
|
|
92964
|
-
const measured = report.routes.filter((route) => route.status === "measured").length;
|
|
92965
|
-
if (measured === total) {
|
|
92966
|
-
return `measured all ${total} discovered route(s)`;
|
|
92967
|
-
}
|
|
92968
|
-
const breakdown = ["skipped", "not-exercised", "failed"].map((status) => ({
|
|
92969
|
-
status,
|
|
92970
|
-
count: report.routes.filter((route) => route.status === status).length
|
|
92971
|
-
})).filter((entry) => entry.count > 0).map((entry) => `${entry.count} ${entry.status.replace("-", " ")}`).join(", ");
|
|
92972
|
-
return `measured ${measured} of ${total} discovered route(s) \u2014 ${breakdown}; the rest is not a verdict about your app`;
|
|
92973
|
-
}
|
|
92974
|
-
function formatReport(report) {
|
|
92975
|
-
const lines = [`next-leak \u2014 ${report.appDir}`, ""];
|
|
92976
|
-
for (const route of report.routes) {
|
|
92977
|
-
lines.push(...routeLines(route, report.parameters));
|
|
92978
|
-
}
|
|
92979
|
-
lines.push(...skippedGuidanceLines(report));
|
|
92980
|
-
const { minGrowthPerCycle, loadRequests, cycles, maxOldSpaceMb } = report.parameters;
|
|
92981
|
-
const resolvedCycles = [
|
|
92982
|
-
...new Set(
|
|
92983
|
-
report.routes.flatMap(
|
|
92984
|
-
(route) => route.status === "measured" && route.resolvedWithCycles !== void 0 ? [route.resolvedWithCycles] : []
|
|
92985
|
-
)
|
|
92986
|
-
)
|
|
92987
|
-
].sort((a, b) => a - b);
|
|
92988
|
-
const cyclesLabel = resolvedCycles.length === 0 ? `${cycles} cycles` : `${cycles} cycles (${resolvedCycles.join(", ")} where resolved)`;
|
|
92989
|
-
lines.push("", coverageLine(report));
|
|
92990
|
-
lines.push(
|
|
92991
|
-
`judged over ${cyclesLabel} \xD7 ${loadRequests} requests, growth gate ${(minGrowthPerCycle / 1024).toFixed(0)} KiB/cycle (${formatGrowth(minGrowthPerCycle / loadRequests * 1e3)}), heap cap ${maxOldSpaceMb} MB`,
|
|
92992
|
-
`snapshots and run.json: ${report.workDir}`,
|
|
92993
|
-
`report: ${report.bundle.htmlReport}`,
|
|
92994
|
-
...report.bundle.issues.map((issue) => `issue draft (${issue.route}): ${issue.file}`)
|
|
92995
|
-
);
|
|
92996
|
-
const inconclusive = report.routes.filter(
|
|
92997
|
-
(route) => route.status === "measured" && effectiveVerdict(route) === "inconclusive"
|
|
92998
|
-
);
|
|
92999
|
-
if (inconclusive.length > 0) {
|
|
93000
|
-
const routeList = inconclusive.map((route) => route.route).join(",");
|
|
93001
|
-
const moreCycles = resolveCycles(report.parameters.cycles);
|
|
93002
|
-
lines.push(
|
|
93003
|
-
"",
|
|
93004
|
-
"hint: inconclusive means sustained sub-threshold growth \u2014 measure longer to resolve it:",
|
|
93005
|
-
` next-leak ${report.appDir} --routes ${routeList} --cycles ${moreCycles}`
|
|
93006
|
-
);
|
|
93007
|
-
}
|
|
93008
|
-
return lines.join("\n");
|
|
93009
|
-
}
|
|
93010
|
-
|
|
93011
|
-
// src/attribution.ts
|
|
93012
|
-
var UNATTRIBUTED = { owner: "unattributed", source: null, packageName: null };
|
|
93013
|
-
function classifySource(rawSource) {
|
|
93014
|
-
let source = rawSource.replace(/^turbopack:\/+(?=\[)/, "");
|
|
93015
|
-
try {
|
|
93016
|
-
source = decodeURIComponent(source);
|
|
93017
|
-
} catch {
|
|
93018
|
-
}
|
|
93019
|
-
const nodeModulesSplit = source.split("/node_modules/");
|
|
93020
|
-
if (nodeModulesSplit.length > 1) {
|
|
93021
|
-
const tail = nodeModulesSplit.at(-1) ?? "";
|
|
93022
|
-
const segments = tail.split("/");
|
|
93023
|
-
const packageName = (segments[0]?.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0]) ?? null;
|
|
93024
|
-
if (packageName === "next" || packageName === "react" || packageName === "react-dom") {
|
|
93025
|
-
return { owner: "framework", source: null, packageName };
|
|
93026
|
-
}
|
|
93027
|
-
return { owner: "dependency", source: null, packageName };
|
|
93028
|
-
}
|
|
93029
|
-
if (source.startsWith("[project]/")) {
|
|
93030
|
-
return { owner: "app", source: source.slice("[project]/".length), packageName: null };
|
|
93031
|
-
}
|
|
93032
|
-
const relative = source.replace(/^(\.\.\/)+/, "");
|
|
93033
|
-
if (relative !== source && !relative.startsWith("[")) {
|
|
93034
|
-
return { owner: "app", source: relative, packageName: null };
|
|
93035
|
-
}
|
|
93036
|
-
return { owner: "framework", source: null, packageName: null };
|
|
93037
|
-
}
|
|
93038
|
-
var OWNER_PRIORITY = {
|
|
93039
|
-
app: 0,
|
|
93040
|
-
dependency: 1,
|
|
93041
|
-
framework: 2,
|
|
93042
|
-
unattributed: 3
|
|
93043
|
-
};
|
|
93044
|
-
var FRAMEWORK_CHAIN_MARKERS = [
|
|
93045
|
-
["fsChecker", "route filesystem checker"],
|
|
93046
|
-
["getDynamicRoutes", "dynamic route matcher"],
|
|
93047
|
-
["NextNodeServer", "Next server"],
|
|
93048
|
-
["AppPageRouteModule", "App Router page module"],
|
|
93049
|
-
["AppRouteRouteModule", "App Router route handler module"],
|
|
93050
|
-
["incrementalCache", "incremental cache"],
|
|
93051
|
-
["IncrementalCache", "incremental cache"],
|
|
93052
|
-
["ReactServer", "React Server Components renderer"],
|
|
93053
|
-
["loadManifests", "manifest loader"],
|
|
93054
|
-
["renderToHTML", "HTML renderer"]
|
|
93055
|
-
];
|
|
93056
|
-
function classifyByChain(retainerChain2) {
|
|
93057
|
-
for (const [marker, component] of FRAMEWORK_CHAIN_MARKERS) {
|
|
93058
|
-
if (retainerChain2.includes(marker)) {
|
|
93059
|
-
return { owner: "framework", source: null, packageName: `next (${component})` };
|
|
93060
|
-
}
|
|
93061
|
-
}
|
|
93062
|
-
return null;
|
|
93063
|
-
}
|
|
93064
|
-
function attributeFinding(finding, registry) {
|
|
93065
|
-
let best = UNATTRIBUTED;
|
|
93066
|
-
for (const id of finding.moduleIds) {
|
|
93067
|
-
const source = registry.get(id);
|
|
93068
|
-
if (source === void 0) {
|
|
93069
|
-
continue;
|
|
93070
|
-
}
|
|
93071
|
-
const candidate = classifySource(source);
|
|
93072
|
-
if (OWNER_PRIORITY[candidate.owner] < OWNER_PRIORITY[best.owner]) {
|
|
93073
|
-
best = candidate;
|
|
93074
|
-
}
|
|
93075
|
-
}
|
|
93076
|
-
if (best.owner === "unattributed") {
|
|
93077
|
-
return classifyByChain(finding.retainerChain ?? "") ?? UNATTRIBUTED;
|
|
93078
|
-
}
|
|
93079
|
-
return best;
|
|
93080
|
-
}
|
|
93081
|
-
function attributeDiff(diff, registry) {
|
|
93082
|
-
const all = [...diff.grownNodes, ...diff.newNodes];
|
|
93083
|
-
const findings = all.map((finding) => attributeFinding(finding, registry));
|
|
93084
|
-
const byGroup = /* @__PURE__ */ new Map();
|
|
93085
|
-
let attributedBytes = 0;
|
|
93086
|
-
for (const [index, attribution] of findings.entries()) {
|
|
93087
|
-
if (attribution.owner === "unattributed") {
|
|
93088
|
-
continue;
|
|
93089
|
-
}
|
|
93090
|
-
const bytes = all[index]?.retainedBytes ?? 0;
|
|
93091
|
-
attributedBytes += bytes;
|
|
93092
|
-
const key = `${attribution.owner}|${attribution.source ?? ""}|${attribution.packageName ?? ""}`;
|
|
93093
|
-
const group = byGroup.get(key) ?? { attribution, bytes: 0 };
|
|
93094
|
-
group.bytes += bytes;
|
|
93095
|
-
byGroup.set(key, group);
|
|
93096
|
-
}
|
|
93097
|
-
let winner = null;
|
|
93098
|
-
for (const group of byGroup.values()) {
|
|
93099
|
-
if (winner === null || group.bytes > winner.bytes) {
|
|
93100
|
-
winner = group;
|
|
93101
|
-
}
|
|
93102
|
-
}
|
|
93103
|
-
const route = winner === null || attributedBytes === 0 ? { ...UNATTRIBUTED, dominance: 0 } : { ...winner.attribution, dominance: winner.bytes / attributedBytes };
|
|
93104
|
-
return { findings, route };
|
|
93105
|
-
}
|
|
93106
|
-
|
|
93107
|
-
// src/environment.ts
|
|
93108
|
-
import { createRequire } from "module";
|
|
93109
|
-
import os from "os";
|
|
93110
|
-
var require2 = createRequire(import.meta.url);
|
|
93111
|
-
var { version: nextLeakVersion } = require2("../package.json");
|
|
93112
|
-
function captureEnvironment(nextVersion) {
|
|
93113
|
-
return {
|
|
93114
|
-
nodeVersion: process.version,
|
|
93115
|
-
platform: os.platform(),
|
|
93116
|
-
arch: os.arch(),
|
|
93117
|
-
cpuModel: os.cpus()[0]?.model ?? null,
|
|
93118
|
-
totalMemoryBytes: os.totalmem(),
|
|
93119
|
-
nextVersion,
|
|
93120
|
-
nextLeakVersion
|
|
93121
|
-
};
|
|
93122
|
-
}
|
|
93123
|
-
|
|
93124
|
-
// src/heap-diff.ts
|
|
93125
|
-
var import_core = __toESM(require_dist(), 1);
|
|
93126
|
-
var import_heap_analysis = __toESM(require_dist4(), 1);
|
|
93127
|
-
var DEFAULTS = {
|
|
93128
|
-
minTypeDeltaBytes: 20 * 1024,
|
|
93129
|
-
grownThresholdBytes: 100 * 1024,
|
|
93130
|
-
newThresholdBytes: 2 * 1024,
|
|
93131
|
-
bigRetainedFloorBytes: 50 * 1024,
|
|
93132
|
-
maxFindings: 10,
|
|
93133
|
-
chainDepth: 7
|
|
93134
|
-
};
|
|
93135
|
-
function summarizeBaseline(heap, options = {}) {
|
|
93136
|
-
const resolved = { ...DEFAULTS, ...options };
|
|
93137
|
-
const summary = {
|
|
93138
|
-
nodeIds: /* @__PURE__ */ new Set(),
|
|
93139
|
-
bigRetained: /* @__PURE__ */ new Map(),
|
|
93140
|
-
typeSelfSizes: /* @__PURE__ */ new Map()
|
|
93141
|
-
};
|
|
93142
|
-
heap.nodes.forEach((node) => {
|
|
93143
|
-
summary.nodeIds.add(node.id);
|
|
93144
|
-
if (node.retainedSize >= resolved.bigRetainedFloorBytes) {
|
|
93145
|
-
summary.bigRetained.set(node.id, node.retainedSize);
|
|
93146
|
-
}
|
|
93147
|
-
summary.typeSelfSizes.set(
|
|
93148
|
-
node.type,
|
|
93149
|
-
(summary.typeSelfSizes.get(node.type) ?? 0) + node.self_size
|
|
93150
|
-
);
|
|
93151
|
-
});
|
|
93152
|
-
return summary;
|
|
93153
|
-
}
|
|
93154
|
-
function truncateLabel(value, max2 = 60) {
|
|
93155
|
-
return value.replaceAll("\n", " ").slice(0, max2);
|
|
93156
|
-
}
|
|
93157
|
-
function walkChain(node, depth) {
|
|
93158
|
-
const parts = [];
|
|
93159
|
-
const nodes = [node];
|
|
93160
|
-
let current = node;
|
|
93161
|
-
const seen = /* @__PURE__ */ new Set([node.id]);
|
|
93162
|
-
for (let i = 0; i < depth; i += 1) {
|
|
93163
|
-
const referrers = current.referrers;
|
|
93164
|
-
if (referrers.length === 0) {
|
|
93165
|
-
break;
|
|
93166
|
-
}
|
|
93167
|
-
const edge = referrers.find(
|
|
93168
|
-
(candidate) => !seen.has(candidate.fromNode.id) && candidate.fromNode.type !== "synthetic" && candidate.type !== "weak"
|
|
93169
|
-
) ?? referrers[0];
|
|
93170
|
-
if (edge === void 0 || seen.has(edge.fromNode.id)) {
|
|
93171
|
-
break;
|
|
93172
|
-
}
|
|
93173
|
-
current = edge.fromNode;
|
|
93174
|
-
seen.add(current.id);
|
|
93175
|
-
nodes.push(current);
|
|
93176
|
-
parts.push(
|
|
93177
|
-
`${truncateLabel(current.name)}#${current.type}[.${truncateLabel(
|
|
93178
|
-
String(edge.name_or_index),
|
|
93179
|
-
40
|
|
93180
|
-
)}]`
|
|
93181
|
-
);
|
|
93182
|
-
}
|
|
93183
|
-
return { parts, nodes };
|
|
93184
|
-
}
|
|
93185
|
-
function retainerChain(node, depth) {
|
|
93186
|
-
return walkChain(node, depth).parts.join(" <- ");
|
|
93187
|
-
}
|
|
93188
|
-
function collectModuleIds(chainNodes) {
|
|
93189
|
-
const ids = [];
|
|
93190
|
-
for (const node of chainNodes) {
|
|
93191
|
-
const isModuleInstance = (node.references ?? []).some(
|
|
93192
|
-
(edge) => edge.name_or_index === "namespaceObject" || edge.name_or_index === "exports"
|
|
93193
|
-
);
|
|
93194
|
-
if (!isModuleInstance) {
|
|
93195
|
-
continue;
|
|
93196
|
-
}
|
|
93197
|
-
for (const edge of node.referrers) {
|
|
93198
|
-
if (edge.type !== "element") {
|
|
93199
|
-
continue;
|
|
93200
|
-
}
|
|
93201
|
-
const id = Number(edge.name_or_index);
|
|
93202
|
-
if (Number.isInteger(id) && !ids.includes(id)) {
|
|
93203
|
-
ids.push(id);
|
|
93204
|
-
}
|
|
93205
|
-
}
|
|
93206
|
-
}
|
|
93207
|
-
return ids;
|
|
93208
|
-
}
|
|
93209
|
-
function diffAgainstBaseline(baseline, after, options = {}) {
|
|
93210
|
-
const resolved = { ...DEFAULTS, ...options };
|
|
93211
|
-
const afterTypeSelfSizes = /* @__PURE__ */ new Map();
|
|
93212
|
-
const grown = [];
|
|
93213
|
-
const fresh = [];
|
|
93214
|
-
after.nodes.forEach((node) => {
|
|
93215
|
-
afterTypeSelfSizes.set(
|
|
93216
|
-
node.type,
|
|
93217
|
-
(afterTypeSelfSizes.get(node.type) ?? 0) + node.self_size
|
|
93218
|
-
);
|
|
93219
|
-
if (node.type === "synthetic") {
|
|
93220
|
-
return;
|
|
93221
|
-
}
|
|
93222
|
-
if (!baseline.nodeIds.has(node.id)) {
|
|
93223
|
-
if (node.retainedSize >= resolved.newThresholdBytes) {
|
|
93224
|
-
const chain = walkChain(node, resolved.chainDepth);
|
|
93225
|
-
fresh.push({
|
|
93226
|
-
kind: "new",
|
|
93227
|
-
nodeType: node.type,
|
|
93228
|
-
name: truncateLabel(node.name),
|
|
93229
|
-
retainedBytes: node.retainedSize,
|
|
93230
|
-
retainerChain: chain.parts.join(" <- "),
|
|
93231
|
-
moduleIds: collectModuleIds(chain.nodes)
|
|
93232
|
-
});
|
|
93233
|
-
}
|
|
93234
|
-
return;
|
|
93235
|
-
}
|
|
93236
|
-
const before = baseline.bigRetained.get(node.id);
|
|
93237
|
-
if (before !== void 0 && node.retainedSize - before >= resolved.grownThresholdBytes) {
|
|
93238
|
-
const chain = walkChain(node, resolved.chainDepth);
|
|
93239
|
-
grown.push({
|
|
93240
|
-
kind: "grown",
|
|
93241
|
-
nodeType: node.type,
|
|
93242
|
-
name: truncateLabel(node.name),
|
|
93243
|
-
retainedBytes: node.retainedSize - before,
|
|
93244
|
-
retainerChain: chain.parts.join(" <- "),
|
|
93245
|
-
moduleIds: collectModuleIds(chain.nodes)
|
|
93246
|
-
});
|
|
93247
|
-
}
|
|
93248
|
-
});
|
|
93249
|
-
const typeDeltas = [];
|
|
93250
|
-
const allTypes = /* @__PURE__ */ new Set([...baseline.typeSelfSizes.keys(), ...afterTypeSelfSizes.keys()]);
|
|
93251
|
-
for (const type of allTypes) {
|
|
93252
|
-
const delta = (afterTypeSelfSizes.get(type) ?? 0) - (baseline.typeSelfSizes.get(type) ?? 0);
|
|
93253
|
-
if (Math.abs(delta) >= resolved.minTypeDeltaBytes) {
|
|
93254
|
-
typeDeltas.push({ type, deltaBytes: delta });
|
|
93289
|
+
continue;
|
|
93290
|
+
}
|
|
93291
|
+
const sampled = sampleValuesFromManifest(manifest, template);
|
|
93292
|
+
const values = {};
|
|
93293
|
+
for (const segment of segments) {
|
|
93294
|
+
values[segment.name] = sampled?.[segment.name] ?? PLACEHOLDER;
|
|
93255
93295
|
}
|
|
93296
|
+
routes[template] = values;
|
|
93256
93297
|
}
|
|
93257
|
-
|
|
93258
|
-
grown.sort((a, b) => b.retainedBytes - a.retainedBytes);
|
|
93259
|
-
fresh.sort((a, b) => b.retainedBytes - a.retainedBytes);
|
|
93260
|
-
return {
|
|
93261
|
-
typeDeltas,
|
|
93262
|
-
grownNodes: grown.slice(0, resolved.maxFindings),
|
|
93263
|
-
newNodes: fresh.slice(0, resolved.maxFindings)
|
|
93264
|
-
};
|
|
93298
|
+
return Object.keys(routes).length === 0 ? null : JSON.stringify({ routes }, null, 2);
|
|
93265
93299
|
}
|
|
93266
|
-
|
|
93267
|
-
|
|
93268
|
-
|
|
93269
|
-
|
|
93270
|
-
|
|
93271
|
-
|
|
93272
|
-
var
|
|
93273
|
-
var
|
|
93274
|
-
|
|
93275
|
-
|
|
93276
|
-
|
|
93277
|
-
|
|
93278
|
-
|
|
93279
|
-
|
|
93280
|
-
|
|
93281
|
-
|
|
93300
|
+
function hasPlaceholders(skeleton) {
|
|
93301
|
+
return skeleton.includes(PLACEHOLDER);
|
|
93302
|
+
}
|
|
93303
|
+
|
|
93304
|
+
// src/unreclaimed-retention.ts
|
|
93305
|
+
var MB = 1024 * 1024;
|
|
93306
|
+
var CLASSES = ["heap", "external", "arrayBuffers"];
|
|
93307
|
+
var MIN_GAP_BYTES = 2 * MB;
|
|
93308
|
+
var MIN_GAP_RATIO = 0.35;
|
|
93309
|
+
var MIN_RETAINED_BYTES = MB;
|
|
93310
|
+
var read = (sample, memoryClass) => memoryClass === "heap" ? sample.heapUsed : memoryClass === "external" ? sample.external : sample.arrayBuffers;
|
|
93311
|
+
function largestGap(values) {
|
|
93312
|
+
return values.reduce((highest, value) => Math.max(highest, value), 0);
|
|
93313
|
+
}
|
|
93314
|
+
function gapsFor(input, memoryClass) {
|
|
93315
|
+
const gaps = [];
|
|
93316
|
+
for (const [index, held] of input.unreclaimedSamples.entries()) {
|
|
93317
|
+
const afterGc = input.memorySamples[index + 1];
|
|
93318
|
+
if (afterGc === void 0) {
|
|
93319
|
+
break;
|
|
93320
|
+
}
|
|
93321
|
+
gaps.push(read(held, memoryClass) - read(afterGc, memoryClass));
|
|
93282
93322
|
}
|
|
93283
|
-
|
|
93284
|
-
|
|
93323
|
+
return gaps;
|
|
93324
|
+
}
|
|
93325
|
+
function assessUnreclaimedRetention(input) {
|
|
93326
|
+
if (input.verdict === "leak" && input.verdictIsWellSupported || input.unreclaimedSamples.length === 0) {
|
|
93327
|
+
return null;
|
|
93285
93328
|
}
|
|
93286
|
-
|
|
93287
|
-
|
|
93288
|
-
|
|
93329
|
+
let worst = null;
|
|
93330
|
+
for (const memoryClass of CLASSES) {
|
|
93331
|
+
const gaps = gapsFor(input, memoryClass);
|
|
93332
|
+
if (gaps.length === 0) {
|
|
93333
|
+
continue;
|
|
93334
|
+
}
|
|
93335
|
+
const largestGapBytes = largestGap(gaps);
|
|
93336
|
+
if (largestGapBytes < MIN_GAP_BYTES) {
|
|
93337
|
+
continue;
|
|
93338
|
+
}
|
|
93339
|
+
const retainedBytes = largestGap(
|
|
93340
|
+
input.memorySamples.slice(1).map((sample) => read(sample, memoryClass))
|
|
93289
93341
|
);
|
|
93290
|
-
|
|
93291
|
-
|
|
93292
|
-
|
|
93293
|
-
const head = Buffer.alloc(Math.min(PROBE_BYTES, size));
|
|
93294
|
-
await handle.read(head, 0, head.length, 0);
|
|
93295
|
-
if (!head.toString("utf8").trimStart().startsWith(SNAPSHOT_HEAD)) {
|
|
93296
|
-
throw new SnapshotError(
|
|
93297
|
-
`not a V8 heap snapshot (missing "snapshot" header): ${file}`
|
|
93298
|
-
);
|
|
93342
|
+
const ratio = largestGapBytes / Math.max(retainedBytes, MIN_RETAINED_BYTES);
|
|
93343
|
+
if (ratio < MIN_GAP_RATIO) {
|
|
93344
|
+
continue;
|
|
93299
93345
|
}
|
|
93300
|
-
|
|
93301
|
-
|
|
93302
|
-
if (!tail.toString("utf8").trimEnd().endsWith("}")) {
|
|
93303
|
-
throw new SnapshotError(
|
|
93304
|
-
`heap snapshot looks truncated (JSON does not close): ${file} \u2014 the measured process may have been killed while writing it`
|
|
93305
|
-
);
|
|
93346
|
+
if (worst === null || ratio >= worst.ratio) {
|
|
93347
|
+
worst = { class: memoryClass, largestGapBytes, retainedBytes, ratio };
|
|
93306
93348
|
}
|
|
93307
|
-
} finally {
|
|
93308
|
-
await handle.close();
|
|
93309
93349
|
}
|
|
93350
|
+
return worst;
|
|
93310
93351
|
}
|
|
93311
|
-
var
|
|
93312
|
-
|
|
93313
|
-
|
|
93314
|
-
|
|
93352
|
+
var mb = (bytes) => `${(bytes / MB).toFixed(2)} MB`;
|
|
93353
|
+
var CLASS_LABEL = {
|
|
93354
|
+
heap: "heap",
|
|
93355
|
+
external: "external memory",
|
|
93356
|
+
arrayBuffers: "arrayBuffers"
|
|
93315
93357
|
};
|
|
93316
|
-
|
|
93317
|
-
|
|
93318
|
-
const after = await loadHeap(afterFile);
|
|
93319
|
-
return diffAgainstBaseline(summary, after, options);
|
|
93358
|
+
function describeUnreclaimedRetention(retention) {
|
|
93359
|
+
return `held ${mb(retention.largestGapBytes)} of ${CLASS_LABEL[retention.class]} between collections that a GC took back (${retention.ratio.toFixed(1)}x what it retains) \u2014 a forced GC reclaims this, a long-running process may not run one often enough to; read seconds after load, so it also includes memory not yet collected`;
|
|
93320
93360
|
}
|
|
93321
93361
|
|
|
93322
|
-
// src/
|
|
93323
|
-
var
|
|
93324
|
-
var
|
|
93325
|
-
|
|
93326
|
-
const
|
|
93327
|
-
|
|
93328
|
-
|
|
93329
|
-
|
|
93330
|
-
|
|
93331
|
-
|
|
93332
|
-
|
|
93333
|
-
|
|
93334
|
-
|
|
93335
|
-
|
|
93336
|
-
|
|
93337
|
-
|
|
93338
|
-
values.push((value & 1) === 1 ? -(value >>> 1) : value >>> 1);
|
|
93339
|
-
value = 0;
|
|
93340
|
-
shift = 0;
|
|
93341
|
-
}
|
|
93362
|
+
// src/report.ts
|
|
93363
|
+
var MB2 = 1024 * 1024;
|
|
93364
|
+
var formatMb = (bytes) => `${(bytes / MB2).toFixed(1)} MB`;
|
|
93365
|
+
var formatGrowth = (bytes) => {
|
|
93366
|
+
const sign = bytes >= 0 ? "+" : "";
|
|
93367
|
+
return `${sign}${(bytes / MB2).toFixed(2)} MB/1000 req`;
|
|
93368
|
+
};
|
|
93369
|
+
var VERDICT_ICON = { leak: "\u2716", stable: "\u2714", inconclusive: "?" };
|
|
93370
|
+
var RSS_MIN_GROWTH_PER_CYCLE = 16 * MB2;
|
|
93371
|
+
var RSS_MIN_TOTAL_GROWTH = 64 * MB2;
|
|
93372
|
+
function hasSustainedRssGrowth(memorySamples) {
|
|
93373
|
+
const rss = memorySamples.map((sample) => sample.rss);
|
|
93374
|
+
const first = rss[1];
|
|
93375
|
+
const last = rss.at(-1);
|
|
93376
|
+
if (first === void 0 || last === void 0 || rss.length < 4) {
|
|
93377
|
+
return false;
|
|
93342
93378
|
}
|
|
93343
|
-
return
|
|
93379
|
+
return classifyTrend(rss, { minGrowthPerCycle: RSS_MIN_GROWTH_PER_CYCLE }).verdict === "leak" && last - first >= RSS_MIN_TOTAL_GROWTH;
|
|
93344
93380
|
}
|
|
93345
|
-
function
|
|
93346
|
-
|
|
93347
|
-
|
|
93348
|
-
|
|
93349
|
-
|
|
93350
|
-
|
|
93381
|
+
function ownerLabel(attribution) {
|
|
93382
|
+
switch (attribution.owner) {
|
|
93383
|
+
case "app":
|
|
93384
|
+
return `${attribution.source ?? "your code"} (your code)`;
|
|
93385
|
+
case "dependency":
|
|
93386
|
+
return `${attribution.packageName ?? "a dependency"} (dependency)`;
|
|
93387
|
+
case "framework":
|
|
93388
|
+
return attribution.packageName === null ? "Next.js/runtime internals" : `${attribution.packageName} (framework)`;
|
|
93389
|
+
case "unattributed":
|
|
93390
|
+
return null;
|
|
93351
93391
|
}
|
|
93352
|
-
return segments;
|
|
93353
93392
|
}
|
|
93354
|
-
function
|
|
93393
|
+
function revalidationLines(route) {
|
|
93394
|
+
return route.revalidatedEverySeconds === void 0 ? [] : [
|
|
93395
|
+
` driven through ISR revalidation (revalidates every ${route.revalidatedEverySeconds}s; without it the load would serve the cache)`
|
|
93396
|
+
];
|
|
93397
|
+
}
|
|
93398
|
+
function abandonLines(route) {
|
|
93399
|
+
if (route.abandon === void 0) {
|
|
93400
|
+
return [];
|
|
93401
|
+
}
|
|
93402
|
+
return [
|
|
93403
|
+
route.abandon.from === "request" ? ` cut ${route.abandon.afterMs}ms after the request was sent, so the client is gone before the response starts` : ` cut ${route.abandon.afterMs}ms after the first byte, so the cut lands mid-stream`
|
|
93404
|
+
];
|
|
93405
|
+
}
|
|
93406
|
+
function confidenceLines(route) {
|
|
93355
93407
|
const lines = [];
|
|
93356
|
-
|
|
93357
|
-
|
|
93358
|
-
|
|
93359
|
-
|
|
93360
|
-
|
|
93361
|
-
|
|
93362
|
-
|
|
93363
|
-
continue;
|
|
93364
|
-
}
|
|
93365
|
-
column += columnDelta;
|
|
93366
|
-
if (segment.length >= 4) {
|
|
93367
|
-
sourceIndex += segment[1] ?? 0;
|
|
93368
|
-
entries.push({ column, sourceIndex });
|
|
93369
|
-
}
|
|
93370
|
-
}
|
|
93371
|
-
lines.push({ entries });
|
|
93408
|
+
if (route.confidence.supersededVerdict !== void 0) {
|
|
93409
|
+
lines.push(
|
|
93410
|
+
` measured ${route.trend.verdict}, withdrawn: the run did not observe what that verdict needs`
|
|
93411
|
+
);
|
|
93412
|
+
}
|
|
93413
|
+
for (const warning of route.confidence.warnings) {
|
|
93414
|
+
lines.push(` \u26A0 low confidence: ${warning.detail}`);
|
|
93372
93415
|
}
|
|
93373
93416
|
return lines;
|
|
93374
93417
|
}
|
|
93375
|
-
function
|
|
93376
|
-
const
|
|
93377
|
-
if (
|
|
93378
|
-
|
|
93418
|
+
function memorySourceLines(route, verdict) {
|
|
93419
|
+
const lines = [];
|
|
93420
|
+
if (route.trend.source === "external" && verdict !== "stable") {
|
|
93421
|
+
const externalCurve = route.memorySamples.map((sample) => formatMb(sample.external)).join(" \u2192 ");
|
|
93422
|
+
lines.push(
|
|
93423
|
+
` verdict comes from EXTERNAL memory (buffers, streams, fetch bodies), not the JS heap: external ${externalCurve}`
|
|
93424
|
+
);
|
|
93379
93425
|
}
|
|
93380
|
-
|
|
93381
|
-
|
|
93382
|
-
|
|
93383
|
-
|
|
93384
|
-
|
|
93385
|
-
found = entry.sourceIndex;
|
|
93426
|
+
if (verdict === "stable" && hasSustainedRssGrowth(route.memorySamples)) {
|
|
93427
|
+
const rssCurve = route.memorySamples.map((sample) => formatMb(sample.rss)).join(" \u2192 ");
|
|
93428
|
+
lines.push(
|
|
93429
|
+
` note: heap is flat but RSS grows ${formatGrowth(route.rssPer1000Requests)} \u2014 not a JS-heap leak (allocator, external buffers or fragmentation): RSS ${rssCurve}`
|
|
93430
|
+
);
|
|
93386
93431
|
}
|
|
93387
|
-
return
|
|
93432
|
+
return lines;
|
|
93388
93433
|
}
|
|
93389
|
-
|
|
93390
|
-
|
|
93391
|
-
|
|
93392
|
-
import path4 from "path";
|
|
93393
|
-
import { z as z4 } from "zod";
|
|
93394
|
-
var flatMapSchema = z4.looseObject({
|
|
93395
|
-
version: z4.number(),
|
|
93396
|
-
sources: z4.array(z4.string()),
|
|
93397
|
-
mappings: z4.string()
|
|
93398
|
-
});
|
|
93399
|
-
var sectionedMapSchema = z4.looseObject({
|
|
93400
|
-
version: z4.number(),
|
|
93401
|
-
sections: z4.array(
|
|
93402
|
-
z4.looseObject({
|
|
93403
|
-
offset: z4.looseObject({ line: z4.number(), column: z4.number() }),
|
|
93404
|
-
map: z4.looseObject({ sources: z4.array(z4.string()), mappings: z4.string() })
|
|
93405
|
-
})
|
|
93406
|
-
)
|
|
93407
|
-
});
|
|
93408
|
-
function normalizeSourceMap(raw) {
|
|
93409
|
-
const flat = flatMapSchema.safeParse(raw);
|
|
93410
|
-
if (flat.success) {
|
|
93411
|
-
return { sources: flat.data.sources, lines: decodeMappings(flat.data.mappings) };
|
|
93412
|
-
}
|
|
93413
|
-
const sectioned = sectionedMapSchema.safeParse(raw);
|
|
93414
|
-
if (!sectioned.success) {
|
|
93415
|
-
return null;
|
|
93434
|
+
function diffFindingLines(route) {
|
|
93435
|
+
if (route.diff === null) {
|
|
93436
|
+
return [];
|
|
93416
93437
|
}
|
|
93417
|
-
const
|
|
93438
|
+
const findings = [...route.diff.grownNodes, ...route.diff.newNodes];
|
|
93439
|
+
return findings.slice(0, 3).map((finding, index) => {
|
|
93440
|
+
const size = formatMb(finding.retainedBytes);
|
|
93441
|
+
const attribution = route.attribution?.findings[index];
|
|
93442
|
+
const owner = attribution === void 0 ? null : ownerLabel(attribution);
|
|
93443
|
+
const detail = owner ?? (finding.retainerChain === "" ? "" : finding.retainerChain);
|
|
93444
|
+
return ` \u21B3 ${finding.kind} [${finding.nodeType}] ${finding.name} ${size}${detail === "" ? "" : ` \u2014 ${detail}`}`;
|
|
93445
|
+
});
|
|
93446
|
+
}
|
|
93447
|
+
function findingLines(route) {
|
|
93418
93448
|
const lines = [];
|
|
93419
|
-
|
|
93420
|
-
|
|
93421
|
-
|
|
93422
|
-
for (const [lineIndex, mapping] of decodeMappings(section.map.mappings).entries()) {
|
|
93423
|
-
const targetLine = section.offset.line + lineIndex;
|
|
93424
|
-
const columnShift = lineIndex === 0 ? section.offset.column : 0;
|
|
93425
|
-
const target = lines[targetLine] ??= { entries: [] };
|
|
93426
|
-
for (const entry of mapping.entries) {
|
|
93427
|
-
target.entries.push({
|
|
93428
|
-
column: entry.column + columnShift,
|
|
93429
|
-
sourceIndex: entry.sourceIndex + sourceBase
|
|
93430
|
-
});
|
|
93431
|
-
}
|
|
93432
|
-
}
|
|
93433
|
-
}
|
|
93434
|
-
for (const line of lines) {
|
|
93435
|
-
line?.entries.sort((a, b) => a.column - b.column);
|
|
93449
|
+
const culprit = route.attribution === null ? null : ownerLabel(route.attribution.route);
|
|
93450
|
+
if (culprit !== null) {
|
|
93451
|
+
lines.push(` culprit: ${culprit}`);
|
|
93436
93452
|
}
|
|
93437
|
-
|
|
93438
|
-
|
|
93453
|
+
lines.push(...diffFindingLines(route));
|
|
93454
|
+
for (const signature of route.signatures) {
|
|
93455
|
+
const flag = signature.historical ? " (historical)" : "";
|
|
93456
|
+
lines.push(` \u26A0 known cause${flag}: ${signature.title} \u2014 ${signature.issue}`);
|
|
93439
93457
|
}
|
|
93440
|
-
return
|
|
93458
|
+
return lines;
|
|
93441
93459
|
}
|
|
93442
|
-
|
|
93443
|
-
|
|
93444
|
-
|
|
93445
|
-
|
|
93446
|
-
let match = ID_IN_GAP.exec(gap);
|
|
93447
|
-
while (match !== null) {
|
|
93448
|
-
last = Number(match[1]);
|
|
93449
|
-
match = ID_IN_GAP.exec(gap);
|
|
93460
|
+
function peakPressureLines(route, parameters) {
|
|
93461
|
+
const retained = route.memorySamples.at(-1)?.heapUsed;
|
|
93462
|
+
if (retained === void 0) {
|
|
93463
|
+
return [];
|
|
93450
93464
|
}
|
|
93451
|
-
|
|
93465
|
+
const pressure = assessPeakPressure({
|
|
93466
|
+
peaks: route.peaks,
|
|
93467
|
+
retainedHeapBytes: retained,
|
|
93468
|
+
maxOldSpaceMb: parameters.maxOldSpaceMb
|
|
93469
|
+
});
|
|
93470
|
+
return pressure === null ? [] : [` \u25B2 peak pressure: ${describePeakPressure(pressure)}`];
|
|
93452
93471
|
}
|
|
93453
|
-
function
|
|
93454
|
-
|
|
93455
|
-
|
|
93456
|
-
|
|
93457
|
-
|
|
93458
|
-
|
|
93459
|
-
|
|
93460
|
-
|
|
93461
|
-
const spanEnd = nextId === null ? lineText.length : idEnd + nextId.index;
|
|
93462
|
-
const entry = map.lines[lineIndex]?.entries.find(
|
|
93463
|
-
(candidate) => candidate.column >= idEnd && candidate.column < spanEnd
|
|
93464
|
-
);
|
|
93465
|
-
const source = entry === void 0 ? void 0 : map.sources[entry.sourceIndex];
|
|
93466
|
-
if (source !== void 0) {
|
|
93467
|
-
result.set(Number(first[1]), source);
|
|
93468
|
-
}
|
|
93469
|
-
}
|
|
93472
|
+
function unreclaimedLines(route, verdict) {
|
|
93473
|
+
const retention = assessUnreclaimedRetention({
|
|
93474
|
+
unreclaimedSamples: route.unreclaimedSamples,
|
|
93475
|
+
memorySamples: route.memorySamples,
|
|
93476
|
+
verdict,
|
|
93477
|
+
verdictIsWellSupported: warrantsIssueDraft(route)
|
|
93478
|
+
});
|
|
93479
|
+
return retention === null ? [] : [` \u25B2 unreclaimed: ${describeUnreclaimedRetention(retention)}`];
|
|
93470
93480
|
}
|
|
93471
|
-
function
|
|
93472
|
-
|
|
93473
|
-
|
|
93474
|
-
if (lineText === void 0) {
|
|
93475
|
-
continue;
|
|
93476
|
-
}
|
|
93477
|
-
let previousColumn = 0;
|
|
93478
|
-
let previousSource = null;
|
|
93479
|
-
for (const entry of mapping.entries) {
|
|
93480
|
-
if (entry.sourceIndex !== previousSource) {
|
|
93481
|
-
const id = lastModuleIdIn(lineText.slice(previousColumn, entry.column));
|
|
93482
|
-
const source = map.sources[entry.sourceIndex];
|
|
93483
|
-
if (id !== null && source !== void 0 && !result.has(id)) {
|
|
93484
|
-
result.set(id, source);
|
|
93485
|
-
}
|
|
93486
|
-
previousSource = entry.sourceIndex;
|
|
93487
|
-
}
|
|
93488
|
-
previousColumn = entry.column;
|
|
93489
|
-
}
|
|
93481
|
+
function routeLines(route, parameters) {
|
|
93482
|
+
if (route.status === "skipped") {
|
|
93483
|
+
return [` \u2013 ${route.route} skipped: ${route.reason}`];
|
|
93490
93484
|
}
|
|
93491
|
-
|
|
93492
|
-
|
|
93493
|
-
const result = /* @__PURE__ */ new Map();
|
|
93494
|
-
if (!code.includes("module.exports=[") && !code.includes("module.exports = [")) {
|
|
93495
|
-
return result;
|
|
93485
|
+
if (route.status === "failed") {
|
|
93486
|
+
return [` \u2716 ${route.route} failed: ${route.reason}`];
|
|
93496
93487
|
}
|
|
93497
|
-
|
|
93498
|
-
|
|
93499
|
-
return result;
|
|
93488
|
+
if (route.status === "not-exercised") {
|
|
93489
|
+
return [` \u2013 ${route.route} not exercised: ${route.reason}`];
|
|
93500
93490
|
}
|
|
93501
|
-
const
|
|
93502
|
-
|
|
93503
|
-
|
|
93504
|
-
return
|
|
93491
|
+
const verdict = effectiveVerdict(route);
|
|
93492
|
+
const curve = route.samples.map(formatMb).join(" \u2192 ");
|
|
93493
|
+
const resolved = route.resolvedWithCycles === void 0 ? "" : ` (resolved at ${route.resolvedWithCycles} cycles)`;
|
|
93494
|
+
return [
|
|
93495
|
+
` ${VERDICT_ICON[verdict]} ${route.route} ${verdict} (${formatGrowth(
|
|
93496
|
+
route.growthPer1000Requests
|
|
93497
|
+
)}) heap ${curve}${resolved}`,
|
|
93498
|
+
...revalidationLines(route),
|
|
93499
|
+
...abandonLines(route),
|
|
93500
|
+
...confidenceLines(route),
|
|
93501
|
+
...memorySourceLines(route, verdict),
|
|
93502
|
+
...peakPressureLines(route, parameters),
|
|
93503
|
+
...unreclaimedLines(route, verdict),
|
|
93504
|
+
...findingLines(route)
|
|
93505
|
+
];
|
|
93505
93506
|
}
|
|
93506
|
-
|
|
93507
|
-
const
|
|
93508
|
-
|
|
93509
|
-
|
|
93510
|
-
|
|
93511
|
-
} catch {
|
|
93512
|
-
return out;
|
|
93507
|
+
function skippedGuidanceLines(report) {
|
|
93508
|
+
const needConfig = report.routes.filter((route) => route.status === "skipped" && route.reason.includes("sample params")).map((route) => route.route);
|
|
93509
|
+
const skeleton = renderConfigSkeleton(needConfig, report.prerender);
|
|
93510
|
+
if (skeleton === null) {
|
|
93511
|
+
return [];
|
|
93513
93512
|
}
|
|
93514
|
-
|
|
93515
|
-
|
|
93516
|
-
|
|
93517
|
-
}
|
|
93513
|
+
const editing = hasPlaceholders(skeleton) ? ` Replace each ${"REPLACE-ME"} with a value that exists in your app.` : ` The values come from paths your build already prerendered.`;
|
|
93514
|
+
return [
|
|
93515
|
+
"",
|
|
93516
|
+
`${needConfig.length} route(s) need sample params. Write this to next-leak.config.json in the app directory:${editing}`,
|
|
93517
|
+
...skeleton.split("\n").map((line) => ` ${line}`)
|
|
93518
|
+
];
|
|
93519
|
+
}
|
|
93520
|
+
function coverageLine(report) {
|
|
93521
|
+
const total = report.routes.length;
|
|
93522
|
+
const measured = report.routes.filter((route) => route.status === "measured").length;
|
|
93523
|
+
if (measured === total) {
|
|
93524
|
+
return `measured all ${total} discovered route(s)`;
|
|
93518
93525
|
}
|
|
93519
|
-
|
|
93526
|
+
const breakdown = ["skipped", "not-exercised", "failed"].map((status) => ({
|
|
93527
|
+
status,
|
|
93528
|
+
count: report.routes.filter((route) => route.status === status).length
|
|
93529
|
+
})).filter((entry) => entry.count > 0).map((entry) => `${entry.count} ${entry.status.replace("-", " ")}`).join(", ");
|
|
93530
|
+
return `measured ${measured} of ${total} discovered route(s) \u2014 ${breakdown}; the rest is not a verdict about your app`;
|
|
93520
93531
|
}
|
|
93521
|
-
|
|
93522
|
-
const
|
|
93523
|
-
for (const
|
|
93524
|
-
|
|
93525
|
-
let map;
|
|
93526
|
-
try {
|
|
93527
|
-
code = await readFile4(file, "utf8");
|
|
93528
|
-
map = JSON.parse(await readFile4(`${file}.map`, "utf8"));
|
|
93529
|
-
} catch {
|
|
93530
|
-
continue;
|
|
93531
|
-
}
|
|
93532
|
-
for (const [id, source] of extractChunkModules(code, map)) {
|
|
93533
|
-
if (!registry.has(id)) {
|
|
93534
|
-
registry.set(id, source);
|
|
93535
|
-
}
|
|
93536
|
-
}
|
|
93532
|
+
function formatReport(report) {
|
|
93533
|
+
const lines = [`next-leak \u2014 ${report.appDir}`, ""];
|
|
93534
|
+
for (const route of report.routes) {
|
|
93535
|
+
lines.push(...routeLines(route, report.parameters));
|
|
93537
93536
|
}
|
|
93538
|
-
|
|
93537
|
+
lines.push(...skippedGuidanceLines(report));
|
|
93538
|
+
const { minGrowthPerCycle, loadRequests, cycles, maxOldSpaceMb } = report.parameters;
|
|
93539
|
+
const resolvedCycles = [
|
|
93540
|
+
...new Set(
|
|
93541
|
+
report.routes.flatMap(
|
|
93542
|
+
(route) => route.status === "measured" && route.resolvedWithCycles !== void 0 ? [route.resolvedWithCycles] : []
|
|
93543
|
+
)
|
|
93544
|
+
)
|
|
93545
|
+
].sort((a, b) => a - b);
|
|
93546
|
+
const cyclesLabel = resolvedCycles.length === 0 ? `${cycles} cycles` : `${cycles} cycles (${resolvedCycles.join(", ")} where resolved)`;
|
|
93547
|
+
lines.push("", coverageLine(report));
|
|
93548
|
+
lines.push(
|
|
93549
|
+
`judged over ${cyclesLabel} \xD7 ${loadRequests} requests, growth gate ${(minGrowthPerCycle / 1024).toFixed(0)} KiB/cycle (${formatGrowth(minGrowthPerCycle / loadRequests * 1e3)}), heap cap ${maxOldSpaceMb} MB`,
|
|
93550
|
+
`snapshots and run.json: ${report.workDir}`,
|
|
93551
|
+
`report: ${report.bundle.htmlReport}`,
|
|
93552
|
+
...report.bundle.issues.map((issue) => `issue draft (${issue.route}): ${issue.file}`)
|
|
93553
|
+
);
|
|
93554
|
+
const inconclusive = report.routes.filter(
|
|
93555
|
+
(route) => route.status === "measured" && effectiveVerdict(route) === "inconclusive"
|
|
93556
|
+
);
|
|
93557
|
+
if (inconclusive.length > 0) {
|
|
93558
|
+
const routeList = inconclusive.map((route) => route.route).join(",");
|
|
93559
|
+
const moreCycles = resolveCycles(report.parameters.cycles);
|
|
93560
|
+
lines.push(
|
|
93561
|
+
"",
|
|
93562
|
+
"hint: inconclusive means sustained sub-threshold growth \u2014 measure longer to resolve it:",
|
|
93563
|
+
` next-leak ${report.appDir} --routes ${routeList} --cycles ${moreCycles}`
|
|
93564
|
+
);
|
|
93565
|
+
}
|
|
93566
|
+
return lines.join("\n");
|
|
93567
|
+
}
|
|
93568
|
+
|
|
93569
|
+
// src/environment.ts
|
|
93570
|
+
import { createRequire } from "module";
|
|
93571
|
+
import os from "os";
|
|
93572
|
+
var require2 = createRequire(import.meta.url);
|
|
93573
|
+
var { version: nextLeakVersion } = require2("../package.json");
|
|
93574
|
+
function captureEnvironment(nextVersion) {
|
|
93575
|
+
return {
|
|
93576
|
+
nodeVersion: process.version,
|
|
93577
|
+
platform: os.platform(),
|
|
93578
|
+
arch: os.arch(),
|
|
93579
|
+
cpuModel: os.cpus()[0]?.model ?? null,
|
|
93580
|
+
totalMemoryBytes: os.totalmem(),
|
|
93581
|
+
nextVersion,
|
|
93582
|
+
nextLeakVersion
|
|
93583
|
+
};
|
|
93539
93584
|
}
|
|
93540
93585
|
|
|
93541
93586
|
// src/load.ts
|
|
@@ -93735,6 +93780,7 @@ var FIRST_BYTE_BUDGET_MS = 5e3;
|
|
|
93735
93780
|
async function runAbandonPhase(options) {
|
|
93736
93781
|
const target = new URL(options.url);
|
|
93737
93782
|
const port = Number(target.port || 80);
|
|
93783
|
+
const fromRequest = options.abandonFrom === "request";
|
|
93738
93784
|
const headerLines = Object.entries(options.headers ?? {}).map(([name, value]) => `${name}: ${value}\r
|
|
93739
93785
|
`).join("");
|
|
93740
93786
|
const request2 = `GET ${target.pathname}${target.search} HTTP/1.1\r
|
|
@@ -93779,7 +93825,10 @@ Host: ${target.host}\r
|
|
|
93779
93825
|
socket.once("connect", () => {
|
|
93780
93826
|
result.sent += 1;
|
|
93781
93827
|
socket.write(request2);
|
|
93782
|
-
timer = setTimeout(
|
|
93828
|
+
timer = setTimeout(
|
|
93829
|
+
giveUp,
|
|
93830
|
+
fromRequest ? options.abandonAfterMs : FIRST_BYTE_BUDGET_MS
|
|
93831
|
+
);
|
|
93783
93832
|
timer.unref();
|
|
93784
93833
|
});
|
|
93785
93834
|
socket.on("data", () => {
|
|
@@ -93787,6 +93836,9 @@ Host: ${target.host}\r
|
|
|
93787
93836
|
return;
|
|
93788
93837
|
}
|
|
93789
93838
|
responseStarted = true;
|
|
93839
|
+
if (fromRequest) {
|
|
93840
|
+
return;
|
|
93841
|
+
}
|
|
93790
93842
|
clearTimeout(timer);
|
|
93791
93843
|
timer = setTimeout(giveUp, options.abandonAfterMs);
|
|
93792
93844
|
timer.unref();
|
|
@@ -93949,6 +94001,7 @@ async function runRitual(options, deps = defaultDeps) {
|
|
|
93949
94001
|
amount,
|
|
93950
94002
|
connections,
|
|
93951
94003
|
abandonAfterMs,
|
|
94004
|
+
...options.abandonFrom !== void 0 && { abandonFrom: options.abandonFrom },
|
|
93952
94005
|
...options.headers !== void 0 && { headers: options.headers }
|
|
93953
94006
|
});
|
|
93954
94007
|
loadOutcomes.push({
|
|
@@ -94330,6 +94383,9 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
94330
94383
|
...headers !== void 0 && { headers },
|
|
94331
94384
|
...routeConfig.abandonAfterMs !== void 0 && {
|
|
94332
94385
|
abandonAfterMs: routeConfig.abandonAfterMs
|
|
94386
|
+
},
|
|
94387
|
+
...routeConfig.abandonFrom !== void 0 && {
|
|
94388
|
+
abandonFrom: routeConfig.abandonFrom
|
|
94333
94389
|
}
|
|
94334
94390
|
});
|
|
94335
94391
|
const confidence = assessConfidence({
|
|
@@ -94344,6 +94400,9 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
94344
94400
|
warmupRequests: options.warmupRequests ?? RITUAL_DEFAULTS.warmupRequests,
|
|
94345
94401
|
...routeConfig.abandonAfterMs !== void 0 && {
|
|
94346
94402
|
abandonAfterMs: routeConfig.abandonAfterMs
|
|
94403
|
+
},
|
|
94404
|
+
...routeConfig.abandonFrom !== void 0 && {
|
|
94405
|
+
abandonFrom: routeConfig.abandonFrom
|
|
94347
94406
|
}
|
|
94348
94407
|
});
|
|
94349
94408
|
const verdict = confidence.supersededVerdict ?? result.trend.verdict;
|
|
@@ -94374,6 +94433,12 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
94374
94433
|
unreclaimedSamples: result.unreclaimedSamples,
|
|
94375
94434
|
unreclaimedTrend: result.unreclaimedTrend,
|
|
94376
94435
|
requestsPerCycle: result.requestsPerCycle,
|
|
94436
|
+
...routeConfig.abandonAfterMs !== void 0 && {
|
|
94437
|
+
abandon: {
|
|
94438
|
+
afterMs: routeConfig.abandonAfterMs,
|
|
94439
|
+
from: routeConfig.abandonFrom ?? "first-byte"
|
|
94440
|
+
}
|
|
94441
|
+
},
|
|
94377
94442
|
timings: result.timings,
|
|
94378
94443
|
loadOutcomes: result.loadOutcomes,
|
|
94379
94444
|
settleOutcomes: result.settleOutcomes,
|
|
@@ -94389,7 +94454,7 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
94389
94454
|
};
|
|
94390
94455
|
}
|
|
94391
94456
|
async function writeEvidenceBundle(report, workDir) {
|
|
94392
|
-
const { renderHtmlReport } = await import("./html-report-
|
|
94457
|
+
const { renderHtmlReport } = await import("./html-report-TN7PYVU4.js");
|
|
94393
94458
|
const { renderIssueMarkdown } = await import("./issue-report-HKA26WNV.js");
|
|
94394
94459
|
for (const route of report.routes) {
|
|
94395
94460
|
if (route.status === "measured" && warrantsIssueDraft(route)) {
|
|
@@ -94534,6 +94599,20 @@ async function runMeasurement(options, deps = defaultDeps2) {
|
|
|
94534
94599
|
}
|
|
94535
94600
|
|
|
94536
94601
|
export {
|
|
94602
|
+
classifySource,
|
|
94603
|
+
attributeFinding,
|
|
94604
|
+
attributeDiff,
|
|
94605
|
+
summarizeBaseline,
|
|
94606
|
+
retainerChain,
|
|
94607
|
+
diffAgainstBaseline,
|
|
94608
|
+
SnapshotError,
|
|
94609
|
+
assertReadableSnapshot,
|
|
94610
|
+
diffSnapshotFiles,
|
|
94611
|
+
decodeVlqLine,
|
|
94612
|
+
decodeMappings,
|
|
94613
|
+
sourceIndexAt,
|
|
94614
|
+
extractChunkModules,
|
|
94615
|
+
extractModuleRegistry,
|
|
94537
94616
|
appPathsManifestSchema,
|
|
94538
94617
|
routesManifestSchema,
|
|
94539
94618
|
discoverPagesRoutes,
|
|
@@ -94555,21 +94634,7 @@ export {
|
|
|
94555
94634
|
renderConfigSkeleton,
|
|
94556
94635
|
hasPlaceholders,
|
|
94557
94636
|
formatReport,
|
|
94558
|
-
classifySource,
|
|
94559
|
-
attributeFinding,
|
|
94560
|
-
attributeDiff,
|
|
94561
94637
|
captureEnvironment,
|
|
94562
|
-
summarizeBaseline,
|
|
94563
|
-
retainerChain,
|
|
94564
|
-
diffAgainstBaseline,
|
|
94565
|
-
SnapshotError,
|
|
94566
|
-
assertReadableSnapshot,
|
|
94567
|
-
diffSnapshotFiles,
|
|
94568
|
-
decodeVlqLine,
|
|
94569
|
-
decodeMappings,
|
|
94570
|
-
sourceIndexAt,
|
|
94571
|
-
extractChunkModules,
|
|
94572
|
-
extractModuleRegistry,
|
|
94573
94638
|
LoadError,
|
|
94574
94639
|
runLoadPhase,
|
|
94575
94640
|
runRitual,
|