mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 08:25:20 +08:00
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:
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
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
Reference in new issue
Block a user