next-leak 0.13.1 → 0.13.2
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 +1 -1
- package/dist/attribution.d.ts +8 -2
- package/dist/{chunk-GBRBWUZZ.js → chunk-5OI4MUND.js} +100 -21
- package/dist/cli.js +1 -1
- package/dist/heap-diff.d.ts +11 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -267,7 +267,7 @@ separates them, because each one has a different fix:
|
|
|
267
267
|
with the traffic, so the longer run decides the same thing. If the heap is
|
|
268
268
|
flat but RSS keeps climbing, the report says so explicitly: that is an
|
|
269
269
|
allocator, external-buffer or fragmentation problem, not a JS-heap leak.
|
|
270
|
-
- **`leak`** — the report names the culprit when attribution resolves: your file (`culprit: src/app/x/page.tsx (your code)`), a dependency (package name), or framework internals. An `ISSUE-<route>.md` draft is generated **when the evidence plainly supports the verdict** — a `leak` carrying low-confidence warnings (growth barely over the threshold, one cycle dominating the mean, too few cycles for its size) gets the verdict but no draft, because a draft is written to be pasted into someone else's tracker. If the leak is app-owned, the draft tells you **not** to file it upstream.
|
|
270
|
+
- **`leak`** — the report names the culprit when attribution resolves: your file (`culprit: src/app/x/page.tsx (your code)`), a dependency (package name), or framework internals. The culprit is whoever holds the most of the growth, each byte counted once even when a container and what it holds are both reported; when more of it is unattributed than any owner holds, no culprit is named rather than a smaller one. An `ISSUE-<route>.md` draft is generated **when the evidence plainly supports the verdict** — a `leak` carrying low-confidence warnings (growth barely over the threshold, one cycle dominating the mean, too few cycles for its size) gets the verdict but no draft, because a draft is written to be pasted into someone else's tracker. If the leak is app-owned, the draft tells you **not** to file it upstream.
|
|
271
271
|
- **`saturating`** — every cycle grew, but by less than the one before, and the
|
|
272
272
|
last by less than half the first. That is the shape of a bounded store
|
|
273
273
|
running out of new keys, not of memory going missing. It matters because the
|
package/dist/attribution.d.ts
CHANGED
|
@@ -32,7 +32,13 @@ export type AttributedDiff = {
|
|
|
32
32
|
};
|
|
33
33
|
/**
|
|
34
34
|
* Attributes every finding and derives the route-level verdict: the
|
|
35
|
-
* owner+source group holding the most
|
|
36
|
-
*
|
|
35
|
+
* owner+source group holding the most exclusive retained bytes wins.
|
|
36
|
+
*
|
|
37
|
+
* What nothing could attribute competes as a group of its own. When it is the
|
|
38
|
+
* largest, the route stays `unattributed`: naming a smaller store as the
|
|
39
|
+
* culprit misleads, and an `app` or `dependency` culprit is what tells the
|
|
40
|
+
* issue draft not to file upstream. On the vercel/next.js#99335 long run the
|
|
41
|
+
* unnamed 19.84 MB map lost to a 9.31 MB fsChecker LRU reported at a
|
|
42
|
+
* dominance of 1, because unattributed bytes were left out of the share.
|
|
37
43
|
*/
|
|
38
44
|
export declare function attributeDiff(diff: HeapDiff, registry: ModuleRegistry): AttributedDiff;
|
|
@@ -92646,6 +92646,12 @@ var FRAMEWORK_CHAIN_MARKERS = [
|
|
|
92646
92646
|
["NextNodeServer", "Next server"],
|
|
92647
92647
|
["AppPageRouteModule", "App Router page module"],
|
|
92648
92648
|
["AppRouteRouteModule", "App Router route handler module"],
|
|
92649
|
+
// Ahead of the incremental cache, which holds it: a chain reaching through
|
|
92650
|
+
// both should name the store that grows. On the vercel/next.js#99335
|
|
92651
|
+
// reproduction its unbounded Map was the largest finding (19.84 MB over
|
|
92652
|
+
// 240,000 requests) and, unnamed, lost the route to the bounded fsChecker
|
|
92653
|
+
// LRU at 9.31 MB with a reported dominance of 1.
|
|
92654
|
+
["SharedCacheControls", "shared cache controls"],
|
|
92649
92655
|
["incrementalCache", "incremental cache"],
|
|
92650
92656
|
["IncrementalCache", "incremental cache"],
|
|
92651
92657
|
["ReactServer", "React Server Components renderer"],
|
|
@@ -92677,17 +92683,60 @@ function attributeFinding(finding, registry) {
|
|
|
92677
92683
|
}
|
|
92678
92684
|
return best;
|
|
92679
92685
|
}
|
|
92686
|
+
function enclosingIndexes(all) {
|
|
92687
|
+
const indexById = /* @__PURE__ */ new Map();
|
|
92688
|
+
for (const [index, finding] of all.entries()) {
|
|
92689
|
+
if (finding.nodeId !== void 0) {
|
|
92690
|
+
indexById.set(finding.nodeId, index);
|
|
92691
|
+
}
|
|
92692
|
+
}
|
|
92693
|
+
return all.map(
|
|
92694
|
+
(finding) => finding.containedIn === void 0 ? void 0 : indexById.get(finding.containedIn)
|
|
92695
|
+
);
|
|
92696
|
+
}
|
|
92697
|
+
function inheritOwners(own, enclosing) {
|
|
92698
|
+
return own.map((attribution, index) => {
|
|
92699
|
+
let current = enclosing[index];
|
|
92700
|
+
for (let hops = 0; attribution.owner === "unattributed" && hops < own.length; hops += 1) {
|
|
92701
|
+
if (current === void 0) {
|
|
92702
|
+
break;
|
|
92703
|
+
}
|
|
92704
|
+
const ancestor = own[current];
|
|
92705
|
+
if (ancestor !== void 0 && ancestor.owner !== "unattributed") {
|
|
92706
|
+
return ancestor;
|
|
92707
|
+
}
|
|
92708
|
+
current = enclosing[current];
|
|
92709
|
+
}
|
|
92710
|
+
return attribution;
|
|
92711
|
+
});
|
|
92712
|
+
}
|
|
92713
|
+
function exclusiveBytes(all, enclosing) {
|
|
92714
|
+
const bytes = all.map((finding) => finding.retainedBytes);
|
|
92715
|
+
for (const [index, parent] of enclosing.entries()) {
|
|
92716
|
+
if (parent !== void 0) {
|
|
92717
|
+
bytes[parent] = (bytes[parent] ?? 0) - (all[index]?.retainedBytes ?? 0);
|
|
92718
|
+
}
|
|
92719
|
+
}
|
|
92720
|
+
return bytes.map((value) => Math.max(0, value));
|
|
92721
|
+
}
|
|
92680
92722
|
function attributeDiff(diff, registry) {
|
|
92681
92723
|
const all = [...diff.grownNodes, ...diff.newNodes];
|
|
92682
|
-
const
|
|
92724
|
+
const enclosing = enclosingIndexes(all);
|
|
92725
|
+
const findings = inheritOwners(
|
|
92726
|
+
all.map((finding) => attributeFinding(finding, registry)),
|
|
92727
|
+
enclosing
|
|
92728
|
+
);
|
|
92729
|
+
const exclusive = exclusiveBytes(all, enclosing);
|
|
92683
92730
|
const byGroup = /* @__PURE__ */ new Map();
|
|
92684
|
-
let
|
|
92731
|
+
let unattributedBytes = 0;
|
|
92732
|
+
let totalBytes = 0;
|
|
92685
92733
|
for (const [index, attribution] of findings.entries()) {
|
|
92734
|
+
const bytes = exclusive[index] ?? 0;
|
|
92735
|
+
totalBytes += bytes;
|
|
92686
92736
|
if (attribution.owner === "unattributed") {
|
|
92737
|
+
unattributedBytes += bytes;
|
|
92687
92738
|
continue;
|
|
92688
92739
|
}
|
|
92689
|
-
const bytes = all[index]?.retainedBytes ?? 0;
|
|
92690
|
-
attributedBytes += bytes;
|
|
92691
92740
|
const key = `${attribution.owner}|${attribution.source ?? ""}|${attribution.packageName ?? ""}`;
|
|
92692
92741
|
const group = byGroup.get(key) ?? { attribution, bytes: 0 };
|
|
92693
92742
|
group.bytes += bytes;
|
|
@@ -92699,7 +92748,7 @@ function attributeDiff(diff, registry) {
|
|
|
92699
92748
|
winner = group;
|
|
92700
92749
|
}
|
|
92701
92750
|
}
|
|
92702
|
-
const route = winner === null ||
|
|
92751
|
+
const route = winner === null || winner.bytes === 0 || unattributedBytes > winner.bytes ? { ...UNATTRIBUTED, dominance: 0 } : { ...winner.attribution, dominance: winner.bytes / totalBytes };
|
|
92703
92752
|
return { findings, route };
|
|
92704
92753
|
}
|
|
92705
92754
|
|
|
@@ -92805,12 +92854,16 @@ function diffAgainstBaseline(baseline, after, options = {}) {
|
|
|
92805
92854
|
if (node.retainedSize >= resolved.newThresholdBytes) {
|
|
92806
92855
|
const chain = walkChain(node, resolved.chainDepth);
|
|
92807
92856
|
fresh.push({
|
|
92808
|
-
|
|
92809
|
-
|
|
92810
|
-
|
|
92811
|
-
|
|
92812
|
-
|
|
92813
|
-
|
|
92857
|
+
node,
|
|
92858
|
+
finding: {
|
|
92859
|
+
kind: "new",
|
|
92860
|
+
nodeType: node.type,
|
|
92861
|
+
name: truncateLabel(node.name),
|
|
92862
|
+
retainedBytes: node.retainedSize,
|
|
92863
|
+
retainerChain: chain.parts.join(" <- "),
|
|
92864
|
+
moduleIds: collectModuleIds(chain.nodes),
|
|
92865
|
+
nodeId: node.id
|
|
92866
|
+
}
|
|
92814
92867
|
});
|
|
92815
92868
|
}
|
|
92816
92869
|
return;
|
|
@@ -92819,12 +92872,16 @@ function diffAgainstBaseline(baseline, after, options = {}) {
|
|
|
92819
92872
|
if (before !== void 0 && node.retainedSize - before >= resolved.grownThresholdBytes) {
|
|
92820
92873
|
const chain = walkChain(node, resolved.chainDepth);
|
|
92821
92874
|
grown.push({
|
|
92822
|
-
|
|
92823
|
-
|
|
92824
|
-
|
|
92825
|
-
|
|
92826
|
-
|
|
92827
|
-
|
|
92875
|
+
node,
|
|
92876
|
+
finding: {
|
|
92877
|
+
kind: "grown",
|
|
92878
|
+
nodeType: node.type,
|
|
92879
|
+
name: truncateLabel(node.name),
|
|
92880
|
+
retainedBytes: node.retainedSize - before,
|
|
92881
|
+
retainerChain: chain.parts.join(" <- "),
|
|
92882
|
+
moduleIds: collectModuleIds(chain.nodes),
|
|
92883
|
+
nodeId: node.id
|
|
92884
|
+
}
|
|
92828
92885
|
});
|
|
92829
92886
|
}
|
|
92830
92887
|
});
|
|
@@ -92837,14 +92894,36 @@ function diffAgainstBaseline(baseline, after, options = {}) {
|
|
|
92837
92894
|
}
|
|
92838
92895
|
}
|
|
92839
92896
|
typeDeltas.sort((a, b) => b.deltaBytes - a.deltaBytes);
|
|
92840
|
-
|
|
92841
|
-
|
|
92897
|
+
const bySize = (a, b) => b.finding.retainedBytes - a.finding.retainedBytes;
|
|
92898
|
+
const keptGrown = grown.sort(bySize).slice(0, resolved.maxFindings);
|
|
92899
|
+
const keptFresh = fresh.sort(bySize).slice(0, resolved.maxFindings);
|
|
92900
|
+
linkContainment([...keptGrown, ...keptFresh]);
|
|
92842
92901
|
return {
|
|
92843
92902
|
typeDeltas,
|
|
92844
|
-
grownNodes:
|
|
92845
|
-
newNodes:
|
|
92903
|
+
grownNodes: keptGrown.map((candidate) => candidate.finding),
|
|
92904
|
+
newNodes: keptFresh.map((candidate) => candidate.finding)
|
|
92846
92905
|
};
|
|
92847
92906
|
}
|
|
92907
|
+
var MAX_DOMINATOR_DEPTH = 1e4;
|
|
92908
|
+
function linkContainment(kept) {
|
|
92909
|
+
const keptIds = new Set(kept.map((candidate) => candidate.node.id));
|
|
92910
|
+
for (const candidate of kept) {
|
|
92911
|
+
let current = candidate.node.dominatorNode;
|
|
92912
|
+
for (let depth = 0; depth < MAX_DOMINATOR_DEPTH && current; depth += 1) {
|
|
92913
|
+
if (current.id === candidate.node.id) {
|
|
92914
|
+
break;
|
|
92915
|
+
}
|
|
92916
|
+
if (keptIds.has(current.id)) {
|
|
92917
|
+
candidate.finding.containedIn = current.id;
|
|
92918
|
+
break;
|
|
92919
|
+
}
|
|
92920
|
+
if (current.dominatorNode?.id === current.id) {
|
|
92921
|
+
break;
|
|
92922
|
+
}
|
|
92923
|
+
current = current.dominatorNode;
|
|
92924
|
+
}
|
|
92925
|
+
}
|
|
92926
|
+
}
|
|
92848
92927
|
var SnapshotError = class extends Error {
|
|
92849
92928
|
/**
|
|
92850
92929
|
* Bytes that had to fit in one string, when that is why the snapshot was
|
package/dist/cli.js
CHANGED
package/dist/heap-diff.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type HeapNodeLike = {
|
|
|
18
18
|
references?: Array<{
|
|
19
19
|
name_or_index: string | number;
|
|
20
20
|
}>;
|
|
21
|
+
/** Immediate dominator, as memlab computes it; absent in most fixtures. */
|
|
22
|
+
dominatorNode?: HeapNodeLike | null;
|
|
21
23
|
};
|
|
22
24
|
export type HeapLike = {
|
|
23
25
|
nodes: {
|
|
@@ -53,6 +55,15 @@ export type NodeFinding = {
|
|
|
53
55
|
retainerChain: string;
|
|
54
56
|
/** Bundler module ids seen along the chain (needs `resolveNumeric`). */
|
|
55
57
|
moduleIds: number[];
|
|
58
|
+
/** Snapshot node id. Absent in run.json files written before it existed. */
|
|
59
|
+
nodeId?: number;
|
|
60
|
+
/**
|
|
61
|
+
* `nodeId` of the nearest other finding that dominates this one, when there
|
|
62
|
+
* is one. A grown container's delta already includes everything it
|
|
63
|
+
* dominates, so without this the same bytes are counted once per level —
|
|
64
|
+
* see `attributeDiff`.
|
|
65
|
+
*/
|
|
66
|
+
containedIn?: number;
|
|
56
67
|
};
|
|
57
68
|
export type HeapDiff = {
|
|
58
69
|
typeDeltas: TypeDelta[];
|
package/dist/index.js
CHANGED