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