Skip to content

perf(alias): skip unused template-var accessors in hook context#2353

Open
max-sixty wants to merge 4 commits intomainfrom
2322-lazy-vars
Open

perf(alias): skip unused template-var accessors in hook context#2353
max-sixty wants to merge 4 commits intomainfrom
2322-lazy-vars

Conversation

@max-sixty
Copy link
Copy Markdown
Owner

build_hook_context used to run git rev-parse <branch> for {{ commit }} and git for-each-ref for {{ upstream }} on every alias and hook dispatch, regardless of whether the pipeline referenced those vars. The referenced_vars_for_config set (already computed for --KEY=VALUE routing) now gates each accessor: a var is populated only if some template in the pipeline references it.

Addresses the parent-side piece of #2322: on macOS hosts with slow execve (~200 ms/fork), simple aliases now skip two guaranteed forks. On healthy systems each saved fork is ~3–5 ms, so the proportional win is similar but the absolute number is smaller.

Scope

  • run_alias and prepare_steps pass the referenced_vars_for_config union to gate accessors.
  • Preview/debug paths (wt step eval, wt switch --execute, wt hook <type> preview, wt step for-each) pass None — they want the full context for inspection.
  • Verbose mode (-v) at alias and hook dispatch sites passes None: the template-variable table prints (unset) for anything not populated, and users running -v are debugging and expect every var.

Accurate error hints

expand_shell_template now takes an optional ValidationScope. When template expansion fails with an undefined-variable error, the "Available variables" hint lists every var the scope supports (via vars_available_in) rather than only the keys currently populated at runtime. Without this, lazy gating would narrow the hint — a template with a typo or a legitimately-unset var would see a shorter list omitting vars the pipeline didn't reference. The hint is meant to answer "what could you have typed," which is a property of the scope, not of this invocation's gated context.

As a bonus the hint now correctly lists scope-valid vars that weren't populated eagerly either (upstream, owner, pr_number, pr_url, hook_name for a pre-start hook), so users chasing "undefined value" for those vars see them suggested.

Benchmarks (benches/alias, warm OS cache, macOS)

The bench matrix now covers three alias variants against the same repo: noop (zero vars), commit ({{ commit }}), everything (all gated vars via | default).

variant warm/1 warm/100 delta vs pre-#2340 noop baseline
noop 45 ms 45 ms −18% / −22%
commit 52 ms 49 ms noop + rev-parse fork
everything 54 ms 49 ms noop + rev-parse + for-each-ref

commit/everything land close to the pre-change baseline because they exercise the same accessors as before. The gap between noop and commit/everything (~4–7 ms) is the cost of the 2 forks lazy gating avoids — extrapolated to Mark's macOS (#2322) that's ~400 ms saved per simple-alias dispatch.

Test plan

  • cargo run -- hook pre-merge --yes — 3318 tests, 0 failures; lints green
  • RUST_LOG=worktrunk=debug wt noop trace confirms noop alias drops from 7 → 5 git subprocesses in the parent; wt showall (references all gated vars) keeps 7

Co-Authored-By: Claude noreply@anthropic.com

max-sixty and others added 3 commits April 20, 2026 21:30
`build_hook_context` used to run `git rev-parse <branch>` for
`{{ commit }}` and `git for-each-ref` for `{{ upstream }}` on every
alias and hook dispatch, even when the pipeline never referenced those
vars. The `referenced_vars_for_config` set (already computed for
`--KEY=VALUE` routing) now gates each accessor: a var is populated only
if some template in the pipeline references it.

For a no-var alias (`echo hello`), parent-side dispatch drops from 7
git subprocesses to 5 — the `rev-parse` and `for-each-ref` disappear.
Addresses issue #2322 where slow `execve` (~200ms/fork on affected
macOS hosts) makes `wt <alias>` 2× slower than the equivalent
subcommand. On healthy systems each saved fork is ~3-5ms, so the
absolute win is smaller but proportionally similar (~15-20%).

## Scope

- `run_alias` and `prepare_steps` (hook pipelines) pass the
  `referenced_vars_for_config` union to gate accessors.
- Preview/debug paths (`wt step eval`, `wt switch --execute`,
  `wt hook <type>` preview, `wt step for-each`, hook preview) pass
  `None` — they want the full context for inspection.
- Verbose mode (`-v`) at alias and hook dispatch sites passes `None`:
  the template-variable table prints `(unset)` for anything not
  populated, and users running `-v` are debugging and expect every var.

## Benchmarks (`benches/alias`, macOS, warm OS cache)

The bench matrix now covers three alias variants against the same repo:
`noop` (zero vars), `commit` ({{ commit }}), `everything` (all gated
vars via `| default`).

After (median, from criterion):
  dispatch/noop/warm/1         45 ms  (baseline 56 ms, −18%)
  dispatch/noop/warm/100       45 ms  (baseline 58 ms, −22%)
  dispatch/commit/warm/1       52 ms  (noop + rev-parse)
  dispatch/commit/warm/100     49 ms
  dispatch/everything/warm/1   54 ms  (noop + rev-parse + for-each-ref)
  dispatch/everything/warm/100 49 ms

The `commit`/`everything` rows land close to the pre-change baseline
because they exercise the same accessors as before.

## Snapshot change

`post_create_upstream_template.snap` — the "Available variables" hint
in template-expansion errors now lists only vars populated for this
invocation (15 instead of 22), since lazy gating skips vars no template
in the pipeline references. The hint is still truthful; users who want
the complete list can pass `-v`.

Co-Authored-By: Claude <noreply@anthropic.com>
`expand_shell_template` now takes an optional `ValidationScope`. When
template expansion fails with an undefined-variable error, the
"Available variables" hint lists every var the scope supports
(via `vars_available_in`) rather than only the keys currently populated
in the runtime context.

Without this, lazy hook-context gating made the hint narrower than
before — a template with a typo or a legitimately unset var (e.g.
`{{ upstream }}` with no tracking configured) would see a shorter list
omitting vars the pipeline didn't reference. That's a real diagnostic
regression: the list is meant to tell users "here's what you could have
typed," which is a property of the scope, not of this invocation's
gated context.

As a bonus, the hint now correctly lists scope-valid vars that were
never populated eagerly either — `upstream`, `owner`, `pr_number`,
`pr_url`, `hook_name` for a pre-start hook — so users chasing
"undefined value" for those vars actually see them suggested.

Callers:
- Hook foreground/background prep and lazy re-expansion pass
  `ValidationScope::Hook(hook_type)` via `CommandOrigin`.
- Alias foreground/dry-run pass `ValidationScope::Alias`.
- `for-each` passes `None` (eager context is already complete).

Snapshot change: `post_create_upstream_template.snap` — hint grew
from 22 to 27 vars, superset of the pre-perf list.

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@worktrunk-bot worktrunk-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lazy gating cleanly delegates to the existing referenced_vars_for_config and the verbose-mode bypass keeps -v debugging accurate. The bench matrix split (noop/commit/everything) is a nice regression guard for future accessor changes. One concern about a silent contract change in stdin JSON — see inline.

The user-facing docs in docs/content/hook.md (## JSON context) and the matching block in src/cli/mod.rs still claim "Hooks receive all template variables as JSON on stdin". With this change, vars not referenced by any template in the pipeline are gated out of the context map, so they're also missing from the JSON piped to the script. A Python hook doing ctx['default_branch'] (or commit, upstream, owner, remote_url) when no template references those vars will now hit a KeyError it didn't hit before — and the failure is opaque from the user's side (their hook script didn't change). Worth either a docs update calling out the new behavior or always populating the JSON eagerly even when the template gates accessors out.

Comment on lines +140 to +141
/// `main_worktree_path` that share accessors with canonical names). When `None`,
/// every accessor runs — used by preview/debug paths that want the full context.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// `main_worktree_path` that share accessors with canonical names). When `None`,
/// every accessor runs — used by preview/debug paths that want the full context.
/// `main_worktree_path` that share accessors with canonical names). When `None`,
/// every accessor runs — used by preview/debug paths that want the full context.
///
/// Vars skipped by gating are also absent from the JSON piped to hook stdin,
/// so scripts that read context fields not referenced in any template will
/// see those keys missing.

@worktrunk-bot
Copy link
Copy Markdown
Collaborator

codecov/patch: 96.12% vs 96.62% target — 0.5% under, ~1–2 lines on this diff. Most likely uncovered candidates, in order: (1) the Alias arm of origin_scope (no alias test currently triggers a template-expansion error to exercise it — post_create_upstream_template.snap only covers the Hook arm); (2) the main_worktree_path deprecated-alias branch in build_hook_context (no test references it); (3) the is_empty() early-return in override_available_vars. The first two are easy to add — happy to push a follow-up commit with an alias-error test plus a main_worktree_path template reference if you'd like, otherwise leaving it to you.

`codecov/patch` on #2353 flagged the new lazy-gating code at 96.12%
(below 96.62% target) — the uncovered statements were the error
propagation paths through `expand_shell_template(...)` in alias
dry-run, for-each, and the background pipeline runner, plus the new
pure helpers in `command_executor`.

Adds:

- Unit tests for `override_available_vars` (scope replaces list,
  `None` leaves list alone, empty list stays empty for syntax errors)
  and `origin_scope` (both CommandOrigin arms).
- `test_config_alias_dry_run_undefined_var` — exercises the
  `expand_shell_template` error path in `render_preview` and
  asserts the scope-complete hint includes `args`.
- `test_for_each_undefined_var` — exercises the same error path in
  the `wt step for-each` runner.

The background-pipeline (`run_pipeline.rs`) error paths stay
uncovered for now — they require a full background-hook integration
that's out of scope for this PR's coverage gap.

Co-Authored-By: Claude <noreply@anthropic.com>
@max-sixty
Copy link
Copy Markdown
Owner Author

one downside of this is that it breaks the json piping to stdin

so I think we should leave to see how much it improves things once everything else merges

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants