perf/upgrade: avoid linear scans of core formula names (~20% faster)#22977
Merged
Conversation
MikeMcQuaid
approved these changes
Jul 6, 2026
MikeMcQuaid
left a comment
Member
There was a problem hiding this comment.
Great idea! I'd suggest an internal Rubocop for this to "ban" the existing pattern.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR speeds up common formula/cask resolution paths used by brew install/upgrade by avoiding repeated linear scans over API name lists and eliminating an unnecessary filesystem emptiness check.
Changes:
- Adds
Homebrew::API.formula_name?andHomebrew::API.cask_token?for O(1) membership checks (Hash#key?) and updates call sites to use them. - Updates
Formula#linked_kegto rely on truthiness instead ofPathname#present?(avoids aDir.empty?syscall). - Adds an internal autocorrecting RuboCop cop (
Homebrew/ApiNameMembership) plus tests and Sorbet RBI to prevent regressions.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| Library/Homebrew/api.rb | Adds formula_name?/cask_token? predicate APIs backed by Hash#key?. |
| Library/Homebrew/formulary.rb | Replaces formula_names include/exclude scans with formula_name?. |
| Library/Homebrew/cask/cask_loader.rb | Replaces cask_tokens include/exclude scans with cask_token?. |
| Library/Homebrew/bundle/installer.rb | Switches Brewfile “available without tap?” checks to predicates. |
| Library/Homebrew/cli/named_args.rb | Uses predicates when core taps aren’t installed and API install is allowed. |
| Library/Homebrew/dev-cmd/edit.rb | Uses predicates to decide whether to auto-tap core/core-cask when editing. |
| Library/Homebrew/utils/analytics.rb | Uses predicates to guard analytics API lookups for formulae/casks. |
| Library/Homebrew/uninstall.rb | Converts excluded-name list to a Set for faster membership checks. |
| Library/Homebrew/formula.rb | Avoids Pathname#present? emptiness check in linked_keg. |
| Library/Homebrew/rubocops/api_name_membership.rb | New cop forbidding include?/exclude? scans on API name lists; autocorrects to predicates. |
| Library/Homebrew/rubocops/all.rb | Requires the new RuboCop cop. |
| Library/Homebrew/test/api_spec.rb | Adds unit coverage for the new predicate methods. |
| Library/Homebrew/test/rubocops/api_name_membership_spec.rb | Adds cop offense + autocorrect + negative coverage tests. |
| Library/Homebrew/test/dev-cmd/edit_spec.rb | Updates stubbing to match predicate-based implementation. |
| Library/Homebrew/test/bundle/installer_spec.rb | Updates stubbing to match predicate-based implementation. |
| Library/Homebrew/sorbet/rbi/dsl/rubo_cop/cop/homebrew/api_name_membership.rbi | Adds generated RBI for the new cop’s node matcher method. |
Files not reviewed (1)
- Library/Homebrew/sorbet/rbi/dsl/rubo_cop/cop/homebrew/api_name_membership.rbi: File type not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
0074c25 to
1e81b7f
Compare
dduugg
commented
Jul 6, 2026
Member
Author
Good call, it's narrower in the patterns that it can find than what this PR fixes, but I've added one to the PR. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
installandupgradeare the 2nd and 3rd most-runbrewcommands in the last 365 days of analytics (5.20% and 4.94% of all invocations). Profilingbrew upgrade --dry-runwith stackprof showed two hot spots in the formula resolution path they share:Formulary::FromAPILoader.try_newcalledHomebrew::API.formula_names.exclude?(name)for every formula lookup. Each call builds a fresh array of all ~7,500 core formula names viaHash#keysand then scans it linearly. Together,Array#include?(9.6%) andHash#keys(3.3%) accounted for ~13% of total wall time.Formula#linked_kegchecked the found path with.present?.Pathname#present?delegates toPathname#empty?, which performs aDir.empty?syscall on the directory, ~3% of total wall time in aggregate.Changes
Homebrew::API.formula_name?(name), an O(1)Hash#key?membership test, and use it inFormulary::FromAPILoader.try_newandFormulary.tap_formula_name_typeinstead of scanningformula_names.linked_kegpath via a truthiness check instead of.present?, avoiding a directory-read syscall per formula.brew upgrade --dry-runoutput is byte-identical before and after. The same lookup path also servesinstall,outdated,infoand any other command resolving core formulae by name.Benchmarks (Hyperfine, 10 runs each, warmup 2,
HOMEBREW_SORBET_RUNTIMEunset, 9 taps and ~30 outdated formulae):brew upgrade --dry-run~20% decrease in wall time (~1 s saved per run).
Remaining call sites and a cop to ban the pattern
A comprehensive scan for other linear membership checks against the API name lists found more, now also converted (with
Homebrew::API.cask_token?added to mirrorformula_name?):bundle/installer.rb:formula_names.exclude?/cask_tokens.exclude?per unqualified Brewfile entry.cli/named_args.rb:formula_names.include?/cask_tokens.include?per named argument (reached when the core taps are not installed, the default setup).utils/analytics.rb: one check perbrew infoformula and one per cask.dev-cmd/edit.rb: four membership checks on named arguments.uninstall.rb: repeatedinclude?against a localformula_namesarray while scanningetcleftovers; now aSet.cask/cask_loader.rb:Cask::CaskLoader::FromAPILoader.try_new(the cask counterpart of theFormularyhot spot) andtap_cask_token_type.As suggested in review, this now includes an internal autocorrecting
Homebrew/ApiNameMembershipcop that bansinclude?/exclude?onHomebrew::API.formula_names/Homebrew::API.cask_tokens. The cop earned its keep immediately: thecask_loader.rb,analytics.rbcask and one of theedit.rbcall sites above were caught by the cop after the manual scan missed them.Each converted check drops from ~48 μs (plus a fresh ~7,500-element
Hash#keysarray allocation) to ~5 μs. Unlike theFormularysite these are per-argument or per-entry rather than per-formula-resolution, so command-level benchmarks are unchanged (brew upgrade --dry-runre-measured at 3.92 s vs 3.91 s before this sweep, within noise); the savings scale with Brewfile size, named-argument count and installed-cask count.tap_auditor.rbandcask/audit.rbscanTap#formula_names(a different, tap-local list) and are left alone.brewcommands to reproduce the bug?brew lgtm(style, typechecking and tests) locally?Claude Code was used to profile
brew upgrade --dry-runwith stackprof, identify the hot spots, implement the changes and write the new spec. Verified by comparing before/afterbrew upgrade --dry-runoutput for byte-identical results, clean before/after Hyperfine benchmarks on otherwise identical trees, and runningbrew lgtmlocally.