perf(alias): skip unused template-var accessors in hook context#2353
perf(alias): skip unused template-var accessors in hook context#2353
Conversation
`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>
worktrunk-bot
left a comment
There was a problem hiding this comment.
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.
| /// `main_worktree_path` that share accessors with canonical names). When `None`, | ||
| /// every accessor runs — used by preview/debug paths that want the full context. |
There was a problem hiding this comment.
| /// `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. |
|
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 |
`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>
|
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 |
build_hook_contextused to rungit rev-parse <branch>for{{ commit }}andgit for-each-reffor{{ upstream }}on every alias and hook dispatch, regardless of whether the pipeline referenced those vars. Thereferenced_vars_for_configset (already computed for--KEY=VALUErouting) 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_aliasandprepare_stepspass thereferenced_vars_for_configunion to gate accessors.wt step eval,wt switch --execute,wt hook <type>preview,wt step for-each) passNone— they want the full context for inspection.-v) at alias and hook dispatch sites passesNone: the template-variable table prints(unset)for anything not populated, and users running-vare debugging and expect every var.Accurate error hints
expand_shell_templatenow takes an optionalValidationScope. When template expansion fails with an undefined-variable error, the "Available variables" hint lists every var the scope supports (viavars_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_namefor 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).commit/everythingland close to the pre-change baseline because they exercise the same accessors as before. The gap betweennoopandcommit/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 greenRUST_LOG=worktrunk=debug wt nooptrace confirms noop alias drops from 7 → 5 git subprocesses in the parent;wt showall(references all gated vars) keeps 7Co-Authored-By: Claude noreply@anthropic.com