feat(analyze): adaptive worker concurrency under a memory budget + quieter rebuild gates + persisted needsFullRebuild marker (OOM ops feedback from a 25k-file repo)
Context
Reindexing a large Java monorepo (~25.7k .java files, single winning-winex-opt repo) after a gitnexus upgrade triggered all three full-rebuild gates at once (schema fingerprint, analysis features, runner identity — expected and correct). The subsequent full re-analyze OOM'd twice on consecutive nights on a memory-constrained host (3 GB RAM + 17 GB swap, --max-old-space-size=8192):
[557175] 12990986 ms: Scavenge 7704.5 (8229.8) -> 7702.0 (8230.8) MB ... allocation failure
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
It ran for 3.7 hours before dying — ~2x slower than the last successful full analyze of the same repo (67 min on the previous build), thrashing in GC (mu = 0.02) for the last stretch. The worker-pool symptoms documented in README ("Analysis runs out of memory", #2649) also showed up in a follow-up run:
{"level":40,"workerIndex":2,"error":"Replacement worker did not report ready within 5000ms —
likely crashed during top-of-script init ... repeated on a large repo? likely main-thread
memory pressure — see the "Analysis runs out of memory" README section, #2649",
"msg":"Worker 2 replacement failed to come online; dropping slot."}
This is a feature request born from ops reality, not a bug report — the gates themselves did the right thing. Three suggestions, in priority order.
1. Adaptive worker concurrency under a memory budget (--memory-budget or auto)
Today the worker count (and thus peak RSS) is effectively fixed, so a full analyze of a 25k-file repo needs a host-sized heap or it dies at hour 3 with nothing to show. Proposal:
- Add an opt-in memory budget (e.g.
--memory-budget-mb=4096or envGITNEXUS_MEMORY_BUDGET_MB). - When set, derive worker count from the budget (start conservative:
max(1, budget / perWorkerEstimate)), and drop worker slots instead of crashing whenworker.replacefails on memory pressure — the pool already has the "dropping slot" concept, it just arrives too late (after the replacement crashed) and the run still dies later on the main thread. - Even without an explicit flag, a cheaper win: when a replacement worker fails to come online and the reason string contains "memory pressure", halve the live worker count once and log it, instead of retrying at the same width.
Trade-off to be explicit about: fewer workers = slower parse, but a run that finishes at 1.5x wall-clock beats a run that OOMs at 3.7h and leaves the repo unindexed.
2. Collapse the three rebuild gates into one diagnostic line
When schema fingerprint + analysis features + runner identity all change in one upgrade (the common case: "I pulled a new build"), the log prints three separate forcing a full ... lines. Operators then have to figure out whether they have three problems or one. Suggestion: detect the multi-hit and print a single summary:
full rebuild required — 3 gates tripped by this build change:
- index schema changed (0061b2888f22 -> 6827b7d6b062)
- analysis capabilities changed (missing: java.heritage-captures, ...)
- analyzer runner identity changed
(all expected after upgrading the analyzer; one full re-analyze clears all three)
3. Persist a needsFullRebuild marker on failure so retries don't look mysterious
When a forced full analyze OOMs, the old (pre-gate) index is still on disk and the metadata still stamps the old fingerprint. The next cron run hits the same gates, tries the same full rebuild, and OOMs again — which is how we burned two consecutive nights before a human looked. Suggestion: on a failed forced rebuild, stamp something like meta.pendingFullRebuildReason = 'schema' | 'capabilities' | 'identity' (or reuse the existing gate evaluation, which already computes this) so that:
gitnexus statuscan say "index awaiting full rebuild since , reason: schema — last attempt failed after 3.7h" instead of looking like a fresh mystery every night;- ops tooling can detect "failed full rebuild" as a distinct state from "index missing".
Happy to turn any of these into a PR — #1 is the one we'd actually use daily (this host reindexes 23 repos nightly on 3 GB RAM).
Source: abhigyanpatwari/GitNexus