Commit Graph

12 Commits

Author SHA1 Message Date
Maximilian Roos 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>
2026-08-09 15:36:11 -07:00
Maximilian Roos 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_
2026-08-08 13:38:00 -07:00
Maximilian Roos 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_.
2026-08-05 10:30:34 -07:00
Maximilian Roos 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_.
2026-07-30 10:32:08 -07:00
Maximilian Roos 18d2bef84b refactor(perf): canonicalize the benchmark / wt-perf system (#3403)
Implements the design proposal from this branch's first commit
(b22aced85, `design/simplify-bench-perf.md` — reviewable there; dropped
from the merge candidate per the `design/` convention). The perf
tooling's emission side was already canonical; this consolidates the
consumption side around one split: capture/harness work in `wt-perf`,
analysis in `worktrunk::trace` with `wt config state logs profile` as
its one CLI surface.

The seven design items:

1. **`wt-perf cache-check` deleted** — its output was the `cache` field
of `logs profile --format=json`. The tend statusline recipe now uses
`logs profile` (its old pipe also fed human-format stderr into a JSON
parser, so it's now correct as well as canonical).
2. **Timeline renderer moved into `worktrunk::trace`**
(`src/trace/timeline.rs`), sharing
`command_label`/`render_table`/`fmt_dur` with the profile so the two
views can't drift (they already had); drops wt-perf's `tabwriter` and
`insta` deps. Durations now render in the profile's fixed-point format.
3. **`Profile.by_context`** (BY CONTEXT table + JSON array):
per-worktree subprocess totals, the one analysis that previously
required trace_processor SQL. The SQL sections in `benches/CLAUDE.md`
and the `trace` module docs are replaced by a `logs profile` pointer;
Perfetto stays for visual critical-path work.
4. **One warm/cold bench runner** (`wt_perf::bench_wt`) owns the
`BatchSize::PerIteration` rationale once, replacing five inline copies.
All runs now assert child exit status (`benches/list.rs` previously
ignored it).
5. **Bench matrix pruned**: `real_repo` keeps only the 8-worktree
variants (scaling shape stays on synthetic `worktree_scaling`),
`cow_copy` deleted (benchmarked a hand-written serial copy against
production rayon — a settled choice), `remove_e2e/first_output` deleted
(duplicate of `first_output/remove`). Ends three gist series with this
PR as the single discontinuity point; saves roughly 15–20 min of the
~80-min daily bench run.
6. **wt-perf CLI tests moved in-package** (native
`CARGO_BIN_EXE_wt-perf`): the dummy `builds.rs` is gone and the nextest
setup script builds only `mock-stub`. Note `cargo test --test
integration` no longer runs these four tests; full-workspace runs still
do.
7. **`parse_config` returns a `SetupConfig` enum** covering `mixed-W-B`;
the two fixture builders share `init_bench_repo`.

**Merge with main (#3401):** the new prune benches and fixtures landed
mid-flight in exactly these files; the resolution routes them through
the consolidated API — `prune-M-U` is a `SetupConfig::Prune` variant,
the pair parsing is one shared `parse_pair`, and `prune-real` keeps its
own cache-managed path (no `--path`, self-repairing). `benches/prune.rs`
keeps its custom `iter_batched` arms deliberately: every variant asserts
the candidate count from stdout, which `bench_wt`'s shape doesn't
express — the exact carve-out the cache-handling docs describe.

Two review passes (correctness + subtraction) then folded in:
`CacheReport` trimmed to its same-context fields (the dropped six
duplicated `Profile` fields or counted cross-context noise the text
report never showed), `render_table` pads by display width (replacing an
ASCII-only invariant the new BY CONTEXT table violated), the timeline's
instant-event row gained test coverage, and Chrome args dropped the
redundant `duration_ms`.

**Decisions surfaced, not taken** (from the review passes): (a) `wt-perf
setup` could become `--persist`-only, dropping the interactive
Enter-to-cleanup path — a UX preference; (b) whether the daily benchmark
cron's gist time series justifies its remaining ~60 min/day is a
cost/value call. Also deferred: measuring whether `skeleton/cold` adds
signal over `skeleton/warm` before cutting it.

Gate green throughout (4374 tests); `clippy --features
shell-integration-tests` clean; `setup`/`timeline` (warm, cold,
chrome)/`logs profile` (text + JSON)/`trace` all driven end-to-end on a
scratch `mixed-4-8` repo.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 03:44:18 -07:00
Maximilian Roos 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>
2026-05-11 17:50:27 -07:00
Maximilian Roos 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>
2026-05-02 19:24:21 -07:00
Maximilian Roos 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>
2026-04-20 12:49:32 -07:00
Maximilian Roos 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>
2026-04-06 15:01:18 -07:00
Maximilian Roos 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>
2026-04-01 13:45:54 -07:00
Maximilian Roos 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>
2026-04-01 10:49:51 -07:00
Maximilian Roos bd87f2a146 fix: background removal blocks for 1s due to shell parsing bug (#1858)
`spawn_detached_unix` constructed the shell command as `sh -c "sleep 1
&& rmdir ...; rm -rf ... &"`. In POSIX shell, `;` has lower precedence
than `&`, so this parses as two statements: `sleep 1 && rmdir ...` runs
**synchronously** (1 second block), then only `rm -rf ... &` is
backgrounded. Every `wt remove` paid a 1-second penalty.

The fix wraps compound commands in braces — `{ sleep 1 && rmdir ...; rm
-rf ...; } &` — so `&` backgrounds the entire group. This affects all
callers of `spawn_detached` (remove, prune, merge, hooks).

Tests that asserted `!path.exists()` after removal now use
`assert_worktree_removed()` which accepts an empty placeholder directory
(the placeholder is cleaned up by the now-correctly-backgrounded `sleep
1 && rmdir`). Also fixes a pre-existing race in
`test_standalone_hook_post_merge` / `post_create` where background hooks
were checked immediately instead of polled.

Also adds `benches/remove.rs` for measuring end-to-end remove
performance.

> _This was written by Claude Code on behalf of maximilian_

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-31 21:05:20 -07:00