next-leak 0.13.0 → 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 +20 -5
- package/dist/attribution.d.ts +8 -2
- package/dist/{chunk-UKWFONEI.js → chunk-5OI4MUND.js} +128 -32
- package/dist/{chunk-QUFL2FXU.js → chunk-GU7IJUNY.js} +14 -0
- package/dist/{chunk-UOAW5UA5.js → chunk-QNNVRQ4L.js} +1 -1
- package/dist/cli.js +5 -11
- package/dist/heap-diff.d.ts +11 -0
- package/dist/{html-report-ZXDEJ6X5.js → html-report-VNTPHLO3.js} +2 -2
- package/dist/index.js +3 -3
- package/dist/manifests.d.ts +3 -0
- package/dist/route-guidance.d.ts +2 -0
- package/dist/runner.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -193,7 +193,10 @@ entry, so the route reads as flat whatever it retains, and that false negative
|
|
|
193
193
|
lands on exactly the leaks being reported now (`use cache`, `cacheComponents`
|
|
194
194
|
and ISR all key on the params). If your app answers 404 for params it never
|
|
195
195
|
prerendered, the run says so through its non-2xx count; drop the marker then.
|
|
196
|
-
When
|
|
196
|
+
When the build prerendered nothing for a route, the value is `REPLACE-ME-{n}`:
|
|
197
|
+
put the shape of a real value in place of `REPLACE-ME` and keep the `-{n}`, or
|
|
198
|
+
the run measures a single cache entry. When a run skips a route it prints the
|
|
199
|
+
same fragment.
|
|
197
200
|
|
|
198
201
|
```json
|
|
199
202
|
```
|
|
@@ -240,7 +243,7 @@ separates them, because each one has a different fix:
|
|
|
240
243
|
| One-time warm-up growth (JIT, lazy caches) | `stable` | The first cycle is excluded from the verdict; warm-up flattens, leaks keep climbing |
|
|
241
244
|
| A route that is expensive, not leaky | `failed` under load it cannot sustain, flat once concurrency fits | Real leaks survive forced GC at any concurrency; saturation disappears when load drops |
|
|
242
245
|
| Growth that pauses and resumes (stepwise) | `leak` | A healthy route gives back 20-30% of its growth; a stepwise leak gives back nothing |
|
|
243
|
-
| A cache filling up under the load that measures it | `saturating` | A bounded store grows by less each cycle as new keys get rarer; a leak does not decelerate |
|
|
246
|
+
| A cache filling up under the load that measures it | `saturating` | A bounded store grows by less each cycle as new keys get rarer; a leak does not decelerate. A bend that lands on a steady rate above the gate is a leak under the fill, and stays `leak` |
|
|
244
247
|
| Memory a forced GC reclaims that production never reclaims in time | `pressure` | Every verdict sample is post-GC; the peaks are sampled under load, and a ceiling every settled cycle comes back to is a regime, not an episode |
|
|
245
248
|
| Native/buffer memory with a flat JS heap | `leak (external)` or an explicit RSS note | Heap, `external` and RSS are sampled and judged separately |
|
|
246
249
|
| A leak in your code vs a dependency vs Next itself | `culprit: src/app/x/page.tsx (your code)` — or the package, or framework internals | Retainer chains mapped through the build's source maps |
|
|
@@ -264,14 +267,22 @@ separates them, because each one has a different fix:
|
|
|
264
267
|
with the traffic, so the longer run decides the same thing. If the heap is
|
|
265
268
|
flat but RSS keeps climbing, the report says so explicitly: that is an
|
|
266
269
|
allocator, external-buffer or fragmentation problem, not a JS-heap leak.
|
|
267
|
-
- **`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.
|
|
268
271
|
- **`saturating`** — every cycle grew, but by less than the one before, and the
|
|
269
272
|
last by less than half the first. That is the shape of a bounded store
|
|
270
273
|
running out of new keys, not of memory going missing. It matters because the
|
|
271
274
|
alternative was calling it a leak: a `use cache` route measured on Next
|
|
272
275
|
16.3.3 came out at +603 MB per 1000 requests that was entirely the cache
|
|
273
276
|
storing what it had been asked to store — the same route dropped to +88 MB
|
|
274
|
-
once the payload was removed.
|
|
277
|
+
once the payload was removed. The bend has to head for zero, though: a store
|
|
278
|
+
filling on top of a leak bends too, and then holds the leak's rate for as
|
|
279
|
+
long as the load runs. So when the last third of the window (at least three
|
|
280
|
+
cycles) sits at one steady rate at or above the gate, within 1.5x of itself,
|
|
281
|
+
the route stays `leak`. Measured on the reproduction for
|
|
282
|
+
[#99335](https://github.com/vercel/next.js/issues/99335), an ISR route
|
|
283
|
+
keeping every new path's cache control in an unbounded map: it closed on four
|
|
284
|
+
cycles of ~2.2 MB, twice the gate, and used to be called `saturating`, with
|
|
285
|
+
no issue draft. Because the shape requires every cycle to clear
|
|
275
286
|
the growth gate, a decelerating curve always ends the window still growing,
|
|
276
287
|
so where it settles is outside what was measured: these routes are
|
|
277
288
|
**measured again** with twice the cycles, like `inconclusive` ones. No issue
|
|
@@ -388,7 +399,11 @@ A route with a revalidation period serves its cache unless the request carries
|
|
|
388
399
|
the build's own `x-prerender-revalidate` header. Measured on that same app: the
|
|
389
400
|
identical run reports `leak` with the header and `stable` without it. next-leak
|
|
390
401
|
reads `previewModeId` from `.next/prerender-manifest.json` and drives those
|
|
391
|
-
routes itself; a header you set in `next-leak.config.json` wins untouched.
|
|
402
|
+
routes itself; a header you set in `next-leak.config.json` wins untouched. A
|
|
403
|
+
dynamic template with nothing prerendered (`generateStaticParams()` returning
|
|
404
|
+
`[]`) counts too: the build records no period for it, but its blocking fallback
|
|
405
|
+
caches every new param on first request, so `{n}` fills that cache while it is
|
|
406
|
+
measured and the report says so. When
|
|
392
407
|
the manifest cannot supply one, the route is reported `not exercised` with no
|
|
393
408
|
verdict, because a flat curve measured against a static cache says nothing about
|
|
394
409
|
the app.
|
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;
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
resolveCycles,
|
|
12
12
|
warrantsIssueDraft,
|
|
13
13
|
withdrawnByDisagreement
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-GU7IJUNY.js";
|
|
15
15
|
import {
|
|
16
16
|
assessPeakPressure,
|
|
17
17
|
assessPressureVerdict,
|
|
@@ -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
|
|
@@ -93235,6 +93314,10 @@ var prerenderManifestSchema = z2.looseObject({
|
|
|
93235
93314
|
srcRoute: z2.string().nullable().optional()
|
|
93236
93315
|
})
|
|
93237
93316
|
).optional(),
|
|
93317
|
+
// Read only for `fallback`, and left untyped: its shape has changed across
|
|
93318
|
+
// Next versions (`null`, `false`, an HTML path, a PPR shell), and a template
|
|
93319
|
+
// the tool cannot classify must not fail the whole manifest.
|
|
93320
|
+
dynamicRoutes: z2.record(z2.string(), z2.looseObject({ fallback: z2.unknown().optional() })).optional(),
|
|
93238
93321
|
preview: z2.looseObject({ previewModeId: z2.string() }).optional()
|
|
93239
93322
|
});
|
|
93240
93323
|
var INTERNAL_PATHS = /* @__PURE__ */ new Set(["/_global-error", "/_not-found"]);
|
|
@@ -94892,7 +94975,7 @@ function renderConfigSkeleton(routeTemplates, manifest = void 0) {
|
|
|
94892
94975
|
const values = {};
|
|
94893
94976
|
for (const segment of segments) {
|
|
94894
94977
|
const prerendered = sampled?.[segment.name];
|
|
94895
|
-
values[segment.name] = prerendered
|
|
94978
|
+
values[segment.name] = varyingValueFrom(prerendered ?? PLACEHOLDER);
|
|
94896
94979
|
}
|
|
94897
94980
|
routes[template] = values;
|
|
94898
94981
|
}
|
|
@@ -94901,6 +94984,12 @@ function renderConfigSkeleton(routeTemplates, manifest = void 0) {
|
|
|
94901
94984
|
function hasPlaceholders(skeleton) {
|
|
94902
94985
|
return skeleton.includes(PLACEHOLDER);
|
|
94903
94986
|
}
|
|
94987
|
+
function writtenConfigNote(skeleton) {
|
|
94988
|
+
if (hasPlaceholders(skeleton)) {
|
|
94989
|
+
return `replace each ${PLACEHOLDER} with the shape of a value that exists in your app, and keep the \`-{n}\` after it: a fixed value measures one cache entry and reads as flat whatever the route retains`;
|
|
94990
|
+
}
|
|
94991
|
+
return "`{n}` gives every request a different value: a prerendered one serves the warm cache and reads as flat whatever the route retains. Use `{n%200}` to revisit a fixed set of keys, and drop the marker only if the app 404s on params it never prerendered.";
|
|
94992
|
+
}
|
|
94904
94993
|
|
|
94905
94994
|
// src/unreclaimed-retention.ts
|
|
94906
94995
|
var MB = 1024 * 1024;
|
|
@@ -94998,10 +95087,10 @@ function ownerLabel(attribution) {
|
|
|
94998
95087
|
}
|
|
94999
95088
|
}
|
|
95000
95089
|
function revalidationLines(route) {
|
|
95001
|
-
if (route.
|
|
95090
|
+
if (route.servedFromIsrCache !== true) {
|
|
95002
95091
|
return [];
|
|
95003
95092
|
}
|
|
95004
|
-
const every = `revalidates every ${route.revalidatedEverySeconds}s`;
|
|
95093
|
+
const every = route.revalidatedEverySeconds === void 0 ? "caches each new key on its first request" : `revalidates every ${route.revalidatedEverySeconds}s`;
|
|
95005
95094
|
return route.revalidationDriven === true ? [
|
|
95006
95095
|
` driven through ISR revalidation (${every}; without it the load would serve the cache) \u2014 this is Next's revalidation path, so a leak that only happens on the path your users take is not in this number`
|
|
95007
95096
|
] : [
|
|
@@ -95032,7 +95121,7 @@ function cacheLines(route) {
|
|
|
95032
95121
|
lines.push(
|
|
95033
95122
|
` the load served keys this route had never cached, so some of this growth is cache residency; bound it with {n%N} in next-leak.config.json`
|
|
95034
95123
|
);
|
|
95035
|
-
if (route.
|
|
95124
|
+
if (route.servedFromIsrCache === true) {
|
|
95036
95125
|
lines.push(
|
|
95037
95126
|
` on this route {n%N} does not isolate that: a bounded key set is served from the ISR cache, so the run would drive revalidation and measure a different path. Compare the two runs rather than trusting either number alone`
|
|
95038
95127
|
);
|
|
@@ -95173,7 +95262,7 @@ function skippedGuidanceLines(report) {
|
|
|
95173
95262
|
if (skeleton === null) {
|
|
95174
95263
|
return [];
|
|
95175
95264
|
}
|
|
95176
|
-
const editing = hasPlaceholders(skeleton) ? ` Replace each ${"REPLACE-ME"} with a value that exists in your app.` : ` The shapes come from paths your build already prerendered; \`{n}\` makes every request use a different one, because reusing a prerendered value serves the warm cache and reads as flat whatever the route retains. Use \`{n%200}\` instead to revisit a fixed set of 200 keys, and drop the marker only if the app 404s on params it never prerendered.`;
|
|
95265
|
+
const editing = hasPlaceholders(skeleton) ? ` Replace each ${"REPLACE-ME"} with the shape of a value that exists in your app, and keep the \`-{n}\` after it: a fixed value measures one cache entry and reads as flat whatever the route retains.` : ` The shapes come from paths your build already prerendered; \`{n}\` makes every request use a different one, because reusing a prerendered value serves the warm cache and reads as flat whatever the route retains. Use \`{n%200}\` instead to revisit a fixed set of 200 keys, and drop the marker only if the app 404s on params it never prerendered.`;
|
|
95177
95266
|
return [
|
|
95178
95267
|
"",
|
|
95179
95268
|
`${needConfig.length} route(s) need sample params. Write this to next-leak.config.json in the app directory:${editing}`,
|
|
@@ -95401,8 +95490,11 @@ function revalidateSecondsFor(manifest, route) {
|
|
|
95401
95490
|
}
|
|
95402
95491
|
return null;
|
|
95403
95492
|
}
|
|
95493
|
+
function cachesOnDemand(manifest, route) {
|
|
95494
|
+
return manifest?.dynamicRoutes?.[route]?.fallback === null;
|
|
95495
|
+
}
|
|
95404
95496
|
function revalidates(manifest, route) {
|
|
95405
|
-
return revalidateSecondsFor(manifest, route) !== null;
|
|
95497
|
+
return revalidateSecondsFor(manifest, route) !== null || cachesOnDemand(manifest, route);
|
|
95406
95498
|
}
|
|
95407
95499
|
function planRevalidation(manifest, route, userHeaders, requestPath) {
|
|
95408
95500
|
const userSupplied = Object.keys(userHeaders ?? {}).some(
|
|
@@ -95419,9 +95511,11 @@ function planRevalidation(manifest, route, userHeaders, requestPath) {
|
|
|
95419
95511
|
}
|
|
95420
95512
|
const previewModeId = manifest?.preview?.previewModeId;
|
|
95421
95513
|
if (previewModeId === void 0 || previewModeId === "") {
|
|
95514
|
+
const seconds = revalidateSecondsFor(manifest, route);
|
|
95515
|
+
const cached = seconds === null ? "is served from the ISR cache" : `revalidates every ${seconds}s`;
|
|
95422
95516
|
return {
|
|
95423
95517
|
kind: "cannot-drive",
|
|
95424
|
-
reason:
|
|
95518
|
+
reason: `${cached}, but the build's prerender-manifest.json carries no previewModeId \u2014 load would serve the cache and measure nothing. Set "${REVALIDATE_HEADER}" in next-leak.config.json to drive it.`
|
|
95425
95519
|
};
|
|
95426
95520
|
}
|
|
95427
95521
|
return { kind: "drive", headers: { [REVALIDATE_HEADER]: previewModeId } };
|
|
@@ -95539,6 +95633,7 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
95539
95633
|
const { deps, options, target, workDir, routeConfig, registry, nextVersion, progress } = context;
|
|
95540
95634
|
const plan = planRevalidation(target.prerender, route.path, routeConfig.headers, requestPath);
|
|
95541
95635
|
const revalidateSeconds = revalidateSecondsFor(target.prerender, route.path);
|
|
95636
|
+
const servedFromIsrCache = revalidates(target.prerender, route.path);
|
|
95542
95637
|
const bounded = boundedMarkerOf(requestPath);
|
|
95543
95638
|
const driven = plan.kind === "drive" ? plan.headers : {};
|
|
95544
95639
|
const cacheDriven = plan.kind === "no-cache-to-drive" || plan.kind === "drive" && bounded === null;
|
|
@@ -95581,7 +95676,7 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
95581
95676
|
warmupRequests: options.warmupRequests ?? RITUAL_DEFAULTS.warmupRequests,
|
|
95582
95677
|
// Decides whether the cache-residency remedy exists on this route: bounding
|
|
95583
95678
|
// the keys of an ISR route hands the requests back to the cache.
|
|
95584
|
-
revalidatesFromCache:
|
|
95679
|
+
revalidatesFromCache: servedFromIsrCache,
|
|
95585
95680
|
...routeConfig.abandonAfterMs !== void 0 && {
|
|
95586
95681
|
abandonAfterMs: routeConfig.abandonAfterMs
|
|
95587
95682
|
},
|
|
@@ -95624,6 +95719,7 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
95624
95719
|
memorySamples: result.memorySamples,
|
|
95625
95720
|
peaks: result.peaks,
|
|
95626
95721
|
...revalidateSeconds !== null && { revalidatedEverySeconds: revalidateSeconds },
|
|
95722
|
+
...servedFromIsrCache && { servedFromIsrCache: true },
|
|
95627
95723
|
...plan.kind === "drive" && { revalidationDriven: true },
|
|
95628
95724
|
...bounded !== null && { keyCardinality: bounded.bound },
|
|
95629
95725
|
unreclaimedSamples: result.unreclaimedSamples,
|
|
@@ -95651,7 +95747,7 @@ async function measureRoute(context, route, requestPath, index, pass = void 0) {
|
|
|
95651
95747
|
};
|
|
95652
95748
|
}
|
|
95653
95749
|
async function writeEvidenceBundle(report, workDir) {
|
|
95654
|
-
const { renderHtmlReport } = await import("./html-report-
|
|
95750
|
+
const { renderHtmlReport } = await import("./html-report-VNTPHLO3.js");
|
|
95655
95751
|
const { renderIssueMarkdown } = await import("./issue-report-4RW5YOT3.js");
|
|
95656
95752
|
for (const route of report.routes) {
|
|
95657
95753
|
if (route.status === "measured" && warrantsIssueDraft(route)) {
|
|
@@ -95948,7 +96044,7 @@ export {
|
|
|
95948
96044
|
runRitual,
|
|
95949
96045
|
runSelfCheck,
|
|
95950
96046
|
renderConfigSkeleton,
|
|
95951
|
-
|
|
96047
|
+
writtenConfigNote,
|
|
95952
96048
|
formatReport,
|
|
95953
96049
|
captureEnvironment,
|
|
95954
96050
|
loadSignatures,
|
|
@@ -11,6 +11,8 @@ var STEPWISE_MAX_DRAWDOWN_RATIO = 0.1;
|
|
|
11
11
|
var ACQUITTAL_MAX_GROWTH_MULTIPLE = 8;
|
|
12
12
|
var SATURATION_MIN_CYCLES = 3;
|
|
13
13
|
var SATURATION_MAX_FINAL_RATIO = 0.5;
|
|
14
|
+
var SETTLED_TAIL_MIN_CYCLES = 3;
|
|
15
|
+
var SETTLED_TAIL_MAX_SPREAD = 1.5;
|
|
14
16
|
function maxDrawdown(samples) {
|
|
15
17
|
let peak = samples[1] ?? 0;
|
|
16
18
|
let worst = 0;
|
|
@@ -46,8 +48,20 @@ function isSaturating(deltas, minGrowth) {
|
|
|
46
48
|
if (last > first * SATURATION_MAX_FINAL_RATIO) {
|
|
47
49
|
return false;
|
|
48
50
|
}
|
|
51
|
+
if (hasSettledAboveGate(deltas, minGrowth)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
49
54
|
return isHeadingDown(deltas);
|
|
50
55
|
}
|
|
56
|
+
function hasSettledAboveGate(deltas, minGrowth) {
|
|
57
|
+
const size = Math.max(SETTLED_TAIL_MIN_CYCLES, Math.ceil(deltas.length / 3));
|
|
58
|
+
const tail = deltas.slice(-size);
|
|
59
|
+
const smallest = Math.min(...tail);
|
|
60
|
+
if (smallest < minGrowth) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return Math.max(...tail) <= smallest * SETTLED_TAIL_MAX_SPREAD;
|
|
64
|
+
}
|
|
51
65
|
function hasArrivedWithoutResuming(deltas, minGrowth) {
|
|
52
66
|
const stop = deltas.findIndex((delta) => delta <= 0);
|
|
53
67
|
if (stop === -1) {
|
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
extractModuleRegistry,
|
|
13
13
|
formatReport,
|
|
14
14
|
freePort,
|
|
15
|
-
hasPlaceholders,
|
|
16
15
|
helpText,
|
|
17
16
|
killActiveChildren,
|
|
18
17
|
parseCliArgs,
|
|
@@ -22,12 +21,13 @@ import {
|
|
|
22
21
|
runSelfCheck,
|
|
23
22
|
terminalSafe,
|
|
24
23
|
unregisterChild,
|
|
25
|
-
validateTarget
|
|
26
|
-
|
|
24
|
+
validateTarget,
|
|
25
|
+
writtenConfigNote
|
|
26
|
+
} from "./chunk-5OI4MUND.js";
|
|
27
27
|
import "./chunk-IKWF7SSB.js";
|
|
28
28
|
import {
|
|
29
29
|
classifyTrend
|
|
30
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-GU7IJUNY.js";
|
|
31
31
|
import "./chunk-VTKOO3YJ.js";
|
|
32
32
|
import "./chunk-6XYFBOL2.js";
|
|
33
33
|
|
|
@@ -907,13 +907,7 @@ ${skeleton}`
|
|
|
907
907
|
return;
|
|
908
908
|
}
|
|
909
909
|
console.log(`wrote ${file}`);
|
|
910
|
-
|
|
911
|
-
console.log("replace each REPLACE-ME with a value that exists in your app");
|
|
912
|
-
} else {
|
|
913
|
-
console.log(
|
|
914
|
-
"`{n}` gives every request a different value: a prerendered one serves the warm cache and reads as flat whatever the route retains. Use `{n%200}` to revisit a fixed set of keys, and drop the marker only if the app 404s on params it never prerendered."
|
|
915
|
-
);
|
|
916
|
-
}
|
|
910
|
+
console.log(writtenConfigNote(skeleton));
|
|
917
911
|
}
|
|
918
912
|
async function main() {
|
|
919
913
|
const parsed = parseCliArgs(process.argv.slice(2));
|
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[];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createRequire as __nextLeakCreateRequire } from 'node:module';import { fileURLToPath as __nextLeakFileURLToPath } from 'node:url';import { dirname as __nextLeakDirname } from 'node:path';const require = __nextLeakCreateRequire(import.meta.url);const __filename = __nextLeakFileURLToPath(import.meta.url);const __dirname = __nextLeakDirname(__filename);
|
|
2
2
|
import {
|
|
3
3
|
renderHtmlReport
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-QNNVRQ4L.js";
|
|
5
|
+
import "./chunk-GU7IJUNY.js";
|
|
6
6
|
import "./chunk-VTKOO3YJ.js";
|
|
7
7
|
import "./chunk-6XYFBOL2.js";
|
|
8
8
|
export {
|
package/dist/index.js
CHANGED
|
@@ -41,14 +41,14 @@ import {
|
|
|
41
41
|
sourceIndexAt,
|
|
42
42
|
summarizeBaseline,
|
|
43
43
|
validateTarget
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-5OI4MUND.js";
|
|
45
45
|
import "./chunk-IKWF7SSB.js";
|
|
46
46
|
import {
|
|
47
47
|
renderHtmlReport
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-QNNVRQ4L.js";
|
|
49
49
|
import {
|
|
50
50
|
classifyTrend
|
|
51
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-GU7IJUNY.js";
|
|
52
52
|
import {
|
|
53
53
|
renderIssueMarkdown
|
|
54
54
|
} from "./chunk-FJVY5MKV.js";
|
package/dist/manifests.d.ts
CHANGED
|
@@ -34,6 +34,9 @@ export declare const prerenderManifestSchema: z.ZodObject<{
|
|
|
34
34
|
initialRevalidateSeconds: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodLiteral<false>]>>;
|
|
35
35
|
srcRoute: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
36
36
|
}, z.core.$loose>>>;
|
|
37
|
+
dynamicRoutes: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
38
|
+
fallback: z.ZodOptional<z.ZodUnknown>;
|
|
39
|
+
}, z.core.$loose>>>;
|
|
37
40
|
preview: z.ZodOptional<z.ZodObject<{
|
|
38
41
|
previewModeId: z.ZodString;
|
|
39
42
|
}, z.core.$loose>>;
|
package/dist/route-guidance.d.ts
CHANGED
|
@@ -38,3 +38,5 @@ export declare function sampleValuesFromManifest(manifest: PrerenderManifest | u
|
|
|
38
38
|
export declare function renderConfigSkeleton(routeTemplates: readonly string[], manifest?: PrerenderManifest | undefined): string | null;
|
|
39
39
|
/** Whether a rendered skeleton still needs the user to fill anything in. */
|
|
40
40
|
export declare function hasPlaceholders(skeleton: string): boolean;
|
|
41
|
+
/** What `--write-config` prints after writing a skeleton: how to edit it. */
|
|
42
|
+
export declare function writtenConfigNote(skeleton: string): string;
|
package/dist/runner.d.ts
CHANGED
|
@@ -113,6 +113,12 @@ export type RouteReport = {
|
|
|
113
113
|
* cache and one measured against a re-render are different experiments.
|
|
114
114
|
*/
|
|
115
115
|
revalidatedEverySeconds?: number;
|
|
116
|
+
/**
|
|
117
|
+
* Set on every route served from the ISR cache, including a dynamic
|
|
118
|
+
* template that caches each new key with no prerendered entry to carry
|
|
119
|
+
* its period — the period is then absent, but the cache is in play.
|
|
120
|
+
*/
|
|
121
|
+
servedFromIsrCache?: true;
|
|
116
122
|
/**
|
|
117
123
|
* Whether the load carried the build's own revalidation header. Set apart
|
|
118
124
|
* from the period because the two answer different questions: the period
|