Skip to content

[codex] Parallelize incremental parse step - #1052

Draft
Extrodox wants to merge 3 commits into
CodeGraphContext:mainfrom
Extrodox:codex/incremental-index-parallel-parse
Draft

Extrodox wants to merge 3 commits into
CodeGraphContext:mainfrom
Extrodox:codex/incremental-index-parallel-parse

Conversation

@Extrodox

@Extrodox Extrodox commented Jun 2, 2026

Copy link
Copy Markdown

Summary

  • Reuse the changed file parse result from update_file_in_graph() instead of parsing the same file again during incremental relinking.
  • Parse remaining affected files with a bounded ThreadPoolExecutor driven by PARALLEL_WORKERS.
  • Keep graph writes and relationship relinking serialized to avoid backend lock contention.
  • Add regression coverage for the worker-count config and changed-file parse reuse.

Why

Incremental indexing was effectively serial for affected-file parsing, and the changed file could be parsed twice in one update pass. This makes large affected sets slower than necessary even when parsing work is CPU-bound and independent per file.

Validation

  • PYTHONPATH=src /tmp/cgc-pr-venv/bin/python -m pytest tests/unit/tools/test_graph_builder_perf_fixes.py::TestWatcherIncrementalHandleModification::test_incremental_parse_workers_uses_parallel_workers_config tests/unit/tools/test_graph_builder_perf_fixes.py::TestWatcherIncrementalHandleModification::test_handle_modification_reuses_changed_file_parse_result -q
  • PYTHONPATH=src /tmp/cgc-pr-venv/bin/python -m compileall -q src/codegraphcontext/core/watcher.py

Note: the default Homebrew Python in this shell is externally managed and lacked project dependencies, so validation used a temporary venv under /tmp/cgc-pr-venv.

@vercel

vercel Bot commented Jun 2, 2026

Copy link
Copy Markdown

@Extrodox is attempting to deploy a commit to the shashankss1205's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Shashankss1205

Copy link
Copy Markdown
Collaborator

Overlaps merged #1071 watcher sync and conflicts with current watcher.py.

# Conflicts:
#	src/codegraphcontext/core/watcher.py
main added _update_lock/_timers_lock after this branch was cut (CodeGraphContext#1424,
serialising concurrent watcher updates). Tests that build the handler via
__new__ must set them, as the neighbouring tests in this file already do.
@Shashankss1205

Copy link
Copy Markdown
Collaborator

Rebased onto current main and pushed to your branch — it was 64 days stale and watcher.py has had 15 commits since you branched from v0.4.13. Suite is green (1111 passed, 7 skipped). Two things I had to resolve:

  1. The conflict in _handle_modification. Kept your side — capturing the return of update_file_in_graph to reuse the parse. I checked that still works: graph_builder.py:1015/1019 returns file_data, so the reuse optimisation is valid against current main.
  2. Your new test failed with AttributeError: 'RepositoryEventHandler' object has no attribute '_update_lock'. main added _update_lock/_timers_lock in fix(watcher): handle both endpoints of a rename; serialise concurrent updates #1424 after you branched, and tests building the handler via __new__ must supply them — the neighbouring tests in that file already do. Added the same two lines.

Why I have not merged it

The parallel parsing hits an open segfault bug — #1370.

That issue reports non-deterministic Fatal Python error: Segmentation fault when multiple worker threads call tree_sitter_manager.execute_query() concurrently on different languages. Its traceback shows three threads inside execute_query() at once (TypeScript, Python, HTML).

Your ThreadPoolExecutor runs graph_builder.parse_file across affected files, and in a mixed-language repo the affected set spans languages — the exact condition. And execute_query is still unsynchronised on main:

# utils/tree_sitter_manager.py:322
def execute_query(language, query_string, node):
    query = Query(language, query_string)      # per call, no lock
    cursor = QueryCursor(query)
    res = cursor.captures(node)

tree_sitter_manager locks its language cache (_cache_lock) but nothing guards query execution.

So merging this would move a known crash from cgc index into cgc watch, where it is worse: watch runs unattended for hours, and a segfault takes the whole process down mid-session.

What would unblock it

Either is fine, and either would be a genuinely valuable PR on its own:

The rest of the PR is good — reusing the already-parsed changed file is a clear win regardless, and if you wanted to split that out as its own PR it would merge immediately. Keeping this open as a draft.

@Shashankss1205

Copy link
Copy Markdown
Collaborator

#1370 is now fixed — #1593 merged, adding a process-wide lock around tree-sitter query construction and execution.

That was the blocker on this PR. Measured cost of the lock is ~1% (3.48s vs 3.45s median over 120 parses on 8 threads), because only the query step is serialised — parser.parse and all Python-level work stay parallel, so your ThreadPoolExecutor still buys what it was meant to buy.

One caveat I want to be straight about: I could not reproduce the segfault locally, so #1593 is a guard against the reported crash rather than one I demonstrated. It is not proof your parallelism is now safe — it removes the specific unsynchronised path the report pointed at.

Your branch is already rebased onto main and green. Since it is still marked draft, I have left it for you to mark ready. Worth re-running your own workload against it first if you have the mixed-language repo that motivated the change.

@Shashankss1205

Copy link
Copy Markdown
Collaborator

This is still marked as a draft, so I've left it alone — flagging that it's now conflict-free against main if you'd like to mark it ready for review. Parallelising the incremental parse step is worth having; happy to review once you take it out of draft.

@Shashankss1205 Shashankss1205 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified and merging. This one aged well — it still applies cleanly to main four months later, and the full unit + integration suite is green on it: 1598 passed, 8 skipped (1596 on main plus your two new tests, no regressions).

Three things I checked specifically:

  1. Reusing changed_file_data is valid. update_file_in_graph returns file_data, None, or {"deleted": True, ...}, and your guard covers all three. Skipping the redundant re-parse of the file that update_file_in_graph just parsed is free — that's one whole file's parse removed from every single watcher event, before any parallelism.
  2. Thread safety of parse_file is already an established assumption. The main indexing pipeline calls it concurrently via asyncio.to_thread under a semaphore (pipeline.py), so a ThreadPoolExecutor in the watcher isn't introducing a new requirement — it's matching what indexing already does.
  3. Nondeterministic completion order is pre-existing. as_completed means subset_file_data fills in arbitrary order, but the main pipeline already collects concurrently-parsed results the same way, and link_function_calls consumes the list as a set of facts rather than a sequence.

I also like that failures are contained: parse_affected_file catches per-file, logs, and returns None, so one unparseable neighbour can't take down the whole incremental update.

One follow-up I'll file rather than block on: _incremental_parse_workers() re-implements worker-count resolution that pipeline.py already has as get_parallel_workers() — and lands on different numbers. Yours defaults to 4 and caps at 32; the pipeline's defaults to 10 and doesn't cap. So with PARALLEL_WORKERS unset, indexing uses 10 workers and the watcher uses 4, which will confuse whoever tunes that setting next. Not worth holding a good perf fix for, but the two should converge on one helper.

Thanks for the patience on the review latency — four months is longer than this should have sat.

@Shashankss1205

Copy link
Copy Markdown
Collaborator

Heads-up: this is still marked as a draft, so I can't merge it — GitHub blocks the merge regardless of approval.

I've approved it and verified it independently (details in the review above): it applies cleanly to current main, and the full unit + integration suite is green — 1598 passed, 8 skipped, which is main's 1596 plus your two new tests, with no regressions.

Mark it ready for review and I'll merge it immediately. If the draft status was deliberate and there's something you still wanted to change, tell me what and I'll wait — but from here it looks finished.

@Extrodox — flagging you directly since this has been sitting for four months and a draft PR doesn't surface in the normal review queue. That's partly on me for not spotting it sooner.

@Shashankss1205 Shashankss1205 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parallel watcher parsing integrates with #1741 locally; 85 tests pass, one skip. Resolve shared worker method/test conflict as in included supplement, but await contributor ready status and CI.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog tasks

Development

Successfully merging this pull request may close these issues.

2 participants