perf(encoder): store page index in chunked slices, not a map - #95
Open
corylanou wants to merge 1 commit into
Open
perf(encoder): store page index in chunked slices, not a map#95corylanou wants to merge 1 commit into
corylanou wants to merge 1 commit into
Conversation
EncodePage already enforces strictly ascending page numbers, so the encoder's page index never needed map semantics: entries can be appended in order and written out directly on Close, dropping the collect-and-sort pass entirely. The map representation retained one bucket entry per encoded page. On large snapshots that dominates encoder memory: a 34M-page database (130 GiB at 4 KiB pages) costs roughly 2.9 GiB in map storage plus transient spikes while the map doubles, which is the main linear term in litestream issue #1477's OOM. A packed 24-byte entry costs ~0.8 GiB for the same file. Entries live in chunks whose capacity doubles from 256 entries up to 64K, so memory stays proportional to the pages actually encoded: the small LTX files written on every sync cost a few KiB, nothing is preallocated from the caller-supplied header commit count (which the compactor copies from unverified input), and growth never copies the whole index. Wire format is unchanged: a golden test pins the file checksum of deterministic sparse and snapshot files captured from the map-based encoder, and a boundary test decodes an index spanning several chunks. The public PageIndexElem decoder API is untouched.
Collaborator
Author
|
Companion Litestream change (per-database maintenance serialization, the other half of the #1477 fix): benbjohnson/litestream#1479 |
6 tasks
corylanou
added a commit
to benbjohnson/litestream
that referenced
this pull request
Aug 27, 2026
Pins github.com/superfly/ltx to the head of superfly/ltx#95 (v0.5.3-0.20260827162011-d457a1ab7844) so this PR builds and soaks with the chunked encoder page index, which is the other half of the #1477 fix. Replace with the tagged ltx release before merging.
corylanou
added a commit
to benbjohnson/litestream
that referenced
this pull request
Aug 27, 2026
Pins github.com/superfly/ltx to the head of superfly/ltx#95 (v0.5.3-0.20260827162011-d457a1ab7844) so this PR builds and soaks with the chunked encoder page index, which is the other half of the #1477 fix. Replace with the tagged ltx release before merging.
Collaborator
Author
|
Evidence for the combined change, built by the soak rig from the litestream PR head with go.mod pinned to this branch (d457a1a), no local replace: 4 GiB / 1,051,216-page fixture, snapshot held resident while L1 compaction runs — overlap peak heap growth 470.6 MB on litestream main + ltx v0.5.2 → 199.6 MB with benbjohnson/litestream#1479 + this PR (snapshot phase 168.6 → 68.8 MB; encoder index 99 MB → 16 MB in the peak profile). Full table and profiles in the litestream PR body. |
This was referenced Aug 27, 2026
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.
Summary
Encoderkept its page index in amap[uint32]PageIndexElemand sorted the keys onClose.EncodePagealready rejects any page number that is not strictly greater than the previous one, so the map never provided anything a slice does not: entries can be appended in write order and streamed out directly.The map is the dominant memory cost of encoding a large file. One entry per page at roughly 90 B live (plus rehash spikes while the map grows) is ~2.9 GiB for a 34M-page snapshot — the main linear term in benbjohnson/litestream#1477, where a 130 GiB database was OOM-killed in a 12 GiB container. A packed 24-byte entry is ~0.8 GiB for the same file.
Header.Commit(whichCompactorcopies from unverified input), and growth never copies the whole index.encodePageIndexdrops the collect-and-sort pass.TestEncoder_PageIndexGoldenpins theFileChecksumof a sparse non-snapshot file and a contiguous snapshot, captured from the map-based encoder onmain(the checksum covers the index tuples in order, so any reordering or offset drift fails it).TestEncoder_PageIndexChunkBoundariesdecodes an index spanning several chunks.PageIndexElemand the decoder API are untouched.Evidence
Measured with the litestream-soak
snapshot-compaction-overlaprig (corylanou/litestream-soak#196): Litestreammain(3b468c9) built against ltxv0.5.2versus this branch, same fixture,runtime.MemStatssampled at 100 ms with GOGC=25, heap profile written at each phase's peak. Heap growth is peakHeapInuseminus the phase's post-GC baseline.Each phase includes a fixed 32 MB of S3 multipart buffers (part size × concurrency+1), so the per-page deltas are larger than the totals suggest: the encoder index goes from ~95 B/page to ~25 B/page. Peak heap profiles (
inuse_space, 4 GiB snapshot phase):Combined with the per-database maintenance serialization in the companion Litestream PR (linked in a comment below), the #1477 overlap drops from ~450 B/page on today's code to ~195 B/page (4 GiB fixture: 470.6 MB → 205.6 MB), i.e. ≈15 GB → ≈6.7 GB at the reporter's 34.2M pages. With that change alone the biggest remaining per-page cost is the decoder-side map noted below (
DecodePageIndex, 81 MB in the 4 GiB compaction profile).Follow-up (not in this PR)
The decoder side (
DecodePageIndex/Decoder.PageIndex) still returns amap[uint32]PageIndexElem, which is now the largest remaining per-page term during compaction (~70 B/page). Changing that is a public API change and belongs in its own PR.Test plan
go test ./...,go vet ./..., gofmtmain(8cb8f8e) before the change and asserted after