fix(code-index): free captured sources once a pass lets them go - #2778
Merged
Merged
Conversation
The byte pool and the physical artifact pool index through Weak, and a Weak keeps its allocation, so every source buffer and artifact header a pass dropped stayed allocated. The worker now drops dead entries once a pass lets go of its capture and build.
|
This was referenced Oct 1, 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.
Part of #2706 (the per-file residue left after the graph owners release). #2775 handled the retained parse pool.
Attribution
jemalloc heap profiling (
prof:true,lg_prof_sample:14; a measurement-only build oftracedecay-cliwithalloc-jemallocand tikv-jemallocatorprofiling, not committed) of an isolated daemon. Corpus: every 20th tracked file (336 files), cold index, settle, then a dump with the graph owners resident and another after their idle release (_rjem_mallctl("prof.dump")via gdb on the scratch daemon). Estimated live bytes by first non-allocator frame:SharedCodeIndexBytePoolV1::intern(captured sources)CodeIndexProductionOwnerV1::extract_file(physical artifact pool)The only per-file sites that outlive the owners are the two weak caches. Every one of their blocks belongs to a capture or generation that is already gone.
Root cause
SharedCodeIndexBytePoolV1andSharedPhysicalCodeArtifactPoolV1index allocations throughWeak. AWeakkeeps the allocation, not just the value, alive. ForArc<[u8]>the bytes are inline, so every captured source stayed allocated after the last capture and build dropped it. Each physical-pool entry likewise kept its artifact's ~1.1 KB header. The byte pool pruned dead entries only when the map doubled, and the artifact pool only under FIFO eviction, so an idle worktree kept every dead source buffer. Those long-lived blocks were allocated amid each capture's transient data, so each one also pinned allocator pages.Change
SharedCodeIndexBytePoolV1::release_dead_entriesdrops every entry no capture or generation holds, and so doesSharedPhysicalCodeArtifactPoolV1::release_dead_entries. A dead weak entry can never be upgraded, so no reuse is lost. The background worker calls it as soon as a pass lets go of its capture and build.last_prune_len) is deleted. Pass-end release replaces it.Fail before / pass after
code_index_scheduler::tests::residency::a_settled_index_keeps_no_captured_source_allocatedchecks a settled three-file index.source_allocations: 3, live_sources: 0.(0, 0).tests/resident_accounting.rs::a_dropped_generation_leaves_nothing_once_the_pool_drops_its_dead_entriesbuilds 300 files into a long-lived pool and drops the generation (counting allocator). The dead entries pinned 338,888 B; after release, 12,656 B remain, within the test's 5% bound. Without the release the bound fails.Runtime proof (shipped allocator)
Debug
production(mimalloc) build, isolated profile undersystemd-run --user --scope -p MemoryMax=6G -p MemorySwapMax=1G. The journey: cold index, settle 120 s, measure; wait for the owners' idle release, settle 60 s, measure. The "before" binary is this branch with the release call removed.With owners released, the remainder now grows by about 11 KB per file between 336 and 1,345 files, against 34–45 KB per file measured in #2706. With owners resident, the uncharged gap is 174.5 MB at 336 files and 154.1 MB at 1,345, so it no longer scales with the corpus.
Checks
cargo test -p tracedecay-code-index -p tracedecay-code-index-runtime: lib 270,code_index_suite182,resident_accounting4, runtime lib 557 (1 ignored) plus 3 doc tests, all passed.cargo clippy -p tracedecay-code-index -p tracedecay-code-index-runtime --all-targets -- -D warnings, the same with--features hotpath --libfor the runtime, andcargo fmt --all -- --check: clean.Left open
A capture made outside the background worker (a branch-generation read) frees its dead entries at the next worker pass.