import type { TrendResult } from "./trend.js"; import type { HeapSample } from "./control-server.js"; import type { PeakSample } from "./ritual.js"; /** * Which ceiling the process came closest to. * * `heap` is the only class bounded by `--max-old-space`; `rss` is what a * container kills. `external`/`arrayBuffers` live in rss, which is why they * are reported through it instead of against a limit that does not apply to * them (vercel/next.js#92287: a healthy heap next to 4.3 GB of arrayBuffers). */ export type PeakPressureClass = "heap" | "rss"; export type PeakPressure = { class: PeakPressureClass; /** Highest value observed for that class, across cycles (bytes). */ peakBytes: number; /** Retained heap the verdict was computed on (bytes). */ retainedBytes: number; /** Heap limit in force, in bytes. */ heapLimitBytes: number; }; export type PeakPressureInput = { peaks: readonly PeakSample[]; /** Post-GC heapUsed of the final sample: what the route actually retains. */ retainedHeapBytes: number; maxOldSpaceMb: number; }; /** * What a route retains once it has served traffic: the floor of the post-GC * cycle samples, not the last of them. * * Every sample here follows a forced collection, but "after a forced * collection" is not "after everything collectable was collected" — a sample * can carry memory the collector had not reached yet. Measured on the * vercel/next.js#92287 reproduction, 2026-09-24, one run's cycle samples were * 217.1 / 125.0 / 45.2 / 194.9 MB, and the next run's were 69.3 / 121.2 / 48.4 * / 41.0. Taking the last put the figure everything here divides by at 194.9 MB * on one run and 41.0 MB on the other, which by itself decided whether the same * app under the same load was reported at all. * * The floor is the honest reading: what a process comes back down to is what it * holds, and everything above it is memory not yet reclaimed. The baseline is * excluded because it precedes any traffic — a route retains nothing before it * has served anything, and dividing by that would make every route look * disproportionate. */ export declare function retainedAfterLoad(memorySamples: readonly HeapSample[]): number | undefined; /** * Whether a route's peak is far enough from the memory its verdict was * computed on to be worth saying out loud. * * On its own this stays outside the verdict, and for the original reason: * `leak`/`stable` are statements about retention after GC, calibrated against * real leaks with no false positives, and a peak is a different axis. A single * high peak is a size, not a direction — an app that reserves 600 MB on its * first cycle and holds that level is doing nothing wrong. * * What the note alone could not carry is the rest of that sentence: a process * that climbs to 3.5 GB and hands it all back is honestly `stable`, and still * OOM-killed in a 1 GB container. `assessPressureVerdict` below is where a run * that reaches the ceiling on every cycle stops being a footnote under a `✔`. */ export declare function assessPeakPressure(input: PeakPressureInput): PeakPressure | null; export type PressureVerdictInput = { /** The post-GC verdict, exactly as the classifier produced it. */ trend: TrendResult; peaks: readonly PeakSample[]; retainedHeapBytes: number; maxOldSpaceMb: number; }; /** * Whether a run that retains nothing is nonetheless heading for the ceiling. * * Every number a verdict is computed from is taken after a forced collection, * and production never runs those. That makes the verdict structurally blind to * a whole class of death: memory a full GC does reclaim, allocated faster than * the runtime reclaims it on its own. Measured on the vercel/next.js#92287 * reproduction, 2026-09-23 — the app grew ~1 MB of `arrayBuffers` per request * to over 3 GB and died, and this tool called it `stable` at -9.00 MB/1000 * requests, because a forced GC handed all of it back before every sample. That * is the trap next-leak exists to warn other people about. * * Three conditions, and no threshold of its own: the ceiling rules are * `assessPeakPressure`'s, asked of each cycle instead of the highest reading. * * - **The post-GC verdict is `stable` or `saturating`.** `leak` is already the * worse news and `inconclusive` is an admission that the series did not * decide — promoting *that* to an accusation would be inventing a finding out * of a measurement that failed. * - **The peak is far enough from what the route retains to be remarked on** — * `assessPeakPressure`, thresholds unchanged. * - **Every settled cycle reached that ceiling**, not just the highest one. * This is the condition that makes the verdict safe. A single high peak is an * episode and gets only the note; a process that returns to the ceiling every * time it serves traffic is describing what it does under load, which is the * thing a container is sized against. * * Returns the trend unchanged when it does not qualify, so `trend.verdict` * stays the raw record everywhere else. */ export declare function assessPressureVerdict(input: PressureVerdictInput): TrendResult; /** * One line, phrased so it never contradicts the verdict next to it. A peak is * the highest value *sampled*: a spike shorter than the poll interval is not * observed, so this is a lower bound. */ export declare function describePeakPressure(pressure: PeakPressure): string;