Skip to content

Commit 3391443

Browse files
committed
Add global GC count to GC.stat
1 parent 1d4e2cd commit 3391443

2 files changed

Lines changed: 93 additions & 11 deletions

File tree

gc/default/default.c

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -766,7 +766,19 @@ typedef struct rb_objspace {
766766
/* The one VM-global GC structure; for now it only holds the page pool. Page bodies are
767767
* carved out of large mmap arenas and reused via a process-wide freelist (per-page
768768
* mmap/munmap would serialize on the kernel's mmap_lock). Leaf lock: no alloc, no GC. */
769-
typedef struct rb_global_objspace {
769+
typedef struct rb_global_objspace rb_global_objspace_t;
770+
771+
/* Why a global (stop-the-world, cross-objspace) GC was started. Counted per
772+
* trigger in global_gc.trigger_counts and exposed by GC.stat. */
773+
enum global_gc_trigger {
774+
global_gc_trigger_shareable, /* per-objspace shareable_objects crossed its limit */
775+
global_gc_trigger_zombie, /* zombie objspace pages crossed the reclamation threshold */
776+
global_gc_trigger_explicit, /* GC.start full mark on a multi-objspace process */
777+
global_gc_trigger_compact, /* GC.compact or autocompact-driven compaction */
778+
global_gc_trigger_count
779+
};
780+
781+
struct rb_global_objspace {
770782
struct {
771783
rb_nativethread_lock_t lock;
772784
struct heap_page_body *freelist; /* bodies to reuse; the next pointer lives in the body */
@@ -807,6 +819,9 @@ typedef struct rb_global_objspace {
807819
bool compacting;
808820
struct rb_objspace **objspaces;
809821
size_t n_objspaces, objspaces_capa;
822+
size_t count; /* total global GCs started */
823+
unsigned long long time_ns; /* accumulated wall time of global GCs */
824+
size_t trigger_counts[global_gc_trigger_count];
810825
} global_gc;
811826

812827
/* Index of every objspace's heap pages, ordered by body address. Writers (page
@@ -817,7 +832,7 @@ typedef struct rb_global_objspace {
817832
size_t n_pages, capa;
818833
uintptr_t lomem, himem;
819834
} page_index;
820-
} rb_global_objspace_t;
835+
};
821836

822837
static rb_global_objspace_t rb_global_objspace_instance;
823838
static rb_global_objspace_t *global_objspace = NULL;
@@ -7859,23 +7874,29 @@ gc_reset_malloc_info(rb_objspace_t *objspace, bool full_mark)
78597874
#endif
78607875
}
78617876

7862-
static void gc_start_global(rb_objspace_t *driver, bool compact);
7877+
static void gc_start_global(rb_objspace_t *driver, bool compact, enum global_gc_trigger trigger);
78637878

78647879
/* Decide whether this collection has to be global. A local GC can reclaim neither
78657880
* shareable objects nor zombie objspaces, so once those grow past their limits only a
78667881
* global GC makes progress. All inputs belong to this objspace. */
78677882
static bool
7868-
gc_need_global_p(rb_objspace_t *objspace)
7883+
gc_need_global_p(rb_objspace_t *objspace, enum global_gc_trigger *trigger)
78697884
{
78707885
if (rb_gc_single_objspace_p()) return false;
7871-
if (objspace->shareable_objects > objspace->shareable_objects_limit) return true;
7886+
if (objspace->shareable_objects > objspace->shareable_objects_limit) {
7887+
*trigger = global_gc_trigger_shareable;
7888+
return true;
7889+
}
78727890
/* A zombie's garbage only a global cycle reclaims, but what survived the last one
78737891
* is live data, so retrigger only once TRIGGER more pages accumulate on top of it.
78747892
* Otherwise one live-heavy unjoined zombie turns every GC stop-the-world forever. */
78757893
{
78767894
size_t zp = rb_gc_vm_zombie_total_pages();
78777895
size_t base = global_objspace->zombie_pages_survivors < zp ? global_objspace->zombie_pages_survivors : zp;
7878-
if (zp - base >= ZOMBIE_PAGES_TRIGGER) return true;
7896+
if (zp - base >= ZOMBIE_PAGES_TRIGGER) {
7897+
*trigger = global_gc_trigger_zombie;
7898+
return true;
7899+
}
78797900
}
78807901
return false;
78817902
}
@@ -7912,9 +7933,12 @@ gc_start_body(rb_objspace_t *objspace, unsigned int reason, bool allow_global)
79127933
* allocation slow path, or an allocation-driven workload slips past every threshold
79137934
* (only a global cycle reclaims dead shareable objects and zombie pages). The
79147935
* exception is the retire GC, which never promotes: a Ractor's death must not STW. */
7915-
if (allow_global && gc_need_global_p(objspace)) {
7916-
gc_start_global(objspace, false);
7917-
return TRUE;
7936+
if (allow_global) {
7937+
enum global_gc_trigger trigger;
7938+
if (gc_need_global_p(objspace, &trigger)) {
7939+
gc_start_global(objspace, false, trigger);
7940+
return TRUE;
7941+
}
79187942
}
79197943

79207944
rb_gc_initialize_vm_context(&objspace->vm_context);
@@ -8629,11 +8653,12 @@ gc_global_mark_generic_fields(rb_objspace_t *driver)
86298653
/* Two Ractors choosing a global GC at once are serialized by the barrier in gc_enter and
86308654
* simply run two cycles back to back. The second is wasted work, not an error. */
86318655
static void
8632-
gc_start_global(rb_objspace_t *driver, bool compact)
8656+
gc_start_global(rb_objspace_t *driver, bool compact, enum global_gc_trigger trigger)
86338657
{
86348658
unsigned int lock_lev;
86358659
gc_enter(driver, gc_enter_event_global, &lock_lev);
86368660

8661+
rb_hrtime_t global_gc_start = rb_hrtime_now();
86378662
GC_ASSERT(is_mark_stack_empty(&driver->mark_stack));
86388663

86398664
gc_global_snapshot_objspaces();
@@ -8832,6 +8857,10 @@ gc_start_global(rb_objspace_t *driver, bool compact)
88328857
}
88338858
driver->profile.count++;
88348859

8860+
global_objspace->global_gc.count++;
8861+
global_objspace->global_gc.trigger_counts[trigger]++;
8862+
global_objspace->global_gc.time_ns += elapsed_hrtime_from(global_gc_start);
8863+
88358864
/* step 10 */
88368865
for (size_t i = 0; i < global_objspace->global_gc.n_objspaces; i++) {
88378866
rb_objspace_t *objspace = global_objspace->global_gc.objspaces[i];
@@ -9121,7 +9150,9 @@ rb_gc_impl_start(void *objspace_ptr, bool full_mark, bool immediate_mark, bool i
91219150
* collector that reclaims shareable and cross-objspace garbage. It stops the world,
91229151
* so auto_compact is honoured here too (mirroring full mark x autocompact locally). */
91239152
if (!rb_gc_single_objspace_p() && (reason & GPR_FLAG_FULL_MARK)) {
9124-
gc_start_global(objspace, compact || ruby_enable_autocompact);
9153+
bool do_compact = compact || ruby_enable_autocompact;
9154+
gc_start_global(objspace, do_compact,
9155+
do_compact ? global_gc_trigger_compact : global_gc_trigger_explicit);
91259156
}
91269157
else {
91279158
garbage_collect(objspace, reason);
@@ -9796,6 +9827,12 @@ enum gc_stat_sym {
97969827
gc_stat_sym_minor_gc_count,
97979828
gc_stat_sym_major_gc_count,
97989829
gc_stat_sym_compact_count,
9830+
gc_stat_sym_global_gc_count,
9831+
gc_stat_sym_global_gc_time_taken,
9832+
gc_stat_sym_global_gc_trigger_shareable_count,
9833+
gc_stat_sym_global_gc_trigger_zombie_count,
9834+
gc_stat_sym_global_gc_trigger_explicit_count,
9835+
gc_stat_sym_global_gc_trigger_compact_count,
97999836
gc_stat_sym_read_barrier_faults,
98009837
gc_stat_sym_total_moved_objects,
98019838
gc_stat_sym_remembered_wb_unprotected_objects,
@@ -9847,6 +9884,12 @@ setup_gc_stat_symbols(void)
98479884
S(malloc_increase_bytes_limit);
98489885
S(minor_gc_count);
98499886
S(major_gc_count);
9887+
S(global_gc_count);
9888+
S(global_gc_time_taken);
9889+
S(global_gc_trigger_shareable_count);
9890+
S(global_gc_trigger_zombie_count);
9891+
S(global_gc_trigger_explicit_count);
9892+
S(global_gc_trigger_compact_count);
98509893
S(compact_count);
98519894
S(read_barrier_faults);
98529895
S(total_moved_objects);
@@ -9933,6 +9976,12 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym)
99339976
SET(minor_gc_count, objspace->profile.minor_gc_count);
99349977
SET(major_gc_count, objspace->profile.major_gc_count);
99359978
SET(compact_count, objspace->profile.compact_count);
9979+
SET(global_gc_count, global_objspace->global_gc.count);
9980+
SET64(global_gc_time_taken, ns_to_ms(global_objspace->global_gc.time_ns));
9981+
SET(global_gc_trigger_shareable_count, global_objspace->global_gc.trigger_counts[global_gc_trigger_shareable]);
9982+
SET(global_gc_trigger_zombie_count, global_objspace->global_gc.trigger_counts[global_gc_trigger_zombie]);
9983+
SET(global_gc_trigger_explicit_count, global_objspace->global_gc.trigger_counts[global_gc_trigger_explicit]);
9984+
SET(global_gc_trigger_compact_count, global_objspace->global_gc.trigger_counts[global_gc_trigger_compact]);
99369985
SET(read_barrier_faults, objspace->profile.read_barrier_faults);
99379986
SET(total_moved_objects, objspace->rcompactor.total_moved);
99389987
SET(remembered_wb_unprotected_objects, objspace->rgengc.uncollectible_wb_unprotected_objects);

test/ruby/test_gc.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,39 @@ def test_stat_constraints
221221
end
222222
end
223223

224+
def test_stat_global_gc
225+
stat = GC.stat
226+
%i[global_gc_count global_gc_time_taken
227+
global_gc_trigger_shareable_count global_gc_trigger_zombie_count
228+
global_gc_trigger_explicit_count global_gc_trigger_compact_count].each do |k|
229+
assert_kind_of(Integer, stat[k], "GC.stat missing or non-integer key #{k}")
230+
end
231+
232+
# The per-trigger counts sum to the total global GC count.
233+
triggers = %i[shareable zombie explicit compact].sum do |t|
234+
GC.stat(:"global_gc_trigger_#{t}_count")
235+
end
236+
assert_equal GC.stat(:global_gc_count), triggers
237+
assert_operator GC.stat(:global_gc_time_taken), :>=, 0
238+
end
239+
240+
def test_stat_global_gc_explicit_trigger
241+
assert_separately([], __FILE__, __LINE__, <<~RUBY, timeout: 60)
242+
omit "requires Ractor" unless defined?(Ractor)
243+
244+
# A second live Ractor makes this a multi-objspace process, so a full
245+
# GC.start runs a global GC (global_gc_trigger_explicit_count).
246+
r = Ractor.new { Ractor.receive }
247+
before = GC.stat(:global_gc_count)
248+
before_explicit = GC.stat(:global_gc_trigger_explicit_count)
249+
GC.start
250+
assert_operator GC.stat(:global_gc_count), :>, before
251+
assert_operator GC.stat(:global_gc_trigger_explicit_count), :>, before_explicit
252+
r.send(true)
253+
r.value
254+
RUBY
255+
end
256+
224257
def test_stat_heap
225258
omit 'stress' if GC.stress
226259

0 commit comments

Comments
 (0)