Skip to content

Cache existing hidden directories in codeSearch to avoid redundant stat calls - #1221

Open
nordicnode wants to merge 2 commits into
CodebuffAI:mainfrom
nordicnode:perf-code-search-dirents
Open

Cache existing hidden directories in codeSearch to avoid redundant stat calls#1221
nordicnode wants to merge 2 commits into
CodebuffAI:mainfrom
nordicnode:perf-code-search-dirents

Conversation

@nordicnode

@nordicnode nordicnode commented Sep 2, 2026

Copy link
Copy Markdown

Summary

  • In sdk/src/tools/code-search.ts, de-duplicate repeated fs.statSync inspections for blessed hidden directories (.agents, .claude, .github, .gitlab, .circleci, .husky) using a bounded in-memory cache with a 1-second burst TTL.
  • 1-second burst TTL: Scoped specifically to rapid bursts of queries (e.g. concurrent tool calls or tight sequential searches within the same agent turn). A short 1s window guarantees that any cross-turn actions (such as scaffolding .github/ or .husky/ during project setup) naturally see fresh filesystem state.
  • O(1) LRU eviction: Bounded to MAX_CACHE_SIZE = 100 directories. On cache hit or insertion, entry recency is refreshed in Map; when full, the least-recently-used entry is evicted rather than clearing the whole cache.
  • Targeted invalidation: clearHiddenDirsCache(dir?) supports invalidating a specific directory or clearing all entries.
  • Realistic framing: Acknowledges that fs.statSync on 6 paths in OS cache is sub-millisecond and ripgrep process execution dominates overall search latency. This change acts as a lightweight burst de-duplication to eliminate redundant disk checks and ENOENT exceptions during rapid searches within the same directory root.
  • Comprehensive test coverage: Replaced static comparisons with dynamic filesystem tests in sdk/src/__tests__/code-search.test.ts verifying:
    • Cache shielding within TTL window.
    • Immediate discovery when a directory is created and clearHiddenDirsCache is called.
    • Immediate discovery when a directory is removed and clearHiddenDirsCache is called.
    • Automatic directory discovery upon 1-second TTL expiration without manual cache clearing.
    • Targeted cache clearing on specific directories.
    • Proper LRU eviction of oldest entries when cache capacity is exceeded.

Test plan

  • bun test src/__tests__/code-search.test.ts (37 passed, 0 failed)
  • bun run --cwd sdk test (542 passed, 0 failed across 41 files)
  • bun run --cwd sdk typecheck (0 errors)
  • bun run build:sdk (successful build)
  • bun freebuff/cli/build.ts 0.0.0-ci (successful binary build)
  • Binary smoke test: bun cli/scripts/smoke-binary.ts cli/bin/freebuff (OK)
  • Prettier check: bun x prettier --check sdk/src/tools/code-search.ts sdk/src/__tests__/code-search.test.ts (All matched files use Prettier code style)

@codebuff-team

Copy link
Copy Markdown
Contributor

The instinct here—avoid repeated fs.statSync calls—is reasonable in spirit, but the execution has real problems:

  1. Staleness bug: with a 30s TTL, if an agent creates .github/ or .husky/ mid-session (e.g., scaffolding a project) and then searches within that window, getExistingHiddenDirs will return the stale (pre-creation) list and skip that directory in ripgrep's search paths. This is a real behavioral regression, not just a performance tweak — agentic workflows that create files and immediately search are exactly the pattern codeSearch needs to support.

  2. Weak test coverage: sdk/src/__tests__/code-search.test.ts's new it('clears cache when clearHiddenDirsCache is called', ...) test asserts first equals second after clearing the cache — but since the underlying filesystem state hasn't changed between calls, this doesn't actually verify that clearing forces a fresh stat, only that the result is coincidentally the same. There's no test demonstrating the cache picks up newly-created or newly-removed hidden directories, which is the actual risk this PR introduces.

  3. Cache eviction is crude: if (hiddenDirsCache.size >= 100) hiddenDirsCache.clear() wipes the entire cache rather than evicting oldest entries — fine for a rare case, but worth noting it's not a real LRU.

  4. Benchmark framing is overstated: fs.statSync on 6 well-known relative paths is already cheap (sub-millisecond, OS-cached); the "3.84x faster" / "99% syscall reduction" framing implies a bottleneck that likely isn't present in real workloads, and doesn't address whether this was ever measured as an actual performance problem in codebuff/freebuff usage.

If you want to pursue this, I'd suggest either dropping the TTL (cache for the lifetime of a single process invocation only, invalidate on directory creation events) or making the TTL much shorter, and adding a test that actually creates/removes a directory and confirms cache behavior around that change.

@codebuff-team codebuff-team added bot:triaged Classified by the community triage bot pr:needs-work Right idea, not mergeable as written labels Sep 3, 2026
@nordicnode

Copy link
Copy Markdown
Author

Thanks for the thorough and constructive review @codebuff-team! All four points have been addressed in the latest commit:

  1. Staleness Bug: Reduced the cache TTL from 30 seconds down to 1 second (1_000 ms). This acts strictly as a short burst de-duplication window for concurrent tool calls or tight sequential searches within the same agent turn, guaranteeing that any subsequent agent turns or project-scaffolding workflows (>1s) see fresh filesystem state.
  2. Weak Test Coverage: Replaced the previous static comparison tests with comprehensive dynamic filesystem tests in sdk/src/__tests__/code-search.test.ts using isolated temporary directories (fs.mkdtempSync). The tests explicitly verify:
    • Dynamic creation is shielded within the 1-second TTL window.
    • Dynamic creation is immediately detected once clearHiddenDirsCache() is called.
    • Dynamic directory removal is immediately detected once clearHiddenDirsCache() is called.
    • Dynamic creation is automatically discovered upon 1-second TTL expiration without manual cache clearing.
    • Targeted directory invalidation (clearHiddenDirsCache(dir)).
    • Least-recently-used eviction when the cache capacity is exceeded.
  3. Cache Eviction: Replaced the blanket cache.clear() with a proper O(1) LRU eviction policy using Map recency re-insertion, bounded to 100 entries. When the cache limit is reached, only the oldest entry is evicted while preserving recently accessed entries.
  4. Benchmark Framing: Updated the PR summary to remove the overstated benchmark claims. The change is now accurately framed as a minor burst de-duplication to eliminate redundant fs.statSync syscalls and caught ENOENT exceptions during rapid multi-search queries in the same directory.

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

Labels

bot:triaged Classified by the community triage bot pr:needs-work Right idea, not mergeable as written

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants