Fix runaway memory usage of the books cache (mmap + LRU) and >4 GiB book corruption#7
Open
McSon2 wants to merge 2 commits into
Open
Fix runaway memory usage of the books cache (mmap + LRU) and >4 GiB book corruption#7McSon2 wants to merge 2 commits into
McSon2 wants to merge 2 commits into
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The LGS keeps every played mode's books fully decompressed in RAM, in a cache that is never evicted (
MathEngine.modesDashMap). 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_rangestores byte offsets asu32, but decompressed books routinely exceed 4 GiB. Every book past the 4 GiB boundary silently wraps and reads corrupted data.Changes
Arc<ModeAssets>, so eviction is safe mid-request.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.costparsing — recent math-sdk publishes emit"cost": 300.0(float), which failed theu64deserializer 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).footprint/vmmapbefore/after.No UI or API changes;
lgs-wasmuntouched (wasm can't mmap and browsers cap memory anyway).🤖 Generated with Claude Code