Skip to content

fix(watcher): reclaim mimalloc pages after in-process fallback reindex#924

Closed
gillingworth86 wants to merge 1 commit into
DeusData:mainfrom
gillingworth86:fix/watcher-inprocess-mem-collect
Closed

fix(watcher): reclaim mimalloc pages after in-process fallback reindex#924
gillingworth86 wants to merge 1 commit into
DeusData:mainfrom
gillingworth86:fix/watcher-inprocess-mem-collect

Conversation

@gillingworth86

Copy link
Copy Markdown

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) — ran cbm_pipeline_run() + cbm_pipeline_free() without the paired cbm_mem_collect() that every other post-pipeline-run site performs:

  • src/mcp/mcp.c:5758, :3833, :2232
  • src/pipeline/pipeline.c:834
  • src/pipeline/pass_parallel.c:799, :995
  • src/graph_buffer/graph_buffer.c:1678

Per 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 and check_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=10000 directly as a child).

Fix

Add cbm_mem_collect() after cbm_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, sampling cbm_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)

  1. Self-re-triggering loop. check_changes() has no handled-latch, so any uncommitted change keeps the watcher firing every poll. Separately, pipeline_incremental.c:671-674 unconditionally re-exports .codebase-memory/graph.db.zst and artifact.c:326-338 stamps a fresh indexed_at on every incremental dump — which re-dirties the repo and deterministically re-triggers the next poll when team-sharing persistence is enabled.
  2. Windows supervised spawn. Worth confirming why the supervised wrap degrades to in-process on Windows — if the subprocess path worked there, RSS would return via child exit regardless of this fix. The missing collect is a real bug on the fallback either way, but the spawn degradation is likely the reason Windows users hit it.
  3. Prevention. Consider folding cbm_mem_collect() into cbm_pipeline_run() (or a cbm_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.

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>
@gillingworth86 gillingworth86 requested a review from DeusData as a code owner July 6, 2026 23:39
@gillingworth86

Copy link
Copy Markdown
Author

CI note (from a fork run of this branch): one test fails — index_bg_paths_route_through_supervisor_issue832 (tests/test_mcp.c:4378) — but only on the Linux gcc/UBSan legs; it passes on all three Windows legs and both macOS legs (5932 passed, 1 failed, 1 skipped).

It looks pre-existing and unrelated to this change. The supervised worker exits nonzero due to UBSan null pointer passed as nonnull runtime errors in src/pipeline/pass_similarity.c:152 and src/pipeline/pass_semantic_edges.c:996,1074 — files this PR does not modify (only src/main.c changed). The failing test also exercises the supervised-subprocess routing, not the in-process fallback line this PR touches. Flagging in case it's a known flaky/UBSan item on main; happy to help run it down.

@gillingworth86

Copy link
Copy Markdown
Author

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.

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