perf(sync): adaptive quick-fire debounce + scoped watcher sync — save-to-graph well under a second at any scale (#1397)
Two changes to the watcher path (the always-on daemon every agent session uses), which previously paid a flat 2s debounce plus a full-tree scan-diff on every save even though the OS events name the exact files: 1. Adaptive debounce: a pending set of ≤2 files fires after a 300ms quiet window; ≥3 keeps the full configured window so agent multi-file bursts coalesce exactly as before. Re-arming preserves trailing-edge semantics; a user-set CODEGRAPH_WATCH_DEBOUNCE_MS remains the authoritative upper bound (quick window never exceeds it, floor 100ms). 2. Scoped sync: watcher-triggered syncs pass their pending paths, and the reconciler stats exactly those — per-path logic identical to the full walk (stat pre-filter, hash confirm, the #1240 removal/resurrection flow) — skipping the O(repo) scan and tracked-load. Strict fallbacks keep the full scan-diff as ground truth: directory removals (#1285 — the events can't name the children), empty pending sets (retry paths), and >500-file storms (branch checkouts, which also self-heal anything event coalescing dropped). filesChecked counts examined PATHS so a deletion-only scoped sync can't mimic the #449 lock-unavailable signature. Measured (warm in-process, the daemon path): dubbo one-file sync work 512→335ms, Swift compiler (27k files) 884→385ms — save-to-fresh-graph ≈0.6-0.7s end-to-end including the quick debounce, from ~2.5-6s perceived before. Gates: scoped-vs-full dumps byte-identical on dubbo AND the Swift compiler; watcher suite 30/30 (3 new: scoped pass-through, dir-removal fallback, quick-fire timing); sync suite 34/34 (4 new scoped-parity cases incl. delete-resurrection and the lock signature); full suite 2,696 ×2 with CODEGRAPH_KERNEL_EXPECT=1. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
157c8e735d
commit
c74e8b05e0
+5
-3
@@ -127,6 +127,8 @@ export interface IndexOptions {
|
||||
|
||||
/** Enable verbose logging (worker lifecycle, memory, timeouts) */
|
||||
verbose?: boolean;
|
||||
/** Watcher fast path: reconcile ONLY these project-relative paths (see ExtractionOrchestrator.sync). */
|
||||
paths?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -778,7 +780,7 @@ export class CodeGraph {
|
||||
try { return this.queries.isNameSegmentVocabEmpty(); } catch { return false; }
|
||||
})();
|
||||
|
||||
const result = await this.orchestrator.sync(options.onProgress);
|
||||
const result = await this.orchestrator.sync(options.onProgress, options.paths);
|
||||
|
||||
// Fold the store phase's WAL BEFORE the post-store reads below
|
||||
// (resolution reads on the main thread) — same rationale as
|
||||
@@ -988,8 +990,8 @@ export class CodeGraph {
|
||||
|
||||
this.watcher = new FileWatcher(
|
||||
this.projectRoot,
|
||||
async () => {
|
||||
const result = await this.sync();
|
||||
async (paths?: string[]) => {
|
||||
const result = await this.sync({ paths });
|
||||
// sync() returns this exact zero-shape iff it failed to acquire the
|
||||
// file lock (a real empty sync always has filesChecked > 0 because
|
||||
// scanDirectory ran). Surface that to the watcher as a typed error
|
||||
|
||||
Reference in New Issue
Block a user