-
-
Notifications
You must be signed in to change notification settings - Fork 765
Ever-increasing memory with a custom runner and a single spec file #2767
Comments
Yes, you've called # Repeated class re-opening does not creating new classes.
2.7.1 :001 > class AC; class DC; end; end
=> nil
2.7.1 :002 > class AC; class DC; end; end
=> nil
2.7.1 :003 > x = []; ObjectSpace.each_object(Class) { |y| x << y if y.to_s =~ /^AC/ }; x
=> [AC::DC, AC]
# However removing the constant does allow new classes to be created with the same name.
2.7.1 :004 > AC.send :remove_const, "DC"
=> AC::DC
2.7.1 :005 > class AC; class DC; end; end
=> nil
2.7.1 :006 > AC.send :remove_const, "DC"
=> AC::DC
2.7.1 :007 > class AC; class DC; end; end
=> nil
2.7.1 :008 > x = []; ObjectSpace.each_object(Class) { |y| x << y if y.to_s =~ /^AC/ }; x
=> [AC::DC, AC::DC, AC::DC, AC] Why do we do this? Because otherwise reloaded files could conflict, its a lot easier to remove the whole thing than it is to try to work out whats changed. Theres also no guarantee you'd be running the same files again, so it makes sense to clean house. In fact if you skip the reset you should find they get different names (there's conflict detection code), to the "world" that the file is reloaded is irrelevant, it's a new spec file to load and parse. Would we expect memory usage to go up? Probably not, running this with a manual GC.compact does seem to keep the memory usage down, but not plateau, so there is possible a leak somewhere, interestingly your memory profile output focuses a lot on the option parser. |
I've been running The memory profile points to optparse as far as "allocated memory/objects" are concerned. But this memory is actually reclaimed by MRI, so I believe it's not relevant in our case. We actually care about "retained memory/objects", and the profile points at rspec-core and specifically For reference, I'm attaching the memory profile I took: memory_profile_3453_1601972084.txt Interestingly, the profile reports only a few megabytes (150MB) worth of retained data whereas the dump was taken when the process' RSS was at 2GB. So I suspect that the profile is not accurate. I'll try to dig into this. That said, if the class objects are never cleared as you demonstrated and are never garbage collected (which they aren't), isn't it expected for the memory to be ever-increasing? |
They are cleared by us, the output you are seeing is their name, not the classes themselves. Unless |
Actually they're class objects (i.e the actual classes themselves). For example: [7] pry(main)> whereami
From: /home/hyu/dev/rspec-memory-issue/test.rb:38 :
33: sleep 3
34:
35: loop do
36: binding.pry if $debug
37:
=> 38: puts "run #{i}"
39: i += 1
40:
41: RSpec.reset
42: RSpec.clear_examples
43:
# demonstrating that it's actually classes that are kept around
[12] pry(main)> RSpec.reset
=> nil
[13] pry(main)> RSpec.clear_examples
=> {}
[14] pry(main)> x = ObjectSpace.each_object(Class).to_a.last
=> RSpec::ExampleGroups::SomethingWsomewhereFoFoFo
[15] pry(main)> x.object_id
=> 47382510390120
[16] pry(main)> x.new
=> #<RSpec::ExampleGroups::SomethingWsomewhereFoFoFo (no description provided)>
[20] pry(main)> x.class
=> Class
[21] pry(main)> x.superclass
=> RSpec::Core::ExampleGroup
[26] pry(main)> x.declaration_locations
=> [["/home/hyu/dev/rspec-memory-issue/spec/foo_spec.rb", 3],
["/home/hyu/dev/rspec-memory-issue/spec/foo_spec.rb", 4],
["/home/hyu/dev/rspec-memory-issue/spec/foo_spec.rb", 5],
["/home/hyu/dev/rspec-memory-issue/spec/foo_spec.rb", 6],
["/home/hyu/dev/rspec-memory-issue/spec/foo_spec.rb", 12],
["/home/hyu/dev/rspec-memory-issue/spec/foo_spec.rb", 13],
["/home/hyu/dev/rspec-memory-issue/spec/foo_spec.rb", 14]]
[18] pry(main)> ObjectSpace.each_object(Class).to_a.count
=> 8251
[19] pry(main)> puts i
1313
=> nil |
Open to suggested improvements, but this isn't a memory leak, it's Ruby being Ruby... We call We don't keep references to the classes. |
I see your point. However, using a simple script, it seems that Ruby does the right thing and garbage collects classes after you remove their respective constants (and therefore they can no longer be referenced so they're eligible for GC): GC.disable
class Foo
class Bar
end
end
puts "Foo.object_id: #{Foo.object_id}"
puts "Foo::Bar.object_id: #{Foo::Bar.object_id}"
puts "Defined classes: "
puts "\t#{ObjectSpace.each_object(Class).select { |c| c.to_s.match?("Foo") }}"
puts
puts "Removed const: #{Foo.send(:remove_const, :Bar)}"
puts
GC.start(immediate_sweep: true, full_mark: true)
puts "GC'ed"
puts
puts "Defined classes: "
puts "\t#{ObjectSpace.each_object(Class).select { |c| c.to_s.match?("Foo") }}"
puts "Foo.object_id: #{Foo.object_id}"
puts "Foo::Bar.object_id: #{Foo::Bar.object_id}" Executing this outputs (as expected):
So from this inconsistent behavior between this script and the original rspec script I deduce that in RSpec's case there's some reference somewhere that prevents all these class definitions from being garbage collected. I'll have to get familiar with RSpec's inner workings, but in the meantime, if you have any pointers or ideas, let me know please. |
No as far as I know we don't hold any reference after a rest, if you find one feel free to open a PR. |
Closing due to inactivity during the monorepo migration, I'm pretty certain theres not much we can do here anyway. |
Subject of the issue
In a custom RSpec runner I'm using, I've noticed an ever-increasing memory usage with each spec file (the same file, actually) that the runner executes. I don't know if this expected or not, so I'm opening an issue here to get your insights as well.
Your environment
Steps to reproduce
Clone the reproduction repository:
Expected behavior
I'd expect memory usage to reach a plateau.
Actual behavior
Memory usage grows indefinitely (reaches some gigabytes after 2-3 minutes). This is probably due to the fact that RSpec creates a class for each describe/it block if the spec file. However, these classes never get garbage collected (which is expected I suppose), but they also get redefined every time the same file gets loaded.
For instance,
ObjectSpace.each_object(RSpec::Core::ExampleGroup).each.count
shows ever increasing values with each loop:Likewise, the defined classes look like this:
So they're repeatedly defined as different class objects. Is this expected behavior?
Thanks in advance.
The text was updated successfully, but these errors were encountered: