bc5cb60
From 00d98eb8a3245fb93a475ecbbbc4c7ec7e6704cd Mon Sep 17 00:00:00 2001
bc5cb60
From: =?UTF-8?q?David=20Rodr=C3=ADguez?= <deivid.rodriguez@riseup.net>
bc5cb60
Date: Tue, 19 May 2020 14:00:00 +0200
bc5cb60
Subject: [PATCH 1/5] Fix performance regression in `require`
bc5cb60
bc5cb60
Our check for `-I` paths should not go through all activated gems.
bc5cb60
---
bc5cb60
 lib/rubygems.rb                         | 10 ++++++++++
bc5cb60
 lib/rubygems/core_ext/kernel_require.rb |  2 +-
bc5cb60
 lib/rubygems/test_case.rb               |  1 +
bc5cb60
 test/rubygems/test_require.rb           | 11 +++++++++++
bc5cb60
 4 files changed, 23 insertions(+), 1 deletion(-)
bc5cb60
bc5cb60
diff --git a/lib/rubygems.rb b/lib/rubygems.rb
bc5cb60
index 843cb49e4a..d1a9a1c7e1 100644
bc5cb60
--- a/lib/rubygems.rb
bc5cb60
+++ b/lib/rubygems.rb
8b746d1
@@ -662,10 +662,20 @@ def self.load_path_insert_index
bc5cb60
     index
bc5cb60
   end
bc5cb60
 
bc5cb60
+  ##
bc5cb60
+  # The number of paths in the `$LOAD_PATH` from activated gems. Used to
bc5cb60
+  # prioritize `-I` and `ENV['RUBYLIB`]` entries during `require`.
bc5cb60
+
bc5cb60
+  def self.activated_gem_paths
bc5cb60
+    @activated_gem_paths ||= 0
bc5cb60
+  end
bc5cb60
+
bc5cb60
   ##
bc5cb60
   # Add a list of paths to the $LOAD_PATH at the proper place.
bc5cb60
 
bc5cb60
   def self.add_to_load_path(*paths)
bc5cb60
+    @activated_gem_paths = activated_gem_paths + paths.size
bc5cb60
+
bc5cb60
     insert_index = load_path_insert_index
bc5cb60
 
bc5cb60
     if insert_index
bc5cb60
diff --git a/lib/rubygems/core_ext/kernel_require.rb b/lib/rubygems/core_ext/kernel_require.rb
bc5cb60
index ed24111bd5..7625ce1bee 100644
bc5cb60
--- a/lib/rubygems/core_ext/kernel_require.rb
bc5cb60
+++ b/lib/rubygems/core_ext/kernel_require.rb
bc5cb60
@@ -47,7 +47,7 @@ def require(path)
bc5cb60
         load_path_insert_index = Gem.load_path_insert_index
bc5cb60
         break unless load_path_insert_index
bc5cb60
 
bc5cb60
-        $LOAD_PATH[0...load_path_insert_index].each do |lp|
bc5cb60
+        $LOAD_PATH[0...load_path_insert_index - Gem.activated_gem_paths].each do |lp|
bc5cb60
           safe_lp = lp.dup.tap(&Gem::UNTAINT)
bc5cb60
           begin
bc5cb60
             if File.symlink? safe_lp # for backward compatibility
bc5cb60
diff --git a/lib/rubygems/test_case.rb b/lib/rubygems/test_case.rb
bc5cb60
index a05a2898d1..53dd495aef 100644
bc5cb60
--- a/lib/rubygems/test_case.rb
bc5cb60
+++ b/lib/rubygems/test_case.rb
8b746d1
@@ -385,6 +385,7 @@ def setup
bc5cb60
     Gem::Security.reset
bc5cb60
 
bc5cb60
     Gem.loaded_specs.clear
bc5cb60
+    Gem.instance_variable_set(:@activated_gem_paths, 0)
bc5cb60
     Gem.clear_default_specs
bc5cb60
     Bundler.reset!
bc5cb60
 
bc5cb60
diff --git a/test/rubygems/test_require.rb b/test/rubygems/test_require.rb
bc5cb60
index f36892f8cc..9f2fe3439a 100644
bc5cb60
--- a/test/rubygems/test_require.rb
bc5cb60
+++ b/test/rubygems/test_require.rb
bc5cb60
@@ -382,6 +382,17 @@ def test_default_gem_require_activates_just_once
bc5cb60
     assert_equal 0, times_called
bc5cb60
   end
bc5cb60
 
bc5cb60
+  def test_second_gem_require_does_not_resolve_path_manually_before_going_through_standard_require
bc5cb60
+    a1 = util_spec "a", "1", nil, "lib/test_gem_require_a.rb"
bc5cb60
+    install_gem a1
bc5cb60
+
bc5cb60
+    assert_require "test_gem_require_a"
bc5cb60
+
bc5cb60
+    stub(:gem_original_require, ->(path) { assert_equal "test_gem_require_a", path }) do
bc5cb60
+      require "test_gem_require_a"
bc5cb60
+    end
bc5cb60
+  end
bc5cb60
+
bc5cb60
   def test_realworld_default_gem
bc5cb60
     testing_ruby_repo = !ENV["GEM_COMMAND"].nil?
bc5cb60
     skip "this test can't work under ruby-core setup" if testing_ruby_repo || java_platform?
bc5cb60
bc5cb60
From ae95885dff6189c5ac59bbdf685cb4ec4751fdef Mon Sep 17 00:00:00 2001
bc5cb60
From: =?UTF-8?q?David=20Rodr=C3=ADguez?= <deivid.rodriguez@riseup.net>
bc5cb60
Date: Tue, 19 May 2020 14:08:19 +0200
bc5cb60
Subject: [PATCH 2/5] Refactor `Gem.load_path_insert_index`
bc5cb60
bc5cb60
---
bc5cb60
 lib/rubygems.rb                         | 13 +++----------
bc5cb60
 lib/rubygems/core_ext/kernel_require.rb |  5 +----
bc5cb60
 2 files changed, 4 insertions(+), 14 deletions(-)
bc5cb60
bc5cb60
diff --git a/lib/rubygems.rb b/lib/rubygems.rb
bc5cb60
index d1a9a1c7e1..ca80326459 100644
bc5cb60
--- a/lib/rubygems.rb
bc5cb60
+++ b/lib/rubygems.rb
8b746d1
@@ -659,7 +659,7 @@ def self.load_path_insert_index
bc5cb60
 
bc5cb60
     index = $LOAD_PATH.index RbConfig::CONFIG['sitelibdir']
bc5cb60
 
bc5cb60
-    index
bc5cb60
+    index || 0
bc5cb60
   end
bc5cb60
 
bc5cb60
   ##
8b746d1
@@ -676,15 +676,8 @@ def self.activated_gem_paths
bc5cb60
   def self.add_to_load_path(*paths)
bc5cb60
     @activated_gem_paths = activated_gem_paths + paths.size
bc5cb60
 
bc5cb60
-    insert_index = load_path_insert_index
bc5cb60
-
bc5cb60
-    if insert_index
bc5cb60
-      # gem directories must come after -I and ENV['RUBYLIB']
bc5cb60
-      $LOAD_PATH.insert(insert_index, *paths)
bc5cb60
-    else
bc5cb60
-      # we are probably testing in core, -I and RUBYLIB don't apply
bc5cb60
-      $LOAD_PATH.unshift(*paths)
bc5cb60
-    end
bc5cb60
+    # gem directories must come after -I and ENV['RUBYLIB']
bc5cb60
+    $LOAD_PATH.insert(Gem.load_path_insert_index, *paths)
bc5cb60
   end
bc5cb60
 
bc5cb60
   @yaml_loaded = false
bc5cb60
diff --git a/lib/rubygems/core_ext/kernel_require.rb b/lib/rubygems/core_ext/kernel_require.rb
bc5cb60
index 7625ce1bee..decf4829f1 100644
bc5cb60
--- a/lib/rubygems/core_ext/kernel_require.rb
bc5cb60
+++ b/lib/rubygems/core_ext/kernel_require.rb
bc5cb60
@@ -44,10 +44,7 @@ def require(path)
bc5cb60
     resolved_path = begin
bc5cb60
       rp = nil
bc5cb60
       Gem.suffixes.each do |s|
bc5cb60
-        load_path_insert_index = Gem.load_path_insert_index
bc5cb60
-        break unless load_path_insert_index
bc5cb60
-
bc5cb60
-        $LOAD_PATH[0...load_path_insert_index - Gem.activated_gem_paths].each do |lp|
bc5cb60
+        $LOAD_PATH[0...Gem.load_path_insert_index - Gem.activated_gem_paths].each do |lp|
bc5cb60
           safe_lp = lp.dup.tap(&Gem::UNTAINT)
bc5cb60
           begin
bc5cb60
             if File.symlink? safe_lp # for backward compatibility
bc5cb60
bc5cb60
From da1492e9d7b28d068fbfbb0ba1cafcc516681567 Mon Sep 17 00:00:00 2001
bc5cb60
From: =?UTF-8?q?David=20Rodr=C3=ADguez?= <deivid.rodriguez@riseup.net>
bc5cb60
Date: Tue, 19 May 2020 14:32:12 +0200
bc5cb60
Subject: [PATCH 3/5] Extract a local outside the loop
bc5cb60
bc5cb60
---
bc5cb60
 lib/rubygems/core_ext/kernel_require.rb | 3 ++-
bc5cb60
 1 file changed, 2 insertions(+), 1 deletion(-)
bc5cb60
bc5cb60
diff --git a/lib/rubygems/core_ext/kernel_require.rb b/lib/rubygems/core_ext/kernel_require.rb
bc5cb60
index decf4829f1..6a7faaf2d1 100644
bc5cb60
--- a/lib/rubygems/core_ext/kernel_require.rb
bc5cb60
+++ b/lib/rubygems/core_ext/kernel_require.rb
bc5cb60
@@ -43,8 +43,9 @@ def require(path)
bc5cb60
     # https://github.com/rubygems/rubygems/pull/1868
bc5cb60
     resolved_path = begin
bc5cb60
       rp = nil
bc5cb60
+      load_path_check_index = Gem.load_path_insert_index - Gem.activated_gem_paths
bc5cb60
       Gem.suffixes.each do |s|
bc5cb60
-        $LOAD_PATH[0...Gem.load_path_insert_index - Gem.activated_gem_paths].each do |lp|
bc5cb60
+        $LOAD_PATH[0...load_path_check_index].each do |lp|
bc5cb60
           safe_lp = lp.dup.tap(&Gem::UNTAINT)
bc5cb60
           begin
bc5cb60
             if File.symlink? safe_lp # for backward compatibility
bc5cb60
bc5cb60
From 22ad5717c38feda2375b53628d15ae3db2195684 Mon Sep 17 00:00:00 2001
bc5cb60
From: =?UTF-8?q?David=20Rodr=C3=ADguez?= <deivid.rodriguez@riseup.net>
bc5cb60
Date: Thu, 21 May 2020 15:20:57 +0200
bc5cb60
Subject: [PATCH 4/5] Fix `$LOADED_FEATURES` cache sometimes not respected
bc5cb60
bc5cb60
In the cases where the initial manually `-I` path resolution succeeded,
bc5cb60
we were passing a full path to the original require effectively skipping
bc5cb60
the `$LOADED_FEATURES` cache. With this change, we _only_ do the
bc5cb60
resolution when a matching requirable path is found in a default gem. In
bc5cb60
that case, we skip activation of the default gem if we detect that the
bc5cb60
required file will be picked up for a `-I` path.
bc5cb60
---
bc5cb60
 lib/rubygems/core_ext/kernel_require.rb | 53 +++++++++++--------------
bc5cb60
 test/rubygems/test_require.rb           | 29 ++++++++++++++
bc5cb60
 2 files changed, 53 insertions(+), 29 deletions(-)
bc5cb60
bc5cb60
diff --git a/lib/rubygems/core_ext/kernel_require.rb b/lib/rubygems/core_ext/kernel_require.rb
bc5cb60
index 6a7faaf2d1..81e37b98bf 100644
bc5cb60
--- a/lib/rubygems/core_ext/kernel_require.rb
bc5cb60
+++ b/lib/rubygems/core_ext/kernel_require.rb
bc5cb60
@@ -39,46 +39,41 @@ def require(path)
bc5cb60
 
bc5cb60
     path = path.to_path if path.respond_to? :to_path
bc5cb60
 
bc5cb60
-    # Ensure -I beats a default gem
bc5cb60
-    # https://github.com/rubygems/rubygems/pull/1868
bc5cb60
-    resolved_path = begin
bc5cb60
-      rp = nil
bc5cb60
-      load_path_check_index = Gem.load_path_insert_index - Gem.activated_gem_paths
bc5cb60
-      Gem.suffixes.each do |s|
bc5cb60
-        $LOAD_PATH[0...load_path_check_index].each do |lp|
bc5cb60
-          safe_lp = lp.dup.tap(&Gem::UNTAINT)
bc5cb60
-          begin
bc5cb60
-            if File.symlink? safe_lp # for backward compatibility
bc5cb60
-              next
bc5cb60
+    if spec = Gem.find_unresolved_default_spec(path)
bc5cb60
+      # Ensure -I beats a default gem
bc5cb60
+      # https://github.com/rubygems/rubygems/pull/1868
bc5cb60
+      resolved_path = begin
bc5cb60
+        rp = nil
bc5cb60
+        load_path_check_index = Gem.load_path_insert_index - Gem.activated_gem_paths
bc5cb60
+        Gem.suffixes.each do |s|
bc5cb60
+          $LOAD_PATH[0...load_path_check_index].each do |lp|
bc5cb60
+            safe_lp = lp.dup.tap(&Gem::UNTAINT)
bc5cb60
+            begin
bc5cb60
+              if File.symlink? safe_lp # for backward compatibility
bc5cb60
+                next
bc5cb60
+              end
bc5cb60
+            rescue SecurityError
bc5cb60
+              RUBYGEMS_ACTIVATION_MONITOR.exit
bc5cb60
+              raise
bc5cb60
             end
bc5cb60
-          rescue SecurityError
bc5cb60
-            RUBYGEMS_ACTIVATION_MONITOR.exit
bc5cb60
-            raise
bc5cb60
-          end
bc5cb60
 
bc5cb60
-          full_path = File.expand_path(File.join(safe_lp, "#{path}#{s}"))
bc5cb60
-          if File.file?(full_path)
bc5cb60
-            rp = full_path
bc5cb60
-            break
bc5cb60
+            full_path = File.expand_path(File.join(safe_lp, "#{path}#{s}"))
bc5cb60
+            if File.file?(full_path)
bc5cb60
+              rp = full_path
bc5cb60
+              break
bc5cb60
+            end
bc5cb60
           end
bc5cb60
+          break if rp
bc5cb60
         end
bc5cb60
-        break if rp
bc5cb60
+        rp
bc5cb60
       end
bc5cb60
-      rp
bc5cb60
-    end
bc5cb60
 
bc5cb60
-    if resolved_path
bc5cb60
-      RUBYGEMS_ACTIVATION_MONITOR.exit
bc5cb60
-      return gem_original_require(resolved_path)
bc5cb60
-    end
bc5cb60
-
bc5cb60
-    if spec = Gem.find_unresolved_default_spec(path)
bc5cb60
       begin
bc5cb60
         Kernel.send(:gem, spec.name, Gem::Requirement.default_prerelease)
bc5cb60
       rescue Exception
bc5cb60
         RUBYGEMS_ACTIVATION_MONITOR.exit
bc5cb60
         raise
bc5cb60
-      end
bc5cb60
+      end unless resolved_path
bc5cb60
     end
bc5cb60
 
bc5cb60
     # If there are no unresolved deps, then we can use just try
bc5cb60
diff --git a/test/rubygems/test_require.rb b/test/rubygems/test_require.rb
bc5cb60
index 9f2fe3439a..2b11e26dfe 100644
bc5cb60
--- a/test/rubygems/test_require.rb
bc5cb60
+++ b/test/rubygems/test_require.rb
bc5cb60
@@ -45,6 +45,35 @@ def refute_require(path)
bc5cb60
     refute require(path), "'#{path}' was not yet required"
bc5cb60
   end
bc5cb60
 
bc5cb60
+  def test_respect_loaded_features_caching_like_standard_require
bc5cb60
+    dir = Dir.mktmpdir("test_require", @tempdir)
bc5cb60
+
bc5cb60
+    lp1 = File.join dir, 'foo1'
bc5cb60
+    foo1 = File.join lp1, 'foo.rb'
bc5cb60
+
bc5cb60
+    FileUtils.mkdir_p lp1
bc5cb60
+    File.open(foo1, 'w') { |f| f.write "class Object; HELLO = 'foo1' end" }
bc5cb60
+
bc5cb60
+    lp = $LOAD_PATH.dup
bc5cb60
+
bc5cb60
+    $LOAD_PATH.unshift lp1
bc5cb60
+    assert_require 'foo'
bc5cb60
+    assert_equal "foo1", ::Object::HELLO
bc5cb60
+
bc5cb60
+    lp2 = File.join dir, 'foo2'
bc5cb60
+    foo2 = File.join lp2, 'foo.rb'
bc5cb60
+
bc5cb60
+    FileUtils.mkdir_p lp2
bc5cb60
+    File.open(foo2, 'w') { |f| f.write "class Object; HELLO = 'foo2' end" }
bc5cb60
+
bc5cb60
+    $LOAD_PATH.unshift lp2
bc5cb60
+    refute_require 'foo'
bc5cb60
+    assert_equal "foo1", ::Object::HELLO
bc5cb60
+  ensure
bc5cb60
+    $LOAD_PATH.replace lp
bc5cb60
+    Object.send :remove_const, :HELLO if Object.const_defined? :HELLO
bc5cb60
+  end
bc5cb60
+
bc5cb60
   # Providing -I on the commandline should always beat gems
bc5cb60
   def test_dash_i_beats_gems
bc5cb60
     a1 = util_spec "a", "1", {"b" => "= 1"}, "lib/test_gem_require_a.rb"
bc5cb60
bc5cb60
From db872c7a18d616f4447bdcca3130be6db9e5cb03 Mon Sep 17 00:00:00 2001
bc5cb60
From: =?UTF-8?q?David=20Rodr=C3=ADguez?= <deivid.rodriguez@riseup.net>
bc5cb60
Date: Sat, 23 May 2020 20:18:41 +0200
bc5cb60
Subject: [PATCH 5/5] Remove direct reference to PR
bc5cb60
bc5cb60
The code is quite different now, so I think the link might be even
bc5cb60
confusing. If you want to know more, use git history.
bc5cb60
---
bc5cb60
 lib/rubygems/core_ext/kernel_require.rb | 1 -
bc5cb60
 1 file changed, 1 deletion(-)
bc5cb60
bc5cb60
diff --git a/lib/rubygems/core_ext/kernel_require.rb b/lib/rubygems/core_ext/kernel_require.rb
bc5cb60
index 81e37b98bf..115ae0cb50 100644
bc5cb60
--- a/lib/rubygems/core_ext/kernel_require.rb
bc5cb60
+++ b/lib/rubygems/core_ext/kernel_require.rb
bc5cb60
@@ -41,7 +41,6 @@ def require(path)
bc5cb60
 
bc5cb60
     if spec = Gem.find_unresolved_default_spec(path)
bc5cb60
       # Ensure -I beats a default gem
bc5cb60
-      # https://github.com/rubygems/rubygems/pull/1868
bc5cb60
       resolved_path = begin
bc5cb60
         rp = nil
bc5cb60
         load_path_check_index = Gem.load_path_insert_index - Gem.activated_gem_paths