Skip to content

perf(nix): cache nix version info on disk - #3020

Merged
mikeland73 merged 2 commits into
mainfrom
mikeland73/perf-nix-version-cache
Oct 26, 2026
Merged

mikeland73 merged 2 commits into
mainfrom
mikeland73/perf-nix-version-cache

Conversation

@mikeland73

Copy link
Copy Markdown
Collaborator

Summary

Implements item 1 of specs/perf-improvements.md.

Every devbox command calls nix.Info() (via EnsureNixInstalled, nix.System(), telemetry, etc.), which runs nix --version --debug (~90ms). The result was only cached per process. This persists the parsed Info under $XDG_CACHE_HOME/devbox/nix-info/ using the existing filecache helper.

Cache key: resolved nix binary path (symlinks followed, usually an immutable /nix/store/... path) + command args (incl. ExtraArgs) + env vars that affect Nix's config (NIX_CONF_DIR, NIX_CONFIG, NIX_USER_CONF_FILES, XDG_CONFIG_HOME, XDG_CONFIG_DIRS, HOME, NIX_STORE_DIR, ...).

Invalidation: each entry stamps (mtime + size) the nix binary and every config file --debug reports — the system nix.conf and every user config path, including ones that don't exist yet, so creating one invalidates too. Entries also expire after 24h as a backstop for changes the stamps can't see (e.g. files pulled in by include in nix.conf). Failures are never cached, and the in-process infoOnce is unchanged.

Also: Package.IsEnabledOnPlatform now returns true early when a package sets neither platforms nor excluded_platforms, so the common case never looks up nix.System().

Timing

devbox global shellenv with the project already up to date, DO_NOT_TRACK=1, 15 runs each:

binary median
main 85.8ms
this PR 16.2ms

Apart from the debug timer lines, the output matched main exactly.

Testing

  • New nix/infocache_test.go uses a fake nix script and checks: a cache hit skips nix, editing nix.conf invalidates, creating a previously missing user config invalidates, changing NIX_CONFIG changes the key, and failures aren't cached.
  • go test -race ./nix/... ./internal/nix/... ./internal/devconfig/... pass
  • golangci-lint reports 0 issues and gofumpt -extra is clean

🤖 Generated with Claude Code

Every devbox command runs `nix --version --debug` (~90ms) to get the Nix
version and system, and the result was only cached per process. Persist
the parsed Info under the XDG cache dir so later commands can skip it.

The cache key is the resolved nix binary path, the command arguments,
and the environment variables that affect Nix's config. The entry also
stamps (mtime + size) the nix binary and every nix.conf that --debug
reports, including ones that don't exist yet, so editing or creating a
config invalidates it. Entries expire after 24h as a backstop for
changes the stamps can't see (e.g. nix.conf include directives).
Failures aren't cached.

Also skip the nix.System() lookup in IsEnabledOnPlatform when a package
doesn't restrict its platforms.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikeland73

Copy link
Copy Markdown
Collaborator Author

@claude ptal

@claude

claude Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Claude finished @mikeland73's task in 20s —— View job


Review of #3020: cache nix version info on disk

I read the diff and the surrounding Info() code. I did not run the tests or the linter. Overall the design is sound and I found no blocking issues. The notes below are minor.

What looks good

  • Failures: Info() only stores on success. A parseInfo error sets infoErr, so it isn't cached. loadCachedInfo also rejects entries with an empty Version.
  • Retry after install: the cmd.err early return still happens before infoOnce, so retrying after installing Nix still works.
  • Missing config files: stamping a missing config file as zero mtime and size means creating it later invalidates the entry. The test covers this.
  • Backstops: the 24h TTL and the resolved-symlink binary path in the key are reasonable. A Nix upgrade changes the store path and therefore the key.
  • IsEnabledOnPlatform shortcut: the semantics are unchanged. With no platforms and no excluded_platforms, the old code would have returned true anyway.

Minor points and nits

  1. Cache key vs. the actual command environment (nix/infocache.go:67): the key reads os.Getenv for the listed variables. If n.Command ever overrides or injects any of these in cmd.Env, such as NIX_CONFIG or NIX_USER_CONF_FILES, the key could miss it. If cmd.Env is populated, derive the values from it, falling back to os.Environ(). If Command only inherits the process environment, this is fine.
  2. Key delimiter (nix/infocache.go:70): fields are joined with \x00 but values aren't length-prefixed. That is fine in practice because env values can't contain NUL. Hashing the key would also keep the filename short if filecache uses the raw key as a filename. Please check how filecache maps keys to files, since NIX_CONFIG can be long and multi-line.
  3. Concurrent writers: parallel devbox processes may both miss and both write. This is only a problem if filecache.Set isn't atomic (write to a temp file, then rename). Worth confirming.
  4. Stamp granularity: mtime plus size can miss an edit that keeps the same size within the filesystem's timestamp granularity. The 24h TTL bounds this, and the test bumps mtime explicitly, so I think it's acceptable.
  5. Stale cache dir: newInfoCache() is built on every Info() call that gets past the Once. That is cheap, but you could construct it inside the Do block only. It already is, so no change is needed.
  6. Test coverage: the test covers a cache hit, nix.conf edit, new user config, NIX_CONFIG and failures. A case for an expired TTL, or for a corrupt cache file falling back to running nix, would round it out. Get errors already fall through to running nix, so a corrupt-file test would be cheap.

Approving from my side, with the cmd.Env check in point 1 and the atomic-write check in point 3 as the two worth confirming before merge.
· branch mikeland73/perf-nix-version-cache

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikeland73
mikeland73 merged commit 30b2b60 into main Oct 26, 2026
28 checks passed
@mikeland73
mikeland73 deleted the mikeland73/perf-nix-version-cache branch October 26, 2026 00:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant