Fix memory leak for multiple runs in the same process #2987
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
As reported on #2767 by @agis, there's a memory leak in RSpec when having multiple runs in the same process.
Causes
After a lot of investigation, I've managed to locate the leak's causes and those caused by the
rspec-core
's gem are resolved with the introduced changes of this PR.RSpec::Core::World -
@filtered_examples
The world's instance variable
@filtered_examples
' keys are classes ofRSpec::Core::ExampleGroup
generated for a run's example groups (a.k.a.RSpec::ExamplesGroups::Foo
).Since the instance variable is not being cleared between runs, it keeps references to these classes (along with their "expensive" state - each group class contains all of its examples along with their reporter/loader/formatter etc).
Note: even though the
RSpec::ExampleGroups.remove_all_constants
does remove the constants, the references above keep them alive.RSpec::Core::AnonymousExampleGroup
Examples that are added to the
RSpec::Core::AnonymousExampleGroup
upon their initialization (ex. RSpec::Core::SuiteHookContext) don't currently get cleared between runs.RSpec::Core::SharedExampleGroup::Registry
The
RSpec::Core::SharedExampleGroup::Registry
maintains references toRSpec::Core::ExampleGroup
classes that were generated by previous runs (as keys in the@shared_example_groups
hash) preventing them from being garbage collected.Benchmarking
Introducing the changes of this PR to the reproduction script made by @agis confirmed the fix.
In the previous state, the memory keeps growing and on the 10.000th iteration has reached ~800Mb.
With this PR's fixes, the memory reaches a plateau pretty early and on the 10.000th iteration is at ~39Mb.
External causes
Besides
rspec-core
, I found other "external" causes for memory leaks depending on the codebase's used libraries.I'll try to open the proper PRs for those as well. Until then, find below some info for each of them along with workarounds in case someone finds them helpful.
rspec-mocks
&rspec-rails
The
RSpec::Mocks::Configuration
also keeps references toRSpec::Core::ExampleGroup
classes.The following
rspec-rails
sectionregisters a before suite hook in RSpec's configuration which in turn alter's
RSpec::Mocks::Configuration
I believe that some of these blocks are being defined through RSpec::Core::ExampleGroup class instances (when their
SuiteHookContext
examples execute) and since they live forever in theRSpec::Mocks::Configuration
instance, they keep the references to theirRSpec::Core::ExampleGroup
alive forever.Workaround (monkey patch):
ActiveRecord
ActiveRecord also keeps
RSpec::Core::ExampleGroup
class instance references as keys in its@@already_loaded_fixtures
class variable here:Workaround (monkey patch):
ActiveSupport
ActiveSupport also keeps a record for each class for which it has loaded its hook here:
The block above is being executed for
base
s that areRSpec::Core::ExampleGroup
classes thus references to them live forever.Workaround (monkey patch)
Closes #2767