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 28be5bea94fee..78a8da245a117 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 @@ def index_by # # %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 @@ def in_order_of(key, series) 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 202da47321fe9..01b9d1c879516 100644 --- a/activesupport/test/core_ext/enumerable_test.rb +++ b/activesupport/test/core_ext/enumerable_test.rb @@ -360,4 +360,13 @@ def test_sole 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