Repository navigation
perf: cut ~2ms of startup by fixing config glob scans and clap double-build - #11025
Conversation
…-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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Central YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughCLI parsing now reuses a single built command and precomputes alias usage. Config discovery delegates filename expansion, while settings loading records additional phase timings. ChangesCLI and configuration flow
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
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
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. Comment |
Greptile SummaryThis PR eliminates two fixed startup costs identified via
Confidence Score: 5/5Safe 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
Reviews (1): Last reviewed commit: "perf: cut ~2ms of startup by fixing conf..." | Re-trigger Greptile |
| 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(), |
There was a problem hiding this comment.
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!
Hyperfine Performance
|
| 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% |
Summary
Profiling
mise version/mise hook-envstartup (viaMISE_TIMINGScheckpoints) found two fixed costs paid by every invocation:load_config_paths()ranglob::glob()for every config filename in every ancestor directory — ~19 patterns × cwd depth ≈ 200 glob compiles + filesystem walks per startup, even though onlyconf.d/*.tomlcontains a wildcard. This accounted for 2–4ms of the firstSettings::try_get(). Fixed by reusingconfig_paths_in_dir_with_filenames(), which stats literal filenames and only globs actual*patterns (same logic the per-dir lookup already used).Cli::parse_from. Now the already-built tree is used for parsing. Themise bdeprecation check is computed before the tree is consumed.Also adds
MISE_TIMINGScheckpoints insideSettings::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.0via.tool-versions):mise versionmise hook-env -s zsh(full run)mise hook-env -s zsh(early-exit fast path)mise exec -- node --versionnode --versionRemaining startup floor (~5ms) is process spawn + dyld on the ~100MB binary plus config/toolset resolution, not these code paths.
Verification
conf.d/*.tomlglobs,.config/mise/config.toml,mise.local.toml, and settings layering (mise config ls,mise env,mise settings)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_chdirpass(While testing this I hit a pre-existing, unrelated bash 3.2
set -ucrash insrc/assets/bash/deactivate.shempty-array expansions, which makestest_hook_env_fast_pathfail 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
miseinvocation: config discovery and CLI parsing no longer pay redundant work on the hot path.load_config_paths()now delegates per-directory discovery toconfig_paths_in_dir_with_filenames()instead of runningglob()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 firstSettings::try_get().CLI startup builds the clap command tree once, then parses with
get_matches_from+FromArgMatchesinstead ofCli::parse_from(which rebuilt the tree). The deprecatedmise balias check runs before parsing andwarn_deprecated_backends_aliastakes a precomputed bool.Settings::try_get()gainsMISE_TIMINGScheckpoints 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
Bug Fixes