Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

v1.2.4 managed views: scoped searches and globs on a ready view are slower than ripgrep; idle verification re-reads the whole checkout every ~4 minutes

Aperta
#216 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
5/5
Tempo stimato
Più di una settimana
Idoneità per principianti
25/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
rust
Ambito
performance

Direzione di ricerca

Start with the query and verification paths in tgrep-core/src/worktrees.rs and tgrep-cli/src/serve/shared/server.rs, then trace managed query handling in tgrep-cli/src/serve/shared/managed.rs. The report identifies full-index filtering, sequential path requests and full reads during idle verification; compare those paths with the reported timings and readiness drops. Done means scoped searches and file globs are faster, idle verification avoids repeated full reads, and readiness remains stable when no changes are detected.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Summary

We run tgrep v1.2.4 managed views (serve --shared) inside the GitHub Copilot runtime. On a large Windows repository (≈293k indexed files), repository-wide selective searches were much faster than ripgrep (0.53 s against ≈13 s). But most coding-agent searches are scoped to one or a few directories. On a ready view those took seconds, where ripgrep takes tens of milliseconds. The idle daemon also re-reads the whole checkout every ≈4 minutes. Twice, the view left readiness for 2–3 minutes although we found nothing in the checkout or its Git directory, other than tgrep's own files, modified after the daemon started.

From the v1.2.4 source, we think five structural costs explain the latency:

  1. A scoped content search resolves candidates over the whole index, then applies the directory scope, glob and type filters.
  2. Shared --files returns every listed path, and the client applies --glob.
  3. The client sends one request per path argument, in sequence.
  4. The daemon has two fixed query threads, and every query reads the policy and commits a catalog write.
  5. Verification of a healthy native view is a full content read every --poll-interval, on the same rayon pool that serves queries.

Separately, readiness closes on every path hint, and Git metadata writes force a full content pass. Because every view also watches the repository's common Git directory, a ref update in any worktree forces a full pass on every view of that repository. Together these can leave a view unavailable for minutes at a time.

Environment

  • tgrep v1.2.4 (0be7a76) on Windows 11 Enterprise, 16 logical cores. The checkout is on a ReFS Dev Drive that isn't marked trusted, with ReFS last-access updates disabled. Shared storage is on NTFS.
  • One managed view with a native watcher. The checkout has ≈1.93M files in ≈168k directories, including ignored ones; the view indexes ≈293k files in a 3.15 GB generation.
  • Daemon: serve --shared <root> --shared-storage <dir> --shared-policy <file> with the default --poll-interval (120 s). Policy: work.workers=1, queue_items=256.
  • Clients: tgrep --shared --content-eligibility content … with ripgrep-style flags, one process per agent tool call. Agents often issue calls in parallel.

Observations

1. Scoped searches on a ready view

These figures come from about 75 minutes of normal agent use. The ready view served every tgrep call below.

Calls n p50 p90 max
Content search, one directory 45 0.80 s 6.43 s 14.54 s
Content search, several directories 22 3.44 s 15.58 s 20.07 s
--files --glob at the repository root 17 2.70 s 6.66 s 7.07 s
ripgrep, other agent calls in the same window 23 ≈0.14 s — 1.18 s

We re-ran the slowest calls with ripgrep and the same arguments. The ripgrep runs came later, on a warm cache and without concurrency, so they overstate the gap somewhat.

tgrep (ms) ripgrep (ms) Output lines Mode Path arguments
20,074 89–91 85 -l 4
15,698 34–39 22 -c 2
15,584 41–43 135 -c 2
14,542 37 38 -c 1
11,712 115–144 9 -l 7
8,456 53–59 428 -c 12
7,498 42–44 84 -c 4
6,913 44–59 95 -c 2
  • The first four rows were one parallel burst, while ripgrep calls running at the same time took 0.1–0.3 s. Together the four calls carried 9 path arguments. Each call sends its paths one at a time and the daemon has 2 query threads, so the ≈20 s burst is consistent with 9 requests of ≈4.4 s each. We didn't trace individual requests.
  • An 8-call burst took 5.1–7.5 s per call. Similar calls at quieter times took 0.4–3.7 s.
  • The slow patterns were regex alternations of 3–6 identifier-like branches with \s*, .* or anchors. Results were small (9–428 files), so this isn't the broad-pattern -l/-c cost from #210.
2. Where the index wins
  • A selective literal at the repository root took 0.53 s with tgrep and ≈13 s with ripgrep. A cold ripgrep run at the repository root hit our 20 s tool timeout.
  • A broad pattern at the root took 48.2 s with tgrep and 37.2 s with ripgrep.
3. Globs

Agent glob calls run --files --hidden --glob <pattern> --glob '!**/.git' at the repository root. tgrep took 2.0–7.1 s, and a full ripgrep walk took 0.95–1.9 s. Each call returned 0–284 files.

4. Idle verification

We measured this with passive OS process counters and didn't query the daemon.

  • A pass starts ≈120 s after the previous one completes and lasts ≈110 s:
    • ≈5–10 s of metadata work at ≈3.4 cores, with no reads;
    • ≈80 s of reads;
    • ≈20 s at ≈2.5 cores, with no reads.
  • One pass made 6.45 GiB of logical reads in ≈657k read operations. It used ≈3.7 cores above baseline on average, with a peak of ≈5.2 cores. Between passes the daemon used ≈0.2 cores.
  • Extrapolated, with no file changes: a ≈230 s cycle (≈48% duty), ≈100 GiB/hour of logical reads and ≈1.8 cores on average, per repository. We didn't measure physical reads.
5. Readiness drops with no detected changes
  • The view reported ready: false twice, for 2m14s and ≈2m54s. The daemon wrote nothing to stderr. Our client falls back to ripgrep while the view isn't ready.
  • The first drop came during normal agent use, which was searches only. The second started after the agent session had gone idle, while we re-ran its searches with ripgrep (reads only).
  • A stat-only walk of the whole checkout, ignored files included, found no file or directory modified or created after the daemon started, 78 minutes before the first drop. The walk compared modification and creation times only, so it can't rule out attribute or ACL changes, and the volume has no active change journal to check them.
  • Under .git, nothing changed after the daemon started except tgrep's own tgrep-daemon-*.json and tgrep-view-*.json files, written at startup. There were no ref, packed-refs, HEAD or index writes.
  • Each drop ended ≈5 s after a new generation, the same size as the previous one, appeared in shared storage: one for the first drop, two (87 s apart) for the second. We read this as one or two reconcile passes that found nothing to change. We couldn't tell what started them; see the questions below.

Code pointers (v1.2.4)

Query path
Verification
Readiness
  • worktrees.rs L495–L509: ready = control.ready && !stale.
  • Every reconcile closes readiness first (L842), and so does every path hint (invalidate_path, L560–L587). A reconcile with no hints is full (L843).
  • Any verification error except ChangedDuringReconcile or a budget failure marks the view stale (L950–L956). So do detected differences (L1249–L1255, L1546–L1548, L1592–L1599).
  • watch_event forces a full invalidation for need_rescan, Any and Other events and events with no paths (managed.rs L822–L824), for Git metadata writes (L829–L855: HEAD, index, config, packed-refs, refs, info, …) and for watcher errors (L888–L897). Watcher errors are written to stderr, and we saw none. Non-ASCII paths, ignore files and hint overflow also force a full pass (worktrees.rs L574–L582).
  • The metadata set is the view's Git directory plus the common directory (managed.rs L709). In recursive mode the root and every metadata directory not nested in another watched directory get a recursive subscription (L711–L737, L993), and the test at L2823–L2832 expects a linked view to watch the whole common directory. So for every view of a repository, a write under <common>/refs or to <common>/packed-refs forces a full pass. A linked view also reacts to the main worktree's <common>/HEAD and <common>/index, which match the same names relative to the common directory.
  • Other notifications under the root become path hints unless they are a Modify of a file whose metadata matches the indexed evidence (L862–L875, worktrees.rs L521–L537). Directory notifications and notifications for files without evidence, such as ignored files, therefore close readiness.
  • Managed views use stale_reads: true (managed.rs L2185–L2187).
  • watch_status already reports watch_events and last_verification (managed.rs L1010–L1041, L1668–L1673).

Impact

  • Coding agents mostly search a few directories at a time, often in parallel. On this repository ripgrep answers those searches in tens of milliseconds and tgrep in seconds. We're therefore considering routing directory-scoped searches and globs to ripgrep and using tgrep only for repository-root content searches.
  • Idle cost is ≈1.8 cores and ≈100 GiB/hour of logical reads per repository (extrapolated), multiplied by the number of repositories with a daemon.
  • Readiness: from the code (not measured separately), any write to Git metadata forces a full content pass. That includes a git add, commit or checkout, or a git status that refreshes the index. On this repository each one means ≈2 minutes of fallback and another ≈6.45 GiB of reads, and agents run Git often. The common directory widens this: a commit or branch update in any worktree, git fetch (refs/remotes) or background maintenance prefetch (refs/prefetch) forces a full pass on every view of the repository, and linked views also react to staging and checkouts in the main worktree. Our agent sessions often run in separate linked worktrees of one repository, served by one daemon, so one commit can queue a full pass for every session's view.

Suggestions

  1. Make candidate resolution scope-aware. Restrict it to the requested prefix before resolving paths, for example with a path-id range per directory, and push glob and type filters in where possible. For --files, apply --glob and type filters on the server.
  2. Accept several path arguments in one request, with one candidate resolution and one snapshot.
  3. Reduce per-query overhead:
    • make the number of query threads configurable, or derive it from the core count;
    • read the policy once rather than per query;
    • batch or defer touch_object instead of committing a transaction per query.
  4. Make verification cheaper:
    • run it on a dedicated, bounded pool or at lower priority so it doesn't compete with queries;
    • trust unchanged versions (the L1489 shortcut) and re-read only a rotating sample, or verify incrementally;
    • give it its own longer or adaptive interval instead of --poll-interval, or allow turning it off while the native watcher is healthy.
  5. Keep the view usable during reconciles:
    • for hinted reconciles, process only the hinted paths and their parents instead of walking and opening every listed file;
    • for Git metadata changes, recompute visibility with a metadata walk and keep the unchanged-version shortcut;
    • report "ready except N pending paths" so clients can search those paths directly instead of losing the whole index.
  6. Narrow what starts a pass:
    • scope Git metadata handling per view: react to the view's own HEAD, index and checked-out branch, and ignore other branches, refs/remotes, refs/prefetch and, for linked views, the main worktree's HEAD and index;
    • in recursive mode, drop directory Modify notifications, or check them against the directory's recorded attributes first, since entry changes arrive as their own events;
    • detect ReadDirectoryChangesW overflow and rescan only then. notify 7 ignores each completion's byte count (windows.rs L312–L405), so the zero-byte completion that signals an overflow is dropped (notify-rs/notify#963, fixed by notify-rs/notify#964, first released in 9.0.0-rc.5; no stable release has it yet). If periodic verification exists to catch missed events, overflow detection could let it run much less often.
  7. Improve observability:
    • record why readiness closed (trigger, error kind, first differing path), with a short history in watch_status;
    • log full passes, verification differences and requeued candidate reads to stderr.

Questions

  • What does a full content read on every verification pass protect against when size and modification time are unchanged? If it's there to catch events the watcher missed, would overflow detection be enough?
  • Can a transient read error on an unchanged tree mark the view stale and force a full pass? An example is a sharing violation while antivirus or the search indexer holds a file open.
  • Is closing ready for a single requeued candidate intended, given that the view keeps serving its publication?
  • What could close readiness when we found nothing modified in the checkout (Observation 5)? From the code we see three candidates: an unfiltered notification (a directory, a file without evidence, or an attribute or ACL change), a requeued candidate read, or a verification difference or error. If it helps, we can sample watch_events and last_verification from view status around the next drop.

Related

  • #210 covers cold readiness. Its §4 covers -l/-c costs for broad patterns and the response cap.
  • #212 covers reconciliation and periodic verification starving when the only work slot is reserved under storage pressure. This issue is about the cost of verification once it runs.
  • #214 covers catalog WAL growth. The per-query touch_object commits are also catalog writes.

We're happy to test a build or collect more data.

Lingua principale
Rust
Stelle
3.5k
Fork
140
Merge medio
14h 59m
PR unite (30g)
28

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di microsoft/tgrep

Tutte le issue di microsoft/tgrep

Issue simili

Altre issue su Rust

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.