Replace GC.stat scope: with a process: keyword

Recently we added some keys to GC.stat in order to measure the GC
performance of Ractor local GC and the associated global GC.

This commit refactors some of those terms: "global" was used in two
places with different meanings which could get confusing.

When it was passed to GC.stat as scope: :global it meant "Give me
aggregate stats for all the GCs running in this process".

Whereas inside the output of GC.stat, global referred to the number of
"global" GCs that are run. That is: stop-the-world GCs that run on
every ractor heap in the process.

Also the global_gc_count that GC.stat reports for a Ractor ignored
global GCs that other Ractors started, even though they swept its heap.

In this commit we fix this confusing behaviour, and make the GC.stat
output more consistent.

We've replaced the scope: parameter to GC.stat with the boolean
process: keyword arg. The default is process: false which means
GC.stat will return stats for the Ractor that executes the code (the
main Ractor if no other Ractors have been created). process: true will
return aggregate stats over all the Ractors in the process (including
dead ones).

Inside GC.stat(process: false), count and global_gc_count now
include every global GC that has swept that Ractor's heap. So Ractor
local gc count == count - global_gc_count.

heap_marked_slots now counts only the marked objects in that Ractor's
own heap. Before, the Ractor that started a global GC counted the
marked objects of every heap, and the other Ractors reported 0.

We also provide a new key global_gc_invocation_count that is the
number of global GCs that have been triggered by the Ractor in question.

We've also moved some keys related to the global page pool out of the
default GC.stat output and into GC.stat(process: true), because they
are process-wide and not Ractor specific.
This commit is contained in:
Matt Valentine-House authored and eightbitraptor committed 2026-10-23 17:05:54 +01:00
1 parent 50f1aca061
commit 01fe91202c
7 files changed
+311 -244
Notes: git 2026-10-23 16:06:30 +00:00

No files matched your search

+20 -15
View File
@@ -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.
+2 -2
View File
@@ -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));
+58 -42
View File
@@ -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 <tt>scope: :ractor</tt> (the default) or <tt>scope: :local</tt>,
# With <tt>process: false</tt> (the default) or +nil+,
# returns statistics for the current Ractor's object space.
# With <tt>scope: :global</tt>, 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
# <tt>scope: :global</tt>, 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 <tt>scope: :global</tt>, the default collector reports cumulative
# With <tt>process: true</tt>, 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:
+26 -25
View File
@@ -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);
+1 -1
View File
@@ -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);
+7 -7
View File
@@ -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)}"
+197 -152
View File
@@ -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