Skip to content
This repository has been archived by the owner on Sep 18, 2020. It is now read-only.

Try to do the same thing with finalizer #1

Merged
merged 6 commits into from
May 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Rake::ExtensionTask.new("living_dead", spec){|ext|
RSpec::Core::RakeTask.new('spec' => 'compile')

task default: :spec
task test: :spec

task :run => 'compile' do
ruby %q{-I ./lib test.rb}
Expand Down
12 changes: 6 additions & 6 deletions ext/living_dead/living_dead.c
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,12 @@ living_dead_tracing_hash(VALUE self)
void
Init_living_dead(void)
{
VALUE mod = rb_mLivingDead = rb_const_get(rb_cObject, rb_intern("LivingDead"));
// VALUE mod = rb_mLivingDead = rb_const_get(rb_cObject, rb_intern("LivingDead"));

// LivingDead.trace is a ruby level method
// rb_define_module_function(mod, "trace", living_dead_trace, 1);
rb_define_module_function(mod, "start", living_dead_start, 0);
rb_define_module_function(mod, "freed_hash", living_dead_freed_hash, 0);
rb_define_module_function(mod, "tracing_hash", living_dead_tracing_hash, 0);
// // LivingDead.trace is a ruby level method
// // rb_define_module_function(mod, "trace", living_dead_trace, 1);
// rb_define_module_function(mod, "start", living_dead_start, 0);
// rb_define_module_function(mod, "freed_hash", living_dead_freed_hash, 0);
// rb_define_module_function(mod, "tracing_hash", living_dead_tracing_hash, 0);

}
120 changes: 50 additions & 70 deletions lib/living_dead.rb
Original file line number Diff line number Diff line change
@@ -1,96 +1,76 @@
require "living_dead/version"
require "living_dead/living_dead"
require "living_dead/object_trace"

require 'stringio'
require 'objspace'

module LivingDead
@tracing_hash = {}

@string_io = StringIO.new
def self.tracing_hash
@tracing_hash
end

def self.trace(*args)
self.start
trace = ObjectTrace.new(*args)

self.tracing_hash[trace.key] = trace
self.freed_hash[trace.key] = false
obj_trace = ObjectTrace.new(*args)
self.tracing_hash[obj_trace.key] = obj_trace
end

def self.traced_objects
gc_start
clear_garbage!
tracing_hash.map do |_, trace|
trace
end
end

private
# GIANT BALL OF HACKS || THERE BE DRAGONS
#
# There is so much I don't understand on why I need to do the things
# I'm doing in this method.
def self.gc_start
# During debugging I found calling "puts" made some things
# mysteriously work, I have no idea why. If you remove this line
# then (more) tests fail. Maybe it has something to do with the way
# GC interacts with IO? I seriously have no idea.
#
@string_io.puts "=="

# Calling flush so we don't create a memory leak.
# Funny enough maybe calling flush without `puts` also works?
# IDK
#
@string_io.flush

# Why do we need this? Well I'll tell you...
# LivingDead calling `singleton_class.instance_eval` does not retain in the simple case
# fails without this.
#
LivingDead.freed_hash

# Calling GC multiple times fixes a different class of things
# Specifically the singleton_class.instance_eval tests.
# It might also be related to calling GC in a block, but changing
# to 1.times brings back failures.
#
# Calling 2 times results in eventual failure https://twitter.com/schneems/status/804369346910896128
# Calling 5 times results in eventual failure https://twitter.com/schneems/status/804382968307445760
# Trying 10 times
#
10.times { GC.start }
end
public

def self.freed_objects
gc_start
freed_hash.map do |key, _|
tracing_hash[key]
end
traced_objects.select { |x| x.freed? }
end

class ObjectTrace
def initialize(obj = nil, object_id: nil, to_s: nil)
@object_id = object_id || obj.object_id
@to_s = to_s&.dup || obj.to_s.dup
private
# Here's the deal: Running GC.start does NOT guarantee
# that all "dead" objects will be cleared. Which is sad
# because this gem would have been really simple if that was
# the case.
#
# The way to work around that is by creating an object that is
# not retained and tracing it with ObjectTrace. We then
# must get Ruby to clear objects in the (object) heap and the stack
# to do that we iteratively create new objects and block objects
# until we can determine that the val that we made and traced is
# no longer retained by Ruby.
#
# When this happens we assume that any other dead objects that the
# user was watching are cleared as well.
#
# This is still not 100% guaranteed, but it's a good place to start.
#
# Some more context:
# https://github.com/schneems/heap_problem/pull/1
def self.clear_garbage!
val = Object.new
obj_trace = ObjectTrace.new(val)
val = nil

n = 100
while obj_trace.retained?
n += 1
block_stack_flush(n)
object_stack_flush(n)
end

def to_s
"#<LivingDead::ObjectTrace:#{ "0x#{ (object_id << 1).to_s(16) }" } @object_id=#{@object_id} @to_s=#{ @to_s.inspect }, @freed=#{ freed? }>"
end

def inspect
to_s
end

def retained?
!freed?
end
GC.start
end

def freed?
!!LivingDead.freed_hash[@object_id]
end
def self.block_stack_flush(n)
1.times { block_stack_flush(n - 1) if n > 0 }
end

def key
@object_id
end
def self.object_stack_flush(n)
val = Object.new
val = nil
object_stack_flush(n - 1) if n > 0
nil
end
end
31 changes: 31 additions & 0 deletions lib/living_dead/object_trace.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require 'weakref'

module LivingDead
class ObjectTrace
def initialize(obj = nil, object_id: nil, to_s: nil)
@object_id = object_id || obj.object_id
@to_s = to_s&.dup || obj.to_s.dup
@weakref = WeakRef.new(obj)
end

def to_s
"#<LivingDead::ObjectTrace:#{ "0x#{ (object_id << 1).to_s(16) }" } @object_id=#{@object_id} @to_s=#{ @to_s.inspect }, @freed=#{ freed? }>"
end

def inspect
to_s
end

def retained?
@weakref.weakref_alive?
end

def freed?
!retained?
end

def key
@object_id
end
end
end
15 changes: 10 additions & 5 deletions spec/living_dead/singleton_class_instance_eval_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,32 @@
describe LivingDead do
context "calling `singleton_class.instance_eval` " do
it "does not retain in the simple case" do
out = run("env ruby #{ fixtures('singleton_class_instance_eval/simple.rb') }")
file = fixtures('singleton_class_instance_eval/simple.rb')
out = run("env ruby #{file}")
expect(out).to match("PASS")
end

it "does retain objects when used with a instance variable" do
out = run("env ruby #{ fixtures('singleton_class_instance_eval/retained.rb') }")
file = fixtures('singleton_class_instance_eval/retained.rb')
out = run("env ruby #{file}")
expect(out).to match("PASS")
end

it "does not retain in a class" do
out = run("env ruby #{ fixtures('singleton_class_instance_eval/in_class.rb') }")
file = fixtures('singleton_class_instance_eval/in_class.rb')
out = run("env ruby #{file}")
expect(out).to match("PASS")
end

it "does not retain in a proc" do
out = run("env ruby #{ fixtures('singleton_class_instance_eval/in_proc.rb') }")
file = fixtures('singleton_class_instance_eval/in_proc.rb')
out = run("env ruby #{file}")
expect(out).to match("PASS")
end

it "does not retain in method in proc" do
out = run("env ruby #{ fixtures('singleton_class_instance_eval/method_in_proc.rb') }")
file = fixtures('singleton_class_instance_eval/method_in_proc.rb')
out = run("env ruby #{file}")
expect(out).to match("PASS")
end
end
Expand Down
15 changes: 10 additions & 5 deletions spec/living_dead/singleton_class_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,32 @@
describe LivingDead do
context "calling `singleton_class` " do
it "does not retain in the simple case" do
out = run("env ruby #{ fixtures('singleton_class/simple.rb') }")
file = fixtures('singleton_class/simple.rb')
out = run("env ruby #{file}")
expect(out).to match("PASS")
end

it "does retain objects when used with a instance variable" do
out = run("env ruby #{ fixtures('singleton_class/retained.rb') }")
file = fixtures('singleton_class/retained.rb')
out = run("env ruby #{file}")
expect(out).to match("PASS")
end

it "does not retain in a class" do
out = run("env ruby #{ fixtures('singleton_class/in_class.rb') }")
file = fixtures('singleton_class/in_class.rb')
out = run("env ruby #{file}")
expect(out).to match("PASS")
end

it "does not retain in a proc" do
out = run("env ruby #{ fixtures('singleton_class/in_proc.rb') }")
file = fixtures('singleton_class/in_proc.rb')
out = run("env ruby #{file}")
expect(out).to match("PASS")
end

it "does not retain in method in proc" do
out = run("env ruby #{ fixtures('singleton_class/method_in_proc.rb') }")
file = fixtures('singleton_class/method_in_proc.rb')
out = run("env ruby #{file}")
expect(out).to match("PASS")
end
end
Expand Down