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.rb886
1 files changed, 882 insertions, 4 deletions
diff --git a/test/ruby/test_gc.rb b/test/ruby/test_gc.rb
index ed854bde65..21448294c2 100644
--- a/test/ruby/test_gc.rb
+++ b/test/ruby/test_gc.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: false
require 'test/unit'
class TestGc < Test::Unit::TestCase
@@ -8,11 +9,15 @@ class TestGc < Test::Unit::TestCase
end
def test_gc
+ prev_stress = GC.stress
+ GC.stress = false
+
assert_nothing_raised do
+ tmp = nil
1.upto(10000) {
tmp = [0,1,2,3,4,5,6,7,8,9]
}
- tmp = nil
+ tmp
end
l=nil
100000.times {
@@ -26,9 +31,882 @@ class TestGc < Test::Unit::TestCase
}
GC.start
assert true # reach here or dumps core
- 100000.times {
- Time.now
+
+ GC.stress = prev_stress
+ end
+
+ def use_rgengc?
+ GC::OPTS.include? 'USE_RGENGC'.freeze
+ 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
+ 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)
+ ensure
+ 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
+ return unless use_rgengc?
+ omit 'stress' if GC.stress
+
+ 3.times { GC.start } # full mark and next time it should be minor mark
+ GC.start(full_mark: false)
+ assert_nil GC.latest_gc_info(:major_by)
+
+ GC.start(full_mark: true)
+ assert_not_nil GC.latest_gc_info(:major_by)
+ end
+
+ def test_start_immediate_sweep
+ omit 'stress' if GC.stress
+
+ GC.start(immediate_sweep: false)
+ assert_equal false, GC.latest_gc_info(:immediate_sweep)
+
+ GC.start(immediate_sweep: true)
+ assert_equal true, GC.latest_gc_info(:immediate_sweep)
+ end
+
+ def test_count
+ c = GC.count
+ GC.start
+ assert_operator(c, :<, GC.count)
+ end
+
+ def test_stat
+ res = GC.stat
+ assert_equal(false, res.empty?)
+ assert_kind_of(Integer, res[:count])
+
+ arg = Hash.new
+ res = GC.stat(arg)
+ assert_equal(arg, res)
+ assert_equal(false, res.empty?)
+ assert_kind_of(Integer, res[:count])
+
+ stat, count = {}, {}
+ 2.times{ # to ignore const cache imemo creation
+ GC.start
+ GC.stat(stat)
+ ObjectSpace.count_objects(count)
+ # repeat same methods invocation for cache object creation.
+ GC.stat(stat)
+ ObjectSpace.count_objects(count)
}
- assert true, '[ruby-dev:39201]' # reach here or dumps core
+ assert_equal(count[:TOTAL]-count[:FREE], stat[:heap_live_slots])
+ assert_equal(count[:FREE], stat[:heap_free_slots])
+
+ # measure again without GC.start
+ 2.times{ # to ignore const cache imemo creation
+ 1000.times{ "a" + "b" }
+ GC.stat(stat)
+ ObjectSpace.count_objects(count)
+ }
+ assert_equal(count[:FREE], stat[:heap_free_slots])
+ end
+
+ def test_stat_argument
+ assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) {GC.stat(:"\u{30eb 30d3 30fc}")}
+ end
+
+ def test_stat_single
+ omit 'stress' if GC.stress
+
+ stat = GC.stat
+ assert_equal stat[:count], GC.stat(:count)
+ assert_raise(ArgumentError){ GC.stat(:invalid) }
+ end
+
+ def test_stat_constraints
+ omit 'stress' if GC.stress
+
+ stat = GC.stat
+ # 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_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_empty_pages]
+
+ if use_rgengc?
+ assert_equal stat[:count], stat[:major_gc_count] + stat[:minor_gc_count]
+ end
+ end
+
+ def test_stat_heap
+ omit 'stress' if GC.stress
+
+ stat_heap = {}
+ stat = {}
+ # Initialize to prevent GC in future calls
+ GC.stat_heap(0, stat_heap)
+ GC.stat(stat)
+
+ GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i|
+ EnvUtil.without_gc do
+ GC.stat_heap(i, stat_heap)
+ GC.stat(stat)
+ end
+
+ 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[:total_allocated_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
+ assert_operator stat_heap[:total_freed_objects], :>=, 0
+ assert_operator stat_heap[:total_freed_objects], :<=, stat_heap[:total_allocated_objects]
+ end
+
+ GC.stat_heap(0, stat_heap)
+ assert_equal stat_heap[:slot_size], GC.stat_heap(0, :slot_size)
+ 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[:HEAP_COUNT]) }
+ end
+
+ def test_stat_heap_all
+ 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)
+
+ GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i|
+ GC.stat_heap(nil, stat_heap_all)
+ GC.stat_heap(i, stat_heap)
+
+ # Remove keys that can vary between invocations
+ %i(total_allocated_objects heap_live_slots heap_free_slots).each do |sym|
+ stat_heap[sym] = stat_heap_all[i][sym] = 0
+ end
+
+ assert_equal stat_heap, stat_heap_all[i]
+ end
+
+ assert_raise(TypeError) { GC.stat_heap(nil, :slot_size) }
+ end
+
+ def test_stat_heap_constraints
+ omit 'stress' if GC.stress
+
+ stat = GC.stat
+ stat_heap = GC.stat_heap
+ 2.times do
+ GC.stat(stat)
+ GC.stat_heap(nil, stat_heap)
+ end
+
+ stat_heap_sum = Hash.new(0)
+ stat_heap.values.each do |hash|
+ hash.each { |k, v| stat_heap_sum[k] += v }
+ end
+
+ 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_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, timeout: 60)
+ GC.measure_total_time = false
+
+ time_before = GC.stat(:time)
+
+ # Generate some garbage
+ Random.new.bytes(100 * 1024 * 1024)
+ GC.start
+
+ time_after = GC.stat(:time)
+
+ # If time measurement is disabled, the time stat should not change
+ assert_equal time_before, time_after
+ RUBY
+ end
+
+ def test_latest_gc_info
+ omit 'stress' if GC.stress
+
+ assert_separately([{"RUBY_GC_HEAP_INIT_BYTES" => "409600"}, "-W0"], __FILE__, __LINE__, <<-'RUBY')
+ GC.start
+ 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
+
+ GC.latest_gc_info(h = {}) # allocate hash and rehearsal
+ GC.start
+ GC.start
+ GC.start
+ GC.latest_gc_info(h)
+
+ assert_equal :force, h[:major_by] if use_rgengc?
+ assert_equal :method, h[:gc_by]
+ assert_equal true, h[:immediate_sweep]
+ assert_equal true, h.key?(:need_major_by)
+
+ GC.stress = true
+ assert_equal :force, GC.latest_gc_info[:major_by]
+ ensure
+ GC.stress = false
+ end
+
+ def test_latest_gc_info_argument
+ info = {}
+ GC.latest_gc_info(info)
+
+ assert_not_empty info
+ assert_equal info[:gc_by], GC.latest_gc_info(:gc_by)
+ assert_raise(ArgumentError){ GC.latest_gc_info(:invalid) }
+ assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) {GC.latest_gc_info(:"\u{30eb 30d3 30fc}")}
+ end
+
+ def test_latest_gc_info_need_major_by
+ return unless use_rgengc?
+ omit 'stress' if GC.stress
+
+ 3.times { GC.start }
+ assert_nil GC.latest_gc_info(:need_major_by)
+
+ 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)
+
+ 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)
+ GC.disable
+ COUNT = 10_000
+ # Some weak references may be created, so allow some margin of error
+ error_tolerance = 100
+
+ # Run full GC to collect stats about weak references
+ GC.start
+
+ before_weak_references_count = GC.latest_gc_info(:weak_references_count)
+
+ # 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)
+
+ before_weak_references_count = GC.latest_gc_info(: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 GC all the WeakMaps
+ GC.start
+
+ assert_operator(GC.latest_gc_info(:weak_references_count), :<=, before_weak_references_count - COUNT + error_tolerance)
+ RUBY
+ end
+
+ def test_stress_compile_send
+ assert_in_out_err([], <<-EOS, [], [], "")
+ GC.stress = true
+ begin
+ eval("A::B.c(1, 1, d: 234)")
+ rescue
+ end
+ EOS
+ end
+
+ def test_singleton_method
+ assert_in_out_err([], <<-EOS, [], [], "[ruby-dev:42832]")
+ GC.stress = true
+ 10.times do
+ obj = Object.new
+ def obj.foo() end
+ def obj.bar() raise "obj.foo is called, but this is obj.bar" end
+ obj.foo
+ end
+ EOS
+ end
+
+ def test_singleton_method_added
+ assert_in_out_err([], <<-EOS, [], [], "[ruby-dev:44436]", timeout: 30)
+ class BasicObject
+ undef singleton_method_added
+ def singleton_method_added(mid)
+ raise
+ end
+ end
+ b = proc {}
+ class << b; end
+ b.clone rescue nil
+ GC.start
+ EOS
+ end
+
+ def test_gc_parameter
+ env = { "RUBY_GC_HEAP_INIT_BYTES" => "#{200000 * 40}" }
+ assert_normal_exit("exit", "", :child_env => env)
+
+ 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_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_BYTES=409600/, "[ruby-core:57928]")
+
+ if use_rgengc?
+ env = {
+ "RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR" => "0.4",
+ }
+ # always full GC when RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR < 1.0
+ assert_in_out_err([env, "-e", "GC.start; 1000_000.times{Object.new}; p(GC.stat[:minor_gc_count] < GC.stat[:major_gc_count])"], "", ['true'], //, "")
+ end
+
+ env = {
+ "RUBY_GC_MALLOC_LIMIT" => "60000000",
+ "RUBY_GC_MALLOC_LIMIT_MAX" => "160000000",
+ "RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR" => "2.0"
+ }
+ assert_normal_exit("exit", "", :child_env => env)
+ assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_MALLOC_LIMIT=6000000/, "")
+ assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_MALLOC_LIMIT_MAX=16000000/, "")
+ assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR=2.0/, "")
+
+ if use_rgengc?
+ env = {
+ "RUBY_GC_OLDMALLOC_LIMIT" => "60000000",
+ "RUBY_GC_OLDMALLOC_LIMIT_MAX" => "160000000",
+ "RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR" => "2.0"
+ }
+ assert_normal_exit("exit", "", :child_env => env)
+ assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_OLDMALLOC_LIMIT=6000000/, "")
+ assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_OLDMALLOC_LIMIT_MAX=16000000/, "")
+ assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR=2.0/, "")
+ end
+
+ ["0.01", "0.1", "1.0"].each do |i|
+ env = {"RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR" => "0", "RUBY_GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO" => i}
+ assert_separately([env, "-W0"], __FILE__, __LINE__, <<~RUBY)
+ GC.disable
+ GC.start
+ assert_equal((GC.stat[:old_objects] * #{i}).to_i, GC.stat[:remembered_wb_unprotected_objects_limit])
+ RUBY
+ end
+ end
+
+ def test_gc_parameter_init_bytes
+ omit "[Bug #21203] This test is flaky and intermittently failing now"
+
+ assert_separately([], __FILE__, __LINE__, <<~RUBY, timeout: 60)
+ GC_HEAP_INIT_BYTES = 2560 * 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
+
+ assert_equal gc_count, GC.stat(:count)
+ RUBY
+
+ 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
+
+ assert_equal gc_count, GC.stat(:count)
+ RUBY
+ end
+
+ def test_profiler_enabled
+ GC::Profiler.enable
+ assert_equal(true, GC::Profiler.enabled?)
+ GC::Profiler.disable
+ assert_equal(false, GC::Profiler.enabled?)
+ ensure
+ GC::Profiler.disable
+ end
+
+ def test_profiler_clear
+ omit "for now"
+ assert_separately([], __FILE__, __LINE__, <<-'RUBY', timeout: 30)
+ GC::Profiler.enable
+
+ GC.start
+ assert_equal(1, GC::Profiler.raw_data.size)
+ GC::Profiler.clear
+ assert_equal(0, GC::Profiler.raw_data.size)
+
+ 200.times{ GC.start }
+ assert_equal(200, GC::Profiler.raw_data.size)
+ GC::Profiler.clear
+ assert_equal(0, GC::Profiler.raw_data.size)
+ RUBY
+ end
+
+ def test_profiler_raw_data
+ GC::Profiler.enable
+ GC.start
+ assert GC::Profiler.raw_data
+ ensure
+ GC::Profiler.disable
+ end
+
+ def test_profiler_total_time
+ GC::Profiler.enable
+ GC::Profiler.clear
+
+ GC.start
+ assert_operator(GC::Profiler.total_time, :>=, 0)
+ ensure
+ GC::Profiler.disable
+ end
+
+ def test_finalizing_main_thread
+ assert_in_out_err([], <<-EOS, ["\"finalize\""], [], "[ruby-dev:46647]")
+ ObjectSpace.define_finalizer(Thread.main) { p 'finalize' }
+ EOS
+ end
+
+ def test_expand_heap
+ assert_separately([], __FILE__, __LINE__, <<~'RUBY')
+ GC.start
+ base_length = GC.stat[:heap_eden_pages]
+ (base_length * 500).times{ 'a' }
+ GC.start
+ base_length = GC.stat[:heap_eden_pages]
+ (base_length * 500).times{ 'a' }
+ GC.start
+ assert_in_epsilon base_length, (v = GC.stat[:heap_eden_pages]), 1/8r,
+ "invalid heap expanding (base_length: #{base_length}, GC.stat[:heap_eden_pages]: #{v})"
+
+ a = []
+ (base_length * 500).times{ a << 'a'; nil }
+ GC.start
+ assert_operator base_length, :<, GC.stat[:heap_eden_pages] + 1
+ RUBY
+ end
+
+ def test_thrashing_for_young_objects
+ # This test prevents bugs like [Bug #18929]
+
+ 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 }
+
+ # 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 }
+
+ # Previous loop may have caused GC to be in an intermediate state,
+ # running a minor GC here will guarantee that GC will be complete
+ GC.start(full_mark: false)
+
+ 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_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
+ 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_COUNT]
+ end
+
+ def test_sweep_in_finalizer
+ bug9205 = '[ruby-core:58833] [Bug #9205]'
+ 2.times do
+ assert_ruby_status([], <<-'end;', bug9205, timeout: 120)
+ raise_proc = proc do |id|
+ GC.start
+ end
+ 1000.times do
+ ObjectSpace.define_finalizer(Object.new, raise_proc)
+ end
+ end;
+ end
+ end
+
+ def test_exception_in_finalizer
+ bug9168 = '[ruby-core:58652] [Bug #9168]'
+ assert_normal_exit(<<-'end;', bug9168, encoding: Encoding::ASCII_8BIT)
+ raise_proc = proc {raise}
+ 10000.times do
+ ObjectSpace.define_finalizer(Object.new, raise_proc)
+ Thread.handle_interrupt(RuntimeError => :immediate) {break}
+ Thread.handle_interrupt(RuntimeError => :on_blocking) {break}
+ Thread.handle_interrupt(RuntimeError => :never) {break}
+ end
+ end;
+ 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')
+ pid = $$
+ Thread.start do
+ 10.times {
+ sleep 0.1
+ Process.kill("INT", pid) rescue break
+ }
+ end
+ f = proc {1000.times {}}
+ loop do
+ ObjectSpace.define_finalizer(Object.new, f)
+ end
+ end;
+ out, err, status = assert_in_out_err(["-e", src], "", [], [], bug10595, signal: :SEGV, timeout: 100) do |*result|
+ break result
+ end
+ unless /mswin|mingw/ =~ RUBY_PLATFORM
+ assert_equal("INT", Signal.signame(status.termsig), bug10595)
+ end
+ assert_match(/Interrupt/, err.first, proc {err.join("\n")})
+ assert_empty(out)
+ end
+
+ def test_finalizer_passed_object_id
+ assert_in_out_err([], <<~RUBY, ["true"], [])
+ o = Object.new
+ obj_id = o.object_id
+ ObjectSpace.define_finalizer(o, ->(id){ p id == obj_id })
+ RUBY
+ end
+
+ def test_verify_internal_consistency
+ assert_nil(GC.verify_internal_consistency)
+ end
+
+ def test_gc_stress_on_realloc
+ assert_normal_exit(<<-'end;', '[Bug #9859]')
+ class C
+ def initialize
+ @a = nil
+ @b = nil
+ @c = nil
+ @d = nil
+ @e = nil
+ @f = nil
+ end
+ end
+
+ GC.stress = true
+ C.new
+ end;
+ end
+
+ def test_gc_stress_at_startup
+ assert_in_out_err([{"RUBY_DEBUG"=>"gc_stress"}], '', [], [], '[Bug #15784]', success: true, timeout: 120)
+ end
+
+ def test_gc_disabled_start
+ EnvUtil.without_gc do
+ c = GC.count
+ GC.start
+ assert_equal 1, GC.count - c
+ 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
+
+ def test_vm_object
+ assert_normal_exit <<-'end', '[Bug #12583]'
+ ObjectSpace.each_object{|o| o.singleton_class rescue 0}
+ ObjectSpace.each_object{|o| case o when Module then o.instance_methods end}
+ end
+ 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"
+ raise
+ end
+ c2 = proc do
+ puts "c2"
+ raise
+ end
+ begin;
+ tap do
+ obj = Object.new
+ ObjectSpace.define_finalizer(obj, c1)
+ ObjectSpace.define_finalizer(obj, c2)
+ obj = nil
+ end
+ end;
+ 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"
+ raise
+ end
+ def self.c2(x)
+ puts "c2"
+ raise
+ end
+ begin;
+ tap do
+ obj = Object.new
+ ObjectSpace.define_finalizer(obj, method(:c1))
+ ObjectSpace.define_finalizer(obj, method(:c2))
+ obj = nil
+ end
+ end;
+
+ assert_normal_exit "#{<<~"begin;"}\n#{<<~'end;'}", '[Bug #20042]'
+ begin;
+ def (f = Object.new).call = nil # missing ID
+ o = Object.new
+ ObjectSpace.define_finalizer(o, f)
+ o = nil
+ GC.start
+ end;
+ end
+
+ def test_object_ids_never_repeat
+ GC.start
+ a = 1000.times.map { Object.new.object_id }
+ GC.start
+ b = 1000.times.map { Object.new.object_id }
+ assert_empty(a & b)
+ end
+
+ def test_ast_node_buffer
+ # https://github.com/ruby/ruby/pull/4416
+ Module.new.class_eval( (["# shareable_constant_value: literal"] +
+ (0..100000).map {|i| "M#{ i } = {}" }).join("\n"))
+ end
+
+ def test_old_to_young_reference
+ EnvUtil.without_gc do
+ require "objspace"
+
+ old_obj = Object.new
+ 4.times { GC.start }
+
+ assert_include ObjectSpace.dump(old_obj), '"old":true'
+
+ 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
+
+ # Takes 4 GC to promote to old generation
+ GC.start
+ assert_include ObjectSpace.dump(young_obj), '"old":true'
+ end
+ end
+
+ 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