ReviewOS

stacks/harness

SSR of / allocates ~11MB of transient garbage per render

#11
Closed glennmichael123 opened this 9 days ago ·
9 days ago

Found while building the §11 budget gate (1557f44). Rendering / repeatedly against a server with an empty projection walks RSS from 80MB to ~280MB over 60 renders, with a visible GC dip around render 40 — so it is heap high-water and GC pressure, not a true leak, but ~11MB of allocation per render of a near-empty page is the underlying number.

Curve (child-process RSS, ps -o rss=):

rendersRSS
080MB
10192MB
20232MB
30253MB
40228MB (GC)
50266MB
60281MB

The budget gate holds today (251MB at 20 sessions against 300MB), so nothing is on fire — but the allocation lives in the stx SSR path (the page is ~20KB at zero profiles, so this is not the payload), which makes it an upstream candidate first. Worth profiling before the next stx bump: if each render churns the whole runtime + component tree through the heap, a hot control surface polling this page pays real GC pauses for it.

Reproduce: scripts/performance-budgets.ts child mode + a fetch loop; the rss-after lines the script prints localize it (renders account for ~165MB of the growth, the 600-event pile ~6MB).

9 days ago

Profiled and mitigated; the root cause moves upstream to stacksjs/stx#1945.

What the profile found (scripts in the session scratchpad, numbers reproducible with a renderView loop):

  • The ~11MB/render is almost entirely off the JS heap: bun:jsc heapStats shows ~1.1MB allocated and nothing retained per render, while RSS climbs 30→209MB over the first ten renders and plateaus ~267MB. Malloc high-water — the GC dip this issue observed was real, but the pages never return to the OS.
  • Bun.gc(true) after every render barely helps (253→224MB over 60): the churn is native-side.
  • The island pipeline is the mass: the client-script bundler's disk cache hits every render, but its 190KB output is re-read, re-wrapped, re-validated and re-inlined per request, with fresh scope ids each time. One <Sidebar> tag → 211KB page, 192KB of it inline script, ~9MB/render.

What changed here (75482a4): the server now caches rendered pages keyed by URL + a state revision bumped in broadcast — the one path every applied event takes. A polling surface pays the render once per state change and 0.6ms after that (was ~124ms); an active turn still renders per event, which is the consistency renders always had. The budget gate busts the cache so "SSR render of /" keeps measuring the real render, and gained a cached-render gate.

The remaining per-render churn is a function of stx internals, tracked upstream with the full profile: stacksjs/stx#1945.

Sign in to comment on this issue.