Skip to content

Commit 4667f8e

Browse files
casperisfinebyroot
andauthored
bundled_gems.rb: Add a fast path (ruby#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 <[email protected]>
1 parent 425e468 commit 4667f8e

File tree

2 files changed

+53
-0
lines changed

2 files changed

+53
-0
lines changed

lib/bundled_gems.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ module Gem::BUNDLED_GEMS
2828
"syslog" => "3.4.0",
2929
}.freeze
3030

31+
SINCE_FAST_PATH = SINCE.transform_keys { |g| g.sub(/\A.*\-/, "") }.freeze
32+
3133
EXACT = {
3234
"abbrev" => true,
3335
"base64" => true,
@@ -97,6 +99,22 @@ def self.find_gem(path)
9799
def self.warning?(name, specs: nil)
98100
# name can be a feature name or a file path with String or Pathname
99101
feature = File.path(name)
102+
103+
# The actual checks needed to properly identify the gem being required
104+
# are costly (see [Bug #20641]), so we first do a much cheaper check
105+
# to exclude the vast majority of candidates.
106+
if feature.include?("/")
107+
# If requiring $LIBDIR/mutex_m.rb, we check SINCE_FAST_PATH["mutex_m"]
108+
# We'll fail to warn requires for files that are not the entry point
109+
# of the gem, e.g. require "logger/formatter.rb" won't warn.
110+
# But that's acceptable because this warning is best effort,
111+
# and in the overwhelming majority of case logger.rb will end
112+
# up required.
113+
return unless SINCE_FAST_PATH[File.basename(feature, ".*")]
114+
else
115+
return unless SINCE_FAST_PATH[feature]
116+
end
117+
100118
# bootsnap expands `require "csv"` to `require "#{LIBDIR}/csv.rb"`,
101119
# and `require "syslog"` to `require "#{ARCHDIR}/syslog.so"`.
102120
name = feature.delete_prefix(ARCHDIR)

test/test_bundled_gems.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
require_relative "rubygems/helper"
2+
require "rubygems"
3+
require "bundled_gems"
4+
5+
class TestBundlerGem < Gem::TestCase
6+
def setup
7+
Gem::BUNDLED_GEMS::WARNED.clear
8+
end
9+
10+
def teardown
11+
Gem::BUNDLED_GEMS::WARNED.clear
12+
end
13+
14+
def test_warning
15+
assert Gem::BUNDLED_GEMS.warning?("rss", specs: {})
16+
assert_nil Gem::BUNDLED_GEMS.warning?("rss", specs: {})
17+
end
18+
19+
def test_no_warning_warning
20+
assert_nil Gem::BUNDLED_GEMS.warning?("some_gem", specs: {})
21+
assert_nil Gem::BUNDLED_GEMS.warning?("/path/to/some_gem.rb", specs: {})
22+
end
23+
24+
def test_warning_libdir
25+
path = File.join(::RbConfig::CONFIG.fetch("rubylibdir"), "rss.rb")
26+
assert Gem::BUNDLED_GEMS.warning?(path, specs: {})
27+
assert_nil Gem::BUNDLED_GEMS.warning?(path, specs: {})
28+
end
29+
30+
def test_warning_archdir
31+
path = File.join(::RbConfig::CONFIG.fetch("rubyarchdir"), "syslog.so")
32+
assert Gem::BUNDLED_GEMS.warning?(path, specs: {})
33+
assert_nil Gem::BUNDLED_GEMS.warning?(path, specs: {})
34+
end
35+
end

0 commit comments

Comments
 (0)