diff --git a/Cargo.toml b/Cargo.toml index ab89f5638a..87f99bd1a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,6 +102,12 @@ syntax-highlighting = ["dep:tree-sitter", "dep:tree-sitter-bash", "dep:tree-sitt # When enabled, run with NEXTEST_NO_INPUT_HANDLER=1 to avoid suspension. # See CLAUDE.md "Nextest Terminal Suspension" section for details. shell-integration-tests = [] +# Enable benchmarks whose fixture is too large for hosted CI: the rust-scale +# prune fixture (~15 GiB of rust-lang/rust worktrees, cached in +# target/bench-repos/). The daily benchmarks workflow runs plain `cargo bench` +# and must never build it; opt in locally with +# `cargo bench --bench prune --features real-repo-benches`. +real-repo-benches = [] # Install git-wt binary so `git wt` works as a git subcommand git-wt = [] @@ -341,6 +347,10 @@ harness = false name = "remove" harness = false +[[bench]] +name = "prune" +harness = false + [[bench]] name = "picker_preview" harness = false diff --git a/benches/CLAUDE.md b/benches/CLAUDE.md index 94aaabf84f..3b1bc7d763 100644 --- a/benches/CLAUDE.md +++ b/benches/CLAUDE.md @@ -23,6 +23,10 @@ cargo bench --bench list cargo bench --bench time_to_first_output # all commands cargo bench --bench time_to_first_output remove # just remove +# wt step prune (scan + removal on the squash-merged fixture) +cargo bench --bench prune # synthetic variants +cargo bench --bench prune --features real-repo-benches prune_real_repo # rust scale (~15 GiB cached fixture) + # Picker preview pre-compute (wt switch preview workload) cargo bench --bench picker_preview # all variants cargo bench --bench picker_preview warm # warm only @@ -57,6 +61,12 @@ startup latency without output rendering or post-output work (mismatch warnings, Supported commands: `switch`, `remove`, `list`. +`wt step prune` deliberately has no `WORKTRUNK_FIRST_OUTPUT` hook: its first +output is data-dependent (the dry-run path collects and sorts every check +result before printing anything, so e2e ≈ time-to-first-output already; the +live path streams whichever check lands first). Use `benches/prune.rs` for +cadence-tracked numbers and the `prune-*` spans (below) for phase attribution. + ## WORKTRUNK_PREVIEW_BENCH Setting `WORKTRUNK_PREVIEW_BENCH=1` runs `wt switch`'s interactive picker prelude @@ -133,6 +143,7 @@ untracked files. |---------|------------|-------| | `wt list` | Yes | Post-skeleton tasks. Exits early under `WORKTRUNK_SKELETON_ONLY=1` / `WORKTRUNK_FIRST_OUTPUT=1` — those skip the writing phase. | | `wt remove` | Yes | `prepare_worktree_removal` → `compute_integration_lazy` writes `is-ancestor` / `has-added-changes` / `merge-add-probe` whenever `BranchDeletionMode` is not `ForceDelete` (CLI `--force` is `force_worktree`, not `--force-delete`). | +| `wt step prune` | Yes | Every scanned worktree/branch runs `integration_reason` → the same probe writes as `wt remove`. First scan after new commits is cold; re-runs are warm (`prune_e2e/dry_run_cold` vs `dry_run_warm`). | | `wt switch ` | No | No sha_cache writers on the direct-switch path. | | `wt switch` (picker) | Yes | Preview pre-compute writes `picker-preview/{log,branch-diff,upstream-diff}-…` entries. Exercised under `WORKTRUNK_PREVIEW_BENCH=1` / `WORKTRUNK_PICKER_DRY_RUN=1`. | | `wt` (completion via `COMPLETE=$SHELL`) | No | Only `for-each-ref` + worktree list. | @@ -160,10 +171,116 @@ worth measuring at CI cadence. - Cold cache penalty: ~4x slower for single worktree - Scaling: Warm cache shows superlinear degradation, cold cache scales better +## Recording `wt remove` / `wt step prune` staging + +The removal commands interleave serial per-target work with parallel scans and +detached background processes; a single e2e number hides which phase moved. +Record them in two layers: + +**Criterion cadence** — `benches/remove.rs` and `benches/prune.rs`. Expected +numbers on an M-series Mac (`prune-4-8` fixture: 4 squash-merged worktrees + +4 squash-merged branches as candidates, 8 two-sided-diverged worktrees + 8 +diverged branches as backdrop, 200 commits, 100 files; `prune_real_repo` runs +a warm dry-run on the `prune-real` fixture — a rust-lang/rust clone with 12 +squash-merged candidate pairs + 24 diverged worktrees and 24 diverged +branches, i.e. 36 linked worktrees, cached and self-repairing in +`target/bench-repos/rust-prune-12-24/`. That group is opt-in — +`cargo bench --bench prune --features real-repo-benches prune_real_repo` — +because its ~15 GiB fixture must never build on a hosted CI runner): + +| Variant | Expected | What it measures | +|---------|----------|------------------| +| `prune_e2e/dry_run_cold` | ~160 ms | full parallel scan, integration probes uncached | +| `prune_e2e/dry_run_warm` | ~90 ms | steady-state re-scan, probes hit sha_cache | +| `prune_e2e/live` | ~620 ms | cold scan + serial removal of the 8 candidates (~60 ms each, under the scan write lock) | +| `prune_real_repo/dry_run_warm` | ~0.3–0.8 s | steady-state scan of 72 items (36 worktrees + 36 branches) at 331k-commit scale | +| `remove_e2e/first_output` | ~86 ms | single-target validation up to first output | + +Cold and live at rust scale are **one-shot timelines, not criterion groups** +(a cold criterion iteration costs ~1 min in re-hashing statuses alone; a live +one consumes the candidates). Expected one-shots on the `prune-real` fixture: + +- **cold dry-run ~5.5 s wall** (~46 s CPU over 472 subprocesses absorbed by + the rayon pool) — dominated by stat-cold `git status` at ~4.5 s per fresh + worktree; the probes are `merge-base --is-ancestor` ~40 ms and `merge-tree + --write-tree` ~130 ms (vs 4–25 ms synthetic, where shallow history walks + bottom out at subprocess-spawn cost) +- **live ~12 s wall** — all 24 removals serialize under the scan write lock + inside the `prune-scan` window: each of the 12 worktree candidates takes + ~0.5–1.7 s (pre-remove re-checks plus drain waits), branch-only candidates + ~50 ms + +This is the "prune takes many seconds" experience users report: worktree +count × stat-cold statuses bounds the scan, and removals extend it serially. +The synthetic fixture can't show it — its statuses are milliseconds — so +scale-sensitive changes need a one-shot on `prune-real` (or +`wt-perf timeline -- -C step prune --dry-run` on a real repo) alongside +the criterion cadence. All rust-scale numbers are I/O-bound and move with +ambient machine load (sibling builds, Spotlight): treat them as shape, not +thresholds, and read criterion "regressed" verdicts on this bench against +`uptime` before believing them. + +Cold prune benches pair `invalidate_caches_auto` with +`restore_worktree_indexes` (see "Deleting a worktree's index isn't a cold +cache" under Cache Handling); the dry-run variants assert the listed +candidate count so that degradation can't come back unnoticed. + +**Phase attribution** — `wt-perf timeline` plus the removal spans. Prune emits +`prune-gather` (worktree+branch enumeration), `prune-scan` (the whole parallel +check region), one `prune-check:` per scanned item, and one +`prune-remove: