From 25396b1c2a74c1f90a264b12d619b9191b6cd6d3 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Fri, 4 Mar 2022 12:50:31 +0100 Subject: [PATCH] Avoid adding constants to Enumerable Ref: https://github.com/aws/aws-sdk-ruby/pull/2670 Some gems like aws-sdk-core use `Object#extend(Enumerable)`. It's not a very good pattern, but it's somehwat handled ok by Ruby. However if Enumerable has constants, then any time the module is extended, the global constant cache is flushed and this has a very negative impact on performance for the virtual machine, and even worse for JITs. --- .../lib/active_support/core_ext/enumerable.rb | 35 ++++++++++++++----- .../test/core_ext/enumerable_test.rb | 9 +++++ 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 28be5bea94..78a8da245a 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -1,13 +1,30 @@ # frozen_string_literal: true -module Enumerable - INDEX_WITH_DEFAULT = Object.new - private_constant :INDEX_WITH_DEFAULT +module ActiveSupport + module EnumerableCoreExt # :nodoc: + module Constants + private + def const_missing(name) + if name == :SoleItemExpectedError + ::ActiveSupport::EnumerableCoreExt::SoleItemExpectedError + else + super + end + end + end + end +end +module Enumerable # Error generated by +sole+ when called on an enumerable that doesn't have # exactly one item. class SoleItemExpectedError < StandardError; end + # HACK: For performance reasons, Enumerable shouldn't have any constants of its own. + # So we move SoleItemExpectedError into ActiveSupport::EnumerableCoreExt. + ActiveSupport::EnumerableCoreExt::SoleItemExpectedError = remove_const(:SoleItemExpectedError) + singleton_class.prepend(ActiveSupport::EnumerableCoreExt::Constants) + # Enumerable#sum was added in Ruby 2.4, but it only works with Numeric elements # when we omit an identity. @@ -106,17 +123,17 @@ module Enumerable # # %i( created_at updated_at ).index_with(Time.now) # # => { created_at: 2020-03-09 22:31:47, updated_at: 2020-03-09 22:31:47 } - def index_with(default = INDEX_WITH_DEFAULT) + def index_with(default = (no_default = true)) if block_given? result = {} each { |elem| result[elem] = yield(elem) } result - elsif default != INDEX_WITH_DEFAULT + elsif no_default + to_enum(:index_with) { size if respond_to?(:size) } + else result = {} each { |elem| result[elem] = default } result - else - to_enum(:index_with) { size if respond_to?(:size) } end end @@ -240,8 +257,8 @@ module Enumerable def sole case count when 1 then return first # rubocop:disable Style/RedundantReturn - when 0 then raise SoleItemExpectedError, "no item found" - when 2.. then raise SoleItemExpectedError, "multiple items found" + when 0 then raise ActiveSupport::EnumerableCoreExt::SoleItemExpectedError, "no item found" + when 2.. then raise ActiveSupport::EnumerableCoreExt::SoleItemExpectedError, "multiple items found" end end end diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb index 202da47321..01b9d1c879 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -360,4 +360,13 @@ class EnumerableTests < ActiveSupport::TestCase assert_raise(expected_raise) { GenericEnumerable.new([1, 2]).sole } assert_raise(expected_raise) { GenericEnumerable.new([1, nil]).sole } end + + def test_doesnt_bust_constant_cache + skip "Only applies to MRI" unless defined?(RubyVM.stat) && RubyVM.stat(:global_constant_state) + + object = Object.new + assert_no_difference -> { RubyVM.stat(:global_constant_state) } do + object.extend(Enumerable) + end + end end