Skip to content

perf: cut ~2ms of startup by fixing config glob scans and clap double-build - #11025

Merged
jdx merged 1 commit into
mainfrom
claude/mise-asdf-benchmark-6db9c1
Jul 15, 2026
Merged

jdx merged 1 commit into
mainfrom
claude/mise-asdf-benchmark-6db9c1

Conversation

@jdx

@jdx jdx commented Jul 15, 2026 •

Copy link
Copy Markdown
Owner

Summary

Profiling mise version / mise hook-env startup (via MISE_TIMINGS checkpoints) found two fixed costs paid by every invocation:

  • load_config_paths() ran glob::glob() for every config filename in every ancestor directory — ~19 patterns × cwd depth ≈ 200 glob compiles + filesystem walks per startup, even though only conf.d/*.toml contains a wildcard. This accounted for 2–4ms of the first Settings::try_get(). Fixed by reusing config_paths_in_dir_with_filenames(), which stats literal filenames and only globs actual * patterns (same logic the per-dir lookup already used).
  • The clap command tree was built twice: once for naked-run/task-arg preprocessing, then again inside Cli::parse_from. Now the already-built tree is used for parsing. The mise b deprecation check is computed before the tree is consumed.

Also adds MISE_TIMINGS checkpoints inside Settings::try_get() so the settings phases (env layers, settings files, defaults, load passes) show up in future profiling.

Benchmarks

macOS arm64 (M-series), hyperfine, warm runs, isolated MISE_DATA_DIR/MISE_CACHE_DIR/MISE_CONFIG_DIR, one tool installed (node@22.17.0 via .tool-versions):

Scenario before after
mise version 8.1 ms 6.6 ms
mise hook-env -s zsh (full run) 10.9 ms 9.3 ms
mise hook-env -s zsh (early-exit fast path) — 6.3 ms
mise exec -- node --version 20.8 ms 18.8 ms
shim node --version 20.7 ms 18.9 ms

Remaining startup floor (~5ms) is process spawn + dyld on the ~100MB binary plus config/toolset resolution, not these code paths.

Verification

  • Config discovery output verified byte-identical against the previous release on a nested layout exercising conf.d/*.toml globs, .config/mise/config.toml, mise.local.toml, and settings layering (mise config ls, mise env, mise settings)
  • e2e: test_config_ls, test_config_ceiling_paths, test_config_ignore, test_config_ignored_paths, test_config_env, test_hook_env, test_hook_env_dir_mtime_stabilizes, test_activate_export, test_exec_chdir pass
  • clippy clean

(While testing this I hit a pre-existing, unrelated bash 3.2 set -u crash in src/assets/bash/deactivate.sh empty-array expansions, which makes test_hook_env_fast_path fail when the e2e harness resolves to the macOS system bash — will address separately.)

🤖 Generated with Claude Code


Note

Low Risk
Performance-only refactors on config discovery and CLI parsing with intended behavior parity; risk is mainly regression in config file ordering or edge-case CLI parsing, not security or data handling.

Overview
Startup performance for every mise invocation: config discovery and CLI parsing no longer pay redundant work on the hot path.

load_config_paths() now delegates per-directory discovery to config_paths_in_dir_with_filenames() instead of running glob() for every config filename pattern. Literal names are resolved with existence checks; only patterns containing * still glob—matching behavior already used elsewhere and cutting hundreds of glob walks during the first Settings::try_get().

CLI startup builds the clap command tree once, then parses with get_matches_from + FromArgMatches instead of Cli::parse_from (which rebuilt the tree). The deprecated mise b alias check runs before parsing and warn_deprecated_backends_alias takes a precomputed bool.

Settings::try_get() gains MISE_TIMINGS checkpoints around env layers, settings file enumeration, defaults, and both load passes for finer profiling.

Reviewed by Cursor Bugbot for commit 590f4bd. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Performance

    • Improved command-line startup efficiency by avoiding repeated command construction during argument parsing.
    • Added more detailed timing measurements for configuration and settings loading.
  • Bug Fixes

    • Centralized configuration file discovery to provide consistent filename and glob handling.
    • Preserved clearer handling of deprecated backend-alias warnings during CLI execution.

…-build

Profiling startup showed two fixed costs paid by every mise invocation:

- load_config_paths() ran glob::glob() for every config filename pattern
  in every ancestor directory (~190 glob compiles + walks for a typical
  cwd depth), even though only conf.d/*.toml contains a wildcard. Reuse
  config_paths_in_dir_with_filenames(), which stats literal filenames
  and only globs actual patterns. This was ~2-4ms of the first
  Settings::try_get() on every startup.

- Cli::run() built the clap command tree twice: once for arg
  preprocessing and again inside Cli::parse_from(). Parse with the
  already-built tree instead.

Also adds MISE_TIMINGS checkpoints inside Settings::try_get() so the
settings phases (env layers, settings files, defaults, load) show up
in future profiling.

Benchmarked on macOS arm64 (hyperfine, warm):
- mise version: 8.8ms -> 6.6ms
- mise hook-env -s zsh (full): ~13ms -> ~9.3ms
- mise hook-env -s zsh (early-exit fast path): ~6.3ms

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: b9c80f28-cb99-41cd-97f4-42659a3c3123

📥 Commits

Reviewing files that changed from the base of the PR and between 83ca012 and 590f4bd.

📒 Files selected for processing (3)
  • src/cli/mod.rs
  • src/config/mod.rs
  • src/config/settings.rs

📝 Walkthrough

Walkthrough

CLI parsing now reuses a single built command and precomputes alias usage. Config discovery delegates filename expansion, while settings loading records additional phase timings.

Changes

CLI and configuration flow

Layer / File(s) Summary
Reuse the built CLI command during parsing
src/cli/mod.rs
Cli::run builds and reuses one Clap command for argument preprocessing and parsing, converts matches through FromArgMatches, and passes precomputed alias usage to the warning function.
Centralize config filename expansion
src/config/mod.rs
load_config_paths delegates per-directory config matching to config_paths_in_dir_with_filenames.
Instrument settings builder phases
src/config/settings.rs
Settings::try_get records timing markers around builder setup, settings preloading, default settings preloading, and configuration loads.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CliRun as Cli::run
  participant ClapCommand as clap::Command
  participant FromArgMatches as clap::FromArgMatches
  participant AliasWarning as warn_deprecated_backends_alias
  CliRun->>ClapCommand: Build command
  CliRun->>ClapCommand: Preprocess and parse arguments
  ClapCommand->>FromArgMatches: Convert matches
  CliRun->>AliasWarning: Pass alias usage
Loading

Poem

A rabbit watched the command line hop,
One Clap tree built instead of two on top.
Config paths gathered in a tidy array,
Timers marked each loading layer.
“Neat!” cried the hare, with a wiggle and cheer—
“The settings trail is clearer here!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main performance changes: config glob scan cleanup and avoiding the clap double-build.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR eliminates two fixed startup costs identified via MISE_TIMINGS profiling, cutting ~2 ms from every mise invocation.

  • Config glob scan: load_config_paths previously called glob() (with its Mutex-guarded cache and glob::glob pattern compilation overhead) for every config filename in every ancestor directory, including the ~18 literal names that contain no wildcard. It now delegates to the pre-existing config_paths_in_dir_with_filenames, which fast-paths literals to a plain path.exists() call and only invokes glob() for the single conf.d/*.toml wildcard pattern.
  • Clap double-build: Cli::command() was called once for pre-processing and a second time internally by Cli::parse_from. The command is now built once, reused via get_matches_from + from_arg_matches, and the mise b deprecation check is extracted before the command is consumed.
  • Settings timing: time!() checkpoints are added inside Settings::try_get() for future profiling, with no logic changes.

Confidence Score: 5/5

Safe to merge — both optimisations are mechanically equivalent to their predecessors on all reachable paths and the verification listed in the PR description covers the config discovery and hook-env fast path.

The config change is a straightforward delegation to a pre-existing function whose ordering (reverse filename iteration, per-wildcard glob, literal stat) exactly matches the old inline logic; final deduplication in load_config_paths eliminates any concern about the added per-directory dedup. The clap change is correct for the happy path; the from_arg_matches error branch is unreachable in practice because get_matches_from exits the process on any parse error. Timing instrumentation in settings is pure observability with no logic impact.

No files require special attention; the single note on the from_arg_matches error branch in src/cli/mod.rs is a dead-code-path observation that has no effect on correctness or startup performance.

Important Files Changed

Filename Overview
src/config/mod.rs load_config_paths now delegates to the pre-existing config_paths_in_dir_with_filenames instead of calling glob() for every filename; ordering and deduplication behaviour are preserved
src/cli/mod.rs Clap command tree built once and reused via get_matches_from + from_arg_matches; deprecated-backends check extracted before cmd is consumed; error fallback rebuilds the tree only on exit paths
src/config/settings.rs Pure observability change: adds time!() checkpoints inside try_get() for MISE_TIMINGS profiling; no logic changes

Fix All in Claude Code

Reviews (1): Last reviewed commit: "perf: cut ~2ms of startup by fixing conf..." | Re-trigger Greptile

Comment thread src/cli/mod.rs
let matches = cmd.get_matches_from(processed_args.iter());
match <Cli as clap::FromArgMatches>::from_arg_matches(&matches) {
Ok(cli) => cli,
Err(err) => err.format(&mut Cli::command()).exit(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Third command build on error path

cmd.get_matches_from() calls process::exit() internally on any parse error, so from_arg_matches is only reached when matching succeeded — making the Err branch unreachable in practice. The fallback to err.format(&mut Cli::command()) therefore rebuilds the command tree a third time only on what is effectively a dead-code path (a clap internal bug or derive-macro inconsistency). This doesn't regress startup performance, but if clap ever exposes a way to format the error without a fresh command, or if try_get_matches_from is used instead, the extra build could be avoided entirely.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

Hyperfine Performance

mise x -- echo

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.7.7 x -- echo 21.9 ± 2.7 19.4 36.4 1.05 ± 0.16
mise x -- echo 20.8 ± 2.0 18.9 52.2 1.00

mise env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.7.7 env 20.8 ± 0.9 18.8 24.9 1.06 ± 0.08
mise env 19.6 ± 1.1 17.6 24.9 1.00

mise hook-env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.7.7 hook-env 21.5 ± 0.9 19.7 26.2 1.04 ± 0.07
mise hook-env 20.7 ± 1.2 18.3 25.4 1.00

mise ls

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.7.7 ls 19.4 ± 0.9 17.4 24.3 1.06 ± 0.07
mise ls 18.2 ± 0.9 16.3 21.7 1.00

xtasks/test/perf

Command mise-2026.7.7 mise Variance
install (cached) 173ms 171ms +1%
ls (cached) 65ms 64ms +1%
bin-paths (cached) 65ms 65ms +0%
task-ls (cached) 134ms 134ms +0%

@jdx
jdx merged commit 91d84e6 into main Jul 15, 2026
34 checks passed
@jdx
jdx deleted the claude/mise-asdf-benchmark-6db9c1 branch July 15, 2026 23:16
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.

1 participant