Skip to content

perf(grep): speed up the native engine and add a search timeout#4113

Merged
esengine merged 1 commit into
main-v2from
grep-perf-timeout
Jun 12, 2026
Merged

perf(grep): speed up the native engine and add a search timeout#4113
esengine merged 1 commit into
main-v2from
grep-perf-timeout

Conversation

@esengine

Copy link
Copy Markdown
Owner

Why

Users on machines without ripgrep reported grep being extremely slow — a single search occasionally running for minutes without returning. With rg present the tool delegates to it, but the native Go fallback was both slow and unbounded.

Profiling a native full walk (6k-file tree, no match, so no early cap exit) attributed the time to two avoidable costs:

Hotspot Share Cause
.gitignore compile (enterCompileIgnoreLines) ~36% the full cumulative pattern set was recompiled into regexes at every directory, re-compiling inherited ancestor rules over and over
per-file searchFile ~47% ~72 KiB allocated per file (8 KiB peek + 64 KiB scanner buffer), churning the GC; walk is fully serial

What changed

  • Memoize the compiled ignore matcher by its pattern set, so identical/inherited rule sets compile once.
  • Reuse the per-file peek and scanner buffers across the serial walk. The decoder path is handed a copy of the peek bytes so its goroutine can't alias the reused buffer after an early cap-reached return.
  • Add a timeout_seconds parameter (default 30s, max 300s). A pathological search now returns the matches found so far with a clear note pointing at timeout_seconds, instead of hanging. Both the native and ripgrep paths honor it through the request context.

Numbers (warm best, synthetic 300-dir / 6k-file tree)

Scenario Before After
ignores engaged 1.31s 0.62s (−52%)
pure file scan 0.61s 0.30s (−52%)

End-to-end: a 20k-file cold tree with timeout_seconds: 1 now aborts at ~1.0s with a partial-results note instead of running 8s+.

Tests

  • TestGrepTimeoutClamp — default / negative / in-range / over-cap clamping.
  • TestGrepTimeoutPreservesPartialResults — a timed-out search keeps the matches found so far and flags the cutoff; a zero-match timeout reports the timeout rather than (no matches); a completed zero-match search still returns (no matches).

Full internal/tool/builtin suite, go vet, and gofmt are clean. Directory-level pruning of ignored/vendor trees is unchanged.

Follow-ups (not in this PR)

  • Parallelizing the native file scan across a worker pool (the remaining cost is serial syscalls; ripgrep wins by going wide).
  • Bundling/auto-resolving rg so the fast path is always taken.

The native search engine (the fallback when ripgrep is absent) walked
serially and paid two avoidable costs on every run, which a profile of a
6k-file no-match walk pinned down:

- it recompiled the full cumulative .gitignore pattern set into regexes at
  every directory, so inherited ancestor rules were compiled over and over
  (~36% of the walk);
- it allocated ~72 KiB per file (8 KiB peek + 64 KiB scanner buffer),
  churning the GC across the whole tree.

Memoizing the compiled matcher by its pattern set and reusing the per-file
buffers across the serial walk roughly halves the warm full-walk time
(6k-file tree: 1.31s -> 0.62s with ignores, 0.61s -> 0.30s without). The
decoder path is handed a copy of the peek bytes so its goroutine can't
alias the reused buffer after an early return.

Also add a model-supplied timeout_seconds (default 30s, max 300s): a
pathological search now returns the matches found so far with a clear note
pointing at timeout_seconds, instead of hanging for minutes. Both the
native and ripgrep paths honor it through the request context.
@esengine esengine requested a review from SivanCola as a code owner June 12, 2026 03:23
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development skills Skill system (internal/skill, internal/tool) labels Jun 12, 2026
@esengine esengine merged commit b1303b0 into main-v2 Jun 12, 2026
14 checks passed
@esengine esengine deleted the grep-perf-timeout branch June 12, 2026 03:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skills Skill system (internal/skill, internal/tool) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant