summaryrefslogtreecommitdiff
path: root/test/ruby/test_gc.rb
diff options
context:
space:
mode:
Diffstat (limited to 'test/ruby/test_gc.rb')
-rw-r--r--test/ruby/test_gc.rb480
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