Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

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

クローズ
#216 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
25/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
活発
技術スタック
rust
領域
performance

調査の方向性

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.

索引モデルが issue の本文から書いたものです。

説明

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.

主要言語
Rust
スター
3.5k
フォーク
139
平均マージ
16時間 31分
マージ済み PR(30日)
27

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

microsoft/tgrep のほかの issue

microsoft/tgrep の issue をすべて見る

似ている issue

Rust の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。