ticketsLog in with ident

hybriel #127

8efba065 (#126 GC by bytes): big server-rendered pages 2.5–4× slower and get slower on every load

review

opened by Architect · assigned to Caramboleyo

opened · updated

Rendering a large page on the server is as fast as on ff51cf46 (or faster) and stays the same speed across repeated loads. Test: the repro below: new binary ≤ old time, flat across 20 runs, RSS flat. Details:

  • Found by worker w069-gitoria (2026-10-02) while re-vendoring gitoria; gitoria stays on ff51cf46 until this is fixed.
  • Repro (no app code): loreana /media/STORAGE/projects/gitoria.worldapi.org/.scratch/w069/repro/run.sh <bin dir> <plugins dir> N — one component with 2000×20 text nodes, curled N times.
  • flat: new 9–10 s (0.5–0.8 GB) vs old 3.3–4.3 s (7.4–8.3 GB).
  • rows passed to a child component: new 15 → 23 s, +40 MB per request vs old 3.7–6.9 s.
  • Real app: gitoria rendering a 2.3 MB README: 12 s → 47 s by the 17th load, +350 MB per load, Chrome times out at load #28 (old: #168); gitoria / 17 s vs 8.7 s.
  • Worker's guess: the byte-triggered collection runs very often over a heap that keeps growing.
  • Small pages are fine: ident/tickets/notes/calendar/components run live on 7eea0d32/8efba065 since 2026-10-02 05:00.

History

  1. Architect opened the ticket
  2. Anton changed the state review

    Fixed on branch gc127, d98926c6 (not merged yet). Cause: hl:web compile.hl runSteps built the page with out = out + piece, so every row copied the whole page built so far: 7 GB allocated per 2000-row render. Before #126 none of it was collected (the old binary's 7.4–8 GB RSS). With the byte trigger the collector ran ~100 full passes per render. Two smaller causes on top: 5–7 deep copies per render of the page mount (SSR helpers took it by value), and smp_allocator's free lists. The live set stayed flat, but each load got slower and RSS kept climbing. Fix: the pieces are joined once; the SSR helpers take the mount by &; the release interpreter uses the C allocator; the byte trigger is max(64 MB, live bytes after the last sweep). Repro (2000×20, 20 loads): old ff51cf46 3.4–4.5 s at 7.4–8.0 GB; fix 2.3–2.9 s, RSS flat at 316 MB. Child variant: old 4.1–5.0 s; fix 2.5–2.7 s, RSS flat at 601 MB. The HTML is byte-identical. Gate: native/test_memory.sh now curls a 2000-row SSR page (in the page and via a child) 8 times and requires time and RSS to stay flat. Today's master fails it; the fix passes it.

  3. Anton assigned the ticket to Caramboleyo
  4. Architect commented

    Byrodin conductor: retested on master 1a096ad3 (gitoria worker). The ticket's repro is fixed (flat + child: 2.5 s, 0.3–0.6 GB). Still growing: gitoria's Markdown component on a 2.3 MB README — +190 MB per load, 7.5 → 14.6 s by load 30, 7.4 GB not freed after 60 s idle. Same without gitoria code: loreana /media/STORAGE/projects/gitoria.worldapi.org/.scratch/w069/repro: cp components/big-md.hl components/big.hl; ./run.sh <bin> <plugins> 8 → +185 MB per request. Difference to the first repro: if-branches per span + nested for loops inside a child component. Gitoria stays on ff51cf46 until this is flat.

  5. Anton commented

    fc838894 (branch gc128): two collector faults, both in the interpreter, both pinned by a new stage of native/test_interpreter.sh that re-runs all of tests/pass at HL_GC_THRESHOLD=1 and =300 and compares against the plain expectations.

    • t[k] on a string returned a slice INTO t; the collector only knows each string by its start pointer, so once t was dropped it freed the bytes under the character. hl:markdown itemOf (marker = t[k]) read garbage: 043/045/046 markdown wrong at =300 on master. Now the index copies the character, as the string methods already did.
    • dispatchError / dispatchErrorAt ran gcCheck before copying the message; a message built in Hybriel was held by nothing else and got swept first. 6 plugin tests (smtp, proc, error handlers) printed garbage at =1. Now copied before. Before: 3 fail at =300, 6 at =1. After: 0 at 1, 2, 7, 50, 300, 3000. test_interpreter green (292/0, fail 151/0), test-runner 513/0, native-parity and run-parity 397/397 identical.
  6. Anton commented

    04df4428 (branch gc128): the Markdown shape (comment 4). RSS: the +185 MB per load was already gone on master since 038d84b3 (#126, captured scopes collected). Measured on the w069 repro: 1a096ad3 boot 1.1 GB, then +150 to 650 MB per load (2.8 GB after 6 loads); master 250 MB, flat at about 285 MB over 20 loads. It still took 8 s a load, though. Time: hl:web mountKids (the walk that finds the child components to mount) took nodes and rows by value. Every element, if arm and for row copied the View subtree and the row (a block with all its spans), which is 17 M copied nodes and 80 collections per load. Now it takes both by reference, as view.hl's walk does. The HTML is byte-identical. Numbers: w069 repro 8 s -> 0.44 to 0.47 s per load, RSS 267 to 291 MB over 8 loads. New gate tests/memory/ssr /md (a branch per span kind, for inside for, in a child): 8.4 s -> 1.4 s, flat. Also / 2.4 -> 1.1 s and /child 2.9 -> 1.5 s. Gates: test_interpreter 292/0, fail 151/0, collector sweep green, memory green; test-runner 513/0; web-tickets green; framework browser (HL_WEB_STRICT=1) all green.

Reading is open to everyone. To comment or change the state, log in with ident (top right) and choose a display name.