diff --git a/NEWS.md b/NEWS.md index c5746260c21..02a104218c4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -69,24 +69,29 @@ Note: We're only listing outstanding class updates. * GC - * `GC.stat` accepts `scope: :ractor` (the default), `scope: :local` - (a synonym for `:ractor`), and `scope: :global`. - With the default collector, the global scope reports cumulative collection - counts and measured marking/sweeping CPU time across Ractors, including - history from destroyed object spaces, without requesting a stop-the-world - snapshot. Unscoped `GC.stat`, `GC.count`, and `GC.total_time` retain their - current-Ractor behavior. + * `GC.stat` accepts a `process:` keyword. `GC.stat` and + `GC.stat(process: false)` report the current Ractor's object space. + With the default collector, `GC.stat(process: true)` reports cumulative + collection counts and measured marking/sweeping CPU time across Ractors, + including history from destroyed object spaces, without requesting a + stop-the-world snapshot. It also reports the `:page_pool_arenas`, + `:page_pool_arenas_freed`, `:page_pool_total_pages` and + `:page_pool_discarded_pages` keys, which the Ractor data does not + include. `GC.count` and `GC.total_time` retain their current-Ractor + behavior. * `GC.stat` reports a new key, `:global_gc_count`: the number of - stop-the-world global garbage collection cycles the current Ractor - initiated. Each global collection is attributed to exactly one Ractor, - so the values sum across Ractors. `GC.stat(:global_gc_count, - scope: :global)` reports the process total, including the counts of - terminated Ractors. + stop-the-world global garbage collection cycles that collected the + current Ractor's heap, whichever Ractor started them. The new key + `:global_gc_invocation_count` reports the global collections that the + current Ractor started. `GC.stat(:global_gc_count, process: true)` + reports the number of global collections in the process, including + those started by terminated Ractors. * A global garbage collection is not counted in `:major_gc_count`. `:count` equals `:minor_gc_count` plus `:major_gc_count` plus - `:global_gc_count` in both scopes. When another Ractor's object space - exists, including one left by a terminated Ractor that is not yet - absorbed, or one was absorbed since the last global collection, + `:global_gc_count` in both the Ractor and the process data. In the + process data each global collection counts once. When another Ractor's + object space exists, including one left by a terminated Ractor that is + not yet absorbed, or one was absorbed since the last global collection, `GC.start` runs a global collection by default, so it increments `:global_gc_count` rather than `:major_gc_count`; pass `global: false` for a local major collection. diff --git a/gc.c b/gc.c index 3e9853889f3..10c9ad6f203 100644 --- a/gc.c +++ b/gc.c @@ -5652,7 +5652,7 @@ rb_gc_latest_gc_info(VALUE key) } static VALUE -gc_stat(rb_execution_context_t *ec, VALUE self, VALUE arg, VALUE global_scope) +gc_stat(rb_execution_context_t *ec, VALUE self, VALUE arg, VALUE process) { if (NIL_P(arg)) { arg = rb_hash_new(); @@ -5661,7 +5661,7 @@ gc_stat(rb_execution_context_t *ec, VALUE self, VALUE arg, VALUE global_scope) rb_raise(rb_eTypeError, "non-hash or symbol given"); } - VALUE ret = rb_gc_impl_stat(RTEST(global_scope) ? NULL : rb_gc_get_objspace(), arg); + VALUE ret = rb_gc_impl_stat(RTEST(process) ? NULL : rb_gc_get_objspace(), arg); if (ret == Qundef) { GC_ASSERT(SYMBOL_P(arg)); diff --git a/gc.rb b/gc.rb index 016a034d0b1..360bbf0dad3 100644 --- a/gc.rb +++ b/gc.rb @@ -54,9 +54,10 @@ module GC # - +false+: collect only the calling Ractor's own objects. Shareable objects and cross-ractor # garbage are not swept. # - # GC.stat counts a local major collection under +:major_gc_count+, a local minor - # collection under +:minor_gc_count+, and a global collection under - # +:global_gc_count+ for the calling Ractor. + # GC.stat counts a local major collection under +:major_gc_count+ and a local + # minor collection under +:minor_gc_count+. A global collection counts under + # +:global_gc_count+ in every Ractor whose heap it collects, and under + # +:global_gc_invocation_count+ in the calling Ractor. # # Note that these keyword arguments are implementation- and version-dependent, # are not guaranteed to be future-compatible, @@ -139,7 +140,8 @@ module GC # # Returns the total number of times garbage collection has occurred: # - # With the default GC, counts the current Ractor's collections. + # With the default GC, counts the collections of the current Ractor's heap, + # including global collections that other Ractors started. # # GC.count # => 385 # GC.start @@ -150,9 +152,9 @@ module GC end # call-seq: - # GC.stat(scope: :ractor) -> new_hash - # GC.stat(key, scope: :ractor) -> value - # GC.stat(hash, scope: :ractor) -> hash + # GC.stat(process: false) -> new_hash + # GC.stat(key, process: false) -> value + # GC.stat(hash, process: false) -> hash # # This method is implementation-specific to CRuby. # @@ -160,10 +162,10 @@ module GC # The particular statistics are implementation-specific # and may change in the future without notice. # - # With scope: :ractor (the default) or scope: :local, + # With process: false (the default) or +nil+, # returns statistics for the current Ractor's object space. - # With scope: :global, returns cumulative collection counts and - # measured GC time across all Ractors. + # With any other value of +process+, returns process-wide statistics; + # see "Process statistics" below. # # With no positional argument or +nil+ given, # returns a hash containing the \GC statistics: @@ -187,6 +189,7 @@ module GC # minor_gc_count: 0, # major_gc_count: 1, # global_gc_count: 0, + # global_gc_invocation_count: 0, # compact_count: 0, # read_barrier_faults: 0, # total_moved_objects: 0, @@ -220,8 +223,10 @@ module GC # The hash includes entries such as: # # - +:count+: - # The total number of garbage collections run since application start - # (the count includes minor, major, and global garbage collections). + # The total number of garbage collections of this Ractor's heap since the + # object space was created. It includes minor, major, and global garbage + # collections, and equals +:minor_gc_count+ plus +:major_gc_count+ plus + # +:global_gc_count+. # - +:time+: # The total time spent in garbage collections (in milliseconds). # - +:marking_time+: @@ -250,15 +255,16 @@ module GC # - +:malloc_increase_bytes_limit+: # When +:malloc_increase_bytes+ crosses this limit, \GC is triggered. # - +:minor_gc_count+: - # The total number of minor garbage collections run since process start. + # The total number of local minor garbage collections of this Ractor's heap. # - +:major_gc_count+: - # The total number of major garbage collections run since process start. + # The total number of local major garbage collections of this Ractor's heap. # Global garbage collections are counted separately. # - +:global_gc_count+: - # The number of global (stop-the-world) garbage collections this Ractor - # initiated. Each global garbage collection is attributed to the Ractor - # that initiated it, so the values sum across Ractors. With - # scope: :global, the process total, including terminated Ractors. + # The number of global (stop-the-world) garbage collections that collected + # this Ractor's heap. Any Ractor can start them. + # - +:global_gc_invocation_count+: + # The number of global garbage collections that this Ractor started. + # Each global garbage collection has exactly one starter. # - +:compact_count+: # The total number of compactions run since process start. # - +:read_barrier_faults+: @@ -290,25 +296,42 @@ module GC # - +:heap_final_slots+: # The total number of slots with pending finalizers to be run. # - +:heap_marked_slots+: - # The total number of objects marked in the last \GC. + # The number of objects in this Ractor's heap marked in the last \GC of + # that heap. # - # === Global scope + # === Process statistics # - # With scope: :global, the default collector reports cumulative + # With process: true, the default collector reports cumulative # collection counts and measured CPU time across all Ractors, retaining - # history from destroyed object spaces: + # history from destroyed object spaces, and the state of the page pool + # that all object spaces share: # - # GC.stat(scope: :global) - # # => {count: 42, minor_gc_count: 32, major_gc_count: 8, - # # global_gc_count: 2, - # # time: 3, marking_time: 2, sweeping_time: 1} + # GC.stat(process: true) + # # => {count: 42, time: 3, marking_time: 2, sweeping_time: 1, + # # minor_gc_count: 32, major_gc_count: 8, global_gc_count: 2, + # # page_pool_arenas: 2, page_pool_arenas_freed: 0, + # # page_pool_total_pages: 64, page_pool_discarded_pages: 0} # - # These totals include both local and global collections, and - # +:count+ equals +:minor_gc_count+ plus +:major_gc_count+ plus - # +:global_gc_count+ in both scopes. + # +:count+ is the number of garbage collections in the process. Each local + # and each global collection counts once, so +:count+ equals + # +:minor_gc_count+ plus +:major_gc_count+ plus +:global_gc_count+. It is not + # the sum of the Ractor +:count+ values, because one global collection + # collects every heap. # - # +:global_gc_count+ attributes each global collection to the Ractor that - # initiated it, and the total retains the counts of terminated Ractors. + # +:global_gc_count+ is the number of global collections in the process, + # however many Ractors each one stops. + # + # The page pool keys are: + # + # - +:page_pool_arenas+: + # The number of page-pool arenas that are mapped now. + # - +:page_pool_arenas_freed+: + # The number of arenas that the page pool has unmapped. + # - +:page_pool_total_pages+: + # The number of page bodies in the mapped arenas. + # - +:page_pool_discarded_pages+: + # The number of free page bodies whose memory the page pool has returned + # to the operating system. # # +:time+ may exceed +:marking_time+ plus +:sweeping_time+ by one millisecond # because raw nanoseconds are summed before conversion. Times measure collector @@ -317,19 +340,12 @@ module GC # Reads combine object-space publications without requesting a stop-the-world # barrier. GC.measure_total_time controls future timing accumulation. Profiler # controls do not affect these totals. A forked child inherits its parent's - # totals. MMTk and wbcheck raise NotImplementedError for this scope. + # totals. MMTk and wbcheck raise NotImplementedError for process statistics. # - # Unknown scopes and unavailable keys raise ArgumentError. + # Unavailable keys raise ArgumentError. # - def self.stat hash_or_key = nil, scope: :ractor - case scope - when :ractor, :local - Primitive.gc_stat hash_or_key, false - when :global - Primitive.gc_stat hash_or_key, true - else - raise ArgumentError, "unknown scope: #{scope.inspect}" - end + def self.stat hash_or_key = nil, process: false + Primitive.gc_stat hash_or_key, process end # call-seq: diff --git a/gc/default/default.c b/gc/default/default.c index 764e8f8ae84..a92e514bcf0 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -607,19 +607,17 @@ STATIC_ASSERT(tdata_unsafe_free_bits_cover_chunk, TDATA_UNSAFE_FREE_CHUNK_CAPA <= 32); struct gc_process_stat_snapshot { - uint64_t count; uint64_t minor_gc_count; uint64_t major_gc_count; - uint64_t global_gc_count; + uint64_t global_gc_invocation_count; uint64_t marking_time_ns; uint64_t sweeping_time_ns; }; struct gc_process_stat_total { - uint64_t count; uint64_t minor_gc_count; uint64_t major_gc_count; - uint64_t global_gc_count; + uint64_t global_gc_invocation_count; uint64_t marking_time_ns; uint64_t sweeping_time_ns; }; @@ -726,6 +724,7 @@ typedef struct rb_objspace { size_t minor_gc_count; size_t major_gc_count; size_t global_gc_count; + size_t global_gc_invocation_count; size_t compact_count; size_t read_barrier_faults; #if RGENGC_PROFILE > 0 @@ -827,7 +826,7 @@ typedef struct rb_objspace { struct rb_gc_vm_context vm_context; /* Process-wide GC statistics publication. Default GC only: other - * implementations reject GC.stat(scope: :global) and never initialize + * implementations reject GC.stat(process: true) and never initialize * this lock. */ struct { rb_nativethread_lock_t lock; @@ -2161,10 +2160,9 @@ static void gc_process_stat_capture(const rb_objspace_t *objspace, struct gc_process_stat_snapshot *out) { - out->count = objspace->profile.count; out->minor_gc_count = objspace->profile.minor_gc_count; out->major_gc_count = objspace->profile.major_gc_count; - out->global_gc_count = objspace->profile.global_gc_count; + out->global_gc_invocation_count = objspace->profile.global_gc_invocation_count; out->marking_time_ns = objspace->profile.marking_time_ns; out->sweeping_time_ns = objspace->profile.sweeping_time_ns; } @@ -2173,8 +2171,9 @@ static void gc_process_stat_publish(rb_objspace_t *objspace) { struct gc_process_stat_snapshot snap; + GC_ASSERT(objspace->profile.count == objspace->profile.minor_gc_count + + objspace->profile.major_gc_count + objspace->profile.global_gc_count); gc_process_stat_capture(objspace, &snap); - GC_ASSERT(snap.count == snap.minor_gc_count + snap.major_gc_count + snap.global_gc_count); rb_native_mutex_lock(&objspace->process_stat.lock); objspace->process_stat.published = snap; rb_native_mutex_unlock(&objspace->process_stat.lock); @@ -2184,10 +2183,9 @@ static void gc_process_stat_add(struct gc_process_stat_total *dst, const struct gc_process_stat_snapshot *src) { - dst->count += src->count; dst->minor_gc_count += src->minor_gc_count; dst->major_gc_count += src->major_gc_count; - dst->global_gc_count += src->global_gc_count; + dst->global_gc_invocation_count += src->global_gc_invocation_count; dst->marking_time_ns += src->marking_time_ns; dst->sweeping_time_ns += src->sweeping_time_ns; } @@ -6316,7 +6314,7 @@ gc_aging(rb_objspace_t *objspace, VALUE obj) } check_rvalue_consistency(objspace, obj); - objspace->marked_slots++; + page->objspace->marked_slots++; } static void @@ -9652,10 +9650,8 @@ gc_start_global(rb_objspace_t *driver, unsigned int reason, bool compact, bool a reason |= GPR_FLAG_GLOBAL; - /* A global GC is a collection of the driver's objspace too, and its profile.count - * below says so, so report it like a local one. The driver is the objspace whose - * count moves, which is the one a hook reading GC.stat would compare against. For - * the same reason it records a profile entry and reports what triggered it. */ + /* A global GC is a collection of the driver's objspace too, so report it like a local + * one: it records a profile entry and reports what triggered it. */ gc_start_record(driver, reason, true); gc_event_hook(driver, RUBY_INTERNAL_EVENT_GC_START); gc_prof_timer_start(driver); @@ -9714,6 +9710,8 @@ gc_start_global(rb_objspace_t *driver, unsigned int reason, bool compact, bool a objspace->last_cycle_pinned = 0; objspace->rgengc.uncollectible_wb_unprotected_objects = 0; objspace->rgengc.old_objects = 0; + objspace->profile.count++; + objspace->profile.global_gc_count++; objspace->rgengc.last_major_gc = objspace->profile.count; objspace->rgengc.need_major_gc = GPR_FLAG_NONE; objspace->marked_slots = 0; @@ -9723,7 +9721,7 @@ gc_start_global(rb_objspace_t *driver, unsigned int reason, bool compact, bool a heap_move_pooled_pages_to_free_pages(heap); } } - driver->profile.global_gc_count++; + driver->profile.global_gc_invocation_count++; global_objspace->global_gc.count++; /* Enable compaction in every objspace before the mark: the unified conservative root @@ -9935,7 +9933,6 @@ gc_start_global(rb_objspace_t *driver, unsigned int reason, bool compact, bool a * which step 10 below still walks. */ gc_tdata_unsafe_drain_objspaces(global_objspace->global_gc.objspaces, global_objspace->global_gc.n_objspaces); - driver->profile.count++; /* step 10 */ for (size_t i = 0; i < global_objspace->global_gc.n_objspaces; i++) { @@ -11005,6 +11002,7 @@ enum gc_stat_sym { gc_stat_sym_minor_gc_count, gc_stat_sym_major_gc_count, gc_stat_sym_global_gc_count, + gc_stat_sym_global_gc_invocation_count, gc_stat_sym_compact_count, gc_stat_sym_read_barrier_faults, gc_stat_sym_total_moved_objects, @@ -11061,6 +11059,7 @@ setup_gc_stat_symbols(void) S(minor_gc_count); S(major_gc_count); S(global_gc_count); + S(global_gc_invocation_count); S(compact_count); S(read_barrier_faults); S(total_moved_objects); @@ -11132,11 +11131,13 @@ gc_process_stat(VALUE hash_or_sym) rb_objspace_t *const current = rb_gc_get_objspace(); if (!gc_during_gc_get(current)) { - GC_ASSERT(total.global_gc_count == direct_global_gc_count); + GC_ASSERT(total.global_gc_invocation_count == direct_global_gc_count); } /* Convert to Ruby values after all collector locks are released. */ uint64_t time_ns = total.marking_time_ns + total.sweeping_time_ns; + uint64_t count = total.minor_gc_count + total.major_gc_count + total.global_gc_invocation_count; + size_t page_pool_arenas = global_objspace->page_pool.arena_count; #define SET64(name, attr) \ if (key == gc_stat_symbols[gc_stat_sym_##name]) \ @@ -11144,13 +11145,17 @@ gc_process_stat(VALUE hash_or_sym) else if (hash != Qnil) \ rb_hash_aset(hash, gc_stat_symbols[gc_stat_sym_##name], ULL2NUM(attr)); - SET64(count, total.count); + SET64(count, count); SET64(time, ns_to_ms(time_ns)); SET64(marking_time, ns_to_ms(total.marking_time_ns)); SET64(sweeping_time, ns_to_ms(total.sweeping_time_ns)); SET64(minor_gc_count, total.minor_gc_count); SET64(major_gc_count, total.major_gc_count); - SET64(global_gc_count, total.global_gc_count); + SET64(global_gc_count, total.global_gc_invocation_count); + SET64(page_pool_arenas, page_pool_arenas); + SET64(page_pool_arenas_freed, global_objspace->page_pool.arenas_unmapped); + SET64(page_pool_total_pages, (uint64_t)page_pool_arenas * PAGE_POOL_ARENA_BODIES); + SET64(page_pool_discarded_pages, global_objspace->page_pool.advised_count); #undef SET64 @@ -11219,6 +11224,7 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) SET(minor_gc_count, objspace->profile.minor_gc_count); SET(major_gc_count, objspace->profile.major_gc_count); SET(global_gc_count, objspace->profile.global_gc_count); + SET(global_gc_invocation_count, objspace->profile.global_gc_invocation_count); SET(compact_count, objspace->profile.compact_count); SET(read_barrier_faults, objspace->profile.read_barrier_faults); SET(total_moved_objects, objspace->rcompactor.total_moved); @@ -11239,11 +11245,6 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym) SET(heap_final_slots, total_final_slots_count(objspace)); SET(heap_marked_slots, objspace->marked_slots); - SET(page_pool_arenas, global_objspace->page_pool.arena_count); - SET(page_pool_arenas_freed, global_objspace->page_pool.arenas_unmapped); - SET(page_pool_total_pages, (size_t)global_objspace->page_pool.arena_count * PAGE_POOL_ARENA_BODIES); - SET(page_pool_discarded_pages, global_objspace->page_pool.advised_count); - #if RGENGC_PROFILE SET(total_generated_normal_object_count, objspace->profile.total_generated_normal_object_count); SET(total_generated_shady_object_count, objspace->profile.total_generated_shady_object_count); diff --git a/gc/gc_impl.h b/gc/gc_impl.h index 1bcb74421d4..fd041891d96 100644 --- a/gc/gc_impl.h +++ b/gc/gc_impl.h @@ -172,7 +172,7 @@ GC_IMPL_FN unsigned long long rb_gc_impl_get_total_time(void *objspace_ptr); GC_IMPL_FN size_t rb_gc_impl_gc_count(void *objspace_ptr); GC_IMPL_FN VALUE rb_gc_impl_latest_gc_info(void *objspace_ptr, VALUE key); /* A NULL objspace requests cumulative process-wide statistics. Collectors that - * do not support this scope must raise NotImplementedError. */ + * do not support them must raise NotImplementedError. */ GC_IMPL_FN VALUE rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym); GC_IMPL_FN VALUE rb_gc_impl_stat_heap(void *objspace_ptr, VALUE heap_name, VALUE hash_or_sym); GC_IMPL_FN const char *rb_gc_impl_active_gc_name(void); diff --git a/sample/gc_stat_ractors.rb b/sample/gc_stat_ractors.rb index a7490eaa611..9cfe97c8ea5 100644 --- a/sample/gc_stat_ractors.rb +++ b/sample/gc_stat_ractors.rb @@ -10,8 +10,8 @@ workers = 4.times.map do |worker_id| GC.start(full_mark: false) { ractor: "worker-#{id}", - marking_time_ms: GC.stat(:marking_time, scope: :ractor), - sweeping_time_ms: GC.stat(:sweeping_time, scope: :ractor) + marking_time_ms: GC.stat(:marking_time), + sweeping_time_ms: GC.stat(:sweeping_time) } end end @@ -20,11 +20,11 @@ workers.each { |worker| worker.send(:start) } workers.each { |worker| p worker.value } p({ ractor: "main", - marking_time_ms: GC.stat(:marking_time, scope: :ractor), - sweeping_time_ms: GC.stat(:sweeping_time, scope: :ractor) + marking_time_ms: GC.stat(:marking_time), + sweeping_time_ms: GC.stat(:sweeping_time) }) puts "process-wide GC statistics:" -p GC.stat(scope: :global) -puts "process marking time (ms): #{GC.stat(:marking_time, scope: :global)}" -puts "process sweeping time (ms): #{GC.stat(:sweeping_time, scope: :global)}" +p GC.stat(process: true) +puts "process marking time (ms): #{GC.stat(:marking_time, process: true)}" +puts "process sweeping time (ms): #{GC.stat(:sweeping_time, process: true)}" diff --git a/test/ruby/test_gc.rb b/test/ruby/test_gc.rb index d8ce0993475..5d2e015f272 100644 --- a/test/ruby/test_gc.rb +++ b/test/ruby/test_gc.rb @@ -312,11 +312,10 @@ class TestGc < Test::Unit::TestCase end def test_page_pool_stat_consistency - omit 'no page pool' unless GC.stat.key?(:page_pool_total_pages) + omit 'default GC only' unless GC.config[:implementation] == 'default' # Freeing arenas back to the OS must keep page_pool_discarded_pages in range. - # An underflowed counter wraps to a huge value, which also makes GC.stat - # allocate a Bignum and perturb the object counts it reports. + # An underflowed counter wraps to a huge value. assert_separately([], __FILE__, __LINE__, <<~RUBY, timeout: 60) 3.times do ary = 200_000.times.map { "x" * 40 } @@ -325,7 +324,7 @@ class TestGc < Test::Unit::TestCase GC.start(full_mark: true, immediate_sweep: true) end - stat = GC.stat + stat = GC.stat(process: true) assert_operator stat[:page_pool_discarded_pages], :<=, stat[:page_pool_total_pages] assert_operator stat[:page_pool_arenas], :>=, 0 # arenas is always 0 if doesn't have mmap RUBY @@ -1072,214 +1071,221 @@ class TestGc < Test::Unit::TestCase RUBY end - def test_stat_global_scope_matches_local_statistics + PROCESS_STAT_KEYS = %i[ + count time marking_time sweeping_time minor_gc_count major_gc_count global_gc_count + page_pool_arenas page_pool_arenas_freed page_pool_total_pages page_pool_discarded_pages + ].freeze + + def test_stat_process_matches_ractor_statistics omit 'default GC only' unless GC.config[:implementation] == 'default' omit 'stress' if GC.stress assert_separately([], __FILE__, __LINE__, <<~'RUBY', timeout: 60) GC.disable local = GC.stat - process = GC.stat(scope: :global) + process = GC.stat(process: true) keys = %i[count minor_gc_count major_gc_count global_gc_count time marking_time sweeping_time] assert_equal local.values_at(*keys), process.values_at(*keys) RUBY end - def test_stat_global_scope_count_consistency + def test_stat_process_count_consistency omit 'default GC only' unless GC.config[:implementation] == 'default' - stat = GC.stat(scope: :global) + stat = GC.stat(process: true) assert_equal stat[:minor_gc_count] + stat[:major_gc_count] + stat[:global_gc_count], stat[:count] end - def test_stat_global_scope_time_rounding + def test_stat_process_time_rounding omit 'default GC only' unless GC.config[:implementation] == 'default' - stat = GC.stat(scope: :global) + stat = GC.stat(process: true) assert_include 0..1, stat[:time] - (stat[:marking_time] + stat[:sweeping_time]) end - def test_stat_global_scope_no_argument_returns_new_hash + def test_stat_process_no_argument_returns_new_hash omit 'default GC only' unless GC.config[:implementation] == 'default' - stat = GC.stat(scope: :global) - keys = %i[count minor_gc_count major_gc_count global_gc_count time marking_time sweeping_time] + stat = GC.stat(process: true) assert_kind_of Hash, stat - assert_not_same stat, GC.stat(scope: :global) - assert_equal keys.sort, stat.keys.sort + assert_not_same stat, GC.stat(process: true) + assert_equal PROCESS_STAT_KEYS.sort, stat.keys.sort assert stat.values.all? { |value| Integer === value } end - def test_stat_global_scope_nil_argument_returns_new_hash + def test_stat_process_nil_argument_returns_new_hash omit 'default GC only' unless GC.config[:implementation] == 'default' - stat = GC.stat(nil, scope: :global) - keys = %i[count minor_gc_count major_gc_count global_gc_count time marking_time sweeping_time] + stat = GC.stat(nil, process: true) assert_kind_of Hash, stat - assert_not_same stat, GC.stat(scope: :global) - assert_equal keys.sort, stat.keys.sort + assert_not_same stat, GC.stat(process: true) + assert_equal PROCESS_STAT_KEYS.sort, stat.keys.sort assert stat.values.all? { |value| Integer === value } end - def test_stat_global_scope_symbol_argument_returns_selected_value + def test_stat_process_symbol_argument_returns_selected_value omit 'default GC only' unless GC.config[:implementation] == 'default' omit 'stress' if GC.stress assert_separately([], __FILE__, __LINE__, <<~'RUBY', timeout: 60) GC.disable - stat = GC.stat(scope: :global) - value = GC.stat(:count, scope: :global) + stat = GC.stat(process: true) + value = GC.stat(:count, process: true) assert_kind_of Integer, value assert_equal stat[:count], value RUBY end - def test_stat_global_scope_supplied_hash_is_updated_and_preserved + def test_stat_process_supplied_hash_is_updated_and_preserved omit 'default GC only' unless GC.config[:implementation] == 'default' - keys = %i[count minor_gc_count major_gc_count global_gc_count time marking_time sweeping_time] buffer = { sentinel: :keep } - assert_same buffer, GC.stat(buffer, scope: :global) + assert_same buffer, GC.stat(buffer, process: true) assert_equal :keep, buffer[:sentinel] - assert_equal keys.sort, (buffer.keys - [:sentinel]).sort - assert buffer.values_at(*keys).all? { |value| Integer === value } + assert_equal PROCESS_STAT_KEYS.sort, (buffer.keys - [:sentinel]).sort + assert buffer.values_at(*PROCESS_STAT_KEYS).all? { |value| Integer === value } end - def test_stat_global_scope_supplied_hash_overwrites_stale_values + def test_stat_process_supplied_hash_overwrites_stale_values omit 'default GC only' unless GC.config[:implementation] == 'default' omit 'stress' if GC.stress assert_separately([], __FILE__, __LINE__, <<~'RUBY', timeout: 60) GC.disable buffer = { count: -1, time: -1, sentinel: :keep } - assert_same buffer, GC.stat(buffer, scope: :global) + assert_same buffer, GC.stat(buffer, process: true) assert_equal :keep, buffer[:sentinel] - assert_equal GC.stat(:count, scope: :global), buffer[:count] - assert_equal GC.stat(:time, scope: :global), buffer[:time] + assert_equal GC.stat(:count, process: true), buffer[:count] + assert_equal GC.stat(:time, process: true), buffer[:time] RUBY end - def test_stat_global_scope_rejects_non_hash_or_symbol_arguments + def test_stat_process_rejects_non_hash_or_symbol_arguments omit 'default GC only' unless GC.config[:implementation] == 'default' - assert_raise(TypeError) { GC.stat(0, scope: :global) } - assert_raise(TypeError) { GC.stat("count", scope: :global) } + assert_raise(TypeError) { GC.stat(0, process: true) } + assert_raise(TypeError) { GC.stat("count", process: true) } end - def test_stat_global_scope_rejects_unknown_keys + def test_stat_process_rejects_unknown_keys omit 'default GC only' unless GC.config[:implementation] == 'default' - assert_raise(ArgumentError) { GC.stat(:no_such_key, scope: :global) } - assert_raise(ArgumentError) { GC.stat(:"café", scope: :global) } + assert_raise(ArgumentError) { GC.stat(:no_such_key, process: true) } + assert_raise(ArgumentError) { GC.stat(:"café", process: true) } end - def test_stat_global_scope_rejects_extra_positional_arguments + def test_stat_process_rejects_extra_positional_arguments omit 'default GC only' unless GC.config[:implementation] == 'default' - assert_raise(ArgumentError) { GC.stat(:count, :time, scope: :global) } + assert_raise(ArgumentError) { GC.stat(:count, :time, process: true) } end - def test_stat_global_scope_rejects_frozen_hash + def test_stat_process_rejects_frozen_hash omit 'default GC only' unless GC.config[:implementation] == 'default' - assert_raise(FrozenError) { GC.stat({ sentinel: 1 }.freeze, scope: :global) } + assert_raise(FrozenError) { GC.stat({ sentinel: 1 }.freeze, process: true) } end - def test_stat_global_scope_counts_deferred_collection_once + def test_stat_process_counts_deferred_collection_once omit 'default GC only' unless GC.config[:implementation] == 'default' omit 'stress' if GC.stress assert_separately([], __FILE__, __LINE__, <<~'RUBY', timeout: 60) GC.disable - before = GC.stat(scope: :global) + before = GC.stat(process: true) GC.start(full_mark: true, immediate_mark: false, immediate_sweep: false) - started = GC.stat(scope: :global) + started = GC.stat(process: true) assert_equal [1, 1], [started[:count] - before[:count], started[:major_gc_count] - before[:major_gc_count]] GC.start(full_mark: false, immediate_mark: true, immediate_sweep: true) - settled = GC.stat(scope: :global) + settled = GC.stat(process: true) assert_equal 1, settled[:count] - started[:count] RUBY end - def test_stat_global_scope_counts_collection_with_profiler_disabled + def test_stat_process_counts_collection_with_profiler_disabled omit 'default GC only' unless GC.config[:implementation] == 'default' omit 'stress' if GC.stress assert_separately([], __FILE__, __LINE__, <<~'RUBY', timeout: 60) GC.disable GC::Profiler.disable - before = GC.stat(:count, scope: :global) + before = GC.stat(:count, process: true) GC.start - assert_equal 1, GC.stat(:count, scope: :global) - before + assert_equal 1, GC.stat(:count, process: true) - before RUBY end - def test_stat_global_scope_profiler_enable_preserves_totals + def test_stat_process_profiler_enable_preserves_totals omit 'default GC only' unless GC.config[:implementation] == 'default' omit 'stress' if GC.stress assert_separately([], __FILE__, __LINE__, <<~'RUBY', timeout: 60) + counts = %i[count time marking_time sweeping_time minor_gc_count major_gc_count global_gc_count] GC.disable GC::Profiler.disable GC.start - before = GC.stat(scope: :global) + before = GC.stat(process: true).slice(*counts) GC::Profiler.enable - assert_equal before, GC.stat(scope: :global) + assert_equal before, GC.stat(process: true).slice(*counts) RUBY end - def test_stat_global_scope_profiler_disable_preserves_totals + def test_stat_process_profiler_disable_preserves_totals omit 'default GC only' unless GC.config[:implementation] == 'default' omit 'stress' if GC.stress assert_separately([], __FILE__, __LINE__, <<~'RUBY', timeout: 60) + counts = %i[count time marking_time sweeping_time minor_gc_count major_gc_count global_gc_count] GC.disable GC::Profiler.enable GC.start - before = GC.stat(scope: :global) + before = GC.stat(process: true).slice(*counts) GC::Profiler.disable - assert_equal before, GC.stat(scope: :global) + assert_equal before, GC.stat(process: true).slice(*counts) RUBY end - def test_stat_global_scope_profiler_clear_preserves_totals + def test_stat_process_profiler_clear_preserves_totals omit 'default GC only' unless GC.config[:implementation] == 'default' omit 'stress' if GC.stress assert_separately([], __FILE__, __LINE__, <<~'RUBY', timeout: 60) + counts = %i[count time marking_time sweeping_time minor_gc_count major_gc_count global_gc_count] GC.disable GC::Profiler.enable GC.start - before = GC.stat(scope: :global) + before = GC.stat(process: true).slice(*counts) GC::Profiler.clear - assert_equal before, GC.stat(scope: :global) + assert_equal before, GC.stat(process: true).slice(*counts) RUBY end - def test_stat_global_scope_profiler_configure_preserves_totals + def test_stat_process_profiler_configure_preserves_totals omit 'default GC only' unless GC.config[:implementation] == 'default' omit 'stress' if GC.stress assert_separately([], __FILE__, __LINE__, <<~'RUBY', timeout: 60) + counts = %i[count time marking_time sweeping_time minor_gc_count major_gc_count global_gc_count] GC.disable GC::Profiler.enable 3.times { GC.start } - before = GC.stat(scope: :global) + before = GC.stat(process: true).slice(*counts) GC::Profiler.configure(max_records: 2) - assert_equal before, GC.stat(scope: :global) + assert_equal before, GC.stat(process: true).slice(*counts) RUBY end - def test_stat_global_scope_retains_finished_ractor_history + def test_stat_process_retains_finished_ractor_history omit 'default GC only' unless GC.config[:implementation] == 'default' omit 'stress' if GC.stress assert_separately([], __FILE__, __LINE__, <<~'RUBY', timeout: 60) Warning[:experimental] = false + counts = %i[count time marking_time sweeping_time minor_gc_count major_gc_count global_gc_count] GC.disable local_before = GC.stat(:count) - process_before = GC.stat(:count, scope: :global) + process_before = GC.stat(:count, process: true) ready = Ractor::Port.new worker = Ractor.new(ready) do |reply| GC.disable @@ -1291,7 +1297,7 @@ class TestGc < Test::Unit::TestCase worker_count, control = ready.receive assert_equal(3, worker_count, "worker count") - live = GC.stat(:count, scope: :global) + live = GC.stat(:count, process: true) assert_equal(3, live - process_before, "live work missing") assert_equal(local_before, GC.stat(:count), "worker changed main count") @@ -1299,21 +1305,21 @@ class TestGc < Test::Unit::TestCase worker.monitor(monitor) control << :finish assert_equal([worker, :exited], monitor.receive, "worker did not exit") - assert_equal(live + 1, GC.stat(:count, scope: :global), "history lost on exit") + assert_equal(live + 1, GC.stat(:count, process: true), "history lost on exit") - global_before = GC.stat(:count, scope: :global) + global_before = GC.stat(:count, process: true) GC.start(full_mark: true, immediate_mark: true, immediate_sweep: true) - assert_equal(1, GC.stat(:count, scope: :global) - global_before, "global count") + assert_equal(1, GC.stat(:count, process: true) - global_before, "global count") local_after_global = GC.stat(:count) - snapshot = GC.stat(scope: :global) + snapshot = GC.stat(process: true).slice(*counts) assert_equal(:finish, worker.value, "worker result") - assert_equal(snapshot, GC.stat(scope: :global), "absorption changed history") + assert_equal(snapshot, GC.stat(process: true).slice(*counts), "absorption changed history") assert_equal(local_after_global, GC.stat(:count), "absorption changed main count") RUBY end - def test_stat_global_scope_preserves_nested_ractor_history + def test_stat_process_preserves_nested_ractor_history omit 'default GC only' unless GC.config[:implementation] == 'default' omit 'stress' if GC.stress @@ -1321,7 +1327,7 @@ class TestGc < Test::Unit::TestCase Warning[:experimental] = false GC.disable local_before = GC.stat(:count) - process_before = GC.stat(:count, scope: :global) + process_before = GC.stat(:count, process: true) ready = Ractor::Port.new outer = assert_in_ractor(ready, value: false) do |reply| GC.disable @@ -1338,15 +1344,15 @@ class TestGc < Test::Unit::TestCase ready.receive - assert_equal(9, GC.stat(:count, scope: :global) - process_before, "nested history missing") + assert_equal(9, GC.stat(:count, process: true) - process_before, "nested history missing") outer.ractor.send(:finish) assert_equal(:finish, outer.value, "outer result") - assert_equal(10, GC.stat(:count, scope: :global) - process_before, "nested history changed") + assert_equal(10, GC.stat(:count, process: true) - process_before, "nested history changed") assert_equal(local_before, GC.stat(:count), "main inherited nested counts") RUBY end - def test_stat_global_scope_counts_global_collection_once + def test_stat_process_counts_global_collection_once omit 'default GC only' unless GC.config[:implementation] == 'default' omit 'stress' if GC.stress @@ -1361,9 +1367,9 @@ class TestGc < Test::Unit::TestCase end ready.receive - before = GC.stat(scope: :global) + before = GC.stat(process: true) GC.start(full_mark: true, immediate_mark: true, immediate_sweep: true) - after = GC.stat(scope: :global) + after = GC.stat(process: true) assert_equal 1, after[:count] - before[:count] assert_equal before[:major_gc_count], after[:major_gc_count] assert_equal 1, after[:global_gc_count] - before[:global_gc_count] @@ -1374,13 +1380,13 @@ class TestGc < Test::Unit::TestCase RUBY end - def test_stat_global_scope_reads_are_coherent_during_ractor_collection + def test_stat_process_reads_are_coherent_during_ractor_collection omit 'default GC only' unless GC.config[:implementation] == 'default' assert_separately([], __FILE__, __LINE__, <<~'RUBY', timeout: 60) Warning[:experimental] = false GC.disable keys = %i[count minor_gc_count major_gc_count global_gc_count time marking_time sweeping_time] - start_count = GC.stat(:count, scope: :global) + start_count = GC.stat(:count, process: true) ready = Ractor::Port.new worker = Ractor.new(ready) do |reply| @@ -1399,7 +1405,7 @@ class TestGc < Test::Unit::TestCase control.receive previous = nil read = lambda do - stat = GC.stat(scope: :global) + stat = GC.stat(process: true) assert_equal(stat[:minor_gc_count] + stat[:major_gc_count] + stat[:global_gc_count], stat[:count], "count invariant") assert_include(0..1, stat[:time] - (stat[:marking_time] + stat[:sweeping_time]), "time rounding") ks.each { |key| assert_operator stat[key], :>=, previous[key], "decreasing #{key}" if previous } @@ -1420,15 +1426,15 @@ class TestGc < Test::Unit::TestCase reader_control << :continue reader_last = reader.value - final = GC.stat(scope: :global) + final = GC.stat(process: true) assert_equal final[:minor_gc_count] + final[:major_gc_count] + final[:global_gc_count], final[:count] assert_include 0..1, final[:time] - (final[:marking_time] + final[:sweeping_time]) - assert reader_last.all? { |key, value| final[key] >= value } + assert keys.all? { |key| final[key] >= reader_last[key] } assert_operator final[:count] - start_count, :>=, 50 RUBY end - def test_stat_global_scope_preserves_measured_time_when_measurement_disabled + def test_stat_process_preserves_measured_time_when_measurement_disabled omit 'default GC only' unless GC.config[:implementation] == 'default' omit 'stress' if GC.stress @@ -1436,7 +1442,7 @@ class TestGc < Test::Unit::TestCase Warning[:experimental] = false GC.disable GC.measure_total_time = false - baseline = GC.stat(scope: :global) + baseline = GC.stat(process: true) ready = Ractor::Port.new worker = assert_in_ractor(ready, value: false) do |reply| GC.disable @@ -1458,7 +1464,7 @@ class TestGc < Test::Unit::TestCase end worker_count, worker_time, worker_marking, worker_sweeping, control = ready.receive - before = GC.stat(scope: :global) + before = GC.stat(process: true) assert_equal worker_count, before[:count] - baseline[:count] assert_operator worker_time, :>=, 2 assert_include worker_time..worker_time + 1, before[:time] - baseline[:time] @@ -1468,21 +1474,22 @@ class TestGc < Test::Unit::TestCase assert_equal [3, 100_000], [after_count - worker_count, retained_count] assert_equal [worker_time, worker_marking, worker_sweeping], [after_time, after_marking, after_sweeping] - after = GC.stat(scope: :global) + after = GC.stat(process: true) assert_equal 4, after[:count] - before[:count] assert_equal before.values_at(:time, :marking_time, :sweeping_time), after.values_at(:time, :marking_time, :sweeping_time) RUBY end - def test_stat_global_scope_fork_inherits_archived_and_live_history + def test_stat_process_fork_inherits_archived_and_live_history omit 'fork not supported' unless Process.respond_to?(:fork) omit 'default GC only' unless GC.config[:implementation] == 'default' omit 'stress' if GC.stress assert_separately([], __FILE__, __LINE__, <<~'RUBY', timeout: 60) Warning[:experimental] = false + counts = %i[count time marking_time sweeping_time minor_gc_count major_gc_count global_gc_count] GC.disable - process_before = GC.stat(:count, scope: :global) + process_before = GC.stat(:count, process: true) assert_in_ractor do GC.disable @@ -1499,15 +1506,15 @@ class TestGc < Test::Unit::TestCase end assert_equal(2, live_ready.receive, "live count") - snapshot = GC.stat(scope: :global) + snapshot = GC.stat(process: true).slice(*counts) assert_equal 6, snapshot[:count] - process_before read, write = IO.pipe pid = Process.fork do read.close - child_initial = GC.stat(scope: :global) + child_initial = GC.stat(process: true).slice(*counts) write.write(Marshal.dump(child_initial)) GC.start(full_mark: true, immediate_mark: true, immediate_sweep: true) - child_after = GC.stat(scope: :global) + child_after = GC.stat(process: true).slice(*counts) write.write(Marshal.dump(child_after)) write.close exit!(0) @@ -1520,78 +1527,80 @@ class TestGc < Test::Unit::TestCase assert_predicate(status, :success?, "child exit status") assert_equal snapshot, child_initial assert_equal 1, child_after[:count] - child_initial[:count] - assert_equal snapshot[:count], GC.stat(:count, scope: :global) + assert_equal snapshot[:count], GC.stat(:count, process: true) live.send(:finish) assert_equal :finish, live.value RUBY end - def test_stat_scope_selects_ractor_or_global_and_validates_options + def test_stat_process_selects_ractor_or_process_data omit 'default GC only' unless GC.config[:implementation] == 'default' omit 'stress' if GC.stress assert_separately([], __FILE__, __LINE__, <<~'RUBY', timeout: 60) Warning[:experimental] = false GC.disable - global_keys = %i[count minor_gc_count major_gc_count global_gc_count time marking_time sweeping_time] + count_keys = %i[count minor_gc_count major_gc_count global_gc_count time marking_time sweeping_time] + page_pool_keys = %i[page_pool_arenas page_pool_arenas_freed page_pool_total_pages page_pool_discarded_pages] + process_keys = count_keys + page_pool_keys local_gauge = :heap_live_slots default = GC.stat assert_include default.keys, local_gauge - [:ractor, :local].each do |scope| - explicit = GC.stat(scope: scope) + assert_include default.keys, :global_gc_invocation_count + assert_empty default.keys & page_pool_keys + page_pool_keys.each do |key| + assert_raise(ArgumentError) { GC.stat(key) } + end + + [false, nil].each do |process| + explicit = GC.stat(process: process) assert_not_same default, explicit - assert_equal default.values_at(*global_keys), explicit.values_at(*global_keys) - assert_include explicit.keys, local_gauge - assert_equal default[:count], GC.stat(:count, scope: scope) - assert_equal default[:count], GC.stat(nil, scope: scope)[:count] + assert_equal default.keys, explicit.keys + assert_equal default.values_at(*count_keys), explicit.values_at(*count_keys) + assert_equal default[:count], GC.stat(:count, process: process) + assert_equal default[:count], GC.stat(nil, process: process)[:count] - buffer = { scope: :global, sentinel: :keep, count: -1 } - assert_same buffer, GC.stat(buffer, scope: scope) - assert_equal [:global, :keep, default[:count]], buffer.values_at(:scope, :sentinel, :count) + buffer = { process: true, sentinel: :keep, count: -1 } + assert_same buffer, GC.stat(buffer, process: process) + assert_equal [true, :keep, default[:count]], buffer.values_at(:process, :sentinel, :count) assert_kind_of Integer, buffer[local_gauge] - assert_raise(FrozenError) { GC.stat({}.freeze, scope: scope) } + assert_raise(FrozenError) { GC.stat({}.freeze, process: process) } end - global = GC.stat(scope: :global) - assert_equal global_keys.sort, global.keys.sort - assert_raise(ArgumentError) { GC.stat(local_gauge, scope: :global) } - assert_equal global[:count], GC.stat(:count, scope: :global) - - [:process, :thread, "global", "ractor", "local", nil, true].each do |scope| - assert_raise(ArgumentError) { GC.stat(scope: scope) } + process = GC.stat(process: true) + assert_equal process_keys.sort, process.keys.sort + [local_gauge, :global_gc_invocation_count].each do |key| + assert_raise(ArgumentError) { GC.stat(key, process: true) } end + assert_equal process[:count], GC.stat(:count, process: true) + [1, :ractor, "false"].each do |value| + assert_equal process.keys, GC.stat(process: value).keys + end + + assert_raise(ArgumentError) { GC.stat(scope: :global) } assert_raise(ArgumentError) { GC.stat(foo: :bar) } - buffer = { scope: :global, sentinel: :keep } - assert_same buffer, GC.stat(buffer) - assert_equal :global, buffer[:scope] - assert_equal :keep, buffer[:sentinel] - assert_kind_of Integer, buffer[local_gauge] + out = { process: false, sentinel: :ok, count: -1 } + assert_same out, GC.stat(out, process: true) + assert_equal [false, :ok], out.values_at(:process, :sentinel) + assert_equal process_keys.sort, (out.keys - [:process, :sentinel]).sort + assert_equal GC.stat(:count, process: true), out[:count] - out = { scope: :local, sentinel: :ok, count: -1 } - assert_same out, GC.stat(out, scope: :global) - assert_equal :local, out[:scope] - assert_equal :ok, out[:sentinel] - assert_equal global_keys.sort, (out.keys - [:scope, :sentinel]).sort - assert_equal GC.stat(:count, scope: :global), out[:count] - - assert_raise(FrozenError) { GC.stat({}.freeze, scope: :global) } + assert_raise(FrozenError) { GC.stat({}.freeze, process: true) } RUBY end - def test_stat_global_scope_is_unsupported_by_non_default_gc + def test_stat_process_is_unsupported_by_non_default_gc omit 'skipped on default GC' if GC.config[:implementation] == 'default' assert_kind_of Hash, GC.stat assert_kind_of Integer, GC.stat(:count) - [:ractor, :local].each do |scope| - assert_kind_of Hash, GC.stat(scope: scope) - assert_kind_of Integer, GC.stat(:count, scope: scope) - end - assert_raise(NotImplementedError) { GC.stat(scope: :global) } - assert_raise(NotImplementedError) { GC.stat(:count, scope: :global) } - assert_raise(NotImplementedError) { GC.stat({}, scope: :global) } + assert_kind_of Hash, GC.stat(process: false) + assert_kind_of Integer, GC.stat(:count, process: false) + assert_raise(NotImplementedError) { GC.stat(process: true) } + assert_raise(NotImplementedError) { GC.stat(:count, process: true) } + assert_raise(NotImplementedError) { GC.stat({}, process: true) } end def test_gc_start_ractor_global_false @@ -1620,7 +1629,7 @@ class TestGc < Test::Unit::TestCase RUBY end - def test_stat_global_gc_count_counts_initiated_cycles_only + def test_stat_global_gc_counts_in_driver omit 'default GC only' unless GC.config[:implementation] == 'default' assert_ractor(<<~'RUBY') r = Ractor.new { Ractor.receive } @@ -1629,70 +1638,106 @@ class TestGc < Test::Unit::TestCase GC.start(global: true) after = GC.stat assert_equal 1, after[:global_gc_count] - before[:global_gc_count] + assert_equal 1, after[:global_gc_invocation_count] - before[:global_gc_invocation_count] assert_equal 1, after[:count] - before[:count] assert_equal before[:major_gc_count], after[:major_gc_count] - assert_equal after[:global_gc_count], GC.stat(:global_gc_count, scope: :ractor) GC.start(global: false) local = GC.stat - assert_equal after[:global_gc_count], local[:global_gc_count] + global_keys = %i[global_gc_count global_gc_invocation_count] + assert_equal after.values_at(*global_keys), local.values_at(*global_keys) assert_equal 1, local[:major_gc_count] - after[:major_gc_count] r.send(:done) RUBY end - def test_stat_global_gc_count_is_per_ractor + def test_stat_global_gc_count_includes_collections_started_by_other_ractors omit 'default GC only' unless GC.config[:implementation] == 'default' assert_ractor(<<~'RUBY') - worker = Ractor.new do - before = GC.stat(:global_gc_count) + ready = Ractor::Port.new + worker = Ractor.new(ready) do |port| + port << :ready + Ractor.receive + before = GC.stat(:global_gc_invocation_count) GC.start(global: true) - GC.stat(:global_gc_count) - before + GC.stat(:global_gc_invocation_count) - before end - main_before = GC.stat(:global_gc_count) - initiated = worker.value - assert_operator initiated, :>=, 1 - assert_equal 0, GC.stat(:global_gc_count) - main_before + ready.receive + GC.disable + main_before = GC.stat + worker << :go + assert_operator worker.value, :>=, 1 + main_after = GC.stat + + collected = main_after[:global_gc_count] - main_before[:global_gc_count] + assert_operator collected, :>=, 1 + assert_equal collected, main_after[:count] - main_before[:count] + assert_equal main_before[:global_gc_invocation_count], main_after[:global_gc_invocation_count] RUBY end - def test_stat_global_scope_global_gc_count_sums_ractor_counts + def test_stat_process_global_gc_count_sums_invocation_counts omit 'default GC only' unless GC.config[:implementation] == 'default' assert_ractor(<<~'RUBY') worker = Ractor.new do Ractor.receive GC.start(global: true) - GC.stat(:global_gc_count) + GC.stat(:global_gc_invocation_count) end GC.disable - before = GC.stat(scope: :global) - assert_equal GC.stat(:global_gc_count), before[:global_gc_count] + before = GC.stat(process: true) + assert_equal GC.stat(:global_gc_invocation_count), before[:global_gc_count] worker << :go worker_count = worker.value assert_operator worker_count, :>=, 1 - after = GC.stat(scope: :global) - assert_equal GC.stat(:global_gc_count) + worker_count, after[:global_gc_count] + after = GC.stat(process: true) + assert_equal GC.stat(:global_gc_invocation_count) + worker_count, after[:global_gc_count] RUBY end - def test_stat_global_scope_global_gc_count_retains_unjoined_ractor + def test_stat_process_global_gc_count_retains_unjoined_ractor omit 'default GC only' unless GC.config[:implementation] == 'default' assert_ractor(<<~'RUBY') done = Ractor::Port.new Ractor.new(done) do |reply| GC.start(global: true) - reply << GC.stat(:global_gc_count) + reply << GC.stat(:global_gc_invocation_count) end worker_count = done.receive assert_operator worker_count, :>=, 1 Thread.pass until Ractor.count == 1 GC.disable - after = GC.stat(scope: :global) - assert_equal GC.stat(:global_gc_count) + worker_count, after[:global_gc_count] + after = GC.stat(process: true) + assert_equal GC.stat(:global_gc_invocation_count) + worker_count, after[:global_gc_count] + RUBY + end + + def test_stat_heap_marked_slots_counts_own_heap_after_global_gc + omit 'default GC only' unless GC.config[:implementation] == 'default' + omit 'stress' if GC.stress + assert_ractor(<<~'RUBY') + ready = Ractor::Port.new + worker = Ractor.new(ready) do |port| + GC.disable + retained = Array.new(200_000) { Object.new } + port << :ready + Ractor.receive + [GC.stat(:heap_marked_slots), retained.size] + end + ready.receive + + GC.disable + GC.start(global: true) + main = GC.stat + assert_operator main[:heap_marked_slots], :<=, main[:heap_available_slots] + + worker << :go + worker_marked, retained = worker.value + assert_operator worker_marked, :>=, retained RUBY end end