diff options
Diffstat (limited to 'test/ruby/test_gc.rb')
| -rw-r--r-- | test/ruby/test_gc.rb | 480 |
1 files changed, 256 insertions, 224 deletions
diff --git a/test/ruby/test_gc.rb b/test/ruby/test_gc.rb index 39b001c3d0..21448294c2 100644 --- a/test/ruby/test_gc.rb +++ b/test/ruby/test_gc.rb @@ -40,16 +40,91 @@ class TestGc < Test::Unit::TestCase end def test_enable_disable + EnvUtil.without_gc do + GC.enable + assert_equal(false, GC.enable) + assert_equal(false, GC.disable) + assert_equal(true, GC.disable) + assert_equal(true, GC.disable) + assert_nil(GC.start) + assert_equal(true, GC.enable) + assert_equal(false, GC.enable) + end + end + + def test_gc_config_full_mark_by_default + config = GC.config + assert_not_empty(config) + assert_true(config[:rgengc_allow_full_mark]) + end + + def test_gc_config_invalid_args + assert_raise(ArgumentError) { GC.config(0) } + end + + def test_gc_config_setting_returns_updated_config_hash + old_value = GC.config[:rgengc_allow_full_mark] + assert_true(old_value) + + new_value = GC.config(rgengc_allow_full_mark: false)[:rgengc_allow_full_mark] + assert_false(new_value) + new_value = GC.config(rgengc_allow_full_mark: nil)[:rgengc_allow_full_mark] + assert_false(new_value) + ensure + GC.config(rgengc_allow_full_mark: old_value) + GC.start + end + + def test_gc_config_setting_returns_config_hash + hash = GC.config(no_such_key: true) + assert_equal(GC.config, hash) + end + + def test_gc_config_disable_major GC.enable - assert_equal(false, GC.enable) - assert_equal(false, GC.disable) - assert_equal(true, GC.disable) - assert_equal(true, GC.disable) + GC.start + + GC.config(rgengc_allow_full_mark: false) + major_count = GC.stat[:major_gc_count] + minor_count = GC.stat[:minor_gc_count] + + arr = [] + (GC.stat_heap[0][:heap_eden_slots] * 2).times do + arr << Object.new + Object.new + end + + assert_equal(major_count, GC.stat[:major_gc_count]) + assert_operator(minor_count, :<=, GC.stat[:minor_gc_count]) assert_nil(GC.start) - assert_equal(true, GC.enable) - assert_equal(false, GC.enable) ensure - GC.enable + GC.config(rgengc_allow_full_mark: true) + GC.start + end + + def test_gc_config_disable_major_gc_start_always_works + GC.config(full_mark: false) + + major_count = GC.stat[:major_gc_count] + GC.start + + assert_operator(major_count, :<, GC.stat[:major_gc_count]) + ensure + GC.config(full_mark: true) + GC.start + end + + def test_gc_config_implementation + omit unless /darwin|linux/.match(RUBY_PLATFORM) + + gc_name = (ENV['RUBY_GC_LIBRARY'] || "default") + assert_equal gc_name, GC.config[:implementation] + end + + def test_gc_config_implementation_is_readonly + omit unless /darwin|linux/.match(RUBY_PLATFORM) + + assert_raise(ArgumentError) { GC.config(implementation: "somethingelse") } end def test_start_full_mark @@ -131,10 +206,9 @@ class TestGc < Test::Unit::TestCase # marking_time + sweeping_time could differ from time by 1 because they're stored in nanoseconds assert_in_delta stat[:time], stat[:marking_time] + stat[:sweeping_time], 1 assert_equal stat[:total_allocated_pages], stat[:heap_allocated_pages] + stat[:total_freed_pages] - assert_operator stat[:heap_sorted_length], :>=, stat[:heap_eden_pages] + stat[:heap_allocatable_pages], "stat is: " + stat.inspect assert_equal stat[:heap_available_slots], stat[:heap_live_slots] + stat[:heap_free_slots] + stat[:heap_final_slots] assert_equal stat[:heap_live_slots], stat[:total_allocated_objects] - stat[:total_freed_objects] - stat[:heap_final_slots] - assert_equal stat[:heap_allocated_pages], stat[:heap_eden_pages] + stat[:heap_tomb_pages] + assert_equal stat[:heap_allocated_pages], stat[:heap_eden_pages] + stat[:heap_empty_pages] if use_rgengc? assert_equal stat[:count], stat[:major_gc_count] + stat[:minor_gc_count] @@ -150,23 +224,19 @@ class TestGc < Test::Unit::TestCase GC.stat_heap(0, stat_heap) GC.stat(stat) - GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT].times do |i| - begin - reenable_gc = !GC.disable + GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i| + EnvUtil.without_gc do GC.stat_heap(i, stat_heap) GC.stat(stat) - ensure - GC.enable if reenable_gc end - assert_equal GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] * (2**i), stat_heap[:slot_size] - assert_operator stat_heap[:heap_allocatable_pages], :<=, stat[:heap_allocatable_pages] + assert_equal GC.stat_heap(i, :slot_size), stat_heap[:slot_size] + assert_operator stat_heap[:heap_live_slots], :<=, stat[:heap_live_slots] + assert_operator stat_heap[:heap_free_slots], :<=, stat[:heap_free_slots] + assert_operator stat_heap[:heap_final_slots], :<=, stat[:heap_final_slots] assert_operator stat_heap[:heap_eden_pages], :<=, stat[:heap_eden_pages] assert_operator stat_heap[:heap_eden_slots], :>=, 0 - assert_operator stat_heap[:heap_tomb_pages], :<=, stat[:heap_tomb_pages] - assert_operator stat_heap[:heap_tomb_slots], :>=, 0 assert_operator stat_heap[:total_allocated_pages], :>=, 0 - assert_operator stat_heap[:total_freed_pages], :>=, 0 assert_operator stat_heap[:force_major_gc_count], :>=, 0 assert_operator stat_heap[:force_incremental_marking_finish_count], :>=, 0 assert_operator stat_heap[:total_allocated_objects], :>=, 0 @@ -179,24 +249,22 @@ class TestGc < Test::Unit::TestCase assert_equal stat_heap[:slot_size], GC.stat_heap(0)[:slot_size] assert_raise(ArgumentError) { GC.stat_heap(-1) } - assert_raise(ArgumentError) { GC.stat_heap(GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT]) } + assert_raise(ArgumentError) { GC.stat_heap(GC::INTERNAL_CONSTANTS[:HEAP_COUNT]) } end def test_stat_heap_all - omit "flaky with RJIT, which allocates objects itself" if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? stat_heap_all = {} stat_heap = {} + # Initialize to prevent GC in future calls + GC.stat_heap(0, stat_heap) + GC.stat_heap(nil, stat_heap_all) - 2.times do - GC.stat_heap(0, stat_heap) + GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i| GC.stat_heap(nil, stat_heap_all) - end - - GC::INTERNAL_CONSTANTS[:SIZE_POOL_COUNT].times do |i| GC.stat_heap(i, stat_heap) # Remove keys that can vary between invocations - %i(total_allocated_objects).each do |sym| + %i(total_allocated_objects heap_live_slots heap_free_slots).each do |sym| stat_heap[sym] = stat_heap_all[i][sym] = 0 end @@ -221,18 +289,17 @@ class TestGc < Test::Unit::TestCase hash.each { |k, v| stat_heap_sum[k] += v } end - assert_equal stat[:heap_allocatable_pages], stat_heap_sum[:heap_allocatable_pages] + assert_equal stat[:heap_live_slots], stat_heap_sum[:heap_live_slots] + assert_equal stat[:heap_free_slots], stat_heap_sum[:heap_free_slots] + assert_equal stat[:heap_final_slots], stat_heap_sum[:heap_final_slots] assert_equal stat[:heap_eden_pages], stat_heap_sum[:heap_eden_pages] - assert_equal stat[:heap_tomb_pages], stat_heap_sum[:heap_tomb_pages] - assert_equal stat[:heap_available_slots], stat_heap_sum[:heap_eden_slots] + stat_heap_sum[:heap_tomb_slots] - assert_equal stat[:total_allocated_pages], stat_heap_sum[:total_allocated_pages] - assert_equal stat[:total_freed_pages], stat_heap_sum[:total_freed_pages] + assert_equal stat[:heap_available_slots], stat_heap_sum[:heap_eden_slots] assert_equal stat[:total_allocated_objects], stat_heap_sum[:total_allocated_objects] assert_equal stat[:total_freed_objects], stat_heap_sum[:total_freed_objects] end def test_measure_total_time - assert_separately([], __FILE__, __LINE__, <<~RUBY) + assert_separately([], __FILE__, __LINE__, <<~RUBY, timeout: 60) GC.measure_total_time = false time_before = GC.stat(:time) @@ -251,9 +318,9 @@ class TestGc < Test::Unit::TestCase def test_latest_gc_info omit 'stress' if GC.stress - assert_separately([], __FILE__, __LINE__, <<-'RUBY') + assert_separately([{"RUBY_GC_HEAP_INIT_BYTES" => "409600"}, "-W0"], __FILE__, __LINE__, <<-'RUBY') GC.start - count = GC.stat(:heap_free_slots) + GC.stat(:heap_allocatable_pages) * GC::INTERNAL_CONSTANTS[:HEAP_PAGE_OBJ_LIMIT] + count = GC.stat(:heap_free_slots) + GC.stat_heap(0, :heap_allocatable_slots) count.times{ "a" + "b" } assert_equal :newobj, GC.latest_gc_info[:gc_by] RUBY @@ -292,70 +359,59 @@ class TestGc < Test::Unit::TestCase 3.times { GC.start } assert_nil GC.latest_gc_info(:need_major_by) - # allocate objects until need_major_by is set or major GC happens - objects = [] - while GC.latest_gc_info(:need_major_by).nil? - objects.append(100.times.map { '*' }) - end + EnvUtil.without_gc do + # allocate objects until need_major_by is set or major GC happens + objects = [] + while GC.latest_gc_info(:need_major_by).nil? + objects.append(100.times.map { '*' }) + GC.start(full_mark: false) + end - # We need to ensure that no GC gets ran before the call to GC.start since - # it would trigger a major GC. Assertions could allocate objects and - # trigger a GC so we don't run assertions until we perform the major GC. - need_major_by = GC.latest_gc_info(:need_major_by) - GC.start(full_mark: false) # should be upgraded to major - major_by = GC.latest_gc_info(:major_by) + # We need to ensure that no GC gets ran before the call to GC.start since + # it would trigger a major GC. Assertions could allocate objects and + # trigger a GC so we don't run assertions until we perform the major GC. + need_major_by = GC.latest_gc_info(:need_major_by) + GC.start(full_mark: false) # should be upgraded to major + major_by = GC.latest_gc_info(:major_by) - assert_not_nil(need_major_by) - assert_not_nil(major_by) + assert_not_nil(need_major_by) + assert_not_nil(major_by) + end end def test_latest_gc_info_weak_references_count assert_separately([], __FILE__, __LINE__, <<~RUBY) - count = 10_000 + GC.disable + COUNT = 10_000 # Some weak references may be created, so allow some margin of error error_tolerance = 100 - # Run full GC to clear out weak references - GC.start - # Run full GC again to collect stats about weak references + # Run full GC to collect stats about weak references GC.start before_weak_references_count = GC.latest_gc_info(:weak_references_count) - before_retained_weak_references_count = GC.latest_gc_info(:retained_weak_references_count) - # Create some objects and place it in a WeakMap - wmap = ObjectSpace::WeakMap.new - ary = Array.new(count) - enum = count.times - enum.each.with_index do |i| - obj = Object.new - ary[i] = obj - wmap[obj] = nil + # Create some WeakMaps + ary = Array.new(COUNT) + COUNT.times.with_index do |i| + ary[i] = ObjectSpace::WeakMap.new end # Run full GC to collect stats about weak references GC.start - assert_operator(GC.latest_gc_info(:weak_references_count), :>=, before_weak_references_count + count - error_tolerance) - assert_operator(GC.latest_gc_info(:retained_weak_references_count), :>=, before_retained_weak_references_count + count - error_tolerance) - assert_operator(GC.latest_gc_info(:retained_weak_references_count), :<=, GC.latest_gc_info(:weak_references_count)) + assert_operator(GC.latest_gc_info(:weak_references_count), :>=, before_weak_references_count + COUNT - error_tolerance) before_weak_references_count = GC.latest_gc_info(:weak_references_count) - before_retained_weak_references_count = GC.latest_gc_info(:retained_weak_references_count) + # Clear ary, so if ary itself is somewhere on the stack, it won't hold all references + ary.clear ary = nil - # Free ary, which should empty out the wmap - GC.start - # Run full GC again to collect stats about weak references + # Free ary, which should GC all the WeakMaps GC.start - # Sometimes the WeakMap has one element, which might be held on by registers. - assert_operator(wmap.size, :<=, 1) - - assert_operator(GC.latest_gc_info(:weak_references_count), :<=, before_weak_references_count - count + error_tolerance) - assert_operator(GC.latest_gc_info(:retained_weak_references_count), :<=, before_retained_weak_references_count - count + error_tolerance) - assert_operator(GC.latest_gc_info(:retained_weak_references_count), :<=, GC.latest_gc_info(:weak_references_count)) + assert_operator(GC.latest_gc_info(:weak_references_count), :<=, before_weak_references_count - COUNT + error_tolerance) RUBY end @@ -382,7 +438,7 @@ class TestGc < Test::Unit::TestCase end def test_singleton_method_added - assert_in_out_err([], <<-EOS, [], [], "[ruby-dev:44436]") + assert_in_out_err([], <<-EOS, [], [], "[ruby-dev:44436]", timeout: 30) class BasicObject undef singleton_method_added def singleton_method_added(mid) @@ -397,32 +453,19 @@ class TestGc < Test::Unit::TestCase end def test_gc_parameter - env = { - "RUBY_GC_HEAP_INIT_SLOTS" => "100" - } - assert_in_out_err([env, "-W0", "-e", "exit"], "", [], []) - assert_in_out_err([env, "-W:deprecated", "-e", "exit"], "", [], - /The environment variable RUBY_GC_HEAP_INIT_SLOTS is deprecated; use environment variables RUBY_GC_HEAP_%d_INIT_SLOTS instead/) - - env = {} - GC.stat_heap.keys.each do |heap| - env["RUBY_GC_HEAP_#{heap}_INIT_SLOTS"] = "200000" - end + env = { "RUBY_GC_HEAP_INIT_BYTES" => "#{200000 * 40}" } assert_normal_exit("exit", "", :child_env => env) - env = {} - GC.stat_heap.keys.each do |heap| - env["RUBY_GC_HEAP_#{heap}_INIT_SLOTS"] = "0" - end + env = { "RUBY_GC_HEAP_INIT_BYTES" => "0" } assert_normal_exit("exit", "", :child_env => env) env = { "RUBY_GC_HEAP_GROWTH_FACTOR" => "2.0", - "RUBY_GC_HEAP_GROWTH_MAX_SLOTS" => "10000" + "RUBY_GC_HEAP_GROWTH_MAX_BYTES" => "409600" } assert_normal_exit("exit", "", :child_env => env) assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_HEAP_GROWTH_FACTOR=2.0/, "") - assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_HEAP_GROWTH_MAX_SLOTS=10000/, "[ruby-core:57928]") + assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_HEAP_GROWTH_MAX_BYTES=409600/, "[ruby-core:57928]") if use_rgengc? env = { @@ -464,115 +507,42 @@ class TestGc < Test::Unit::TestCase end end - def test_gc_parameter_init_slots - assert_separately([], __FILE__, __LINE__, <<~RUBY) - # Constant from gc.c. - GC_HEAP_INIT_SLOTS = 10_000 - GC.stat_heap.each do |_, s| - multiple = s[:slot_size] / (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]) - # Allocatable pages are assumed to have lost 1 slot due to alignment. - slots_per_page = (GC::INTERNAL_CONSTANTS[:HEAP_PAGE_OBJ_LIMIT] / multiple) - 1 - - total_slots = s[:heap_eden_slots] + s[:heap_allocatable_pages] * slots_per_page - assert_operator(total_slots, :>=, GC_HEAP_INIT_SLOTS, s) - end - RUBY + def test_gc_parameter_init_bytes + omit "[Bug #21203] This test is flaky and intermittently failing now" - env = {} - # Make the heap big enough to ensure the heap never needs to grow. - sizes = GC.stat_heap.keys.reverse.map { |i| (i + 1) * 100_000 } - GC.stat_heap.keys.each do |heap| - env["RUBY_GC_HEAP_#{heap}_INIT_SLOTS"] = sizes[heap].to_s - end - assert_separately([env, "-W0"], __FILE__, __LINE__, <<~RUBY) - SIZES = #{sizes} - GC.stat_heap.each do |i, s| - multiple = s[:slot_size] / (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]) - # Allocatable pages are assumed to have lost 1 slot due to alignment. - slots_per_page = (GC::INTERNAL_CONSTANTS[:HEAP_PAGE_OBJ_LIMIT] / multiple) - 1 - - total_slots = s[:heap_eden_slots] + s[:heap_allocatable_pages] * slots_per_page - - # The delta is calculated as follows: - # - For allocated pages, each page can vary by 1 slot due to alignment. - # - For allocatable pages, we can end up with at most 1 extra page of slots. - assert_in_delta(SIZES[i], total_slots, s[:heap_eden_pages] + slots_per_page, s) - end - RUBY - - # Check that the configured sizes are "remembered" across GC invocations. - assert_separately([env, "-W0"], __FILE__, __LINE__, <<~RUBY) - SIZES = #{sizes} + assert_separately([], __FILE__, __LINE__, <<~RUBY, timeout: 60) + GC_HEAP_INIT_BYTES = 2560 * 1024 - # Fill size pool 0 with transient objects. - ary = [] - while GC.stat_heap(0, :heap_allocatable_pages) != 0 - ary << Object.new + gc_count = GC.stat(:count) + # Fill up all heaps to the byte-derived init slot count + GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i| + slot_size = GC.stat_heap(i, :slot_size) + init_slots = GC_HEAP_INIT_BYTES / slot_size + capa = (slot_size - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] - (2 * RbConfig::SIZEOF["void*"])) / RbConfig::SIZEOF["void*"] + while GC.stat_heap(i, :heap_eden_slots) < init_slots + Array.new(capa) + end end - ary.clear - ary = nil - - # Clear all the objects that were allocated. - GC.start - # Check that we still have the same number of slots as initially configured. - GC.stat_heap.each do |i, s| - multiple = s[:slot_size] / (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]) - # Allocatable pages are assumed to have lost 1 slot due to alignment. - slots_per_page = (GC::INTERNAL_CONSTANTS[:HEAP_PAGE_OBJ_LIMIT] / multiple) - 1 - - total_slots = s[:heap_eden_slots] + s[:heap_allocatable_pages] * slots_per_page - - # The delta is calculated as follows: - # - For allocated pages, each page can vary by 1 slot due to alignment. - # - For allocatable pages, we can end up with at most 1 extra page of slots. - assert_in_delta(SIZES[i], total_slots, s[:heap_eden_pages] + slots_per_page, s) - end + assert_equal gc_count, GC.stat(:count) RUBY - # Check that we don't grow the heap in minor GC if we have alloctable pages. - env["RUBY_GC_HEAP_FREE_SLOTS_MIN_RATIO"] = "0.3" - env["RUBY_GC_HEAP_FREE_SLOTS_GOAL_RATIO"] = "0.99" - env["RUBY_GC_HEAP_FREE_SLOTS_MAX_RATIO"] = "1.0" - env["RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR"] = "100" # Large value to disable major GC - assert_separately([env, "-W0"], __FILE__, __LINE__, <<~RUBY) - SIZES = #{sizes} - - # Run a major GC to clear out dead objects. - GC.start - - # Disable GC so we can control when GC is ran. - GC.disable - - # Run minor GC enough times so that we don't grow the heap because we - # haven't yet ran RVALUE_OLD_AGE minor GC cycles. - GC::INTERNAL_CONSTANTS[:RVALUE_OLD_AGE].times { GC.start(full_mark: false) } - - # Fill size pool 0 to over 50% full so that the number of allocatable - # pages that will be created will be over the number in heap_allocatable_pages - # (calculated using RUBY_GC_HEAP_FREE_SLOTS_MIN_RATIO). - # 70% was chosen here to guarantee that. - ary = [] - while GC.stat_heap(0, :heap_allocatable_pages) > - (GC.stat_heap(0, :heap_allocatable_pages) + GC.stat_heap(0, :heap_eden_pages)) * 0.3 - ary << Object.new + env = { "RUBY_GC_HEAP_INIT_BYTES" => "#{800 * 1024}" } + assert_separately([env, "-W0"], __FILE__, __LINE__, <<~RUBY, timeout: 60) + GC_HEAP_INIT_BYTES = 800 * 1024 + + gc_count = GC.stat(:count) + # Fill up all heaps to the byte-derived init slot count + GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i| + slot_size = GC.stat_heap(i, :slot_size) + init_slots = GC_HEAP_INIT_BYTES / slot_size + capa = (slot_size - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] - (2 * RbConfig::SIZEOF["void*"])) / RbConfig::SIZEOF["void*"] + while GC.stat_heap(i, :heap_eden_slots) < init_slots + Array.new(capa) + end end - GC.start(full_mark: false) - - # Check that we still have the same number of slots as initially configured. - GC.stat_heap.each do |i, s| - multiple = s[:slot_size] / (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]) - # Allocatable pages are assumed to have lost 1 slot due to alignment. - slots_per_page = (GC::INTERNAL_CONSTANTS[:HEAP_PAGE_OBJ_LIMIT] / multiple) - 1 - - total_slots = s[:heap_eden_slots] + s[:heap_allocatable_pages] * slots_per_page - - # The delta is calculated as follows: - # - For allocated pages, each page can vary by 1 slot due to alignment. - # - For allocatable pages, we can end up with at most 1 extra page of slots. - assert_in_delta(SIZES[i], total_slots, s[:heap_eden_pages] + slots_per_page, s) - end + assert_equal gc_count, GC.stat(:count) RUBY end @@ -648,15 +618,26 @@ class TestGc < Test::Unit::TestCase def test_thrashing_for_young_objects # This test prevents bugs like [Bug #18929] - assert_separately([], __FILE__, __LINE__, <<-'RUBY') + assert_separately([], __FILE__, __LINE__, <<-'RUBY', timeout: 60) # Grow the heap @ary = 100_000.times.map { Object.new } # Warmup to make sure heap stabilizes 1_000_000.times { Object.new } - before_stats = GC.stat + # We need to pre-allocate all the hashes for GC.stat calls, because + # otherwise the call to GC.stat/GC.stat_heap itself could cause a new + # page to be allocated and the before/after assertions will fail + before_stats = {} + after_stats = {} + # stat_heap needs a hash of hashes for each heap; easiest way to get the + # right shape for that is just to call stat_heap with no argument before_stat_heap = GC.stat_heap + after_stat_heap = GC.stat_heap + + # Now collect the actual stats + GC.stat before_stats + GC.stat_heap nil, before_stat_heap 1_000_000.times { Object.new } @@ -664,24 +645,47 @@ class TestGc < Test::Unit::TestCase # running a minor GC here will guarantee that GC will be complete GC.start(full_mark: false) - after_stats = GC.stat - after_stat_heap = GC.stat_heap + GC.stat after_stats + GC.stat_heap nil, after_stat_heap # Debugging output to for failures in trunk-repeat50@phosphorus-docker debug_msg = "before_stats: #{before_stats}\nbefore_stat_heap: #{before_stat_heap}\nafter_stats: #{after_stats}\nafter_stat_heap: #{after_stat_heap}" # Should not be thrashing in page creation - assert_equal before_stats[:heap_allocated_pages], after_stats[:heap_allocated_pages], debug_msg - assert_equal 0, after_stats[:heap_tomb_pages], debug_msg + assert_in_epsilon before_stats[:heap_allocated_pages], after_stats[:heap_allocated_pages], 0.5, debug_msg assert_equal 0, after_stats[:total_freed_pages], debug_msg - # Only young objects, so should not trigger major GC - assert_equal before_stats[:major_gc_count], after_stats[:major_gc_count], debug_msg + RUBY + end + + def test_heaps_grow_independently + # [Bug #21214] + + assert_separately([], __FILE__, __LINE__, <<-'RUBY', timeout: 60) + COUNT = 1_000_000 + + def allocate_small_object = [] + def allocate_large_object = Array.new(10) + + @arys = Array.new(COUNT) do + # Allocate 10 small transient objects + 10.times { allocate_small_object } + # Allocate 1 large object that is persistent + allocate_large_object + end + + # Running GC here is required to prevent this test from being flaky because + # the heap for the small transient objects may not have been cleared by the + # GC causing heap_available_slots to be slightly over 2 * COUNT. + GC.start + + heap_available_slots = GC.stat(:heap_available_slots) + + assert_operator(heap_available_slots, :<, COUNT * 2, "GC.stat: #{GC.stat}\nGC.stat_heap: #{GC.stat_heap}") RUBY end def test_gc_internals - assert_not_nil GC::INTERNAL_CONSTANTS[:HEAP_PAGE_OBJ_LIMIT] - assert_not_nil GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] + assert_not_nil GC::INTERNAL_CONSTANTS[:HEAP_COUNT] end def test_sweep_in_finalizer @@ -712,6 +716,7 @@ class TestGc < Test::Unit::TestCase end def test_interrupt_in_finalizer + omit 'randomly hangs on many platforms' if ENV.key?('GITHUB_ACTIONS') bug10595 = '[ruby-core:66825] [Bug #10595]' src = <<-'end;' Signal.trap(:INT, 'DEFAULT') @@ -727,7 +732,7 @@ class TestGc < Test::Unit::TestCase ObjectSpace.define_finalizer(Object.new, f) end end; - out, err, status = assert_in_out_err(["-e", src], "", [], [], bug10595, signal: :SEGV) do |*result| + out, err, status = assert_in_out_err(["-e", src], "", [], [], bug10595, signal: :SEGV, timeout: 100) do |*result| break result end unless /mswin|mingw/ =~ RUBY_PLATFORM @@ -768,17 +773,21 @@ class TestGc < Test::Unit::TestCase end def test_gc_stress_at_startup - assert_in_out_err([{"RUBY_DEBUG"=>"gc_stress"}], '', [], [], '[Bug #15784]', success: true, timeout: 60) + assert_in_out_err([{"RUBY_DEBUG"=>"gc_stress"}], '', [], [], '[Bug #15784]', success: true, timeout: 120) end def test_gc_disabled_start - begin - disabled = GC.disable + EnvUtil.without_gc do c = GC.count GC.start assert_equal 1, GC.count - c - ensure - GC.enable unless disabled + end + + EnvUtil.without_gc do + c = GC.count + GC.start(immediate_mark: false, immediate_sweep: false) + 10_000.times { Object.new } + assert_equal 1, GC.count - c end end @@ -790,6 +799,8 @@ class TestGc < Test::Unit::TestCase end def test_exception_in_finalizer_procs + require '-test-/stack' + omit 'failing with ASAN' if Thread.asan? assert_in_out_err(["-W0"], "#{<<~"begin;"}\n#{<<~'end;'}", %w[c1 c2]) c1 = proc do puts "c1" @@ -810,6 +821,8 @@ class TestGc < Test::Unit::TestCase end def test_exception_in_finalizer_method + require '-test-/stack' + omit 'failing with ASAN' if Thread.asan? assert_in_out_err(["-W0"], "#{<<~"begin;"}\n#{<<~'end;'}", %w[c1 c2]) def self.c1(x) puts "c1" @@ -853,28 +866,47 @@ class TestGc < Test::Unit::TestCase end def test_old_to_young_reference - original_gc_disabled = GC.disable + EnvUtil.without_gc do + require "objspace" - require "objspace" + old_obj = Object.new + 4.times { GC.start } - old_obj = Object.new - 4.times { GC.start } + assert_include ObjectSpace.dump(old_obj), '"old":true' - assert_include ObjectSpace.dump(old_obj), '"old":true' + young_obj = Object.new + old_obj.instance_variable_set(:@test, young_obj) - young_obj = Object.new - old_obj.instance_variable_set(:@test, young_obj) + # Not immediately promoted to old generation + 3.times do + assert_not_include ObjectSpace.dump(young_obj), '"old":true' + GC.start + end - # Not immediately promoted to old generation - 3.times do - assert_not_include ObjectSpace.dump(young_obj), '"old":true' + # Takes 4 GC to promote to old generation GC.start + assert_include ObjectSpace.dump(young_obj), '"old":true' end + end - # Takes 4 GC to promote to old generation - GC.start - assert_include ObjectSpace.dump(young_obj), '"old":true' - ensure - GC.enable if !original_gc_disabled + def test_finalizer_not_run_with_vm_lock + assert_ractor(<<~'RUBY', timeout: 30) + Thread.new do + loop do + Encoding.list.each do |enc| + enc.names + end + end + end + + o = Object.new + ObjectSpace.define_finalizer(o, proc do + sleep 0.5 # finalizer shouldn't be run with VM lock, otherwise this context switch will crash + end) + o = nil + 4.times do + GC.start + end + RUBY end end |
