summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJean byroot Boussier <jean.boussier+github@shopify.com>2024-07-23 01:35:22 +0200
committerGitHub <noreply@github.com>2024-07-22 16:35:22 -0700
commit4667f8ec10269b0b5deca459f098abbdf3bae4ec (patch)
treef45c22083ad90ac7efa2affed4001e874dd1fa46
parent425e468d25a70740cef3ed676e9b82f7902e077a (diff)
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>
-rw-r--r--lib/bundled_gems.rb18
-rw-r--r--test/test_bundled_gems.rb35
2 files changed, 53 insertions, 0 deletions
diff --git a/lib/bundled_gems.rb b/lib/bundled_gems.rb
index eea70ca19a..b1e5470fc7 100644
--- a/lib/bundled_gems.rb
+++ b/lib/bundled_gems.rb
@@ -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)
diff --git a/test/test_bundled_gems.rb b/test/test_bundled_gems.rb
new file mode 100644
index 0000000000..36f7324336
--- /dev/null
+++ b/test/test_bundled_gems.rb
@@ -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