Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

Commit cb245d3

Browse files
committed
WIP: Optimize the selection of applicable filterable items.
Before, we did a linear scan over the list of filterable items (e.g. `config.include` modules), and checked if each is applicable to a particular example or example group. This linear scan re-performs the same work over and over again, when you consider there may be many examples/groups that lack the same metadata keys that would be used for inclusion. The solution is to memoize that work. We can’t simply use the example or group’s metadata as the memorization key, though, as each example and group have different metadata (e.g. the description and/or location are almost always different). Instead, we keep track of what the applicable metadata keys are, and we use those keys as the basis for getting a subset of the metadata hash of just the keys we care about. We can then use this new hash as the basis of a memoization key. This isn’t done yet (the FilterableItemRepository still needs specs and should handle edge cases like metadata lambdas, and we also need to apply this to hook filtering and some other places) but it’s far enough along that I wanted to push it up for feedback. Some specs are failing.
1 parent 31ed2c2 commit cb245d3

File tree

3 files changed

+44
-17
lines changed

3 files changed

+44
-17
lines changed

lib/rspec/core/configuration.rb

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -301,8 +301,8 @@ def initialize
301301
@start_time = $_rspec_core_load_started_at || ::RSpec::Core::Time.now
302302
# rubocop:enable Style/GlobalVars
303303
@expectation_frameworks = []
304-
@include_modules = []
305-
@extend_modules = []
304+
@include_modules = FilterableItemRepository.new(:any?)
305+
@extend_modules = FilterableItemRepository.new(:any?)
306306
@mock_framework = nil
307307
@files_or_directories_to_run = []
308308
@color = false
@@ -331,7 +331,7 @@ def initialize
331331
@profile_examples = false
332332
@requires = []
333333
@libs = []
334-
@derived_metadata_blocks = []
334+
@derived_metadata_blocks = FilterableItemRepository.new(:any?)
335335
end
336336

337337
# @private
@@ -1050,7 +1050,7 @@ def exclusion_filter
10501050
# @see #extend
10511051
def include(mod, *filters)
10521052
meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering)
1053-
include_modules << [mod, meta]
1053+
include_modules.add(mod, meta)
10541054
end
10551055

10561056
# Tells RSpec to extend example groups with `mod`. Methods defined in
@@ -1084,7 +1084,7 @@ def include(mod, *filters)
10841084
# @see #include
10851085
def extend(mod, *filters)
10861086
meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering)
1087-
extend_modules << [mod, meta]
1087+
extend_modules.add(mod, meta)
10881088
end
10891089

10901090
# @private
@@ -1344,13 +1344,13 @@ def disable_monkey_patching!
13441344
# end
13451345
def define_derived_metadata(*filters, &block)
13461346
meta = Metadata.build_hash_from(filters, :warn_about_example_group_filtering)
1347-
@derived_metadata_blocks << [meta, block]
1347+
@derived_metadata_blocks.add(block, meta)
13481348
end
13491349

13501350
# @private
13511351
def apply_derived_metadata_to(metadata)
1352-
@derived_metadata_blocks.each do |filter, block|
1353-
block.call(metadata) if filter.empty? || MetadataFilter.apply?(:any?, filter, metadata)
1352+
@derived_metadata_blocks.items_for(metadata).each do |block|
1353+
block.call(metadata)
13541354
end
13551355
end
13561356

@@ -1470,9 +1470,9 @@ def update_pattern_attr(name, value)
14701470
@files_to_run = nil
14711471
end
14721472

1473-
def each_applicable_module(modules, filterable)
1474-
modules.each do |mod, filters|
1475-
yield mod if filters.empty? || filterable.apply?(:any?, filters)
1473+
def each_applicable_module(module_repository, filterable)
1474+
module_repository.items_for(filterable.metadata).each do |mod|
1475+
yield mod
14761476
end
14771477
end
14781478

lib/rspec/core/metadata_filter.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,34 @@ def silence_metadata_example_group_deprecations
8686
end
8787
end
8888
end
89+
90+
class FilterableItemRepository
91+
def initialize(predicate)
92+
@applicable_metadata_keys = Set.new
93+
@items = []
94+
@memoized_lookups = Hash.new do |hash, applicable_metadata|
95+
hash[applicable_metadata] = @items.select do |(_, item_meta)|
96+
item_meta.empty? || MetadataFilter.apply?(predicate, item_meta, applicable_metadata)
97+
end.map { |item, _| item }
98+
end
99+
end
100+
101+
def add(item, metadata)
102+
@items << [item, metadata]
103+
@applicable_metadata_keys.merge(metadata.keys)
104+
@memoized_lookups.clear
105+
end
106+
107+
def items_for(metadata)
108+
@memoized_lookups[applicable_metadata_from metadata]
109+
end
110+
111+
private
112+
113+
def applicable_metadata_from(metadata)
114+
# TODO: ensure this differentiates between `key: nil` and key missing
115+
Hash[ @applicable_metadata_keys.to_a.zip(metadata.values_at(*@applicable_metadata_keys)) ]
116+
end
117+
end
89118
end
90119
end

spec/rspec/core/shared_example_group_spec.rb

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,8 @@ module Core
106106
it "delegates include on configuration" do
107107
implementation = Proc.new { def bar; 'bar'; end }
108108
define_shared_group(:foo => :bar, &implementation)
109-
a = RSpec.configuration.include_modules.first
110-
expect(Class.new.send(:include, a[0]).new.bar).to eq('bar')
111-
expect(a[1]).to eq(:foo => :bar)
109+
a = RSpec.configuration.include_modules.items_for(:foo => :bar).first
110+
expect(Class.new.send(:include, a).new.bar).to eq('bar')
112111
end
113112
end
114113

@@ -122,9 +121,8 @@ module Core
122121
it "delegates include on configuration" do
123122
implementation = Proc.new { def bar; 'bar'; end }
124123
define_shared_group("name", :foo => :bar, &implementation)
125-
a = RSpec.configuration.include_modules.first
126-
expect(Class.new.send(:include, a[0]).new.bar).to eq('bar')
127-
expect(a[1]).to eq(:foo => :bar)
124+
a = RSpec.configuration.include_modules.items_for(:foo => :bar).first
125+
expect(Class.new.send(:include, a).new.bar).to eq('bar')
128126
end
129127

130128
describe "hooks for individual examples that have matching metadata" do

0 commit comments

Comments
 (0)