zbench

zsh-bench Integration

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "zbench" with this command: npx skills add edmundmiller/dotfiles/edmundmiller-dotfiles-zbench

zsh-bench Integration

Proper benchmarking of interactive zsh using romkatv/zsh-bench. Measures real user-visible latency, NOT time zsh -lic exit (which is meaningless).

Commands

hey zbench # Run + display with threshold indicators (auto-compares if baseline exists) hey zbench-save # Run + save as baseline + append history hey zbench-compare # Run + explicit diff against baseline hey zbench-check # Exit non-zero if over threshold (for git bisect) hey zbench-baseline # Show saved baseline (no run) hey zbench-history # Show TSV history

All commands accept extra zsh-bench args: hey zbench --iters 4 for quick runs.

Metrics & Thresholds

From romkatv's blind perception study — values at or below threshold are indistinguishable from zero:

Metric Threshold What it means

first_prompt_lag_ms

50ms Time to see prompt after opening terminal

first_command_lag_ms

150ms Time until first command can execute

command_lag_ms

10ms Delay between Enter and next prompt

input_lag_ms

20ms Keystroke-to-screen latency

Indicators: 🟢 ≤50% (headroom) · 🟡 ≤100% (imperceptible) · 🟠 ≤200% (noticeable) · 🔴 >200% (sluggish)

exit_time_ms is shown but not used for thresholds — it doesn't measure interactive performance.

Git Bisect Workflow

Find which commit made the shell slow:

git bisect start git bisect bad HEAD git bisect good <known-good-commit> git bisect run hey zbench-check

zbench-check exits non-zero when any metric exceeds its threshold.

File Layout

benchmarks/zsh-bench/ ├── <Host>.json # Current baseline per host └── history/ └── <Host>.tsv # Append-only history (timestamp, git_rev, metrics) packages/zsh-bench.nix # Nix package (romkatv/zsh-bench with internal/ helpers) bin/hey.d/zbench.just # Hey recipes bin/zbench-report # Python — parse, compare, format results

Baselines are per-host (MacTraitor-Pro.json , Seqeratop.json ) because hardware varies.

Typical Workflow

1. Establish baseline on a clean build

hey zbench-save

2. Make zsh config changes

vim config/zsh/.zshrc hey rebuild

3. Check for regressions

hey zbench # Shows comparison vs baseline

4. If satisfied, update baseline

hey zbench-save

Debugging Slow Startup

Phase Timing Script

Don't guess — measure. Paste this into zsh -c '...' to time each phase of startup:

zsh -c ' zmodload zsh/datetime export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}" export XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" export ZDOTDIR="${ZDOTDIR:-$XDG_CONFIG_HOME/zsh}" export ZSH_CACHE="${ZSH_CACHE:-$XDG_CACHE_HOME/zsh}" function _source { [[ -f "$1" ]] && source "$1"; } function _cache { local cache_dir="$XDG_CACHE_HOME/zsh"; local cache_file="$cache_dir/$1.zsh" if [[ ! -f "$cache_file" ]] || [[ "$commands[$1]" -nt "$cache_file" ]]; then mkdir -p "$cache_dir"; "$@" > "$cache_file"; fi source "$cache_file" }

t0=$EPOCHREALTIME source $ZDOTDIR/.zshenv 2>/dev/null; t1=$EPOCHREALTIME source $ZDOTDIR/config.zsh; t2=$EPOCHREALTIME

... add phases matching your .zshrc ...

source $ZDOTDIR/completion.zsh 2>/dev/null; t3=$EPOCHREALTIME _source $ZDOTDIR/extra.zshrc; t4=$EPOCHREALTIME

printf "zshenv: %4.0fms\n" $(( (t1-t0)*1000 )) printf "config: %4.0fms\n" $(( (t2-t1)*1000 )) printf "completion: %4.0fms\n" $(( (t3-t2)*1000 )) printf "extra: %4.0fms\n" $(( (t4-t3)*1000 )) printf "TOTAL: %4.0fms\n" $(( (t4-t0)*1000 )) '

Adapt phases to match the actual .zshrc . The gap between this total and zsh-bench is overhead from /etc/zshrc (nix-darwin generated) and deferred plugin loading.

To drill into extra.zshrc , time each source line individually — one slow alias file can dominate.

Known Culprits (ranked by typical impact)

Culprit Typical cost Fix

Redundant compinit 2000-3000ms Ensure compinit runs exactly once. Check EOF of .zshrc , /etc/zshrc , and completion.zsh — easy to end up with 2+ calls. Use compinit -C -d "$cache" with 24h staleness check.

Nix store globs 200-400ms for f in /nix/store/foo/*.zsh is slow — thousands of dirs. Cache the resolved path to a file.

Shell startup file scanning 100-500ms Functions that grep /sed across many files at startup (e.g., fixing session files). Move to on-demand or a cron job.

Uncached eval "$(tool init)"

40-100ms each brew shellenv , direnv hook zsh , fnm env , zoxide init zsh , entire completion zsh . Use _cache pattern to write output to file, re-eval only when binary changes.

Double brew shellenv

40-80ms nix-homebrew adds eval "$(brew shellenv)" to /etc/zshrc . If you handle it in .zshenv , set enableZshIntegration = false in nix-homebrew config.

Plugin manager overhead 10-40ms Antidote's antidote load does staleness checks. If static file exists, source it directly and skip antidote init entirely.

Deferred plugins 0ms startup antidote kind:defer is free at startup but zsh-bench won't detect has_syntax_highlighting /has_autosuggestions . This is fine.

The _cache Pattern

Central to fast startup. Already defined in .zshrc :

function _cache { local cache_dir="$XDG_CACHE_HOME/zsh" local cache_file="$cache_dir/$1.zsh" if [[ ! -f "$cache_file" ]] || [[ "$commands[$1]" -nt "$cache_file" ]]; then mkdir -p "$cache_dir" "$@" > "$cache_file" fi source "$cache_file" }

Usage:

_cache zoxide init zsh # instead of eval "$(zoxide init zsh)" _cache direnv hook zsh # instead of eval "$(direnv hook zsh)" _cache entire completion zsh # instead of source <(entire completion zsh)

Invalidates when the binary changes ($commands[$1] mtime check). Delete ~/.cache/zsh/*.zsh to force regeneration.

Replay Mode

Use zsh-bench --iters 1 --scratch-dir /tmp/zbench-debug then dbg/replay --scratch-dir /tmp/zbench-debug to watch what zsh-bench actually sees.

Key Design Decisions

  • Uses zsh-bench's non-raw output (median of 16 iterations) for stable numbers.

  • --raw gives per-iteration arrays — useful for variance analysis but not default.

  • Baselines stored as JSON for easy programmatic comparison.

  • History stored as TSV for easy column -t , awk , or import into spreadsheets.

  • Regression detection: flags changes > 20% or > 5ms (whichever is larger).

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

tmux-status-debug

No summary provided by upstream source.

Repository SourceNeeds Review
General

jj-history-investigation

No summary provided by upstream source.

Repository SourceNeeds Review
General

agenix-secrets

No summary provided by upstream source.

Repository SourceNeeds Review
General

using-jj-workspaces

No summary provided by upstream source.

Repository SourceNeeds Review