Skip to content

Commit

Permalink
Merge pull request #44528 from Shopify/fixtures-accessor-footprint
Browse files Browse the repository at this point in the history
Reduce the memory footprint of fixtures accessors
  • Loading branch information
byroot committed Feb 24, 2022
2 parents 53f703b + 05d80fc commit 00b03b8
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 26 deletions.
11 changes: 11 additions & 0 deletions activerecord/CHANGELOG.md
@@ -1,3 +1,14 @@
* Reduce the memory footprint of fixtures accessors.

Until now fixtures accessors were eagerly defined using `define_method`.
So the memory usage was directly dependent of the number of fixtures and
test suites.

Instead fixtures accessors are now implemented with `method_missing`,
so they incur much less memory and CPU overhead.

*Jean Boussier*

* Fix `config.active_record.destroy_association_async_job` configuration

`config.active_record.destroy_association_async_job` should allow
Expand Down
70 changes: 44 additions & 26 deletions activerecord/lib/active_record/test_fixtures.rb
Expand Up @@ -24,6 +24,7 @@ def after_teardown # :nodoc:
class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances
class_attribute :pre_loaded_fixtures, default: false
class_attribute :lock_threads, default: true
class_attribute :fixture_sets, default: {}
end

module ClassMethods
Expand Down Expand Up @@ -55,35 +56,15 @@ def fixtures(*fixture_set_names)

def setup_fixture_accessors(fixture_set_names = nil)
fixture_set_names = Array(fixture_set_names || fixture_table_names)
methods = Module.new do
unless fixture_set_names.empty?
self.fixture_sets = fixture_sets.dup
fixture_set_names.each do |fs_name|
fs_name = fs_name.to_s
accessor_name = fs_name.tr("/", "_").to_sym

define_method(accessor_name) do |*fixture_names|
force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
return_single_record = fixture_names.size == 1
fixture_names = @loaded_fixtures[fs_name].fixtures.keys if fixture_names.empty?

@fixture_cache[fs_name] ||= {}

instances = fixture_names.map do |f_name|
f_name = f_name.to_s if f_name.is_a?(Symbol)
@fixture_cache[fs_name].delete(f_name) if force_reload

if @loaded_fixtures[fs_name][f_name]
@fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find
else
raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'"
end
end

return_single_record ? instances.first : instances
end
private accessor_name
key = fs_name.match?(%r{/}) ? -fs_name.to_s.tr("/", "_") : fs_name
key = -key.to_s if key.is_a?(Symbol)
fs_name = -fs_name.to_s if fs_name.is_a?(Symbol)
fixture_sets[key] = fs_name
end
end
include methods
end

def uses_transaction(*methods)
Expand Down Expand Up @@ -283,5 +264,42 @@ def instantiate_fixtures
def load_instances?
use_instantiated_fixtures != :no_instances
end

def method_missing(name, *args, **kwargs, &block)
if fs_name = fixture_sets[name.to_s]
access_fixture(fs_name, *args, **kwargs, &block)
else
super
end
end

def respond_to_missing?(name, include_private = false)
if include_private && fixture_sets.key?(name.to_s)
true
else
super
end
end

def access_fixture(fs_name, *fixture_names)
force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload
return_single_record = fixture_names.size == 1

fixture_names = @loaded_fixtures[fs_name].fixtures.keys if fixture_names.empty?
@fixture_cache[fs_name] ||= {}

instances = fixture_names.map do |f_name|
f_name = f_name.to_s if f_name.is_a?(Symbol)
@fixture_cache[fs_name].delete(f_name) if force_reload

if @loaded_fixtures[fs_name][f_name]
@fixture_cache[fs_name][f_name] ||= @loaded_fixtures[fs_name][f_name].find
else
raise StandardError, "No fixture named '#{f_name}' found for fixture set '#{fs_name}'"
end
end

return_single_record ? instances.first : instances
end
end
end

0 comments on commit 00b03b8

Please sign in to comment.