intuit / intuit/infigraph

group build: rebuilding LanguageRegistry per-repo in cross_service.rs dominates remote-Neo4j reindex time (~60% of link step)

Open
#34 0 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
Rust
Stars
88
Forks
16
PR merge metrics
No merged PRs in 30d

Description

## Summary

Reproduced locally: `infigraph group build --full` against 10 microservice fixture repos (25 files total) with a real Neo4j 5 + Postgres/pgvector backend (via podman, both on localhost — so this understates the real-world cost, since localhost hides network latency entirely).

**Total: ~38-40s.** Step-by-step breakdown of the 5 steps in `GroupAction::Build` (`crates/infigraph-cli/src/group_commands.rs`):

| Step | Time | Share |
|---|---|---|
| 1. Index (parallel, 10 repos) | ~4s | 10% |
| 2. Sync contracts | ~9s | 23% |
| **3. Link cross-service calls** | **~25s** | **65%** |
| 4. Combined graph (skipped — remote Neo4j already namespaced) | ~0s | — |
| 5. Docs + embeddings | ~0.2s | <1% |

## Root cause (measured, not inferred — see methodology below)

Step 3 (`link_cross_service_calls`, `crates/infigraph-core/src/multi/cross_service.rs`) calls `detect_cross_service_deps` first, which contains a loop over every repo in the group (`for repo_name in &group.repos`), and `link_cross_service_calls` itself has a second loop over every distinct caller service. **Both loops call `build_registry()` (in practice, `infigraph_languages::bundled_registry()`) once per iteration** — once per repo/caller-service, not once for the whole `group build` run.

`bundled_registry()` compiles and registers all 62 tree-sitter language packs (parsing each language's entity/relation query files against its grammar) on every call — genuinely CPU-bound work, independent of I/O or network. With instrumentation added around the three sub-phases (`build_registry()`, `Infigraph::open()`, `.init()`) and rerun against the same podman repro:

```
[perf] detect_cross_service_deps first-loop: build_registry=8517ms, open=0ms, init=160ms, query=111ms
[perf] per-caller: build_registry=6896ms, open=0ms, init=147ms, edge-write=252ms, wall=7.3s
```

**`build_registry()` alone accounts for ~15.4s of Step 3's ~25s (~60%).** By contrast:
- `Infigraph::open()`: negligible (0ms measured).
- `.init()` (Neo4j connect + `init_schema`'s 8 `CREATE CONSTRAINT/INDEX IF NOT EXISTS` statements): 147-160ms total across 10 calls — cheap, because the Bolt connection is cached process-wide via `NEO4J_CONN: OnceLock` (`crates/infigraph-core/src/graph/neo4j_backend.rs`) and Neo4j's `IF NOT EXISTS` no-ops are fast.
- Actual Neo4j data queries (`MATCH`/`MERGE`/`CREATE` for the cross-service edges themselves): 111-252ms total — also cheap.

**A third, even more severe instance of the same pattern exists** at the "shared-contract detection" loop further down in the same file (`for repo_name in &group.repos { ... for (file, symbol_hint, spec_path) in spec_hits { let lang_registry = build_registry()?; ... } }`) — this one rebuilds the registry once **per spec-fetch match found**, not even once per repo. It didn't show up prominently in this measurement only because the fixture repos have no `/openapi.json`/`/swagger.json` fetches to match; at real scale (repos that do fetch OpenAPI specs) this would compound further.

## Why this reads as a "remote Neo4j" problem specifically

On local embedded Kuzu, each repo's `Infigraph::open()` genuinely needs its own DB handle (each repo is its own on-disk Kuzu database) — so per-repo overhead is expected and was already the design. The registry-rebuild cost is *masked* there because opening 10 separate small embedded Kuzu DBs is itself not free, so the extra language-registry compile time is proportionally less noticeable. On remote Neo4j, all repos share one namespaced graph — there is no structural reason to reopen a connection or, especially, rebuild the entire language registry, per repo. The registry cost is backend-agnostic CPU work that was always being paid redundantly; remote mode just has less of everything else to hide it behind.

## Fix path investigated, correctness constraint found

The natural fix — hoist `build_registry()` out of the loops so it runs once per `group build` invocation — is blocked by a type constraint, not a logic issue: **`LanguageRegistry` does not implement `Clone`**, because it stores `ParserBackend::Custom(Box)` (used by the ANTLR-based grammar-plugin system for FormCalc/ConverterCalc/Interview-style DSLs), and `Box` isn't `Clone`-able without the trait itself supporting object-safe cloning (e.g. a `dyn_clone`-style pattern).

`Infigraph::open()` takes `LanguageRegistry` by value (moved into the struct), and has ~19 call sites across the workspace (`crates/infigraph-cli/*`, `crates/infigraph-mcp/*`, `crates/infigraph-core/*`), so changing its signature to accept a reference or `Arc` is a real, if mostly mechanical, cross-cutting change — not something to do speculatively inside a narrower perf fix.

## Proposed fix (scoped, not yet implemented)

1. Change `Infigraph::open()` to accept `Arc` instead of an owned `LanguageRegistry` (small, contained signature change — no `Clone`/`CustomExtractor` changes needed, since `Arc` doesn't require the inner type to be `Clone`).
2. Update the ~19 call sites to wrap their existing registry construction in `Arc::new(...)` — mechanical, since none of them currently need to mutate the registry after construction.
3. In `cross_service.rs`, build the registry once (wrapped in `Arc`) at the top of `detect_cross_service_deps` and `link_cross_service_calls`, and clone the `Arc` (cheap refcount bump, not a deep clone) for each loop iteration's `Infigraph::open()` call.
4. Apply the same fix to the third loop (shared-contract/spec-fetch detection) further down in `cross_service.rs`.
5. Regression test: time `link_cross_service_calls` against a synthetic group with N>1 repos before/after, assert the registry-build cost doesn't scale linearly with repo count post-fix (e.g. via a counter/mock on `build_registry` in a test harness, or a coarse wall-clock assertion).

## Methodology note (for whoever picks this up)

The first pass at this investigation incorrectly attributed the cost to network round-trip count (assumed the per-edge `MERGE`/`MATCH`/`CREATE` loop and `init_schema`'s 8 DDL statements were the bottleneck). That was wrong — measured edge-write time was only 250ms total, and `init()` including `init_schema` was 147-160ms total. Always add real timing instrumentation around each candidate sub-phase and rerun against a live repro rather than attributing cost from reading code alone; the actual bottleneck (CPU-bound registry compilation, not network I/O) was the opposite of the initial hypothesis.

## Repro setup (for reference)

- `podman machine start`, then Neo4j 5 (`NEO4J_AUTH=neo4j/testpass`) and `pgvector/pgvector:pg16` (with an `infigraph`/`infigraph` user+db created to match `PostgresMetaStore::connect_from_env`'s default `DATABASE_URL`).
- Built with `cargo build --release -p infigraph-cli --features remote`.
- `INFIGRAPH_BACKEND=neo4j`, `NEO4J_URI=bolt://localhost:7687`, `NEO4J_USER=neo4j`, `NEO4J_PASSWORD=testpass`, `DATABASE_URL="host=localhost user=infigraph password=infigraph dbname=infigraph"`.
- `infigraph group create perf-test`, `infigraph group add perf-test tests/fixtures/microservices/` for the 10 non-elixir fixture services, then `infigraph group build perf-test --full`.

Contributor guide

Open the contributing guide

Research direction

Start in crates/infigraph-core/src/multi/cross_service.rs and inspect Infigraph::open plus its workspace call sites. Reproduce the reported group build with the release remote feature and timing setup, then verify that registry construction is shared across the loops and that the measured cost no longer scales linearly with repository count.

Written by the indexing model from the issue text.

Assessment

Tech stack
neo4j, rust
Domain
backend, databases, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.