perf(conf.d): cut shell startup latency by ~30% #130

Merged
rootiest merged 16 commits from perf/startup-latency into main 2026-09-08 05:30:06 +00:00
Owner

What

conf.d/tailscale.fish was a Cobra-generated completion script living in conf.d/, so fish sourced it — and it self-primed by running the tailscale binary — on every shell start, interactive or not. conf.d/cheat.fish was the same mistake. Both are relocated to completions/, where fish loads them lazily on first <TAB>.

Nine more conf.d/*.fish files (abbr, bash_expands, key_bindings, starship, theme, auto-pull, wakatime, logging-events, paru-wrapper/yay-wrapper) do work only an interactive session can use, and had no status is-interactive guard — so every non-interactive fish -c paid for abbreviations, key bindings, the theme, etc. Each now guards.

Measured (interleaved, same machine, before/after this branch)

main this branch
fish -c 'true' 91.3 ms 64.9 ms (−26.4 ms)
fish -i -c 'exit' 148.6 ms 134.3 ms (−14.2 ms)

Why each guard is safe (not just faster)

Every guarded file was traced for script callers before guarding — see individual commit messages for the one-line evidence each. Two files were deliberately not guarded despite being candidates:

  • conf.d/__fish_config_op_registry.fish — it defines the variables the guard helper reads, and that helper fails open on a missing registry entry. Guarding it would silently re-enable every opinionated component inside scripts.
  • conf.d/tricks.fish — mixed file with script-callable functions (backup, psmem) alongside interactive-only pieces.

tailscale<TAB> and completion laziness verified in a real PTY, not just complete -C.

Out of scope, recorded not fixed

See AGENTS/JOB-BRIEF-FINDINGS.md in this branch (untracked — copy it out if you want to keep it) for two deferred items: a double-source of tricks.fish on CachyOS (4.55 ms/shell, needs its own branch because the fix requires hard-coding a foreign registry identity), and a conflicting-wrapper bug between paru-wrapper.fish and __fish_config_sync_logging.fish.

Testing

fish tests/run-tests.fish — 226/226 lint, 17/17 functional, 317/317 vault. python3 docs/verify-manual.py — 74/74. New regression test (test_conf_d_is_lazy_in_scripts) proven red-then-green by stashing the guards and reverting.

## What `conf.d/tailscale.fish` was a Cobra-generated completion script living in `conf.d/`, so fish sourced it — and it self-primed by running the `tailscale` binary — on **every shell start**, interactive or not. `conf.d/cheat.fish` was the same mistake. Both are relocated to `completions/`, where fish loads them lazily on first `<TAB>`. Nine more `conf.d/*.fish` files (`abbr`, `bash_expands`, `key_bindings`, `starship`, `theme`, `auto-pull`, `wakatime`, `logging-events`, `paru-wrapper`/`yay-wrapper`) do work only an interactive session can use, and had no `status is-interactive` guard — so every non-interactive `fish -c` paid for abbreviations, key bindings, the theme, etc. Each now guards. ## Measured (interleaved, same machine, before/after this branch) | | main | this branch | |---|---|---| | `fish -c 'true'` | 91.3 ms | **64.9 ms** (−26.4 ms) | | `fish -i -c 'exit'` | 148.6 ms | **134.3 ms** (−14.2 ms) | ## Why each guard is safe (not just faster) Every guarded file was traced for script callers before guarding — see individual commit messages for the one-line evidence each. Two files were deliberately **not** guarded despite being candidates: - `conf.d/__fish_config_op_registry.fish` — it defines the variables the guard helper reads, and that helper fails open on a missing registry entry. Guarding it would silently re-enable every opinionated component inside scripts. - `conf.d/tricks.fish` — mixed file with script-callable functions (`backup`, `psmem`) alongside interactive-only pieces. `tailscale<TAB>` and completion laziness verified in a real PTY, not just `complete -C`. ## Out of scope, recorded not fixed See `AGENTS/JOB-BRIEF-FINDINGS.md` in this branch (untracked — copy it out if you want to keep it) for two deferred items: a double-source of `tricks.fish` on CachyOS (4.55 ms/shell, needs its own branch because the fix requires hard-coding a foreign registry identity), and a conflicting-wrapper bug between `paru-wrapper.fish` and `__fish_config_sync_logging.fish`. ## Testing `fish tests/run-tests.fish` — 226/226 lint, 17/17 functional, 317/317 vault. `python3 docs/verify-manual.py` — 74/74. New regression test (`test_conf_d_is_lazy_in_scripts`) proven red-then-green by stashing the guards and reverting.
rootiest added 15 commits 2026-09-08 00:58:13 +00:00
conf.d/tailscale.fish is 252 lines of Cobra-generated completion that fish
sourced on every shell start, and its self-priming block executed the
tailscale binary to warm the completion cache. completions/ is the directory
fish autoloads on first <TAB>.

Measured: 19.2 ms off both interactive and non-interactive startup.
The Cobra-generated block ran `complete --do-complete "tailscale "` to flush
pre-existing completions before erasing them, which executed the tailscale
binary. From completions/ it has no job: fish autoloads only the first match
on $fish_complete_path and the repo's completions/ precedes the vendor dir,
so the vendor file is never sourced.

Verified byte-identical completion output across four probes with the vendor
file present. A comment at the deletion site records the reasoning.
Thirteen lines, every one a `complete -c cheat` registration, sourced on
every shell start from the wrong directory. Startup cost was already ~0
because its command substitutions are lazy, but completions/ is where fish
expects the file and the move is free.
61 abbreviations were declared on every fish -c. Abbreviations expand only
in the line editor, so nothing outside an interactive session can use them.
The six expand_* functions are only ever reached through abbr --function,
which fires during interactive expansion.
fish_user_key_bindings is invoked only by the interactive reader.
Defines fish_prompt only. The guard precedes the op-guard and the type -q
PATH scan so both are skipped in scripts. Scripts fall back to the repo's
autoloadable functions/fish_prompt.fish, which nothing invokes anyway.
fish_color_* is consumed only by the syntax highlighter. The guard sits
below the existing cleanup branch so stale-FZF_DEFAULT_OPTS cleanup keeps
running where it does today; the FZF value itself is a persisted universal
and survives regardless.
The --on-variable PWD handler backgrounds a git fast-forward. A script that
cd's was firing it, which is also where AGENTS.md Task #4's credential
prompt could surface from a background job.
fish_postexec is emitted only by the interactive reader (verified), so the
handler could never fire in a script. No telemetry behaviour changes.
__fish_config_sync_logging ran on every fish -c, mkdir+touching the C5
sentinel on disk from every subshell. Its consumers — the Kitty watcher and
the paru/yay wrappers — are interactive-context, and every interactive shell
still reconciles the state.
Neither file defines a function or sets a global; their only effect is
writing ~/.local/bin/<tool>, which every interactive session does anyway.
Combined 10.7 ms off every fish -c.
Follows the relocation out of conf.d/. The completions/ branch of the file
tree is expanded from the real directory at build time, so it needs no
hand-written children. Generated docs are regenerated separately.
docs/fish-config.1 is left for CI to regenerate: the local pandoc is 3.10.2
against CI's 3.1.3, so rebuilding it here emits ~4700 lines of formatter
churn unrelated to this change. The component registry rebuilt identically.
Spawns one non-interactive child against the sandboxed config and asserts on
what loaded -- abbreviations, key bindings, bang-expansions, the C5 event
handlers and the tailscale completion machinery must all be absent. Exit
codes name which guard regressed. No wall-clock assertions, so it cannot
flake.
rootiest added 1 commit 2026-09-08 05:29:51 +00:00
rootiest merged commit a994f9e91c into main 2026-09-08 05:30:06 +00:00
rootiest deleted branch perf/startup-latency 2026-09-08 05:30:07 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rootiest/fish-config#130