Profiling `require "bundler/setup"` in a 450-gem app: 13 findings, a series of small PRs

Aperta
#9,998 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

@linyiru ci sta già lavorando.

Dal 31/10/2026.

  • #10003 di @linyiru — aperta
  • #10005 di @linyiru — aperta
  • #10006 di @linyiru — aperta

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
15/100
Tipo di issue
Refactoring
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
ruby

Direzione di ricerca

This is a proposal bundle, not one task: it names 13 independent boot-time changes, most in Bundler's setup path, plus RubyGems' load_defaults and Gem.register_default_spec. Start with one named method such as SpecSet#validate_deps or LockfileParser, and run require "bundler/setup" under a profiler to confirm the cost. Done means the spec for that area still passes and the timing improves, but the maintainer has not yet said which of these changes they want.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

I profiled the boot of a Rails application with 450 activated gems (1,500-line lockfile, 443 installed gemspecs, a few path: and git: gems) and found 13 places in RubyGems and Bundler where boot does work it does not need. I'm about to open several small PRs at once, so this issue explains them as a set and gives you one place to say no to any of them.

Summary

Measured on ruby/rubygems master (c2380f6), Ruby 3.4.12, macOS arm64, hyperfine with the three variants interleaved in one run (min of 50 runs):

require "bundler/setup" vs master
master 142.9 ms
the 8 PRs below that I consider free, merged together 114.3 ms −20%
plus the 4 changes I'm not sending (see below) 107.8 ms −25%

ruby -e 1 itself (with RubyGems) goes from 26.0 to 25.3 ms with the one RubyGems startup change.

Every PR keeps behaviour the same, adds a spec or test, and was checked to fail without the change where that is possible. I ran spec/bundler, spec/runtime, spec/lock, the install/exec/binstubs/help command specs, and the affected RubyGems tests with each change and with all of them merged.

PRs, each independent

No cost, I believe: simpler or equal code.

PR Change Measured
#9997 Bundler::StubSpecification#contains_requirable_file? delegates to the stub instead of loading the full gemspec through method_missing 7 find_active_stub_by_path lookups: 54–96 ms → 13 ms (matters to gems that call it, such as ffi-compiler ≤ 1.4.2)
#9999 SpecSet#validate_deps looks dependencies up by name instead of names.include? (a fresh array per call) and none? over every spec 9.8 → 2.6 ms
#10000 add_default_gems_to checks the stub's name before loading the full default gemspec replace_entrypoints 8.4 → 5.8 ms
#10001 Autoload the vendored FileUtils instead of requiring it in bundler.rb −2.9 ms
#10002 Gem::Requirement.default builds new instead of parsing ">= 0" 1,144 calls in setup, −0.9 ms
#10003 LockfileParser checks validity with one regexp instead of a second split, and walks lines with each_line lockfile parse 13.4 → 10.9 ms

Small cost, worth it in my view; each PR says what the cost is.

PR Change Measured Cost
#10004 Add each lockfile source's spec names once after parsing instead of `Array# ` per spec line (O(n²)) lockfile parse 13.1 → 10.0 ms
#10005 Build Bundler::UI::Shell's Thor shell on first output −12 ms (23 Thor files no longer loaded by bundler/setup) lazy initialization in UI::Shell, with the CLI still building it up front for --no-color; Installer now requires Thor itself
#10006 load_defaults indexes $LOADED_FEATURES once instead of scanning it for each of ~800 default gem files ruby -e 1: −0.7 to −1.7 ms, every Ruby process an optional argument on Gem.register_default_spec

Not sending, unless you want them

These change semantics or add state that has to be kept correct, for 1–3 ms each. I'd rather ask than send them:

  • Gem::Specification.reset keeps the default gemspecs in the load cache. Bundler calls Gem.clear_paths twice during setup, which drops the 60 default gemspecs load_defaults just evaluated, so they are evaluated again (−2.9 ms after the add_default_gems_to PR). Changes what reset resets.
  • setup_manpath only looks at load path entries ending in lib (−1.7 ms on master, which already uses glob_files_in_dir). Changes which entries are scanned.
  • Source::Git#revision returns the locked revision without building a GitProxy (−2 ms, 4 files). Couples Source::Git to how GitProxy treats a given revision, and skips the ambiguous-ref check at runtime.
  • Memoize RubygemsIntegration#default_stubs, which reads the default specifications directory a second and third time (−1.1 ms). A new cache to invalidate.

Larger costs I did not try to fix

  • source "https://rubygems.org" in the Gemfile makes normalize_uri load the vendored URI library, 16 files, 6–8 ms, on every boot that never fetches anything. remotes are URI objects compared and hashed in several places, so making this lazy is a design change.
  • Bundler reads the stub of every installed gem (443 here, 13–14 ms, of which ~6.4 ms is the open/readline floor) although bundler/setup needs only the locked ones.
  • Every Ruby process evaluates the ~60 default gemspecs in load_defaults (~5.3 ms) to build the require-path map.

Happy to adjust any of the PRs, or to close the ones you'd rather not take.

Lingua principale
Ruby
Stelle
4k
Fork
2k
Merge medio
1g 17h
PR unite (30g)
50

Preparare l'ambiente

Apri in Codespaces

Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di ruby/rubygems

Tutte le issue di ruby/rubygems

Issue simili

Altre issue su Ruby

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.