bundled_gems.rb: Add a fast path (#11221)

bundled_gems.rb: Add a fast path

[Bug #20641] `Gem::BUNDLED_GEMS.warning?` adds a lot of extra
work on top of `require`. When the call end up atually loading code
the overhead is somewhat marginal.

However it's not uncommon for code to go some late `require` in some
paths, so it's expected that calling `require` with something already
required is somewhat fast, and `bundled_gems.rb` breaks this assumption.

To avoid this, we can have a fast path that in most case allow to
short-circuit all the heavy computations. If we extract the feature
basename and it doesn't match any of the bundled gems we care about
we can return very early.

With this change `require 'date'` is now only 1.33x slower on Ruby
3.3.3, than it was on Ruby 3.2.2, whereas before this change it
was at least 100x slower.

Co-authored-by: Jean Boussier <jean.boussier@gmail.com>
This commit is contained in:
Jean byroot Boussier 2024-07-23 01:35:22 +02:00 committed by GitHub
parent 425e468d25
commit 4667f8ec10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 53 additions and 0 deletions

View File

@ -28,6 +28,8 @@ module Gem::BUNDLED_GEMS
"syslog" => "3.4.0",
}.freeze
SINCE_FAST_PATH = SINCE.transform_keys { |g| g.sub(/\A.*\-/, "") }.freeze
EXACT = {
"abbrev" => true,
"base64" => true,
@ -97,6 +99,22 @@ module Gem::BUNDLED_GEMS
def self.warning?(name, specs: nil)
# name can be a feature name or a file path with String or Pathname
feature = File.path(name)
# The actual checks needed to properly identify the gem being required
# are costly (see [Bug #20641]), so we first do a much cheaper check
# to exclude the vast majority of candidates.
if feature.include?("/")
# If requiring $LIBDIR/mutex_m.rb, we check SINCE_FAST_PATH["mutex_m"]
# We'll fail to warn requires for files that are not the entry point
# of the gem, e.g. require "logger/formatter.rb" won't warn.
# But that's acceptable because this warning is best effort,
# and in the overwhelming majority of case logger.rb will end
# up required.
return unless SINCE_FAST_PATH[File.basename(feature, ".*")]
else
return unless SINCE_FAST_PATH[feature]
end
# bootsnap expands `require "csv"` to `require "#{LIBDIR}/csv.rb"`,
# and `require "syslog"` to `require "#{ARCHDIR}/syslog.so"`.
name = feature.delete_prefix(ARCHDIR)

35
test/test_bundled_gems.rb Normal file
View File

@ -0,0 +1,35 @@
require_relative "rubygems/helper"
require "rubygems"
require "bundled_gems"
class TestBundlerGem < Gem::TestCase
def setup
Gem::BUNDLED_GEMS::WARNED.clear
end
def teardown
Gem::BUNDLED_GEMS::WARNED.clear
end
def test_warning
assert Gem::BUNDLED_GEMS.warning?("rss", specs: {})
assert_nil Gem::BUNDLED_GEMS.warning?("rss", specs: {})
end
def test_no_warning_warning
assert_nil Gem::BUNDLED_GEMS.warning?("some_gem", specs: {})
assert_nil Gem::BUNDLED_GEMS.warning?("/path/to/some_gem.rb", specs: {})
end
def test_warning_libdir
path = File.join(::RbConfig::CONFIG.fetch("rubylibdir"), "rss.rb")
assert Gem::BUNDLED_GEMS.warning?(path, specs: {})
assert_nil Gem::BUNDLED_GEMS.warning?(path, specs: {})
end
def test_warning_archdir
path = File.join(::RbConfig::CONFIG.fetch("rubyarchdir"), "syslog.so")
assert Gem::BUNDLED_GEMS.warning?(path, specs: {})
assert_nil Gem::BUNDLED_GEMS.warning?(path, specs: {})
end
end