Skip to content

Prewarm Bootsnap caches after brew update#23108

Merged
MikeMcQuaid merged 1 commit into
mainfrom
bootsnap-prewarm
Jul 14, 2026
Merged

Prewarm Bootsnap caches after brew update#23108
MikeMcQuaid merged 1 commit into
mainfrom
bootsnap-prewarm

Conversation

@MikeMcQuaid

Copy link
Copy Markdown
Member
  • Bootsnap compiles Ruby files into instruction sequence and YAML caches on first load, so an update that changes files leaves the next command to recompile them at startup, on the user's time.
  • The cost is largest for updates under Library/Homebrew/vendor: portable Ruby bumps rotate the whole cache key (it hashes the Ruby version and gem directory listing), cold-starting every file at once, and vendored gem bumps rewrite entire gem trees, much of which update-report's own run never loads and so cannot recompile as a side effect.
  • Loading the brew install and brew fetch command graph with a fully cold cache took 0.91s vs 0.56s warm on an M-series Mac in this checkout, so ~350ms of pure cache compilation; the gap is a multiple of that on older machines.
  • After an explicit brew update that changed Library/Homebrew/vendor, update-report now spawns a detached background Ruby that requires global, cmd/install, cmd/fetch and cmd/upgrade, recompiling the caches for the most common next commands while the user reads the update report.
  • Code-only updates skip the prewarm: update-report has already recompiled most of what the next command loads (the residual was ~20-40ms here), so a background process is not worth it.
  • Auto-update runs also skip it: the command that triggered them loads the same files immediately afterwards, so a parallel prewarm would only duplicate its work.
  • Homebrew::Bootsnap.prewarm! reuses the existing enabled? gate, is skipped under HOMEBREW_TESTS and spawns the interpreter from HOMEBREW_RUBY_EXEC_ARGS with the parent's $LOAD_PATH, stdio on /dev/null and its own process group, then detaches, so the update exits immediately and a prewarm failure cannot break it.
  • Verified: across a vendored gem bump a detached child appears once update-report exits and compiles whatever its run did not load (736 -> 1055 cache files after a minimal report run), while code-only updates, auto-update runs and HOMEBREW_NO_BOOTSNAP=1 spawn no child.

  • Have you followed our Contributing guidelines?
  • Have you checked for other open Pull Requests for the same change?
  • Have you explained what your changes do? Performance claims (e.g. "this is faster") must include Hyperfine benchmarks.
  • Have you explained why you'd like these changes included, not just what they do?
  • For bug fixes, have you given step-by-step brew commands to reproduce the bug?
  • Have you written new tests (excluding integration tests)? Here's an example.
  • Have you successfully run brew lgtm (style, typechecking and tests) locally?

  • AI was used to generate or assist with generating this PR.

Anthropic Fable 5 max with local review and testing.


Copilot AI review requested due to automatic review settings July 14, 2026 16:43

Copilot AI left a comment

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.

Pull request overview

This PR adds an opt-in (by circumstance) Bootsnap cache “prewarm” step after an explicit brew update, intended to shift Ruby/YAML cache compilation work into the time while the user is reading the update report, improving perceived startup latency for subsequent common commands.

Changes:

  • Add Homebrew::Bootsnap.prewarm! to spawn a detached background Ruby process that requires global and common command entrypoints to compile Bootsnap caches.
  • Invoke Homebrew::Bootsnap.prewarm! from update-report only when the update changed Library/Homebrew/vendor, and only for non-auto-update runs.
  • Add RSpec coverage for prewarm! behavior and gating.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
Library/Homebrew/startup/bootsnap.rb Adds prewarm! helper to compile Bootsnap caches in a detached background Ruby process.
Library/Homebrew/cmd/update-report.rb Triggers prewarm after explicit updates that modified Library/Homebrew/vendor.
Library/Homebrew/test/startup/bootsnap_spec.rb Adds specs for Bootsnap.prewarm! spawning behavior and disabled/test gating.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Library/Homebrew/startup/bootsnap.rb
Comment thread Library/Homebrew/cmd/update-report.rb
Comment thread Library/Homebrew/test/startup/bootsnap_spec.rb
- Bootsnap compiles Ruby files into instruction sequence and YAML
  caches on first load, so an update that changes files leaves the
  next command to recompile them at startup, on the user's time.
- The cost is largest for updates under `Library/Homebrew/vendor`:
  portable Ruby bumps rotate the whole cache key (it hashes the
  Ruby version and gem directory listing), cold-starting every
  file at once, and vendored gem bumps rewrite entire gem trees,
  much of which `update-report`'s own run never loads and so
  cannot recompile as a side effect.
- Loading the `brew install` and `brew fetch` command graph with a
  fully cold cache took 0.91s vs 0.56s warm on an M-series Mac in
  this checkout, so ~350ms of pure cache compilation; the gap is a
  multiple of that on older machines.
- After an explicit `brew update` that changed
  `Library/Homebrew/vendor`, `update-report` now spawns a detached
  background Ruby that requires `global`, `cmd/install`,
  `cmd/fetch` and `cmd/upgrade`, recompiling the caches for the
  most common next commands while the user reads the update
  report.
- Code-only updates skip the prewarm: `update-report` has already
  recompiled most of what the next command loads (the residual
  was ~20-40ms here), so a background process is not worth it.
- Auto-update runs also skip it: the command that triggered them
  loads the same files immediately afterwards, so a parallel
  prewarm would only duplicate its work.
- `Homebrew::Bootsnap.prewarm!` reuses the existing `enabled?`
  gate, is skipped under `HOMEBREW_TESTS` and spawns the
  interpreter from `HOMEBREW_RUBY_EXEC_ARGS` with the parent's
  `$LOAD_PATH`, stdio on `/dev/null` and its own process group,
  then detaches, so the update exits immediately and a prewarm
  failure cannot break it.
- Verified: across a vendored gem bump a detached child appears
  once `update-report` exits and compiles whatever its run did
  not load (736 -> 1055 cache files after a minimal report run),
  while code-only updates, auto-update runs and
  `HOMEBREW_NO_BOOTSNAP=1` spawn no child.
@MikeMcQuaid MikeMcQuaid added this pull request to the merge queue Jul 14, 2026
Merged via the queue into main with commit 0b4b55e Jul 14, 2026
65 of 66 checks passed
@MikeMcQuaid MikeMcQuaid deleted the bootsnap-prewarm branch July 14, 2026 22:57
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.

3 participants