-
-
Notifications
You must be signed in to change notification settings - Fork 764
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix memory leak for multiple runs in the same process #2987
base: main
Are you sure you want to change the base?
Conversation
The `RSpec::ExampleGroups.remove_all_constants` will unset the RSpec::Core::ExampleGroup subclass constants that where generated during a run, though the `@filtered_examples` hash's keys still reference them preventing them from being Garbage collected.
Examples that are added to the RSpec::Core::AnonymousExampleGroup upon their initialization (ex. RSpec::Core::SuiteHookContext) don't currently get cleared between runs. See RSpec::Core::Example#initialize
The RSpec::Core::SharedExampleGroup maintains references to RSpec::Core::ExampleGroup classes that were generated to previous runs preventing them to be garbage collected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👋 thanks for this, we would need specs for the changes and in particular for the shared example group to make sure we aren't accidentally clearing shared example definitions that are needed, I'm assuming your code will just remove the ones attached to now cleared specs but it'd be good to verify that none of the other ways are specifying them are cleared accidentally
@@ -180,6 +180,10 @@ def find(lookup_contexts, name) | |||
shared_example_groups[:main][name] | |||
end | |||
|
|||
def reset | |||
shared_example_groups.delete_if { |k, _| k != :main } | |||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We would need a spec for this, as its important child shared example groups are still usable, its intentional they are not cleared (but unintentional that references to deleted example groups are kept around)
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