Skip to content

perf(core): ShadowSnapshot burns minutes of CPU when cwd contains a large ignored tree (target/, node_modules/) #146

Description

@BunsDev

Summary

ShadowSnapshot (crates/core/src/snapshot/shadow.rs) can stall a session for minutes and burn tens of CPU-minutes when the working directory contains a large ignored tree (e.g. a Rust target/ dir). Observed: coven-code --print "..." run from src-rust/ in this repo produced no output for 4+ minutes while consuming ~39 CPU-minutes across the tokio worker pool; a sample trace put all the time under ShadowSnapshot in String/slice equality and tokio::fs::metadata.

Root cause (two compounding defects)

1. The repo-root .gitignore is invisible when cwd is a subdirectory.
The shadow repo's --work-tree is the session cwd. When that cwd is a subdirectory of the real repository (here src-rust/, while the src-rust/target/ rule lives in the repo root .gitignore), ls-files --others --exclude-standard in stage() never sees the rule — gitignore chains above the worktree root are not consulted, and sync_excludes() only copies the user-level exclude file. Result: stage() enumerates the entire target/ tree (hundreds of thousands of files).

2. Quadratic ignore intersection.
check_ignore_user() then pipes that list to git check-ignore --stdin -z (which correctly flags all of it against the real repo) but intersects the result with:

let raw: Vec<&str> = r.text.split('\0').filter(|s| !s.is_empty()).collect();
files.iter().copied().filter(|f| raw.contains(f)).collect()

raw.contains(f) is a linear scan per file — O(n*m) string comparisons. With n ~ m ~ 10^5+, that is ~10^10 compares per snapshot, which matches the hot frames in the sample (slice_contains, String::eq, Vec::partial_eq).

Impact

  • Any session whose cwd has a large ignored tree (Rust target/, node_modules/, build output) pays this on every turn: first token latency of minutes and sustained multi-core spin.
  • Correctness is unaffected (ignored files do not leak into patches) — this is purely a performance defect.

Suggested fixes

  1. One-liner for the quadratic term: collect raw into a HashSet<&str> before the intersection in check_ignore_user().
  2. Stop enumerating ignored trees at all: make the shadow repo honor the ignore rules the real repository would apply to the worktree — e.g. seed the shadow's info/exclude from the real repo's effective ignore decisions for the worktree (or run the untracked-file enumeration against the real repo instead of the shadow when the cwd is inside one).

Fix 1 removes the CPU blowup; fix 2 removes the pointless tokio::fs::metadata crawl of ~10^5 files. Both are worth doing.

Repro

cd <coven-code checkout>/src-rust   # large target/ present, ignored only by the parent .gitignore
env -u ANTHROPIC_API_KEY ./target/debug/coven-code --print "Reply with exactly one word: pong"
# stalls for minutes before the first token; sample(1) shows ShadowSnapshot hot

Found while smoke-testing fb989ab (claude CLI runtime); unrelated to that change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcrate:coreCore crate (src-rust/crates/core)

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions