From: "parker (Parker Finch)" Date: 2022-10-18T21:33:21+00:00 Subject: [ruby-core:110410] [Ruby master Bug#19041] Weakref is still alive after major garbage collection Issue #19041 has been updated by parker (Parker Finch). tenderlovemaking (Aaron Patterson) wrote in #note-7: > That method may not be putting the object in a register. Something else may have put it in a register or in the stack, and it just happens that no other machine code has overwritten the register or stack memory. There's some evidence that the `weakref_alive?` method is putting it in a register or the stack. Running garbage collection _immediately_ after calling `weakref_alive?` will fail to collect the underlying object. But if there's a `sleep` between the `weakref_alive?` and running garbage collection then the garbage collection will succeed in collecting the underlying object. To test if it was the `weakref_alive?` call itself that was causing the issue I ran a few different scenarios: ``` # This version does not manifest the issue. (It makes it through two iterations # and terminates.) require "weakref" iterations = 0 while iterations < 2 print "\r#{iterations}" obj = WeakRef.new(Object.new) while obj.weakref_alive? # Sleep to give registers a chance to clear. sleep(0.5) GC.start end iterations += 1 end ``` ``` # This version does manifest the issue. (It gets stuck in the inner loop and # never terminates.) require "weakref" iterations = 0 while iterations < 2 print "\r#{iterations}" obj = WeakRef.new(Object.new) while obj.weakref_alive? # Sleep to give registers a chance to clear. sleep(0.5) # Call the `WeakRef#weakref_alive?` method to see if that causes the issue # to manifest. (It does, GC does _not_ clear out the underlying Object after # this.) obj.weakref_alive? GC.start end iterations += 1 end ``` ``` # This version does not manifest the issue. (It makes it through two iterations # and terminates.) require "weakref" iterations = 0 while iterations < 2 print "\r#{iterations}" obj = WeakRef.new(Object.new) while obj.weakref_alive? # Sleep to give registers a chance to clear. sleep(0.5) # Reference the WeakRef object to see if that causes the issue to # manifest. (It does not, GC still clears out the underlying Object here.) obj GC.start end iterations += 1 end ``` ``` # This version does not manifest the issue. (It makes it through two iterations # and terminates.) require "weakref" iterations = 0 while iterations < 2 print "\r#{iterations}" obj = WeakRef.new(Object.new) while obj.weakref_alive? # Sleep to give registers a chance to clear. sleep(0.5) # Call another method on the WeakRef object to see if that causes the issue # to manifest. (It does not, GC still clears out the underlying Object # here.) obj.object_id GC.start end iterations += 1 end ``` Sorry for the wall of code there ��� the summary is that the issue only seems to manifest when the `weakref_alive?` method is called immediately before garbage collecting. The fact that the behavior is predictable in those different scenarios makes me think that the `weakref_alive?` method is doing something that adds a reference to the underlying `Object` to a register or the stack. Is there another explanation for the behavior there that I'm missing? --- > If you dump the heap (`ObjectSpace.dump_all`), you'll probably see one of the roots (probably VM?) pointing at the object. Unfortunately the heap dump won't tell you _how_ it found the reference, just that the reference exists. You could find whether it's a register or stack memory by adding some debugging code to the GC or by tracing the machine code via lldb. Thanks @tenderlovemaking! I didn't know about `ObjectSpace.dump_all`. I'll try exploring those options to see if I can pin down how it's finding the reference to the Object. Heads up that it will likely take me a while since I'm not yet familiar with C and lldb. ---------------------------------------- Bug #19041: Weakref is still alive after major garbage collection https://bugs.ruby-lang.org/issues/19041#change-99722 * Author: parker (Parker Finch) * Status: Closed * Priority: Normal * ruby -v: ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-darwin21] * Backport: 2.7: UNKNOWN, 3.0: UNKNOWN, 3.1: UNKNOWN ---------------------------------------- I am able to get into an infinite loop waiting for garbage collection to take a WeakRef. ### Reproduction Process The following script prints a "0", then a "1", and then hangs forever. I expect it to keep printing numbers. ``` require "weakref" iterations = 0 loop do print "\r#{iterations}" obj = WeakRef.new(Object.new) GC.start while obj.weakref_alive? iterations += 1 end ``` ### Ruby Version I have tested this on Ruby 3.1.2, 3.1.0, 3.0.4, 3.0.0, 2.7.6, and 2.7.0 on macOS. All exhibit this behavior. ### Further Investigation #### Sleeping Sleeping before the garbage collection allows the loop to continue. The below exhibits the expected behavior: ``` require "weakref" iterations = 0 loop do print "\r#{iterations}" obj = WeakRef.new(Object.new) (sleep(0.5); GC.start) while obj.weakref_alive? iterations += 1 end ``` However, sleeping _after_ the garbage collection still shows the buggy behavior (loop hangs): ``` require "weakref" iterations = 0 loop do print "\r#{iterations}" obj = WeakRef.new(Object.new) (GC.start; sleep(0.5)) while obj.weakref_alive? iterations += 1 end ``` #### Running Garbage Collection Multiple Times Explicitly running garbage collection multiple times allows the loop to continue. This has the expected behavior, more numbers continue to be printed: ``` require "weakref" iterations = 0 loop do print "\r#{iterations}" obj = WeakRef.new(Object.new) while obj.weakref_alive? GC.start GC.start GC.start end iterations += 1 end ``` However, with certain rubies, running those garbage collection calls in a `times` block prevents even a single iteration from completing. The following prints only "0" with ruby 3.0.4 on macOS, ruby 2.7.6 on macOS, and ruby 3.1.2 on linux (`ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux]` on a virtual machine). It shows the expected behavior on ruby 3.1.2 on macOS. ``` require "weakref" iterations = 0 loop do print "\r#{iterations}" obj = WeakRef.new(Object.new) 3.times { GC.start } while obj.weakref_alive? iterations += 1 end ``` -- https://bugs.ruby-lang.org/ Unsubscribe: