Skip to content

Fix runaway memory usage of the books cache (mmap + LRU) and >4 GiB book corruption#7

Open
McSon2 wants to merge 2 commits into
simnJS:mainfrom
McSon2:fix/lgs-books-memory
Open

Fix runaway memory usage of the books cache (mmap + LRU) and >4 GiB book corruption#7
McSon2 wants to merge 2 commits into
simnJS:mainfrom
McSon2:fix/lgs-books-memory

Conversation

@McSon2

@McSon2 McSon2 commented Jul 12, 2026

Copy link
Copy Markdown

Problem

The LGS keeps every played mode's books fully decompressed in RAM, in a cache that is never evicted (MathEngine.modes DashMap). Real publish files are 0.5–1 GiB compressed and 5–19 GiB decompressed per mode, so a session that opens a couple of games / buys a couple of bonuses quickly exceeds physical RAM. Measured on a 24 GB M4 Pro: 27 GB footprint after 14 minutes, 32 GB of swap filled, machine unusable.

There is also a correctness bug hiding behind the memory one: id_to_range stores byte offsets as u32, but decompressed books routinely exceed 4 GiB. Every book past the 4 GiB boundary silently wraps and reads corrupted data.

Changes

  1. mmap-backed books — each books file is stream-decompressed into an unlinked temp file and mmap'd read-only. Pages are file-backed and clean: the OS reclaims them under memory pressure instead of swapping. Footprint for four cached modes (~15 GiB decompressed) drops from 27 GB to 22 MB dirty.
  2. u64 offsets — fixes corrupted reads past 4 GiB. Verified by forcing the last book of a 5.4 GiB BONUS file: payout now matches the lookup table exactly (it didn't before).
  3. LRU eviction — at most 8 modes / 40 GiB decompressed kept; in-flight spins hold their Arc<ModeAssets>, so eviction is safe mid-request.
  4. Index built during decompression — ids are harvested at line starts while the stream is written out (IndexingWriter), instead of re-reading and JSON-parsing the multi-GiB buffer afterwards. Falls back to the previous exhaustive serde scan whenever the line index doesn't cover every id in the weights table (covers the adjacent-records case in the existing tests). Cold load of a 19 GiB mode: 68 s → 21 s; warm hits unchanged.
  5. Tolerant cost parsing — recent math-sdk publishes emit "cost": 300.0 (float), which failed the u64 deserializer and 500'd every endpoint of the game. Accept int or float and round.

Testing

  • cargo test -p lgs (the two indexing tests now exercise the fast path and the fallback respectively).
  • Manual against real publish files (tachyon, mad-formula): spins on all modes, forced events at the highest ids of >4 GiB books checked against the lookup tables, eviction + reload cycles, and footprint/vmmap before/after.

No UI or API changes; lgs-wasm untouched (wasm can't mmap and browsers cap memory anyway).

🤖 Generated with Claude Code

McSon2 and others added 2 commits July 13, 2026 00:33
…viction

The LGS kept every played mode's books fully decompressed in an anonymous
heap buffer, cached forever. With real publish files (0.5-1 GiB compressed,
5-19 GiB decompressed per mode) a session touching a few modes grew past
physical RAM and pushed the whole machine into swap: measured 27 GB
footprint after 14 minutes on a 24 GB machine.

- Stream-decompress each books file into an unlinked temp file and mmap it
  read-only. Pages are file-backed and clean, so the OS reclaims them under
  memory pressure instead of swapping. Measured footprint for four cached
  tachyon modes (~15 GiB decompressed): 22 MB dirty, down from 27 GB.
- Evict least-recently-used modes beyond 8 entries or 40 GiB of total
  decompressed bytes. In-flight spins keep their Arc<ModeAssets> alive, so
  eviction is safe mid-request.
- Widen id_to_range offsets from u32 to u64. Decompressed books routinely
  exceed 4 GiB (tachyon BONUS: 5.4 GiB), where u32 offsets silently wrapped
  and corrupted every read past the 4 GiB boundary. Verified by forcing the
  last book of a 5.4 GiB file: payout now matches the lookup table exactly.
- Build the id index while the decompressed stream is being written
  (IndexingWriter harvests ids at line starts), instead of re-reading and
  JSON-parsing the multi-GiB buffer afterwards. Falls back to the previous
  exhaustive serde scan whenever the line-based index does not cover every
  id present in the weights table (e.g. records not separated by newlines).
  Cold mode load on a 19 GiB books file: 68 s -> 21 s; warm hits unchanged.
- Size the id map modestly and shrink_to_fit once counts are known; the
  previous len/512 pre-allocation reserved ~500 MB per large mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Recent math-sdk publishes emit "cost": 300.0 rather than an integer, which
failed the u64 deserializer and broke every endpoint of the game with a 500.
Accept both forms and round to the RGS integer cost model.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant