mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
codex/remove-codex-cloud-specific-tests
85 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
897f7d7193 |
fix(tests): extend the spawn pin to benches; correct the pin's cost note (#3792)
Follow-up to #3784, prompted by a history audit of the spawn-flake family. Benches still spawned `env!("CARGO_BIN_EXE_wt")` — the uplifted path the suite stopped spawning — so a concurrent build could fail a bench run's spawns; all 11 sites now route through `wt_bin()` and `test_wt_spawns_are_pinned` scans `benches/` too, making the rule exceptionless. The pin's docstring also claimed the hardlink shares the `deps/` artifact's inode: true where cargo uplifts by hardlink (Linux), but macOS uplifts by copy-on-write clone — the pin keeps the clone, whose blocks stay shared with `deps/` (measured: cloning the 70 MB binary consumes 8 KB), so the no-cost conclusion stands with the mechanism now stated per platform, plus why nothing sweeps the directory. > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
00ab0ffa26 |
Canonicalize benchmark fixtures and variants (#3761)
Benchmark fixtures still encoded the benchmark that first needed each repository state, which left overlapping recipes and variants after the earlier harness consolidation. This change reduces the fixture catalog to two provenance-based bases: `Generated` builds an ordinary Git repository locally, while `Imported` copies the pinned `rust-lang/rust` corpus. Worktree, branch, and remote-ref populations remain parameters on `Generated`; prune candidates and backdrop are overlays that work with either base. The generated base deliberately combines heterogeneous worktree states, history-spread branches, and optional remote refs so ordinary list, completion, picker, first-output, alias, remove, and prune benchmarks can share it. Imported history-spread branches and clean base-tip worktrees carry their own commits, preserving the base populations without making them incidental prune candidates when overlays advance the default branch. The benchmark matrix now keeps single-factor contrasts: list scaling uses the 1- and 8-worktree endpoints; alias dispatch has a startup floor, two population endpoints, and one warm/cold variable-resolution pair; completion keeps one full-surface case; remove and prune vary cache or hook state only where the command exercises it. Historical recipes, redundant cache rows, and intermediate scaling points are removed. Manual setup paths live under `target/`, and the benchmark guide documents the resulting fixture and cache model. Tests: `cargo run -- hook pre-merge --yes` after merging current `main` (4,571 tests); targeted Criterion test-mode runs; `cargo test -p wt-perf`; benchmark check, clippy, formatting, and diff checks. > _This was written by Codex on behalf of max-sixty_ |
||
|
|
970976bd32 |
Consolidate benchmark recipes and cases (#3721)
Benchmark fixtures had accumulated around individual call sites, leaving the same repository shapes and command modes expressed several ways. This change makes repository state the organizing concept: benchmark groups select semantic `FixtureRecipe`s, share table-driven cases, and retain separate fixtures only when a controlled contrast, destructive precondition, or disproportionate setup cost requires one. The real-repository list benchmarks now share one pinned `rust-lang/rust` fixture with eight worktrees and fifty branches spread across history. The list matrix keeps default, branch, warm, and cold coverage without maintaining several “real” repository handles. Remove and prune cases share the same case machinery, while the destructive large-repository prune state remains separate. The scheduled workflow now converts Criterion estimates directly with `jq`, removing the one-off Python converter and its tests. The benchmark guide records the canonical-fixture principle and the remaining recipe-to-group mapping. Tests: `cargo run -- hook pre-merge --yes` (4,551 tests); `cargo bench --bench list large_repository -- --test`; `cargo test -p wt-perf`; benchmark check, clippy, formatting, and diff checks. > _This was written by Codex on behalf of max-sixty_. |
||
|
|
28a7c00448 |
fix(prewarm): faithful -vv traces and a universal git-config preload (#3705)
`wt-perf timeline -- config state get` showed three `git config --list -z` forks per invocation, despite `RepoCache.all_config` promising one bulk read. Root-causing attributed the three to three separate `Repository` constructions (the command's, the help pager's in `pager::git_config_pager`, the `-vv` diagnostic epilogue's), each with a fresh `RepoCache` — and to two mechanisms that kept the prewarm preload from serving them: 1. **`-vv` disabled prewarm.** `prewarm_at` fast-pathed on `GIT_COMMON_DIR_CACHE.contains_key` alone. Under `-vv`, `log_files::init` constructs a `Repository` during `logging::init` — before `Repository::prewarm()` runs in `main` — which populates exactly that key, so prewarm skipped the git-config and user-config preloads entirely. Every `-vv` trace therefore overstated the on-demand config forks of a normal run (a plain repo does one `git config --list -z` total, not three), which is what produced the original report. Each prewarm thread now gates on the cache it populates. 2. **`extensions.worktreeConfig` repos never got the preload.** `prewarm_git_config` declines the discovery-path read there (#2779: it misses main-worktree `config.worktree` overrides like `core.bare`), so every fresh construction re-forked from the common dir on first `all_config()` access. A post-pass after the prewarm threads join now re-runs `git config --list -z` from the resolved `git_common_dir` — byte-for-byte the invocation `all_config` would fork on demand — and preloads that merged map, so these repos get the same memory-hit constructions as everyone else. Fork counts for `wt config state get` (start+complete pairs deduplicated, verified via `RUST_LOG=worktrunk=debug` and `wt-perf timeline`): | Repo class | Before | After | |---|---|---| | plain repo, normal run | 1 | 1 | | `worktreeConfig` repo, normal run | 3 | 2 | | `worktreeConfig` repo, `-vv` | 3 (prewarm skipped) | 2 (prewarm traced, zero on-demand) | The two on-demand construction sites this obviates fixing individually: the pager's `Repository::current()` and the diagnostic's now both consume the preload, so no plumbing of `&Repository` through `show_help_in_pager` was needed. Timing, release builds of `bbd24fb8f` vs this branch on the worktrunk dev repo (`worktreeConfig` class), hyperfine `-N --warmup 10 --runs 80`, load < 4: `wt config state get` 73.4 ms ± 4.6 → 68.7 ms ± 3.8 (−4.7 ms mean, consistent with the one saved fork). Plain repos are unchanged by construction (same fork count and code path). Tests: the #2779 decline test now pins the inverse property (the preload lands and holds the merged common-dir map, with `core.bare = true` as the poison pin); new tests pin that a `Repository` constructed before prewarm no longer suppresses the config preload, that a partially-warm prewarm runs only the cold threads without overwriting an existing preload (and a fully-warm repeat early-returns), that a failed common-dir re-read (missing dir, corrupt config) leaves the preload empty for on-demand `all_config` to surface, and that a prewarm-less construction still reads `all_config` from the common dir (the on-demand #2779 branch). The `is_bare`/`repo_path` regression tests now exercise the preload-consuming path. > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
aba7bad089 |
perf(completion): overlap the git reads a Tab press waits on (#3664)
Shell completion forks a whole `wt` process on every Tab press, with the user's finger still on the key, and it has nowhere to hide work: nothing paints progressively, no cache survives the process, and no shell caches a dynamic completion. It was doing every git read in sequence. Measured on `mixed-80-80-1400`, M4 Max, same binary before and after: | | before | after | | |---|---|---|---| | `wt switch <Tab>` | 45.7 ms | 27.4 ms | 1.66× | On this repo's own checkout, `wt switch <Tab>` goes 47.9 ms → 29.0 ms; `wt <Tab>` (subcommand names, no branch work) goes 22.2 ms → 12.8 ms. ## The benchmark first `benches/completion.rs` ran against fixtures carrying two remote-tracking refs and no worktrees — a shape nobody has. It never reached the completer's 100-candidate threshold, where every remote-only branch is discarded *after* being scanned, so the most expensive call on the path was measured at a size that hid it. It now runs one repo, on the `mixed` fixture `full` already uses: `mixed-80-80-1400` — 80 worktrees in four states, 80 more branches forked across 200 commits of history, and 1400 remote-tracking refs. That covers all three categories `BranchCompleter` distinguishes, puts both candidate slow calls (`for-each-ref refs/remotes/` and `worktree list --porcelain`) in one process at a size where each is expensive, and lands past the 100-entry threshold. `completion_switch/mixed` reports 23.1 ms; the fixture builds in 9.7 s. The remote-ref count is the one dimension `mixed` was missing, so it goes on `create_mixed_repo` as an optional third component of the handle. `full` passes 0 and is unaffected, so its tracked series has no discontinuity — and its own version of the same gap (`wt list` scans `refs/remotes/` too, at two refs) becomes a one-argument change if we want it. `completion_switch`'s own history does break, and knowingly: its two ids (`branches_only`, `with_worktrees`) collapse into one `mixed`, and `criterion-to-jsonl.py` keys the nightly gist rows by `<group>/<id>`, so those two series stop accumulating. Worth paying — they measured the fixture that hid this cost in the first place, so carrying them forward would mean maintaining a benchmark that structurally cannot see the thing it exists to watch. Worth calling out explicitly, since #3669 landed while this was open and states that it preserves all 41 existing Criterion IDs: this PR is the one place that number changes, deliberately, and 41 becomes 40. Deliberately not a variant matrix. Completion has no phases worth timing apart, and the calls within a wave overlap, so no split of the wall time says which one moved — localizing a regression is a trace's job, as `benches/CLAUDE.md` already says for `full`. ## The changes - **Call `Repository::prewarm` on this path.** It already exists for exactly this, filling the git-discovery, git-config, and user-config caches on three threads — but completion answers and returns long before `main` reaches it, so those reads were each a separate serial fork. - **Run the three scans behind `branches_for_completion` concurrently.** They are independent git calls the function only joins in memory; in sequence the user waits for their sum where they could wait for the max. - **Stop scanning `refs/remotes/` where a remote-only branch can never be offered** (`wt remove`, worktree-only arguments). The completer decides that once, up front, and it drives the scan as well as the filter. - **Return to the completion handler before `main` builds the rayon pool** — `available_parallelism() * 2` OS threads, spawned eagerly, on a path that reaches no rayon. `wt switch <Tab>` goes from six serial forks to five in two concurrent waves; `wt remove <Tab>` to four. ## A bug found on the way `branches_for_completion` groups remote refs through a `HashMap` and sorts by timestamp with a stable sort, so on tied timestamps the order is whatever that process's hashing produced — three consecutive completions print three different orders. Fixed by breaking the tie on name, in its own commit. ## Verification - Candidate sets byte-identical to the pre-change binary across bash/fish/zsh/nu at 64 completion points — including the 100-candidate threshold either side, `--create` suppression, and outside a git repo. - `cargo run -- hook pre-merge --yes` green on the merge commit, after merging current `main` in: 4491 tests, all lints. - `cargo bench --bench completion -- --test` green — worth running explicitly now that `run_and_check` asserts the measured subprocess succeeded, since the assert is new to this bench. Merged `main` twice while this was open. The second, #3669, consolidated the bench harness onto a shared `FixtureRepo` plus `wt_command` / `run_and_check` and touched all four bench files this branch changes; every conflict was resolved toward main's harness. Notably the completion bench now uses main's `run_completion`, which asserts the measured subprocess actually succeeded — the version here did not, so a `wt` that failed under benchmark would have been silently timed. ## Review feedback folded in The draft-stage review caught the `add_remote_refs` rationale overclaiming, and it was right. The comment said the round-robin makes the refs "name distinct objects", with the scan reading "one commit object per ref". It can only spread over the history it has: at 200 base commits, 1400 refs land 7 apiece on ~200 distinct commits, and git parses each object once and reuses it for the rest of the `for-each-ref`. So the scan pays ~200 parses plus a cheap iteration hit per ref, not 1400 parses. That gap is now stated rather than papered over. Closing it would need a history as deep as the ref count, and `BASE_COMMITS` is shared with `full` — deepening it would slow that fixture and shift the repo `full` has been measured on, to sharpen a per-object parse this bench isn't primarily about. The ref count is the dimension it exists to vary. (The review raised this against `long_lived_clone`, the parallel fixture since deleted; the same overclaim had survived into the shared-fixture version.) ## Not addressed Two things from the report that prompted this, both left alone deliberately: - The dominant per-fork tax on the reporter's machine is their `~/.gitconfig` symlink chain landing on NFS, re-resolved by every git child. That's local environment, not code — though it is exactly why overlapping the forks helps them more than the numbers above suggest. - `worktrunk.state.<branch>.*` markers accumulate without bound: on this checkout, 79 branches have markers and 72 of those branches no longer exist. Real, but a separate subsystem with its own design question about when a marker may be dropped. > _This was written by Claude Code on behalf of @max-sixty_ |
||
|
|
7fe28bf574 |
Consolidate benchmark fixtures and harness (#3669)
Benchmark targets had each reimplemented temporary-repository ownership, linked-worktree paths, subprocess isolation, and warm/cold loops. This adds a shared `FixtureRepo` and canonical command/cache helpers while leaving each scenario-specific workload and destructive lifecycle explicit. All 41 existing Criterion IDs, fixture dimensions, command arguments, and cache ordering are preserved. The completion benchmark now also fails when its measured subprocess fails instead of silently accepting the result. Tests: `cargo run -- hook pre-merge --yes`; `cargo test -p wt-perf`; `cargo check --benches -p worktrunk -p wt-perf`; representative Criterion test-mode runs for completion, list, first-output, remove, and prune. > _This was written by Claude Code on behalf of max_. |
||
|
|
e91b5fa885 |
perf(prune): parallelize removals on the scan lock's read side (#3631)
`wt step prune` executed removals one at a time under the write side of `check_lock` while the candidate scan fanned out on the read side. On the rust-scale fixture that serialized ~2.6 s of a ~2.9 s live prune. Removals now run concurrently: the main thread streams scan results into a FIFO queue of per-candidate jobs, and a worker pool sized like the rayon pool executes them under the read side of the lock. **Why the read side is safe.** The lock exists for the Windows `.git/config` race (#2801): `git branch -D` rewrites config via lockfile plus atomic rename, and a concurrent reader fails on the rename. The removal chain has since moved to the CAS `git update-ref -d`, and (verified empirically, by inode-watching `.git/config` across each command) neither it nor `git worktree remove` / `git worktree prune` touches config; `branch -D` remains reachable only through arms prune never takes. Concurrent `git worktree prune` runs are idempotent and exit 0, and `update-ref -d` does not consult worktree checkout state, so nothing in the hook-free chain needs exclusivity. The write side remains for the removals that do: hook-bearing candidates (a foreground `pre-remove` body is an arbitrary command with streamed output), `--foreground` candidates (TTY spinner), the fail-hard metadata-pruning candidates (`StaleDetached`, plan-less `Prunable`), and the deferred current worktree. `removal_needs_write` in `src/commands/step/prune.rs` is the decision table; the `check_lock` field doc carries the full analysis. **Output and determinism.** Skip lines ride the same FIFO queue as removals, so `RAYON_NUM_THREADS=1` (one worker) reproduces the exact serial output order the snapshot tests pin; with more workers, whole lines interleave (one `anstream` `eprintln!` writes a line under a single stderr lock). Live `--format=json` output is now sorted by scan index, matching dry-run, with the current worktree still last. The first failing removal flips an abort flag that drains the rest of the queue unexecuted, matching the serial loop's abort-on-first-error. **Measured.** `prune_e2e/live` criterion: ~615 ms to ~398 ms. Rust-scale live one-shot (36 worktrees, 24 candidates, warm): 2.9 s to 0.56 s wall, all 24 removals overlapping in the trace (6.5 s of subprocess time). **Abort semantics.** The first failing removal flips an abort flag: in-flight removals complete (their per-candidate lines already printed), queued ones drain unexecuted, no summary prints, and the first error propagates — the same abort-on-first-error the serial loop had, where an error mid-loop likewise left earlier removals done and later candidates untouched. A mid-scan check error takes the same abort path. **Testing.** New `test_prune_removals_run_concurrently` proves the overlap causally: two orphan-branch removals barrier on each other inside a git shim, and serialized removals trip the shim's timeout sentinel (verified: the test fails under forced `RAYON_NUM_THREADS=1`). `test_prune_removal_failure_aborts_remaining_queue` and `test_prune_concurrent_removal_failures_report_first` pin the abort semantics deterministically (single-worker FIFO, and a barrier forcing two concurrent failures). The config-race canary keeps its role with its premise rewritten: a regression to a config-rewriting deletion is what it would now catch. The deterministic-output snapshot tests pass unchanged. The one patch line no test reaches deterministically is the worker's early `return` when the result receiver is gone (a scan error racing an in-flight removal). Accepted residuals, documented in code: a `packed-refs.lock` contended past git's ~1 s retry budget reads as `RetainedRaced` (fail-closed; unobserved at 24-way concurrency on local disks), and concurrent fail-soft `git worktree prune` races leave at worst stale metadata for the next prune to collect. 🤖 Generated with [Claude Code](https://claude.com/claude-code) > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e548196f89 |
docs(bench): refresh rust-scale prune one-shots after the removal-chain trim (#3622)
Re-measurement on the rebuilt `rust-prune-12-24` fixture after #3617 trimmed the removal chain: live one-shot ~12 s to ~2.9 s (two runs, 2.91 s / 2.89 s), worktree candidates ~155-210 ms each now that the status is fsmonitor-served, branch-only ~40-100 ms. Full-cold dry-run unchanged in shape. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5ecdf2bb78 |
perf(remove, prune): one check per guarantee in the removal chain (#3617)
`wt step prune` scans in parallel, but each removal ran a serial chain of ~17 git subprocesses under the scan write lock: a re-run of `prepare_worktree_removal` (the scan had already run it and kept only `is_ok()`), a post-hook "safety refresh" (status + fresh snapshot + integration re-compute) that ran even when no pre-remove hooks executed, and a final pre-rename clean check that ran right after the fsmonitor daemon stop, paying a full un-fsmonitored re-stat. At rust-lang/rust scale that post-stop status is the 0.5-1.7 s per-removal driver. This trims the chain to one mechanism per guarantee, with no behavioral additions: - **Prune passes the scan-time plan through** (`CheckOutcome.plan`) instead of re-preparing under the write lock. `Prunable` candidates still prepare in `try_remove` because preparing them prunes stale metadata, which must stay serialized. - **`refresh_removal_safety_after_pre_remove` is deleted.** `prepare_worktree_removal` now computes the integration verdict and effective target for display (carried on `RemoveResult::RemovedWorktree`), the pre-rename clean check catches hook-dirtied worktrees, and `delete_branch_if_safe`'s CAS re-decides the branch deletion against fresh refs. The clean check also moves into `remove_worktree_with_cleanup`, so the foreground and picker paths get their final gate adjacent to the rename rather than relying on the deleted refresh. - **The clean check runs before the fsmonitor daemon stop.** The stop must stay before the rename: on Windows the daemon holds a handle on the worktree that would fail the rename, and git's graceful stop resolves the daemon by worktree path, so it cannot reach it after the move (verified: a healthy daemon self-exits and unlinks its socket within ~500 ms of the rename; the client from the renamed path reports "not running"). - **`prune-gather` stops shelling out**: the main worktree comes from `git worktree list` ordering (documented: main lists first; `list_worktrees()` filters bare entries) instead of a `git rev-parse --git-dir` per worktree. Criterion on the `prune-4-8` fixture: `dry_run_probe_cold` 197 ms to 147 ms (-25%), `dry_run_warm` 110 ms to 62 ms (-43%), `live` 783 ms to 615 ms (-21%). A `prune-12-8` one-shot timeline (24 candidates, live): wall 3.14 s to 2.09 s; per-worktree removals 170-210 ms to 112-134 ms, now dominated by the ~60 ms daemon stop. The rust-scale numbers in `benches/CLAUDE.md` are marked for re-measurement on the next fixture rebuild. **Consistency corner, unchanged in kind but wider in trigger:** a worktree that becomes dirty after `handle_removed_worktree_output` starts (in practice: a pre-remove hook dirtying it) now fails at the pre-rename check, which is after the cd directive is written (and, in background mode, after the "Removing … in background" message and retention hint print), and the shell wrapper applies the cd file regardless of exit code. So in that corner the shell moves to the primary worktree while the dirty worktree is preserved with an error. The same outcome already existed for a file appearing between the refresh and the final check; nothing is removed in either case. Testing: full pre-merge gate green (4,578 tests, no snapshot changes), plus a new shim-based test covering the skip path when a stale (`Prunable`) candidate's preparation fails at removal time. The hook-dirtied corner has no dedicated test; existing dirty-worktree removal tests cover the command-entry gate. The one remaining uncovered added line is the `?` propagation closing the live scan closure (`prune.rs:1023`), which has no deterministic trigger. > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
166e1190ba |
refactor(perf): canonicalize wt-perf prune-fixture layering + completion config (#3548)
## What Three behavior-preserving canonicalizations of the `wt-perf` benchmark fixtures, from a design pass on "why does prune need its own generator, and can the fixtures consolidate?" 1. **Share the prune-population layer.** `create_prune_repo_at` (synthetic) and `create_prune_real_repo_at` (rust-scale) both layered the same two populations onto their base repo — `add_diverged_backdrop` (the scanned-but-never-removed backdrop) + `add_squash_merged` (the removable candidates). Extracted into `add_prune_populations`, so the base repo (synthetic `create_repo_at` vs. rust clone) is now the *only* thing the two fixtures differ in, spelled out once instead of twice. 2. **Canonicalize `completion.rs` onto `RepoConfig::branches`.** Its two inline `RepoConfig` literals were exactly `RepoConfig::branches(50, 0)` (one with `worktrees` overridden), so they now use the canonical preset via struct-update instead of respelling all seven fields. 3. **Document the fixture → bench mapping.** `benches/CLAUDE.md` listed the `wt-perf setup` handles in one section and the bench groups in another with nothing connecting them, so a bench and its fixture (`full` and `mixed`) read as overlapping names for the same thing. Added a mapping table and the rule behind it. ## Why this is the whole safe consolidation The design question was whether prune could drop its bespoke generator for a "standard one with different params" — ideally one diverse repo serving prune *and* the other benches. The answer is that prune's generator is **already** a thin composition (`create_repo_at` + two additive layers), and it can't fold into the shared `mixed`/`full` fixture without breaking three prune-specific invariants: a known/cleanly-separated candidate count (the destructive `live` path re-creates exactly the consumed candidates), untracked-only dirt (the `git reset --mixed` index-heal depends on it — `mixed`'s staged-dirt states violate it), and a provably-unintegrated backdrop. And benchmark fixtures optimize for *attribution*, not realism: the uniform single-axis fixtures (`worktree_scaling`, etc.) exist precisely because the diverse `full` fixture can't localize a regression, so diversifying them would lose that. On the `full`/`mixed` naming specifically: they can't be canonicalized to one name, because fixtures and benches are **many-to-one** — `RepoConfig::typical` alone backs five bench groups (`skeleton`, `worktree_scaling`, `first_output`, `picker_preview`, `remove_e2e`), two of which sit in the same file measuring different things. `full`/`mixed` only looks 1:1 by coincidence. Renaming also isn't free: `.github/scripts/criterion-to-jsonl.py` keys each time-series row by the criterion output path (`<group>/<id>`) for the daily gist append, so a rename orphans that series. Documented rather than renamed. ## Proposed follow-up (not in this PR) The one remaining real duplication is two *implementations* of "two-sided-diverged population forked across history depth": `add_diverged_backdrop` (plumbing-based, scales to rust) vs. the diverged branch states inside `create_mixed_repo` (checkout-based, synthetic-only). They can't naively merge — the checkout path would make `prune-real` take minutes per branch — but a shared plumbing primitive could back both. That rewrites a carefully-tuned fixture feeding the `full` bench with **no shape-assertion test to catch drift**, so I'd land a fixture-shape test first (count refs by state + assert integration status). Happy to do it if wanted. ## Verification fmt `--check` clean; `clippy -p wt-perf --all-targets` and `clippy --bench completion` clean; `pre-commit` clean on the doc; `cargo test -p wt-perf` passes (incl. the prune-fixture tests `squash_merged_fixture_is_content_integrated` / `prune_fixture_state_classifies_lifecycle`, which exercise `create_prune_repo_at` → `add_prune_populations`); all benches build. Table contents verified against the code by grep. > _This was written by Claude Code on behalf of Maximilian Roos_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a7c4a7a120 |
refactor(wt-perf): place fixtures under target/wt-perf, not the cache dir (#3547)
## What Move every `wt-perf` on-disk fixture out of the per-user cache dir and under the cargo target dir at `<target>/wt-perf/`, resolved by a renamed `wt_perf_fixture_dir()`: - `setup <config>` fixtures → `<target>/wt-perf/<config>` (was `~/.cache/wt-perf/<config>`). - The rust-lang/rust clone and `prune-real` fixtures → `<target>/wt-perf/bench-repos/` (was `~/.cache/wt-perf/bench-repos/`). - `wt_perf_cache_dir()` → `wt_perf_fixture_dir()`. The target dir is derived from the **running executable's own path** (it lives inside whichever dir cargo built into — `<target>/debug/wt-perf`, `<target>/release/deps/<bench>`), so it honors `CARGO_TARGET_DIR`, a config-file `build.target-dir`, and cargo-llvm-cov's `target/llvm-cov-target/` — none of which a bare `CARGO_TARGET_DIR` env read covers. Falls back to `<workspace>/target` if the binary isn't under a recognizable profile dir. The `etcetera` dependency is dropped. - The `WT_PERF_CACHE_DIR` env override (and the empty-value guard it required) is removed; the benchmarks workflow now caches the deterministic `target/wt-perf/bench-repos` path directly. - In-process throwaway fixtures (`create_repo`) are unchanged — they keep using `tempfile::TempDir`. This reverses the location decision from #3542, which had moved these into `~/.cache/wt-perf`. ## Why `target/` is the conventional home for build/test-generated artifacts — gitignored, reaped by `cargo clean`, and already where criterion writes (`target/criterion`). Keeping every wt-perf fixture there gives one predictable location under the repo that `cargo clean` fully resets. Deriving the target dir from the running binary (rather than assuming `<workspace>/target`) keeps that property intact even when the target dir is relocated. ## Tradeoff (accepted) `target/` is **not** shared across git worktrees (worktrees don't share it) and is wiped by `cargo clean`. So the ~15 GiB `prune-real` rust clone re-clones per worktree and after every `cargo clean` — the cost #3542 avoided by using the cache dir. This is deliberate and documented on `wt_perf_fixture_dir`; it's cheap for the synthetic `setup` fixtures, which rebuild in seconds. ## Testing - `cargo build -p wt-perf --all-targets`, `cargo test -p wt-perf` — clean. - New unit test `target_dir_from_exe_finds_cargo_target` covers the resolution logic (relocated `CARGO_TARGET_DIR`, bench binary under `release/deps/`, cargo-llvm-cov's nested target, closest-profile-wins, and the outside-any-target fallback). - `pre-commit run --all-files` (fmt, clippy, yaml, typos, cargo-lock, custom hooks) — clean. - Smoke-tested end-to-end: `setup branches-2` lands at `<worktree>/target/wt-perf/branches-2` and writes nothing to `~/.cache/`; a copy of the binary run from a fake `<dir>/debug/wt-perf` correctly resolves fixtures to `<dir>/wt-perf/`, proving a relocated target dir is honored. > _This was written by Claude Code on behalf of Maximilian_ --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4121dc979c |
refactor(wt-perf): cache fixtures in the platform cache dir, not /tmp or target/ (#3542)
## What Move `wt-perf`'s benchmark/debug fixture repos out of `std::env::temp_dir()` and `target/bench-repos/` into the per-user cache dir, resolved by a new `wt_perf_cache_dir()`: - `$WT_PERF_CACHE_DIR` if set, else `<cache>/wt-perf` via `etcetera::choose_base_strategy().cache_dir()` — `~/.cache/wt-perf` (or `$XDG_CACHE_HOME/wt-perf`) on Linux **and** macOS. This matches worktrunk's own base-dir convention (`src/config/user/path.rs` resolves user config through the same XDG-on-macOS strategy). - `setup <config>` fixtures → `<cache>/<config>` (was `temp_dir()/wt-perf-<config>`). `--path` still overrides. - Real cloned fixtures (`prune-real`, the rust-lang/rust clone) → `<cache>/bench-repos/` (was `target/bench-repos/`). - In-process throwaway fixtures (`create_repo`) are unchanged — they keep using `tempfile::TempDir`. ## Why Both old homes were wrong for a *reused* fixture (these are given stable names, persist between runs, and are referenced from docs and `-C <path>` — that's a cache, not a temp file): - **A fixed name in a shared `/tmp` (Linux)** collides across users: `/tmp` is sticky (`1777`), so a second user's `setup` hits `remove_dir_all().unwrap()` on a dir they don't own → `EPERM` → panic. It's also a predictable-path/TOCTOU hazard, and `systemd-tmpfiles` reaps `/tmp` per-file at 10 days by `max(atime,mtime,ctime)` — on a `noatime` mount an actively-*read* fixture still loses cold `.git/objects/pack/*.pack` mid-use → silent git corruption. - **On macOS**, `temp_dir()` is already `/var/folders/.../T` (per-user, `0700`), so the docs' `/tmp/wt-perf-*` paths were simply wrong there. - **`target/`** is per-worktree (git worktrees don't share it) and wiped by `cargo clean`, so the ~15 GiB rust clone was re-cloned per worktree — worst for the most expensive fixture, in a worktree-heavy workflow. The cache dir is per-user (no collision, no TOCTOU), stable and discoverable (docs/`-C` references still work), shared across worktrees (clone once per machine), and survives `cargo clean` — matching sccache, cargo, rustup, Go, Bazel, and Hugging Face, which all place large reusable caches there rather than in `/tmp`. ## Migration notes - First bench/setup run after this rebuilds the cache under the new location (the old `target/bench-repos/` is no longer read). Existing fixtures can be dropped with `rm -rf target/bench-repos`. - `.github/workflows/benchmarks.yaml` sets `WT_PERF_CACHE_DIR` and caches `$WT_PERF_CACHE_DIR/bench-repos`, so the cached path and the path wt-perf writes stay pinned together (no drift if a runner sets `$XDG_CACHE_HOME`); key unchanged. - Docs (`benches/CLAUDE.md`, `src/commands/CLAUDE.md`) updated to the new paths, noting that `wt-perf setup` prints the exact path. ## Testing - `cargo build --workspace --all-targets`, `cargo clippy --workspace --all-targets --features shell-integration-tests -- -D warnings` — clean. - `cargo test -p wt-perf` — passes. - Smoke-tested default resolution (`~/.cache/wt-perf/…` on macOS), `$WT_PERF_CACHE_DIR` override, and the `prune-real --path` rejection (now names the resolved cache path). > _This was written by Claude Code on behalf of Maximilian_ --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
1ebd7b043d | docs: correct stale references found in nightly survey (#3490) | ||
|
|
c310767216 |
refactor(bench): canonicalize prune cold to probe-cold via bench_wt (#3437)
Prune's Criterion "cold" had two structural problems: it over-invalidated (deleting worktree indexes models a state users never see, and needed the `restore_worktree_indexes` pairing just to keep prune's clean-worktree gate from silently dropping candidates), and its per-iteration stdout assertions kept it off the canonical `bench_wt` runner. This PR fixes both. **Probe-cold is now the canonical prune cold state.** `invalidate_probe_caches` clears only `.git/wt/cache/`, the exact state a repo is in right after fetching new commits — probes re-run at real cost, git's own caches (indexes with stat data, commit graph, `worktrunk.default-branch`) stay warm. `bench_wt` picks its invalidation via a new `CacheState` enum (`Warm` / `Cold` / `ProbeCold`), and every prune dry-run arm is now a `bench_wt` one-liner. `restore_worktree_indexes` is demoted to a private fixture-healing helper. **Verification moved out of the timed loops.** Timed iterations assert only exit status, like every other bench. Fixture correctness is pinned once after setup: `verify_candidates` runs one dry-run that must list exactly the expected candidates (8 synthetic, 24 rust-scale, 0 on the live fixture's backdrop). A live run that removes nothing trips the next iteration's candidate re-creation (branch-name collision) instead of being silently timed. **The rust-scale cold cell is now repeatable.** `prune_e2e/dry_run_cold` is renamed to `dry_run_probe_cold` (the metric changed meaning, so the old gist series ends deliberately), and a new `prune_real_repo/dry_run_probe_cold` measures at rust scale what was previously a hand-run one-shot — feasible because probe-cold iterations cost ~0.6 s where full-cold costs ~1 min. Measured on a quiet machine, all with tight CIs: synthetic probe-cold 159 ms / warm 96 ms / live 704 ms; rust-scale warm 247 ms / probe-cold 630 ms. ## Testing All five bench arms run end-to-end with the fixture checks passing (synthetic groups plus the feature-gated rust-scale group against a freshly rebuilt 13 GiB fixture). `cargo test -p wt-perf`, clippy on both feature paths, and the rustdoc `-Dwarnings` gate all pass. Benchmarks don't run on PR CI; the daily workflow picks up the renamed series. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
350ea1f6a8 |
refactor(perf): make wt-perf setup persist-only and trim the CLI/lib surface (#3433)
Follow-up to #3403, applying the deferred `--persist` decision and two sibling subtractions found sweeping the perf tooling for the same shape (a vestigial option or dual path guarding nothing). - **`wt-perf setup` is persist-only.** The `--persist` flag and the interactive "Press Enter to clean up (or Ctrl+C to keep)" path are gone — it was the one blocking, stdin-reading path in an otherwise scriptable tool, and it guarded little: default-path fixtures land in `/tmp/wt-perf-<config>`, which the next `setup <config>` run already wipes and rebuilds. `prune-real` never offered cleanup, so setup's exit behavior is now uniform. - **`timeline --repo` deleted.** It only restated the traced repo: `run_timeline` resolves the repo from `-C`/cwd, `--repo` defaulted to exactly that, and the only documented use passed the identical path to both flags. `--cold` now invalidates the repo being traced — the only coherent target. - **`wt_perf` lib surface tightened.** Seven helpers plus `PruneFixtureState` had no callers outside `lib.rs` and are now private (`ensure_rust_repo`, `clone_rust_repo_at`, `history_spread_shas`, `add_diverged_backdrop`, `create_mixed_repo_at`, `create_prune_real_repo_at`, `prune_fixture_state`); the public surface now matches what the benches and CLI actually use. Doc references to them are plain code spans rather than intra-doc links — rustdoc's `private-intra-doc-links` lint rejects public docs linking private items even under `--document-private-items`. Checked and deliberately kept: `wt-perf trace` and `wt-perf invalidate` (distinct documented workflows), the `TraceResult`/`CacheReport` re-exports (public field types of `TraceEntry`/`Profile`), and the `create_repo`/`create_repo_at` two-layer fixture API (both layers used). Gate green: 4387 tests, clippy/fmt/doctests/rustdoc (`-Dwarnings`) clean. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
18d2bef84b |
refactor(perf): canonicalize the benchmark / wt-perf system (#3403)
Implements the design proposal from this branch's first commit
(
|
||
|
|
5e58827a28 |
bench(prune): rust-scale prune fixture, criterion benches, and removal trace spans (#3401)
Makes `wt step prune` / `wt remove` staging performance measurable and reproducible. Users report prune taking many seconds while the synthetic benches scan in ~150 ms; this adds the observability and fixtures that reproduce the gap and attribute it. No `wt` runtime behavior changes — the `src/` diff is ~29 lines of trace spans. ## What a reviewer needs - **Trace spans** (`src/commands/step/prune.rs`, `src/commands/process.rs`, `src/git/fsmonitor.rs`): `prune-gather` / `prune-scan` / `prune-check:<ref>` / `prune-remove:<label>` plus `internal-sweep` around `wt remove`'s end-of-command janitor. A `prune-remove` span starts before the write-lock acquisition, so it reads as "how long this removal stalled the run" (documented in benches/CLAUDE.md). - **Fixtures** (`tests/helpers/wt-perf/src/lib.rs`, the bulk of the diff): `prune-M-U` — M squash-merged candidate pairs (content-integrated, the post-PR-squash shape) against a two-sided-diverged backdrop forked across history; `prune-real[-M-U]` — the same shape on a rust-lang/rust clone, default 12+24 → 36 linked worktrees. The real fixture is cache-managed under `target/bench-repos/` (~15 GiB, minutes to build): `ensure_prune_real_repo` classifies it Intact/Consumed/Broken on reuse, re-creates candidates consumed by a live prune in ~1 min (round derived from the surviving squash commits, no sidecar state), and heals invalidated worktree indexes. - **Benchmarks** (`benches/prune.rs`): criterion groups `prune_e2e` (dry-run cold/warm + live with per-iteration candidate re-creation) and `prune_real_repo` (warm dry-run only). The real group is gated behind a new `real-repo-benches` cargo feature so the nightly benchmarks workflow — plain `cargo bench` on a hosted runner — never builds a fixture bigger than the runner's disk and the actions cache cap. ## Measured (M-series Mac, in benches/CLAUDE.md) Live prune on the rust-scale fixture: **~12 s wall** — a 5.4 s stat-cold scan (36 `git status` at ~4.5 s each, absorbed by the rayon pool) plus 24 removals serialized under the scan write lock (~0.5–1.7 s per worktree candidate). Warm re-scan: 0.25–0.8 s. This reproduces the reported "prune takes seconds" experience and points optimization at status cost and removal overlap, not the integration probes. Rider fix picked up en route: `docs.anthropic.com/en/docs/build-with-claude/claude-code` now 404s (failing lychee on every push, including on main), so the llm-commits installation link and the `wt config plugins` install-hint now point at `code.claude.com/docs/en/setup`. Also fixes a latent fixture bug: `history_spread_shas` divided by its hardcoded 5000-commit cap, so on short synthetic histories every "history-spread" fork silently collapsed to the tip. ## Testing wt-perf unit tests cover the fixture lifecycle (state classification, derived repair rounds, squash content-integration); every `ensure_prune_real_repo` path (build, cached, repair, index-heal, foreign cwd) was exercised end-to-end locally, as were both criterion groups and the one-shot timelines. An integration test asserts the `wt remove -vv` trace surfaces the fsmonitor sweep (span plus daemon count), pinning the sweep's observability contract. The rust-scale numbers are I/O-bound and ambient-load-sensitive — documented as shape, not thresholds. > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
e9bedf5b03 |
fix(bench): restore worktree indexes after cache invalidation (#3359)
## Summary Deleting a worktree's `.git/index` isn't a cold cache: git treats a missing index as empty, so `git status` reports every tracked file as a staged deletion — a different repo state, not a cold cache. That flips any clean-worktree gate a benchmarked command exercises (e.g. `wt step prune`'s removability check would silently drop every worktree candidate against such a worktree; verified empirically on a `timeline --cold` prune run). Adds `wt_perf::restore_worktree_indexes`, which `git reset -q`s every worktree back to a clean `git status` after `invalidate_caches_auto`, and documents the trap in `benches/CLAUDE.md`. `restore_worktree_indexes` has no callers on this branch yet — an upcoming prune-benchmark branch is its first consumer. ## Test plan - [x] `cargo build -p wt-perf` — clean, no warnings - [x] `cargo clippy -p wt-perf --all-targets` — clean - [x] `cargo run -- hook pre-merge --yes` — all pre-commit hooks, fmt, clippy, full nextest suite (4309 passed), doctests, and docs pass > _This was written by Claude Code on behalf of Maximilian Roos_ Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
28c937dbbf |
bench(list): consolidate synthetic wt list benches into one full group (#3335)
Consolidates the narrow synthetic `wt list` benchmarks into a single `full` group backed by one combined fixture, so one bench exercises the whole surface (many worktrees AND many branches in varied states) instead of several groups that each covered worktrees-only or branches-only in a uniform state. ## What changed - **New `full` group** (`benches/list.rs`) runs `create_mixed_repo(24, 120)` cold + warm: 24 worktrees and 120 branches in mixed clean/dirty/staged working trees and merged/ahead/diverged branch states, with branch divergence spread across history depth (the GH #461 cost driver). `full/warm` ≈ 147ms, `full/cold` ≈ 480ms locally. - **Folded in:** the old `full` (worktrees-only — a duplicate of `worktree_scaling` despite the name), `many_branches` (100 uniform branches), and the warm-only `rerun_warm` seed. No coverage dropped: `worktree_scaling` keeps the worktree-count scaling sweep, and the combined fixture's branch side subsumes `many_branches`. - **Kept standalone:** `divergent_branches` (the pure #461 deep-divergence stress) and `worktree_scaling` — both are cited in `src/main.rs` as the `rayon_thread_count` tuning anchors, and together they are the per-side regression trackers. - **Fixture extension** (`create_mixed_repo` in `tests/helpers/wt-perf`): added the deep-divergence shape it lacked — deeper base history (120 → 200 commits), checkpoints every 5, behind/diverged branches forking at points fanned across the full depth, and multi-commit diverged chains. Signature unchanged, so `wt-perf setup mixed-W-B` still works. - **Attribution:** a `full` wall time can't be decomposed by side (worktree- and branch-side git subprocesses overlap on the rayon pool), so `benches/CLAUDE.md` documents the `wt-perf timeline` `args.context` trace query (query #3) as the way to localize a regression. - `run_benchmark` now takes `cold_cache: bool` instead of `&BenchConfig`, decoupling it from the fixture type. Removed a stale `timeout_effect` doc reference (no such group exists). Benchmark-only change (plus the `wt-perf` test helper and `benches/CLAUDE.md`); no production code. All six `list` groups compile and run; clippy `-Dwarnings`, fmt, and the `wt-perf` tests are clean. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f045e0f449 |
perf(list): cut warm-cache wt list re-run ~36% by killing redundant per-row git forks (#3334)
## What
A warm re-run of `wt list` (where `.git/wt/cache/` is already populated)
is throughput / critical-path bound on per-worktree and per-branch git
subprocesses. Two of worktrunk's three cache layers — `Arc<RepoCache>`
and the process-global path/merge-base/commit-tree `DashMap`s — live in
process memory and don't survive across invocations, so every re-run
re-forks work that's content-addressed or already in hand. The "~1 ms
recompute, not worth a disk file" assumption behind keeping merge-base
and commit→tree in memory is false at scale on macOS (10–25 ms per fork
× dozens of rows).
This eliminates three classes of those forks. On a 24-worktree /
120-branch mixed-state fixture, subprocess work drops **3629 ms → 1013
ms**; the criterion `rerun_warm` benchmark goes **212 → 135 ms median**
(−36%, p<0.05). What remains is the irreducible working-tree floor —
per-worktree `git status`, and `add -A`/`diff`/`write-tree` for dirty
worktrees — which depends on live working-tree state and genuinely can't
be cached.
## The three changes
1. **`%T` primes the commit→tree cache** (`diff.rs`). The pre-skeleton
`git log --no-walk` batch already resolves each commit for `%ct`; adding
`%T` rides along for free and primes `commit_tree`, so the per-row
`CommittedTreesMatch` / `WouldMergeAdd` tree lookups never fork `git
rev-parse <sha>^{tree}`.
2. **Persist merge-base** (`sha_cache.rs`, `diff.rs`). New
content-addressed `merge-base/` sha_cache kind (in-memory front over
disk back) plus a `sha1 == sha2` short-circuit. The `AheadBehind` orphan
check forked `git merge-base` per row even when ahead/behind counts were
already cache-warm; now it's a file read on re-runs.
3. **Prime worktree root/git-dir** (`mod.rs`, `collect/mod.rs`). `git
worktree list` already gives every worktree's path; seeding
`WORKTREE_ROOTS`/`GIT_DIRS` from it (git-dir read from each `.git`
entry, mirroring `prewarm_rev_parse`'s canonicalization) eliminates the
per-worktree `git rev-parse --show-toplevel` / `--git-dir` forks. Runs
post-skeleton — its values are consumed only by the worker pool, so it
stays off the skeleton critical path and is skipped under
`WORKTRUNK_SKELETON_ONLY` (measured: skeleton time unchanged at 24.4
ms).
All three are behavior-preserving: caches are content-addressed and
never stale, and any value that can't be derived cleanly falls back to
the original subprocess.
## Benchmark
Adds `cargo bench --bench list rerun_warm` (24 worktrees + 120 branches
in varied states: clean / unstaged / staged+untracked worktrees; merged
/ ahead / diverged / identical branches), a reusable
`wt_perf::create_mixed_repo` fixture, and a `wt-perf setup mixed-W-B`
config. The existing list benches are warm-capable but each cover
worktrees-only or branches-only in a single uniform state. A
`TODO(bench-full-combined)` marks consolidating these into one cold+warm
"full" scenario with per-feature trace attribution.
## Reviewer orientation
- Cache mechanics: `src/git/repository/sha_cache.rs` (new kind) and the
`# Caching` docstring in `src/git/repository/mod.rs` (updated to
document the in-memory-front-over-disk-back decision and correct the
stale cost premise).
- The list-collect path: `src/commands/list/collect/mod.rs` module
docstring (caching table + `%T` fork doc updated).
- Tests: cache priming, merge-base roundtrip (incl. orphan), and a
prime-vs-subprocess equivalence test that pins the derived git-dir
against the real `git rev-parse` across main + linked worktrees.
> _This was written by Claude Code on behalf of max_
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
c1c76b150d |
refactor(trace): decouple human trace.log from machine trace.jsonl (#3297)
At `-vv`, worktrunk's `trace.log` carried machine-parseable `[wt-trace] ts=… tid=… seq=… cmd="git status" dur_us=12300 ok=true` lines *alongside* a `$ git status [ctx]` start echo — so every command appeared twice in two inconsistent renderings, and the human file was cluttered with `key=value` machine fields. The root cause: `trace.log` was doing double duty as both the human artifact and the machine-parsed source, even though `trace.jsonl` (added in #3232) already carries the same fields losslessly and nothing read it as input. This decouples the two. `trace.log` + stderr become purely human; `trace.jsonl` becomes the sole machine format. ```text # BEFORE — two inconsistent renderings of one command [h] $ git rev-parse --show-toplevel [worktrunk.rev-parse-dedup] [h] [wt-trace] ts=11593 tid=8 seq=5 context=worktrunk.rev-parse-dedup cmd="git rev-parse --show-toplevel" dur_us=6101 ok=true # AFTER — start ($) and finish (✓) pair, one `cmd [ctx]` rendering, no clutter [h] $ git rev-parse --show-toplevel [worktrunk.rev-parse-dedup] [h] ✓ git rev-parse --show-toplevel [worktrunk.rev-parse-dedup] 6.1ms ``` Commands render `$ …` (start) → `✓`/`✗ … dur` (finish), in-process spans `◷ name dur`, milestones `· event` — the leading glyph names the line type at a glance. ## What a reviewer needs - **`src/trace/parse.rs`** — rewritten to parse `trace.jsonl` (one JSON object per line) via a `kind`-dispatch (`cmd_completed`/`cmd_errored`/`instant`/`span`), skipping non-records (`{"message":…}` logs, `$ cmd` echoes, non-JSON). Cleaner than inferring kind from which fields are present. - **`src/logging.rs`** — `format_wt_trace` now emits the human line; `style_stderr_line` bolds the command for `$`/`✓`/`✗` lines so a start and its finish read as a pair. The machine fields (`ts`/`tid`/`seq`) live only in `trace.jsonl` (via the separate JSON visitor). - **Consumers repointed at `trace.jsonl`**: `wt config state logs profile`, the `wt-perf` helper (`timeline` now runs `wt -vv` and reads the file, located via `git rev-parse --git-common-dir`), and the `diagnostic.md` profile section (while still inlining the human `trace.log`). - **Docs** — help text, the FAQ file inventory (added `trace.jsonl`), and the `[wt-trace]`-grammar descriptions across `emit.rs`/`log_files.rs`/`benches/CLAUDE.md` updated. One behavior note: `wt-perf timeline` now writes trace files to the repo's `.git/wt/logs/` (a side effect of `-vv`) where the old `RUST_LOG=debug` path didn't — expected for a `-vv`-based perf helper. ## Testing Well-covered: parser (14 unit tests), renderer + stderr styling (8), `logs profile`/`diagnostic`/`switch`/`completion` integration tests migrated to JSON fixtures, plus a `wt_target_dir` unit test for the wt-perf `-C` resolution. Verified `logs profile`, `wt-perf timeline` (text + `--chrome`), and `cache-check` end-to-end on a real `-vv` capture. An independent review pass found no correctness issues; its findings (a missed doc, two wt-perf robustness fixes) are folded in. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c70eedf152 |
feat(switch): run the interactive picker on Windows (#3217)
## Run the `wt switch` interactive picker on Windows
The picker was gated `#[cfg(unix)]` because its preview-tab switching
(alt-1…7 jump to a tab; tab/shift-tab cycle) was implemented as skim
`execute-silent` keybindings that shelled out to `echo`/`tr`/`mv`
through a per-process state file. skim runs keybind commands through the
platform shell — `cmd.exe` on Windows, which has neither `tr` nor `mv` —
so that was the hard blocker. skim 4.x (the ratatui/crossterm rewrite
worktrunk already depends on) supports Windows.
This replaces the shell keybindings with native handling: the active tab
is now a process-wide in-memory `AtomicU8` (`PreviewStateData`), and the
keys are bound to `Action::Custom` callbacks inserted directly into
skim's `options.keymap` (resolved with skim's own `parse_key`, so they
match its event-loop lookup exactly). Each callback sets the mode and
returns `Event::RunPreview`. This drops the state file, the
`ModeWatcher` background poller, and `shell_escape::unix` — a net
simplification on every platform, not just a Windows shim.
With the shell dependency gone, the `#[cfg(unix)]` gate comes off the
whole picker, along with the now-stale gates on its dependencies — both
in source (`GitHubPrInfo`, `open_pr_status`, `SwitchPipeline`, the
column-grid types, `ShowConfig`, `PickerProgressHandler`,
`format_aligned`, `generate_summary`) and in `Cargo.toml`, where the
picker's TUI stack (`skim`/`ratatui`/`ansi-to-tui`/`tokio`) moved out of
`[target.'cfg(unix)'.dependencies]` into the main table so it's present
in the Windows dependency graph. The FAQ is updated accordingly.
### Where to look
- `src/commands/picker/preview.rs` — `PreviewStateData` is now
in-memory; `PreviewMode::next`/`prev` rotation.
- `src/commands/picker/mod.rs` — `install_preview_tab_keybindings` (the
native bindings) and a `ModeWatcher`-free `run_skim`.
- `Cargo.toml` — TUI deps relocated out of the unix-only target table.
- `src/commands/{mod,worktree/mod,worktree/switch}.rs`, `src/main.rs` —
picker / `SwitchPipeline` gate removal.
- `src/commands/list/{ci_status,layout,collect,render}.rs`,
`src/summary.rs` — transitive gate / dead-code-suppression removal.
### Testing
Unit tests cover the rotation logic (`PreviewMode::next`/`prev`) and the
keymap wiring; the existing PTY integration tests in
`tests/integration_tests/switch_picker.rs` drive the real picker and
assert tab switching end-to-end (alt-N jump, tab/shift-tab cycle +
wrap). CI is green on all three platforms — `test (windows)` confirms
skim 4.8 + frizbee and their transitive deps compile and the suite
passes on Windows MSVC, which is the question this PR set out to answer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d9b8eb12e4 |
fix: picker cache, switch ref resolution, and statusline scans (#2842)
Correctness and performance fixes for worktrunk's git-operation paths —
the interactive picker, `wt switch` reference resolution, and the
statusline — surfaced by an automated `clawpatch` review and then
independently re-reviewed against the `reviewing-code` checklist (all
SOLID / Clean-design, zero critical issues).
## Fixes
- **The picker uses fresh repository state for each removal** — after a
removal mutated git's worktree inventory, the next `alt-r` reload could
plan against a stale cache.
- **`wt switch` prefers an exact local branch over stripping a remote
prefix** — a local branch literally named `origin/foo` was previously
retargeted.
- **`wt switch` fails closed on a malformed config when selecting a PR
provider** — a typo in `forge.platform` was silently swallowed and fell
back to GitHub.
- **A single-row statusline skips the repo-wide ahead/behind scan** — a
performance fix for large repositories.
- **Benchmark fix** — the piped-list time-to-first-output benchmark
measured a boundary that emitted no output.
- A clippy `collapsible_if` cleanup.
## Scope
Pure code — the clawpatch tooling and its state files are excluded. One
commit from the source branch that made the statusline skip network
CI/summary tasks was intentionally dropped: a statusline configured with
a CI segment is treated as an explicit opt-in to that network access, so
the statusline keeps its current behavior.
## Coverage
`codecov/patch` reports a gap on `invoke()`'s `changed_directory`
branch. Covering it in-process requires calling `invoke()`, which
mutates process-global cwd (`std::env::set_current_dir`) — a global side
effect `tests/CLAUDE.md` ("No Global State Mutations in Tests") forbids.
Per that doc's own guidance — accept a coverage gap rather than take
global side effects for coverage — the branch is left uncovered, and the
`codecov/patch` miss on those lines is accepted as a justified false
positive.
## Testing
`cargo run -- hook pre-merge --yes` — 3791 tests pass, lints clean.
> _This was written by Claude Code on behalf of Maximilian Roos_
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
50a30208cd |
bench(alias): use BatchSize::PerIteration for cold-cache variants (#2731)
## Summary Follow-up to #2728. `benches/alias.rs` was the last `iter_batched(invalidate_caches_auto, …, BatchSize::SmallInput)` site in the bench suite. Under `SmallInput`, criterion calls the setup closure once per batch and times the routines back-to-back inside one timing window — so only iter 1 per batch is actually cold, and iters 2-N hit the `worktrunk.default-branch` git-config entry that `build_hook_context` wrote on iter 1 (default-branch resolution is ~17ms on a synthetic repo per `benches/CLAUDE.md`). `BatchSize::PerIteration` runs `setup → time(routine)` per iter, so every measurement is genuinely cold. ## Measurement `dispatch/cold/1` (stub alias, 1 worktree): | | Before | After | |---|---|---| | Median | 23.14 ms | 23.94 ms | | Range | [22.34, 24.01] | [22.91, 25.03] | Median +6.9% (p=0.05, "Change within noise threshold"). The correction is small — alias dispatch is cheap (~23ms) and only the default-branch cache is being repopulated, so each warm iter saved ~1ms — but the direction is right and it brings the bench in line with the rest of the suite (#2728) and the `benches/CLAUDE.md` "Cache Handling" guidance. ## Test plan - [x] `cargo run -- hook pre-merge --yes` — 3684 tests pass, 0 failed - [x] `dispatch/cold/1` run before+after on this branch with `--save-baseline` / `--baseline` Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d7c8428e16 |
bench: use BatchSize::PerIteration for cold-cache variants (#2728)
## Summary Cold benchmark variants in `list.rs`, `remove.rs`, and `time_to_first_output.rs` were passing `BatchSize::SmallInput` to `iter_batched`. Under `SmallInput`, criterion calls the setup closure once per batch up front and then runs the routines back-to-back inside a single timing window — so when setup is `invalidate_caches_auto`, only iter 1 per batch is actually cold and iters 2-N read from the cache the previous routine just populated. The reported "cold" medians were warm-biased averages. `BatchSize::PerIteration` switches to `setup → time(routine)` per iter, so every measured iter is genuinely cold. The setup is far cheaper than a `wt` subprocess, so per-iter `Instant::now` overhead doesn't dominate. Codex flagged this on the sibling `picker_preview` bench (#2721); that PR landed `PerIteration` for its cold path, and this aligns the rest of the bench suite. ## Measured corrections | Variant | Before median | After median | Spread (before → after) | Median Δ | |---|---|---|---|---| | `list full/cold/8` | 113.5 ms | 107.1 ms | 16.0 → 3.9 ms | −5.7% (n.s., p=0.14) | | `remove_e2e/first_output` | 48.2 ms | 86.4 ms | 2.6 → 17.9 ms | **+44.4%** (p<0.001) | | `first_output/remove` | 50.5 ms | 69.6 ms | 2.4 → 0.65 ms | **+23.0%** (p<0.001) | `remove_e2e/first_output` is the starkest correction — `compute_integration_lazy` writes `is-ancestor` / `has-added-changes` / `merge-add-probe` entries, so warm-bias was substantial. `first_output/remove` is the cleanest demonstration of variance tightening (3.7× tighter spread). `list full/cold/8` doesn't move the median significantly but the upper-tail outliers that warm-bias was inflating disappear. ## Scope Only the four `iter_batched(… invalidate_caches_auto, …, BatchSize::SmallInput)` call sites are switched: - `benches/list.rs` `run_benchmark` (covers `skeleton`, `full`, `worktree_scaling`, `many_branches`, `divergent_branches` cold variants) - `benches/list.rs` `bench_real_repo` cold path - `benches/remove.rs` `first_output` - `benches/time_to_first_output.rs` `remove` Other `BatchSize::SmallInput` sites in `remove.rs` (`no_hooks`, `with_hooks`) use `recreate_worktree` as setup — that doesn't invalidate a cache the routine repopulates, so they stay as-is. `benches/CLAUDE.md` "Cache Handling" gets a paragraph so future bench authors reach for `PerIteration` when the setup invalidates a cache the routine repopulates. ## Test plan - [x] `cargo run -- hook pre-merge --yes` — 3667 tests pass, 0 failed - [x] Each cold variant run before+after on this branch with `--save-baseline` / `--baseline` Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
80ee2cb365 |
bench: measure wt switch picker preview pre-compute workload (#2721)
## Summary - Adds `picker_preview` benchmark group measuring "process spawn → all preview tasks drained" for `wt switch`'s interactive picker. - Introduces `WORKTRUNK_PREVIEW_BENCH=1`, an early-exit gate inside `handle_picker` that runs the full prelude (collect, speculative spawn, skeleton, initial + deferred precompute, `orchestrator.wait_for_idle()`) and returns before skim launches or any JSON / stderr I/O. Shares the dry-run path; behavior with the env var unset is unchanged. - Closes the coverage gap behind #2662 / #2683 / #2685 / #2704, which were tuned against `wt list` as a proxy because no direct picker measurement existed. ## Why this measurement Picker submits one preview-compute task per row to the global rayon pool. The user-visible quantity to optimize is the responsiveness window between picker launch and "all previews ready" (j/k navigation hits cached content). Option 1 from the task — headless wall clock to drain — is the cleanest measurable proxy and avoids the PTY route, which hits the documented nextest/SIGTTOU pain on `shell-integration-tests`. PTY-driven first-interactive-ready can be a follow-up. ## Variants - `picker_preview/warm/typical-8` - `picker_preview/cold/typical-8` Cold uses `BatchSize::PerIteration` (not `SmallInput`): `SmallInput` calls `setup` for an entire batch up front and then runs timed routines back-to-back, so only the first iter in each batch is genuinely cold — the rest hit a freshly populated `.git/wt/cache/`. `PerIteration` invalidates immediately before every measured iteration; setup is far cheaper than `wt switch`, so per-iter `Instant::now` doesn't dominate. `sample_size(10)` + `measurement_time(35s)` per #2685's lead — slow benches don't benefit from the default 30 samples. `cfg(unix)`-gated with a no-op `main` on Windows; the picker is Unix-only and `wt switch` (no args) hits the unavailable path before the env var is consulted. ## Sample run ``` picker_preview/warm/typical-8 time: [185.62 ms 191.72 ms 200.77 ms] picker_preview/cold/typical-8 time: [209.34 ms 226.23 ms 239.29 ms] ``` ## Test plan - [x] `cargo bench --bench picker_preview` runs cleanly on both variants - [x] `cargo run -- hook pre-merge --yes` — 3667 tests pass - [x] New `test_picker_preview_bench_produces_no_output` asserts `WORKTRUNK_PREVIEW_BENCH=1` keeps stdout/stderr empty (covers the env-gated branch, locks the no-I/O contract) - [x] Smoke test: `wt switch` with `WORKTRUNK_PREVIEW_BENCH` unset still hits the TTY error path (user-visible behavior unchanged) - [x] Smoke test: `WORKTRUNK_PICKER_DRY_RUN=1` still emits the cache JSON dump (regression check) - [x] `/review-codex` pass clean after iterating on three findings (packed-refs fix already on `main` via #2697 once branch was rebased; `BatchSize::PerIteration` for true per-iter invalidation; `cfg(unix)` gate for Windows) > _This was written by Claude Code on behalf of Maximilian Roos_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
de3ef902da | fix(bench): stop deleting packed-refs in invalidate_caches_auto (#2697) | ||
|
|
706fb5a1d9 |
fix(bench): trim real_repo sampling; restore sample_size(10) criterion floor (#2695)
## Summary
Two changes to `benches/list.rs`, no production code.
**1. Trim `bench_real_repo` sampling.** It inherited the
`criterion_group` default `sample_size(30)` / `measurement_time(15s)`.
On rust-lang/rust `wt list` runs ~2s warm (dominated by one deep `git
for-each-ref %(ahead-behind:main)` walk) and several times that for
`cold/8`, where every iteration also rebuilds eight 59k-entry git
indexes via `git status`. At >1s/iter Criterion can't fit 30 samples in
15s, so it just runs 30 single-iteration samples per variant — the group
as a whole runs ~15–20 min, dominated by `real_repo/cold/{4,8}`. Set
`sample_size(10)` (criterion 0.8's hard minimum) +
`measurement_time(20s)`, mirroring what #2685 did for
`bench_real_repo_many_branches`. The expensive cold variants drop from
30 to 10 iterations → group's measured time roughly thirds (~6–8 min).
No change to what `wt list` runs or measures.
**2. Fix a latent panic in `bench_real_repo_many_branches`.** #2685 set
it to `sample_size(5)`, but criterion 0.8's `sample_size` does
`assert!(n >= 10)` (`benchmark_group.rs:97`), so `cargo bench --bench
list` currently **panics on `main` during group registration** — for any
filter, before any benchmark runs. Restored to `sample_size(10)`. (This
was also a prerequisite for running `bench_real_repo` to validate change
#1.)
## Why not sparse-checkout the rust clone
A sparse checkout would make the per-variant setup near-instant and `git
status` / `git diff --shortstat HEAD` on the rust worktrees ~10ms
instead of ~1.8s / ~0.8s — but those costs are exactly what the `cold`
(cold 59k-entry index rebuild) and `warm` (working-tree scan over 59k
files) variants exist to measure. Sparse-checkout would turn "cold index
on a huge repo" into "cold index on ~20 files", erasing the documented
`cold ~4×` insight. The mechanism works; it's the wrong tradeoff.
Sharing one rust workspace across the warm/cold pair was also considered
and dropped — after the sampling trim, setup is a small slice (~1.5–2
min of ~7 min), and sharing would require up to ~10 GB of simultaneous
rust checkouts (CI disk risk) for a marginal gain.
## Before / after
`real_repo/warm/1` (rust-lang/rust clone, one worktree): median
**1.809s** (30 samples) → **1.794s** (10 samples). Criterion's
regression check on the after run: `change: [−23.1% −7.0% +10.8%] (p =
0.59)` → "No change in performance detected." The cold variants — the
bulk of the group — drop 3× by arithmetic (30→10 single iterations of
the same operation).
## Test plan
- [x] `cargo run -- hook pre-merge --yes` — 3645 tests pass, clippy +
fmt + pre-commit hooks pass
- [x] `cargo bench --bench list -- 'real_repo/warm/1'` — runs cleanly,
`sample_size(10)` accepted ("Collecting 10 samples"), result above
- [x] `cargo clippy --benches -- -D warnings`, `cargo check --benches`,
`cargo fmt --check`
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
df35af933b |
perf(bench): cheaper cargo bench --bench list (lazy fixtures + lighter real-repo sampling) (#2685)
## Summary Two changes that make `cargo bench --bench list` cheaper to run, especially when filtered. No production code, no change to what the benches measure on a full run. **1. Lazy fixture creation.** The synthetic `wt list` bench groups built their fixture repos *eagerly* at group-registration time: `bench_skeleton`, `bench_full`, `bench_worktree_scaling`, `bench_many_branches`, and `bench_divergent_branches` looped over their `(worktrees, cold)` variants and called `create_repo` for every one — before Criterion applied its positional `FILTER`. So `cargo bench --bench list skeleton/warm/8` still paid to build all 6 skeleton fixtures plus all 6 `full`, all 6 `worktree_scaling`, both `many_branches` (100 branches each), and both `divergent_branches` (200 branches × 20 commits each) before measuring one benchmark. This moves `create_repo` (and the `setup_fake_remote` / `git status` prep) inside the `bench_with_input` / `bench_function` closure — exactly how `bench_real_repo` and `bench_real_repo_many_branches` already work. Criterion invokes that closure once per benchmark and only for benchmarks the filter selects, so an excluded variant now creates no fixture at all. *Hermeticity is unchanged*: each benchmark still gets its own fresh `TempDir` + repo via `create_repo`, created exactly once, before `b.iter` (so never inside the measured region), alive for the whole measurement loop, dropped right after. Marginally cleaner if anything — a filtered-out benchmark now produces zero state instead of a stray TempDir. **2. Trim `real_repo_many_branches` sampling.** That group ran 10 samples against a 60s `measurement_time` — but at ~3.7s per `wt list --branches` iteration on rust-lang/rust, Criterion filled the budget with ~2 iterations per sample (~74s of measured time per `bench_function`). The warm-path variance is dominated by the slowest single subprocess (a deep `git merge-base` walking history), not measurement noise, so 5 samples already pins the signal. Cut to `sample_size(5)` + `measurement_time(20s)` — ≈ one iteration per sample (~18s), so the group's two bench_functions drop from ~3.6min to ~1.7min of measured time. (Cutting `sample_size` alone would have saved nothing — Criterion sizes iterations-per-sample to fill `measurement_time`.) ## Effect - `cargo bench --bench list <non-matching-filter>` → ~0.2s (measured), down from minutes of fixture churn - `cargo bench --bench list skeleton/warm/1` → 1 fixture built, was 6 - `real_repo_many_branches` group → ~1.7min measured, was ~3.6min - Full unfiltered `cargo bench --bench list` → fixture count unchanged; real-repo group ~2min faster ## Test plan - [x] `cargo check --benches` - [x] `cargo bench --bench list -- --test skeleton` — all 6 variants pass - [x] `cargo bench --bench list -- --test <non-matching>` — 0.23s, no fixtures created - [x] `cargo run -- hook pre-merge --yes` — 3585 tests pass, lints pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
db1beb61b8 |
Add wt-perf timeline subcommand for trace capture & rendering (#2558)
## Summary Adds `wt-perf timeline` to standardize how we capture and read traces for one-off `wt` invocations (alias dispatch, `wt list`, etc.). Replaces the prior `RUST_LOG=debug wt … 2>&1 | wt-perf trace > trace.json` dance with a single command that runs `wt`, captures stderr, parses `[wt-trace]` records, and either pretty-prints a sorted text timeline or emits Chrome Trace Format JSON for Perfetto. The text mode is the path I expect to use most — it's a column-aligned table sorted by start time, with subprocess totals and an externally-measured wall (`spawn → wait`) so the unobserved prelude/epilogue is visible: ``` ts(ms) dur tid kind name 0.000 11µs 1 span init_logging 0.043 3.979ms 1 span prewarm 0.058 3.882ms 1 cmd git rev-parse --git-common-dir … HEAD [wt-perf-typical-1] … 3 subprocesses totaling 11.315ms (slowest: 3.882ms git rev-parse … HEAD) traced: 14.838ms (first → last [wt-trace] record) wall: 19.367ms (spawn → wait; +4.529ms untraced prelude/epilogue) ``` ## What's in the diff - **`tests/helpers/wt-perf/src/main.rs`** — new `Timeline` subcommand. Resolves `wt` as a sibling of `current_exe()` (Windows-aware via `EXE_SUFFIX`), optionally invalidates caches with `--cold [--repo PATH]`, runs `wt` with `RUST_LOG=debug`, captures stderr, and renders. Uses `tabwriter` for column alignment, `Duration`'s std `Debug` for compact unit display, and `insta` for inline snapshot tests. - **`tests/helpers/wt-perf/Cargo.toml`** — new deps: `tabwriter` (release), `insta` (dev). - **`CLAUDE.md`** — fixes stale `--skip` syntax in the Benchmarks section (Criterion takes a positional FILTER, per #2547), replaces the non-existent `bench_list_by_worktree_count` example, adds a Traces section. - **`benches/CLAUDE.md`** — leads "Generating traces" with `wt-perf timeline`; the manual stderr-redirect pipeline is now framed as the path for traces from a log already on disk. - **`src/trace/mod.rs`** module doc — same usage block update so library doc and CLAUDE.md don't drift. - **`wt-perf setup`** printed suggestion — hints at `wt-perf timeline` instead of the manual stderr redirect. `wt-perf trace` (stdin/file → Chrome JSON) and `wt-perf cache-check` are unchanged — they still serve the case where you have a captured log and want to convert or analyze it without re-running. ## Test plan - [x] `cargo run -- hook pre-merge --yes` (3432 tests, lints, doctests) passes locally - [x] `wt-perf timeline -- -C \$REPO stub` end-to-end on a typical-1 fixture - [x] `wt-perf timeline --cold --repo \$REPO -- -C \$REPO stub` invalidates and reruns - [x] `wt-perf timeline --chrome -- -C \$REPO stub` produces valid JSON (loaded into `python -m json.tool`) - [x] `cargo test -p wt-perf --bin wt-perf` — 2 inline insta snapshots pass - [ ] CI green 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
05514ef44c |
docs(bench): replace --skip with positional FILTER (#2547)
Criterion 0.8 (the maintained criterion-rs fork — already the latest) has no `--skip` flag; only positional substring `[FILTER]` and `--exact`. Four invocations in `benches/CLAUDE.md` and the `benches/alias.rs` header comment used `--skip`, which fails with `unexpected argument '--skip' found`. Rewritten to use positional filters with concrete worked examples, plus a short note that Criterion has no exclusion flag and that benchmark IDs follow `<group>/<label>/<param>`. For the alias bench, `dispatch/warm` and `dispatch/cold` replace the previous `--skip cold` / `--skip with_vars` examples. Verified `cargo bench --bench alias dispatch/warm -- --measurement-time 5 --sample-size 10` runs exactly the two stub-warm variants. Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
c710147c8c |
fix(bench): unbreak wt-perf invalidate; consolidate env-isolation helpers (#2540)
## Summary Two bugs in `wt_perf::invalidate_caches_auto`, plus consolidation of the env-isolation helpers shared between tests and benches. ## Bugs fixed **(1) Silent unset under inherited `GIT_DIR`.** `invalidate_caches_auto` shelled to `git config --unset worktrunk.default-branch` via `worktrunk::shell_exec::Cmd`. `Cmd::apply_common_settings` re-injects normalized inherited `GIT_*` path vars resolved against the wt-perf process's startup cwd (issue #1914 — correct for `wt`-from-git-alias where parent and child target the same repo). For wt-perf, which operates on an explicit, possibly-unrelated repo path, the override redirected the unset to the wrong git dir; the resulting non-zero exit was swallowed by `let _ =`, and the cache stayed put. Reproducer: ```bash cd /tmp && GIT_DIR=.git target/release/wt-perf invalidate /tmp/wt-prof-test # previously: silently leaves worktrunk.default-branch in place # now: actually unsets it ``` **(2) Linked worktrees never cleared.** `invalidate_caches_auto` resolved `<repo_path>/.git` naively. When `repo_path` is a *linked* worktree, `.git` is a file (gitdir-pointer), not a directory, so every `fs::remove_*` call no-op'd silently — `.git/wt/cache/`, `.git/index`, `packed-refs`, commit-graph all survived. Existing bench callers all pass the main worktree, so no current bench was affected, but the function's contract was broken for linked-worktree inputs. ## Fixes - wt-perf no longer routes through `worktrunk::shell_exec::Cmd`. All `git` subprocess work uses `std::process::Command` directly via a small `git_command()` helper that delegates to `worktrunk::testing::configure_git_cmd`. The inherited-`GIT_*` normalization (which was the root of bug 1) doesn't apply to plain `Command`. - `invalidate_caches_auto` resolves the git common dir from the filesystem — reads `.git` as dir-or-pointer, strips `worktrees/<name>` for linked worktrees — so the cache root is correct regardless of which worktree of a repo is named. - The `let _ =` on the unset is gone; non-success/non-5 exits print a one-line `eprintln!` so a future regression can't hide the same way. ## Consolidation Touched four near-duplicate env-isolation helpers (`configure_cli_command`, `configure_git_cmd`, `configure_git_env`, `wt_perf::isolate_cmd`). The shared core is now in `worktrunk::testing`: - `pub fn isolate_subprocess_env(cmd, user_config: Option<&Path>)` — strips host context (`GIT_*`, `WORKTRUNK_*`, `NO_COLOR`, `SHELL`, `PSModulePath`) and sets baseline `WORKTRUNK_*_PATH`. Used by `configure_cli_command` (test-side, layers on test determinism) and every bench (no extra layer — benches want realism). - `pub fn scrub_git_path_vars(cmd)` — env_remove the five `GIT_*` repo-discovery vars. Called defensively from `configure_git_cmd`/`configure_git_env` so test code that spawns `git` outside a `wt`-parent isn't vulnerable to the same redirect. - `wt_perf::isolate_cmd` deleted. Five bench files now import `worktrunk::testing::isolate_subprocess_env` directly. - Path strings unified: `/nonexistent/wt/{config,approvals}.toml` and `/etc/xdg/worktrunk/config.toml` (preserved as the real XDG path). 47 snapshots regenerated. ## Test plan - [x] `cargo clippy --all-targets --workspace` clean - [x] 1072 + 607 lib unit tests pass - [x] 1589 integration tests pass (vs. baseline's 3 pre-existing failures, which the snapshot regen also resolved) - [x] All 5 benches compile (`cargo bench --no-run`) - [x] Bug 1 (GIT_DIR redirect): unset succeeds - [x] Bug 2 (linked worktree): cache and config both cleared from any worktree path - [x] Vanilla path: still works 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
528d702e6b |
perf(bench): add with_vars alias variant to dispatch group (#2534)
## Summary
- Pairs with the existing stub variant to bracket the dispatch curve.
Body references vars that drive the expensive paths in
`build_hook_context` — `commit` / `short_commit` (rev-parse fork),
`default_branch` (cold detection), `primary_worktree_path` (lookup).
- On main today, `build_hook_context` computes every var unconditionally
— so with_vars tracks stub within noise. The variant becomes informative
once a filter scopes context build to referenced vars only, at which
point stub drops to a post-filter floor and with_vars holds near today's
uniform cost, making the filter's delta visible.
- Existing stub labels (`dispatch/{warm,cold}/{1,100}`) are unchanged —
nightly bench gist stays continuous. New variants land under
`dispatch/with_vars/{warm,cold}/{1,100}`.
- `upstream` is omitted because the bench repo has no remote; adding
`setup_fake_remote` would change what the stub variant measures and
break label continuity.
## Test plan
- [x] `cargo run -- hook pre-merge --yes` — green (3425 tests)
- [x] Quick run of with_vars/warm/{1,100}: ~33–35ms (in line with stub
measurements)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
ffc8abe82a |
docs(network): codify local-first network access policy (#2536)
## Summary - Adds a **Network Access** subsection to `CLAUDE.md` "Command Execution Principles": worktrunk is local-first; the only fall-through-to-the-wire helper is the first `Repository::default_branch()` call per repo. A TTL cache does not authorize background polling. - Updates the `default_branch()` doc comment to state the contract explicitly — it's the sole "look it up if missing" helper allowed to hit the wire. - Notes in `benches/CLAUDE.md` that `setup_fake_remote` keeps every cold-cache benchmark on the local `<r>/HEAD` fallback (~17 ms) rather than the real `git ls-remote` path (100 ms–2 s in the wild). The policy makes the rule visible before someone adds the next "lookup" path that silently walks out to the wire — alias dispatch, hook context build, or `wt statusline` triggering CI status per prompt are the existing instances of the failure mode (left as separate follow-ups, not in this PR). ## Test plan - [x] Docs-only — no behaviour change, no test impact 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b27dc90777 |
refactor(bench): rename alias bench's NOOP_CONFIG → STUB_CONFIG (#2533)
## Summary - `noop` is misleading — the alias still forks `/bin/sh -c 'echo hello'`, which is the dominant cost the bench measures. - `stub` accurately conveys "minimal alias body, parent-side dispatch dominates" without claiming the dispatch itself is free. - Bench output labels (`dispatch/warm/1`, `dispatch/cold/100`, …) are unchanged — the rename is internal const + alias name + header comment. ## Test plan - [x] `cargo run -- hook pre-merge --yes` — green (3425 tests pass, all lints pass) - [x] `cargo check --benches` — clean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
c33cb8c665 |
Fix copy-ignored symlink destination boundary (#2501)
## Summary Fixes a destination-boundary issue in `wt step copy-ignored`. Previously, a linked worktree could contain a symlinked destination directory such as `target -> /tmp/outside`. When `copy-ignored` copied ignored files from another worktree, it would create nested directories and files through that symlink, writing outside the destination worktree. `--force` made the same boundary issue more risky because it could overwrite outside files. This PR adds guarded copy entry points for `copy-ignored` that resolve the destination ancestry with `canonicalize_with_parents` and reject paths whose resolved parent chain escapes the destination worktree root. The guard checks parent ancestry rather than the final leaf so existing leaf-symlink behavior stays intact: without `--force` leaf symlinks are skipped, and with `--force` the symlink itself is replaced. ## Changes - Add `copy_leaf_within_root` and `copy_dir_recursive_within_root`. - Route `wt step copy-ignored` through the guarded copy functions. - Add Unix regression tests for symlinked destination directories, including the `--force` case. ## Tests - `cargo fmt --all --check` - `cargo clippy --all-targets --all-features -- -D warnings` - `cargo test --lib copy` - `cargo test --test integration step_copy_ignored` --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: worktrunk-bot <worktrunk-bot@users.noreply.github.com> |
||
|
|
810cdc6636 |
feat(remove): add TTY progress spinner to --foreground (#2420)
## Summary Adds a stderr spinner (`⠼ Removing 7,272 files · 64.5 MiB`) to `wt remove --foreground`, driven by the same machinery #2413 introduced for `wt step copy-ignored`. Large worktrees (e.g. a fat `node_modules`) no longer hang with no feedback while the trash directory is unlinked. ## Approach Generalizes the spinner from copy-specific to verb-parameterized: - `src/copy_progress.rs` → `src/progress.rs`; `CopyProgress` → `Progress`; `start(verb)` takes `"Copying"` or `"Removing"`; method renamed `file_copied` → `record`. The TTY gate, 300ms startup delay, IEC-byte formatting, and Drop-based line clearing all carry over unchanged. A private `Progress::enabled(verb)` ctor is shared between `start()` (TTY-gated) and the test code that needs an enabled instance without a real terminal. - New `src/remove_dir.rs` — `remove_dir_with_progress(path, &Progress)` walks the tree iteratively, unlinks files on a dedicated 4-thread `REMOVE_POOL` (matching `copy::COPY_POOL`), then `rmdir`s deepest-first. Returns `(files, bytes)` so the post-op summary can match the spinner. Best-effort throughout — read/unlink/rmdir errors are silently skipped so the caller can always report the count of leaves we did manage to remove (the worktree is already pruned at this point, a stuck trash entry is recoverable). - `src/output/handlers.rs` — `cleanup_staged_with_progress` helper gates on `verbosity() >= 1`. Plumbed into both foreground branches (named + detached HEAD). `print_message` now takes `Option<(usize, u64)>`; background passes `None`. - The foreground success line gains a gray `(N files · X MiB)` suffix using `format_stats_paren`, matching the spinner's units (per the user-output skill's "stats parentheses" convention). `remove_worktree_with_cleanup`, the background path, and the picker flow are intentionally untouched. ## Snapshot stability The byte total walks the renamed worktree's `.git` pointer file, whose content is the gitdir's absolute path — so the byte count is sensitive to the temp-dir prefix (macOS `/var/folders/...` vs Linux `/tmp/...` vs Windows). An insta filter normalizes the byte value to `[BYTES]` inside the `(N files · X UNIT)` paren shape; the deterministic copy-ignored summary (`Copied N files · X B` — no surrounding parens) stays asserted unchanged. ## Verification - Unit tests in `progress` and `remove_dir` modules (including a permission-skip test for the read_dir-fails path) - Integration snapshots updated to include the new gray suffix - `cargo run -- hook pre-merge --yes` passes locally - Manual TTY smoke test under tmux: spinner renders, ticks, gets cleared before the summary; success line shows `(10,001 files · [BYTES] KiB)` for a 10k-file worktree - Manual pipe smoke test: no spinner frames, summary still shows the counts 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
da349b30d8 |
feat(step-copy-ignored): add TTY progress spinner (#2413)
Large copies of `node_modules`, `build/`, or `target/` ran silently — if the walk or the copy was slow, users couldn't tell whether `wt step copy-ignored` was making progress or hung. Adds a stderr-only spinner: `⠼ Copying 7,272 files · 64.5 MiB`, updated in place at ~10Hz, only while a real TTY is on the other end. The post-copy summary repeats the same units (`✓ Copied 7,272 files · 64.5 MiB`) so the spinner's information isn't dropped on completion. Gate: stderr-is-TTY, `verbosity == 0` (verbose mode already lists every entry), and not `--dry-run`. A 300ms startup delay keeps sub-second copies silent — short copies never render a line at all. Non-TTY output is byte-for-byte unchanged apart from the new byte total in the summary. ### Approach - New `src/copy_progress.rs`: `CopyProgress` is `Option<Inner>` — disabled is `None`, enabled holds an `Arc<Shared>` of atomic counters plus a crossterm ticker `JoinHandle`. `start()` is named deliberately (not `new()`) because it spawns a thread — `Default` semantics would be misleading. `Drop` joins the ticker and clears the line. - `copy_leaf` returns `Result<Option<u64>>` (bytes on copy, `None` on skip). Bytes come from the `symlink_metadata` we already stat, so no extra syscall — actually one fewer on Unix since we now reuse it for permissions too. - `copy_dir_recursive` returns `Result<(usize, u64)>` and takes `&CopyProgress`; rayon workers bump `progress.file_copied(bytes)`. The summary in `step_copy_ignored` reuses the totals. - New skill section in `.claude/skills/writing-user-outputs/SKILL.md` pointing future authors at `CopyProgress` as the spinner pattern. ### Testing Unit tests cover `format_count`, `format_bytes` (IEC), `format_line`, `render_line`/`clear_line` (against an in-memory `Vec<u8>`), the disabled no-op path, and the enabled ticker lifecycle (via a test-module-local helper that bypasses the TTY check). Manual smoke via `script(1)`: big copies render the in-place spinner cleanly; small copies (<300ms) render nothing beyond the summary; pipes, `-v`, and `--dry-run` are unchanged. `codecov/patch` covers ~94% of the diff. The remaining ~10 uncovered lines are the enabled branch of `start()` (Arc allocation + thread spawn), which can't be reached without a real TTY in tests. Equivalent logic is exercised by the test-module helper. Closes #2401. Thanks to @tehdb for the suggestion. > _This was written by Claude Code on behalf of Maximilian_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
073a128ad3 |
perf(alias): benchmark parent-side alias dispatch overhead (#2340)
## Summary
Adds `benches/alias.rs` to measure the parent-side dispatch overhead of
`wt <alias>` — the wall-clock cost paid *before* the alias body runs
(config load, repo open, template context build, fork+exec of the child
shell).
Issue #2322 reports `wt <alias>` as dramatically slower than the
equivalent subcommand. Commit
|
||
|
|
83d6d076bb |
bench: invalidate wt caches per iteration in remove/TTFO benches (#2341)
## Summary `wt remove` populates `.git/wt/cache/` via `compute_integration_lazy` whenever `BranchDeletionMode` isn't force-delete (the CLI's `--force` is `force_worktree`, not `--force-delete`). The `remove_e2e/first_output` and `first_output/remove` benches used `b.iter` with no invalidation, so iter 1 was cold and iter 2+ hit the cache — reported timings reflected warm-cache cost, not the first-invocation TTFO users see. ## Changes - `benches/remove.rs::first_output` and `benches/time_to_first_output.rs::remove` switch to `iter_batched` + `invalidate_caches_auto`. - `wt_perf::invalidate_caches_auto` now also unsets `worktrunk.default-branch` (git config). That cache contributes ~17ms per iteration on a typical-8 repo (166ms with default-branch cached vs 183ms fully cold) — measurable but small enough that always clearing it is simpler than introducing a second mode. - `benches/CLAUDE.md` documents the rule, the full list of what `invalidate_caches_auto` clears (including what it deliberately preserves — `worktrunk.history`, `worktrunk.hints.*`, `worktrunk.state.*`, `.git/wt/logs/`, `.git/wt/trash/`), and which commands populate `.git/wt/cache/`. Audit found no other benches affected: `completion.rs` and `cow_copy.rs` don't touch the cache; `list.rs` already wires warm/cold via `BenchConfig`; `wt switch` and `wt list` under `WORKTRUNK_FIRST_OUTPUT=1` exit before the writers run; `remove.rs::no_hooks`/`with_hooks` short-circuit at `same_commit` because `recreate_worktree` rebuilds the branch at main's HEAD. > _This was written by Claude Code on behalf of Maximilian Roos_ Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
d2d38dd458 |
docs(perf): require --progressive in wt-perf trace pipeline and trim duplication (#2258)
Two TTY-gated trace events (`Skeleton rendered`, `First result received`) don't fire when `wt list` detects a non-TTY stdout, which every piped `wt-perf` invocation does. Every documented capture pipeline now passes `--progressive` so all seven instant events flow through `wt-perf trace` / `cache-check`. Fixes 10 sites across module docs, clap `after_long_help`, setup eprintln, and `benches/CLAUDE.md`. While in the area, I also consolidated some of the scattered examples: - Dropped the `# CLI Usage` block from `tests/helpers/wt-perf/src/lib.rs` — library docs shouldn't carry CLI examples, and they fully duplicate `wt-perf --help`. - Trimmed the `main.rs` module docstring from four example invocations to a one-liner pointing at `wt-perf --help`. - Dropped the "with benchmark repo" variant from `cache-check`'s `after_long_help`, since it's structurally identical to the first example with `-C <path>` prepended. - Replaced the stdin-hint and no-entries error in `wt-perf` (which repeated the full pipeline) with a pointer to `<subcommand> --help`. - Pruned `src/trace/chrome.rs`'s standalone usage example to defer to `crate::trace`. Verified end-to-end: `RUST_LOG=debug wt list --progressive 2>&1 | wt-perf trace` emits all seven expected instant events in the Chrome Trace JSON. > _This was written by Claude Code on behalf of Maximilian Roos_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
c3484b3b63 |
docs(perf): drop redundant grep wt-trace from wt-perf pipelines (#2257)
The trace parser already filters for the `[wt-trace]` marker internally
(`parse_lines` → `parse_line` uses `line.find("[wt-trace] ")?`), so the
upstream `grep` was redundant. Dropping it makes the documented
pipelines shorter and removes an inconsistency between callers that used
`grep wt-trace` vs `grep '\[wt-trace\]'`.
Changes span 12 sites across 6 files: module docs, `wt-perf`'s own CLI
help (trace/cache-check subcommands, setup eprintln, stdin hint,
empty-input error), `benches/CLAUDE.md`, and the `running-tend` skill.
Also fixed a stale reference in `src/trace/chrome.rs` to the old
`analyze-trace` binary name (now `cargo run -p wt-perf -- trace`).
Verified end-to-end: `RUST_LOG=debug wt list 2>&1 | wt-perf trace`
produces valid Chrome Trace JSON, and `| wt-perf cache-check` produces
the expected structured report.
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
d50bdfcde0 |
Cache remaining expensive tasks, unify wt list and picker (#2098)
Adds persistent SHA-keyed caching for `is_ancestor`, `has_added_changes`, and `branch_diff_stats` — the last three operations still running unconditionally in both `wt list` and the `wt switch` picker. With all five merge-base-dependent tasks now cached (joining `merge-tree-conflicts` and `merge-add-probe` from PR #2085), the `stale_branches` skip mechanism becomes redundant: the picker's "skip tasks on branches > 50 behind main" optimization was papering over slow first-run cost that the cache now handles eternally. ## What changed **New caches** (`src/git/repository/probe_cache.rs`): three new asymmetric SHA-keyed kinds — `is-ancestor`, `has-added-changes`, `diff-stats` — all using the same LRU-swept persistent file layout as the existing kinds. `branch_diff_stats` skips its cache when sparse checkout is active (path filters make the result environment-dependent). **Dead plumbing removed**: `EXPENSIVE_TASKS` constant, `CollectOptions::stale_branches` field, `skip_expensive_for_stale` parameter on `collect()`, the env-var gate in `list/mod.rs`, and two obsolete tests + snapshots. Both `wt list` and the picker now run the same task set. **`batch_ahead_behind` unconditional**: previously only the picker called it. Making it unconditional replaces N `rev-list --count` calls with one `for-each-ref`, a pure win for `wt list` too. **Branch-ref semantics unified across all tasks**: all task implementations now prefer the branch name over `ctx.branch_ref.commit_sha` when one is present. Previously, `IsAncestorTask`, `BranchDiffTask`, `MergeTreeConflictsTask`, and `CommittedTreesMatchTask` used the worktree's current HEAD sha, which during a rebase-in-progress is transiently at the replayed commit rather than the branch tip. This produced contradictory rows: `is_ancestor=true` + `1 ahead / 1 behind`. Now all tasks consistently report the branch's state, matching `git status` semantics. Addressed worktrunk-bot review feedback. **Caching docs updated**: the `## Caching` section in `collect/mod.rs` (from PR #2097) now lists the three tasks as cached rather than "cacheable but uncached". Also addressed worktrunk-bot PR overlap observation. **Benchmarks**: `invalidate_caches_auto()` now clears `.git/wt/cache/` so cold-cache benchmarks exercise cold state. The `warm_optimized` variant in `real_repo_many_branches` collapsed since it no longer measures anything different from `warm`. ## Testing New probe_cache unit tests cover roundtrip + tamper-based cache consultation for each new kind, plus a `clear_all_covers_all_kinds` regression test. Snapshot updates for `test_list_maximum_status_with_git_operation` and `test_list_json_with_git_operation` reflect the branch-ref fix — mid-rebase worktrees now show `✗` (real conflicts) and actual diff stats instead of the transient HEAD's empty state. > _This was written by Claude Code on behalf of @max-sixty_ Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
679fe539fe |
Deprecate --no-verify in favor of --no-hooks (#1932)
`--no-hooks` describes what the flag does — skip hooks. `--no-verify` was inherited from git's naming but doesn't match worktrunk's semantics (there's no "verification" step being skipped). `--no-verify` remains as a hidden alias that emits a deprecation warning, retained for at least one release cycle per the project's deprecation policy. Changes across switch, remove, merge, step commit, and step squash: - `--no-hooks` is the canonical visible flag - `--no-verify` hidden, emits `▲ --no-verify is deprecated; use --no-hooks instead` - Error hints (`↳ To skip pre-merge hooks, re-run with --no-hooks`), info messages, help text, docs, and config examples all updated - `resolve_verify()` helper in main.rs deduplicates the deprecation logic - Backward-compatibility test verifies `--no-verify` still works > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
6326068b80 |
perf: switch diff stats from --numstat to --shortstat (#1917)
The three callsites that compute diff totals (`branch_diff_stats`, `working_tree_diff_stats`, `diff_stats_summary`) were using `--numstat`, which outputs one line per changed file. Since we only sum the totals, `--shortstat` gives the same data in a single line — reducing command output from O(files) to O(1) per worktree. Most noticeable with `-vv`, where all command output is logged at debug level. A branch touching 500 files now produces 1 line of debug output instead of 500. The `--numstat` path (via `parse_numstat_line`) is retained for `log_formatter.rs`, which needs per-file stats for individual commits. `DiffStats::from_numstat` and `LineDiff::from_numstat` are removed — no production callers remained. > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
f6e89009e5 |
fix: detect squash-merged branches when merge-tree conflicts (#1820)
## Problem `wt step prune` (and `wt remove`) fail to detect squash-merged branches when the default branch has since modified the same files that the branch touched. This is because `git merge-tree --write-tree` reports conflicts when both sides changed the same files, and the code conservatively treated conflicts as "not integrated." The same issue affects `wt list` — the `WouldMergeAdd` task calls the same function, so the integration symbol `⊂` is not shown for these branches. ## Solution When `git merge-tree --write-tree` conflicts, fall back to **patch-id matching**: compute the branch's squashed patch-id (`git diff-tree -p merge-base..branch | git patch-id --verbatim`) and check if any commit on the target has a matching patch-id. This detects squash merges because the squash-merge commit on the target has the exact same content changes as the branch. Uses `--verbatim` (not `--stable`) to avoid false positives from whitespace normalization — `--stable` strips whitespace, so tabs-vs-spaces would produce matching patch-ids even though file content differs. The fallback is only triggered when merge-tree conflicts — the happy path (no conflicts) is unchanged. The merge-tree → patch-id sequence is extracted into `Repository::merge_integration_probe()`, a shared method used by both `wt list` (parallel tasks) and `wt remove`/`wt merge` (sequential path). This fixes a drift where the two call sites handled patch-id errors differently. ## Known limitation The patch-id fallback checks if the branch's diff was **ever** applied to the target, not whether the effect **persists**. If a squash merge is later reverted on the target and then the same files are modified (causing merge-tree conflicts), the historical squash-merge commit's patch-id still matches. This is documented in #1818. A follow-up could add a revert-detection check after finding a match. ## Testing - `test_remove_squash_merged_then_same_files_modified` — reproduces the exact scenario from #1818 (branch modifies file, squash-merged, then target modifies same file) - `test_prune_squash_merged_same_files_modified` — verifies `wt step prune --dry-run` detects the branch - All existing squash-merge tests continue to pass (5 remove + 22 prune tests) Closes #1818 > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Maximilian Roos <5635139+max-sixty@users.noreply.github.com> Co-authored-by: Maximilian Roos <m@maxroos.com> |
||
|
|
6279905525 |
docs: restructure trace queries around performance questions (#1884)
Replaces the ad-hoc trace query collection (including the narrow status-vs-trees overlap query) with queries organized around three performance questions: 1. **Where does time go?** — slowest commands + task type breakdown (kept from previous PR, reorganized) 2. **How parallel are we?** — new query comparing total subprocess time to subprocess span 3. **What's on the critical path?** — phase durations from milestones + per-worktree breakdown via `EXTRACT_ARG`. Honestly noted as incomplete — the trace format doesn't capture task dependencies, so Perfetto visualization is better for real critical path analysis. All queries validated against a real trace from a `typical-4` benchmark repo. > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
afa0a478c9 |
refactor: isolate benchmark commands and consolidate shared utilities into wt-perf (#1878)
Add `isolate_cmd` and `run_git_ok` to the shared `wt-perf` crate and call `isolate_cmd` from all benchmark command sites. Previously only `benches/remove.rs` isolated commands from the host environment; the other benchmarks inherited host `GIT_*`, `WORKTRUNK_*`, `SHELL`, and `NO_COLOR` vars, making results non-reproducible across environments. `isolate_cmd` takes `Option<&Path>` for the user config — `None` uses a nonexistent path (defaults only), `Some(path)` uses a real config file (e.g., `remove.rs` with hooks). `run_git_ok` is the non-panicking variant of `run_git` for best-effort cleanup operations. > _This was written by Claude Code on behalf of @max-sixty_ Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
ea1eb0fda3 |
docs: complete trace event SQL categories in benchmarks (#1873)
The task type breakdown query in `benches/CLAUDE.md` only had 5
categories, leaving most commands as "other". Surveyed all git commands
from `wt list`, `wt list --branches`, and `wt list --full --branches`
traces against a `typical-4` benchmark repo to build a complete taxonomy
of 22 categories.
Key fixes: tightened `%rev-parse%tree%` to `%rev-parse%{tree}%` (was
misclassifying `--is-inside-work-tree` as `trees_match`), added
`rev_list` for commit counting, `diff_3dot`/`diff_wt` for full-mode
diffs, `sparse_checkout`, `llm_summary` for Claude commit summaries, and
all remaining standard categories. Verified zero commands fall to
"other" across all three trace modes.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
b1ab14c488 |
chore: remove run_git_ok helper from remove benchmark (#1871)
Inline the two cleanup calls using `Cmd::new("git")...run()` directly
instead of a dedicated `run_git_ok` wrapper — the same pattern used
throughout the codebase for fallible git commands.
Follow-up to #1858.
> _This was written by Claude Code on behalf of maximilian_
---------
Co-authored-by: Claude <noreply@anthropic.com>
|