-
-
Notifications
You must be signed in to change notification settings - Fork 775
perf(install_state): replace per-tool .mise.backend files with single index #7341
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
When config explicitly provides tool options, start with registry defaults only instead of inheriting cached options from .mise.backend. This prevents stale cached options (e.g., old 'url' option) from conflicting with new config options (e.g., platform-specific URLs). Fixes #7034 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
Instead of discarding all cached options when config provides new options, selectively filter out only install-time-only options while preserving post-install options like bin_path. Each backend now implements install_time_option_keys() to declare which options only affect installation/download and should not be inherited from cached .mise.backend when config provides its own options. - http: url, checksum, version_list_url, version_regex, version_json_path, format - github/gitlab: asset_pattern, url, version_prefix - ubi: exe, matching, matching_regex, provider - cargo: features, default-features, bin - go: tags - pipx: extras, pipx_args, uvx_args, uvx This allows: - Changing url/asset_pattern/checksum config without reinstall issues - Preserving post-install options like bin_path for binary discovery Fixes #7034 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
… index Replace individual `.mise.backend` files for each installed tool with a single `.mise.meta.toml` index file. This reduces startup I/O from N file reads (one per installed tool) to a single file read. Changes: - New index file at `~/.local/share/mise/installs/.mise.meta.toml` - TOML format: `[tools]` section with `short = "full"` entries - Auto-migration from legacy `.mise.backend` files on first run - Auto-cleanup of stale entries when tool directories are deleted - Atomic writes using temp file + rename pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR optimizes startup performance by replacing per-tool .mise.backend files with a single .mise.meta.toml index file. This reduces I/O operations from N file reads (one per installed tool) to a single file read. The implementation includes automatic migration from the legacy format and cleanup of stale entries when tool directories are deleted.
Key changes:
- Introduces centralized backend metadata index at
~/.local/share/mise/installs/.mise.meta.toml - Implements automatic migration from legacy
.mise.backendfiles with cleanup after reading - Adds install-time-only option filtering to prevent conflicts when config options override cached metadata
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/toolset/install_state.rs | Core implementation of index-based backend metadata system with migration logic |
| src/runtime_symlinks.rs | Updates directory cleanup to include new .mise.meta.toml file |
| src/config/config_file/mise_toml.rs | Implements filtering of install-time-only options when merging config options |
| src/backend/ubi.rs | Defines install-time-only option keys for UBI backend |
| src/backend/pipx.rs | Defines install-time-only option keys for PIPX backend |
| src/backend/mod.rs | Adds dispatcher function for install-time-only option keys by backend type |
| src/backend/http.rs | Defines install-time-only option keys for HTTP backend |
| src/backend/go.rs | Defines install-time-only option keys for Go backend |
| src/backend/github.rs | Defines install-time-only option keys for GitHub/GitLab backend |
| src/backend/cargo.rs | Defines install-time-only option keys for Cargo backend |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let index = read_index(); | ||
| let mut jset = JoinSet::new(); | ||
| for dir in file::dir_subdirs(&dirs::INSTALLS)? { | ||
| let index = index.clone(); |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cloning the entire index (BTreeMap) for each spawned task is inefficient. Since the index is only read in these tasks, wrap it in Arc before the loop to share a single allocation across all tasks instead of creating N copies.
| let original_len = index.len(); | ||
| index.retain(|short, _| { | ||
| let tool_dir = dirs::INSTALLS.join(short.to_kebab_case()); | ||
| tool_dir.exists() | ||
| }); | ||
|
|
||
| // Write back if we removed any stale entries | ||
| if index.len() != original_len { |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The cleanup logic uses index.len() comparison to detect changes, which requires iterating the map twice (once for retain, once for len). Consider using a boolean flag set during retain or checking if any tools were removed to avoid the redundant length comparison.
| let original_len = index.len(); | |
| index.retain(|short, _| { | |
| let tool_dir = dirs::INSTALLS.join(short.to_kebab_case()); | |
| tool_dir.exists() | |
| }); | |
| // Write back if we removed any stale entries | |
| if index.len() != original_len { | |
| let mut removed_any = false; | |
| index.retain(|short, _| { | |
| let tool_dir = dirs::INSTALLS.join(short.to_kebab_case()); | |
| let exists = tool_dir.exists(); | |
| if !exists { | |
| removed_any = true; | |
| } | |
| exists | |
| }); | |
| // Write back if we removed any stale entries | |
| if removed_any { |
src/toolset/install_state.rs
Outdated
| // Clean up entries for tools whose directories no longer exist | ||
| let original_len = index.len(); | ||
| index.retain(|short, _| { | ||
| let tool_dir = dirs::INSTALLS.join(short.to_kebab_case()); |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The kebab-case conversion is inconsistent with the index key usage. In line 216, the code looks up using dir directly, but cleanup converts keys with to_kebab_case(). If index keys are already in kebab-case, this conversion is redundant; if not, the lookup at line 216 may fail. Ensure consistent key formatting throughout the index operations.
| // Clean up entries for tools whose directories no longer exist | |
| let original_len = index.len(); | |
| index.retain(|short, _| { | |
| let tool_dir = dirs::INSTALLS.join(short.to_kebab_case()); | |
| // Normalize all keys to kebab-case | |
| let index: BTreeMap<String, String> = index | |
| .into_iter() | |
| .map(|(k, v)| (k.to_kebab_case(), v)) | |
| .collect(); | |
| // Clean up entries for tools whose directories no longer exist | |
| let original_len = index.len(); | |
| let mut index = index; | |
| index.retain(|short, _| { | |
| let tool_dir = dirs::INSTALLS.join(short); |
src/toolset/install_state.rs
Outdated
| }) | ||
| .unwrap_or_default(); | ||
| let lines: Vec<&str> = body.lines().filter(|f| !f.is_empty()).collect(); | ||
| let short = lines.first().unwrap_or(&dir).to_string(); |
Copilot
AI
Dec 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using unwrap_or with a reference to dir then converting to String is inefficient. Consider using lines.first().map(|s| s.to_string()).unwrap_or_else(|| dir.to_string()) to avoid the unnecessary reference and conversion.
| let short = lines.first().unwrap_or(&dir).to_string(); | |
| let short = lines.first().map(|s| s.to_string()).unwrap_or_else(|| dir.to_string()); |
The .mise.backend.json format was deprecated long ago. Remove migration support to simplify the codebase. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Get directory listing once with file::dir_subdirs() and use HashSet lookup instead of calling .exists() for each tool when cleaning up stale index entries. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
The index file uses kebab-cased directory names as keys, but lookups were using the original short name (e.g., "gitlab:jdxcode/mise-test-fixtures"). This caused tools to not be found after installation. - write_backend_meta now uses ba.short.to_kebab_case() as the key - get_tool_full, backend_type, and list_versions now convert short to kebab-case before lookup - migrate_to_index uses the directory name (already kebab-cased) as key 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
7852f7b to
d04b840
Compare
During parallel tool installs, multiple processes could read the same index state, each insert their entry, and the second write would overwrite the first, losing data. Use LockFile to hold an exclusive lock during the entire read-modify-write operation to prevent concurrent modifications. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
| // Delete the legacy file after reading | ||
| if let Err(err) = file::remove_file(&path) { | ||
| debug!("Failed to remove legacy .mise.backend file: {err}"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Migration deletes legacy files before confirming write success
The read_legacy_backend_meta function deletes legacy .mise.backend files immediately after reading them (via file::remove_file), before migrate_to_index calls write_index. If the index write fails (e.g., disk full, permissions), the legacy files are already deleted and all backend metadata is permanently lost. The deletion should only occur after confirming the new index file was successfully written.
Additional Locations (1)
| .ok() | ||
| .and_then(|content| toml::from_str::<BackendIndex>(&content).ok()) | ||
| .map(|parsed| parsed.tools.into_iter().collect()) | ||
| .unwrap_or_else(migrate_to_index); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Corrupted index file causes irreversible data loss
When the index file exists but contains invalid TOML (corruption), read_index falls back to migrate_to_index. However, after a successful prior migration, the legacy .mise.backend files were deleted. The migration will find no legacy files and return an empty map, causing all backend metadata to be silently lost. The code silently swallows the parse error with .ok() instead of distinguishing between "file missing" (migrate) and "file corrupted" (error).
|
|
||
| if index.len() != original_len { | ||
| let _ = write_index(&index); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Cleanup write in read_index races with write_backend_meta
The read_index function performs an unlocked write during stale entry cleanup (line 73), but write_backend_meta acquires a lock and also calls read_index. If init_tools calls read_index concurrently with write_backend_meta, the cleanup write could overwrite entries that were just added by write_backend_meta, causing newly installed tool metadata to be lost.
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.12.9 x -- echo |
20.9 ± 0.6 | 19.6 | 23.8 | 1.00 |
mise x -- echo |
21.3 ± 0.7 | 20.0 | 24.4 | 1.02 ± 0.04 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.12.9 env |
20.8 ± 0.9 | 19.2 | 26.6 | 1.01 ± 0.06 |
mise env |
20.6 ± 0.7 | 19.2 | 23.7 | 1.00 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.12.9 hook-env |
20.9 ± 0.7 | 19.4 | 23.5 | 1.00 |
mise hook-env |
21.8 ± 1.2 | 19.6 | 26.6 | 1.04 ± 0.07 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2025.12.9 ls |
17.8 ± 0.8 | 16.1 | 20.7 | 1.00 |
mise ls |
20.8 ± 0.9 | 18.6 | 24.3 | 1.17 ± 0.07 |
ls is 17% |
xtasks/test/perf
| Command | mise-2025.12.9 | mise | Variance |
|---|---|---|---|
| install (cached) | 108ms | -12% | |
| ls (cached) | 67ms | -12% | |
| bin-paths (cached) | 76ms | 83ms | -8% |
| task-ls (cached) | 298ms | -86% |
Summary
.mise.backendfiles per installed tool with a single.mise.meta.tomlindex fileIndex File Format
Test plan
🤖 Generated with Claude Code
Note
Consolidates per-tool
.mise.backendfiles into a single atomic, locked.mise.meta.tomlindex and updates tool init and lookups to use it, plus adjusts symlink cleanup.~/.local/share/mise/installs/.mise.meta.tomlwithread_index/write_indexand TOML schema (BackendIndex.tools)..mise.backendfiles and prune entries for missing tool dirs.write_backend_metato avoid races.init_toolsreads the index once (removes per-tool file reads) and buildsInstallStateTools from it.get_tool_full,backend_type, andlist_versions.remove_missing_symlinksnow ignores metadata files.mise.backend(legacy) and.mise.meta.tomlwhen pruning empty install dirs.Written by Cursor Bugbot for commit dc3407c. This will update automatically on new commits. Configure here.