fix(watcher): reclaim mimalloc pages after in-process fallback reindex#924
fix(watcher): reclaim mimalloc pages after in-process fallback reindex#924gillingworth86 wants to merge 1 commit into
Conversation
watcher_index_fn routes re-indexing through the supervised subprocess (#832) so the long-lived server hands RSS back to the OS on each cycle. But the in-process degrade fallback — taken when the supervisor is off or the child spawn fails — ran cbm_pipeline_run()+cbm_pipeline_free() without the paired cbm_mem_collect() that every other post-pipeline-run site performs (mcp.c:5758, pipeline.c:834, pass_parallel.c:799/995, graph_buffer.c:1678). Because this fallback is the watcher's unattended path and fires on every poll while the working tree is dirty, the worker-thread pages mimalloc abandons at pool teardown were never returned. RSS ratcheted into tens of GB overnight indexing a small repo (observed: ~55GB private commit on ~200 files, Windows, where the supervised spawn degrades to in-process). Add the collect() to match the established pattern and bound the fallback path's peak. Signed-off-by: Greg Illingworth <g.illingworth86@gmail.com>
|
CI note (from a fork run of this branch): one test fails — It looks pre-existing and unrelated to this change. The supervised worker exits nonzero due to UBSan |
|
Closing this — on further investigation the runaway was a local configuration issue on my side (auto-index was pointed at a home directory and tried to graph the whole tree), not the code path this change touches. Withdrawing to avoid noise. Apologies. |
Summary
Fixes unbounded RSS growth in the watcher's unattended re-index path — observed ~55 GB private commit indexing a ~200-file repo on Windows, left running overnight.
Root cause
watcher_index_fn(src/main.c) routes re-indexing through the supervised subprocess (#832) so the long-lived server returns RSS to the OS on child exit. But the in-process degrade fallback — taken when the supervisor is off or the child spawn fails (src/mcp/index_supervisor.c:64-76) — rancbm_pipeline_run()+cbm_pipeline_free()without the pairedcbm_mem_collect()that every other post-pipeline-run site performs:src/mcp/mcp.c:5758,:3833,:2232src/pipeline/pipeline.c:834src/pipeline/pass_parallel.c:799,:995src/graph_buffer/graph_buffer.c:1678Per the note at
src/foundation/mem.c:140-147, mimalloc v3 does not reclaim pages a worker thread abandons at exit;mi_collect(true)(i.e.cbm_mem_collect()) is required to return them. Each full reindex spins up and tears down worker-thread pools (pass_parallel.c), abandoning pages every cycle. Because this fallback is the watcher's unattended path andcheck_changes()(src/watcher/watcher.c:512-530) re-fires on every poll while the working tree is dirty (no handled-latch), the fallback runs every 5–60 s and RSS ratchets indefinitely.This reproduces on Windows specifically because the supervised index spawn appears to degrade to the in-process path there — the leaking process was the main server itself running the pipeline in-process (spawning
git log --name-only --max-count=10000directly as a child).Fix
Add
cbm_mem_collect()aftercbm_pipeline_free()in the fallback path, matching the established pattern. One line + explanatory comment.Testing
Suggested regression: loop the in-process reindex (
CBM_INDEX_SUPERVISOR=0) against a small fixture kept dirty, samplingcbm_mem_rss()(src/foundation/mem.c:177) — RSS climbs monotonically before the fix, plateaus after. I could not build locally (Windows box without a C toolchain); relying on fork CI to validate build + smoke.Related observations (not changed here — flagged for maintainers)
check_changes()has no handled-latch, so any uncommitted change keeps the watcher firing every poll. Separately,pipeline_incremental.c:671-674unconditionally re-exports.codebase-memory/graph.db.zstandartifact.c:326-338stamps a freshindexed_aton every incremental dump — which re-dirties the repo and deterministically re-triggers the next poll when team-sharing persistence is enabled.cbm_mem_collect()intocbm_pipeline_run()(or acbm_pipeline_run_and_collect()wrapper) so call sites can't drift out of the pattern — this is now the 5th site relying on the manual pairing.