From 058583bee530b613030b2c7ac285e52582773260 Mon Sep 17 00:00:00 2001 From: Yi Lin Date: Wed, 22 Jul 2026 07:38:05 +0000 Subject: [PATCH 1/8] Update Ruby for the new GC disable API --- gc/default/default.c | 17 +++++++++++++++-- internal/mmtk.h | 4 ++-- vm.c | 20 ++++++++++++-------- 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 4df8fc82270e59..36f59593c7e0cf 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -1659,7 +1659,15 @@ void rb_gc_impl_gc_disable(void *objspace_ptr, bool finish_current_gc) { WHEN_USING_MMTK({ - mmtk_disable_collection(); + // mmtk_disable_collection() can transiently fail (e.g. a GC is in progress), so retry + // until it succeeds. Call rb_thread_check_ints() on each spin: a GC pause elsewhere may + // be waiting on this very thread to acknowledge a ractor-barrier/interrupt before it can + // finish, so busy-spinning without it risks deadlock. + if (mmtk_is_collection_enabled()) { + while (!mmtk_disable_collection()) { + rb_thread_check_ints(); + } + } return; }) @@ -3554,7 +3562,12 @@ rb_gc_impl_shutdown_call_finalizer(void *objspace_ptr) rb_gc_set_obj_free_on_exit_started(); // Disable GC like the default GC does. - mmtk_disable_collection(); + // mmtk_disable_collection() can transiently fail (e.g. a GC is in progress), so retry + // until it succeeds. See the comment in rb_gc_impl_gc_disable() about why + // rb_thread_check_ints() is needed here. + while (!mmtk_disable_collection()) { + rb_thread_check_ints(); + } // Running data/file finalizers on exit, the MMTk style. // When using MMTk, we maintain a list of obj_free candidates in the Rust code, diff --git a/internal/mmtk.h b/internal/mmtk.h index 7a24b5a9fe0473..0bc9a30e58c6eb 100644 --- a/internal/mmtk.h +++ b/internal/mmtk.h @@ -220,9 +220,9 @@ void mmtk_prepare_to_fork(void); void mmtk_after_fork(MMTk_VMThread tls); -void mmtk_enable_collection(void); +int32_t mmtk_enable_collection(void); -void mmtk_disable_collection(void); +int32_t mmtk_disable_collection(void); bool mmtk_is_collection_enabled(void); diff --git a/vm.c b/vm.c index 63fb83e9586dde..cce1b0c5c565dc 100644 --- a/vm.c +++ b/vm.c @@ -4609,13 +4609,6 @@ Init_VM(void) */ rb_define_global_const("TOPLEVEL_BINDING", rb_binding_new()); -#if USE_MMTK - if (rb_mmtk_enabled_p()) { - // Now that the VM says it's time to enable GC, we enable GC for MMTk, too. - mmtk_enable_collection(); - } -#endif - #ifdef _WIN32 rb_objspace_gc_enable(vm->gc.objspace); #endif @@ -4674,7 +4667,18 @@ Init_BareVM(void) // When creating a ractor, it will bind mutator. // We pass NULL as the tls because `Collection::spawn_gc_thread` in the mmtk-ruby binding does not use it anyway. mmtk_initialize_collection(NULL); - // Note: GC is disabled at this moment. + // mmtk_initialize_collection() leaves collection enabled; explicitly disable it here so + // it stays off until Init_VM() re-enables it once the VM is fully bootstrapped. + // mmtk_disable_collection() can transiently fail (e.g. a GC is in progress), so retry + // until it succeeds; otherwise Init_VM()'s matching mmtk_enable_collection() call would + // panic ("Trying to enable GC when it is not disabled"). + // No safepoint/yield call here (unlike the loops in gc/default/default.c): the main + // thread's execution context and ractor aren't set up yet at this point in bootstrap + // (that happens further down in this function), so rb_thread_check_ints() would + // dereference unset thread-local state. This is also the only thread in the process at + // this point, so there is no other ractor for a GC pause to be waiting on. + while (!mmtk_disable_collection()) { + } }) // setup main thread From f48ed2adb733b8e3f693a635486b0d2c3c92e53d Mon Sep 17 00:00:00 2001 From: Yi Lin Date: Thu, 23 Jul 2026 01:18:58 +0000 Subject: [PATCH 2/8] Add back the enable in Init_VM --- vm.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/vm.c b/vm.c index cce1b0c5c565dc..6d788261971522 100644 --- a/vm.c +++ b/vm.c @@ -4609,6 +4609,13 @@ Init_VM(void) */ rb_define_global_const("TOPLEVEL_BINDING", rb_binding_new()); +#if USE_MMTK + if (rb_mmtk_enabled_p()) { + // Now that the VM says it's time to enable GC, we enable GC for MMTk, too. + mmtk_enable_collection(); + } +#endif + #ifdef _WIN32 rb_objspace_gc_enable(vm->gc.objspace); #endif @@ -4670,8 +4677,8 @@ Init_BareVM(void) // mmtk_initialize_collection() leaves collection enabled; explicitly disable it here so // it stays off until Init_VM() re-enables it once the VM is fully bootstrapped. // mmtk_disable_collection() can transiently fail (e.g. a GC is in progress), so retry - // until it succeeds; otherwise Init_VM()'s matching mmtk_enable_collection() call would - // panic ("Trying to enable GC when it is not disabled"). + // until it succeeds; otherwise this disable would silently not take effect, and MMTk + // could run a GC during the bootstrap window this is meant to protect. // No safepoint/yield call here (unlike the loops in gc/default/default.c): the main // thread's execution context and ractor aren't set up yet at this point in bootstrap // (that happens further down in this function), so rb_thread_check_ints() would From 9fc689671d217ea9bf5a7e82ded7c9e136239dc5 Mon Sep 17 00:00:00 2001 From: Yi Lin Date: Thu, 23 Jul 2026 03:52:17 +0000 Subject: [PATCH 3/8] No need to check before disable. No loop in Init_BareVM --- gc/default/default.c | 6 ++---- vm.c | 17 +++++++---------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/gc/default/default.c b/gc/default/default.c index 36f59593c7e0cf..9e4979fac10f16 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -1663,10 +1663,8 @@ rb_gc_impl_gc_disable(void *objspace_ptr, bool finish_current_gc) // until it succeeds. Call rb_thread_check_ints() on each spin: a GC pause elsewhere may // be waiting on this very thread to acknowledge a ractor-barrier/interrupt before it can // finish, so busy-spinning without it risks deadlock. - if (mmtk_is_collection_enabled()) { - while (!mmtk_disable_collection()) { - rb_thread_check_ints(); - } + while (!mmtk_disable_collection()) { + rb_thread_check_ints(); } return; }) diff --git a/vm.c b/vm.c index 6d788261971522..f587a1e2b7a441 100644 --- a/vm.c +++ b/vm.c @@ -4675,16 +4675,13 @@ Init_BareVM(void) // We pass NULL as the tls because `Collection::spawn_gc_thread` in the mmtk-ruby binding does not use it anyway. mmtk_initialize_collection(NULL); // mmtk_initialize_collection() leaves collection enabled; explicitly disable it here so - // it stays off until Init_VM() re-enables it once the VM is fully bootstrapped. - // mmtk_disable_collection() can transiently fail (e.g. a GC is in progress), so retry - // until it succeeds; otherwise this disable would silently not take effect, and MMTk - // could run a GC during the bootstrap window this is meant to protect. - // No safepoint/yield call here (unlike the loops in gc/default/default.c): the main - // thread's execution context and ractor aren't set up yet at this point in bootstrap - // (that happens further down in this function), so rb_thread_check_ints() would - // dereference unset thread-local state. This is also the only thread in the process at - // this point, so there is no other ractor for a GC pause to be waiting on. - while (!mmtk_disable_collection()) { + // it stays off until Init_VM() re-enables it once the VM is fully bootstrapped. This is + // the only thread in the process at this point, and no GC has had a chance to run yet + // (nor could one be requested at this point in bootstrap), so this must always succeed + // immediately; if it doesn't, something is fundamentally broken, so abort rather than + // spin or silently continue with GC still enabled. + if (!mmtk_disable_collection()) { + rb_bug("mmtk_disable_collection() failed right after mmtk_initialize_collection()"); } }) From 6bf8fc12406aeecc26dd51510c18c3d40b84d763 Mon Sep 17 00:00:00 2001 From: Yi Lin Date: Thu, 23 Jul 2026 04:02:50 +0000 Subject: [PATCH 4/8] Use abort instead --- vm.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/vm.c b/vm.c index f587a1e2b7a441..02ad5a45ca29b5 100644 --- a/vm.c +++ b/vm.c @@ -4680,8 +4680,16 @@ Init_BareVM(void) // (nor could one be requested at this point in bootstrap), so this must always succeed // immediately; if it doesn't, something is fundamentally broken, so abort rather than // spin or silently continue with GC still enabled. + // + // Deliberately not rb_bug() here: rb_bug()'s crash-report path (rb_vm_bugreport()) is + // documented to be able to trigger a secondary SIGSEGV when walking frames on an + // abnormal VM state (see the comment in bug_report_file() in error.c), and at this point + // in bootstrap the main thread's execution context and ractor aren't set up yet -- so + // reporting via rb_bug() here risks turning a clean diagnostic into a mystery crash. if (!mmtk_disable_collection()) { - rb_bug("mmtk_disable_collection() failed right after mmtk_initialize_collection()"); + fprintf(stderr, "[FATAL] mmtk_disable_collection() failed right after " + "mmtk_initialize_collection()\n"); + abort(); } }) From 0e716800d77c4cff0c3c0b9140dda4ee4306d5c8 Mon Sep 17 00:00:00 2001 From: Kunshan Wang Date: Thu, 23 Jul 2026 16:43:27 +0800 Subject: [PATCH 5/8] Fix disable GC block and xmalloc during GC We use `rb_mmtk_block_for_gc` to block for GC. When using MMTk, we don't disable GC for xmalloc during GC. --- gc.c | 23 +++++++++++++++++++++++ gc/default/default.c | 16 +++++++++------- internal/gc.h | 8 ++++++-- internal/mmtk_support.h | 3 +++ mmtk_support.c | 2 +- 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/gc.c b/gc.c index 72fe4a0e47bd6c..61022368d0a7af 100644 --- a/gc.c +++ b/gc.c @@ -5894,6 +5894,29 @@ rb_gc_after_fork(rb_pid_t pid) rb_gc_impl_after_fork(rb_gc_get_objspace(), pid); } +bool +rb_gc_during_gc_could_malloc_region_start() +{ + // Do nothing when using MMTk. + WHEN_USING_MMTK({ + return false; + }) + + return rb_gc_disable_no_rest(); +} + +void rb_gc_during_gc_could_malloc_region_end(bool already_disabled) +{ + // Do nothing when using MMTk. + WHEN_NOT_USING_MMTK({ + return; + }) + + if (already_disabled == Qfalse) { + rb_gc_enable(); + } +} + bool rb_gc_obj_shareable_p(VALUE obj) { diff --git a/gc/default/default.c b/gc/default/default.c index 9e4979fac10f16..73961ca67e74b9 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -1,4 +1,5 @@ #include "ruby/internal/config.h" +#include "vm_core.h" #include @@ -1659,12 +1660,12 @@ void rb_gc_impl_gc_disable(void *objspace_ptr, bool finish_current_gc) { WHEN_USING_MMTK({ - // mmtk_disable_collection() can transiently fail (e.g. a GC is in progress), so retry - // until it succeeds. Call rb_thread_check_ints() on each spin: a GC pause elsewhere may - // be waiting on this very thread to acknowledge a ractor-barrier/interrupt before it can - // finish, so busy-spinning without it risks deadlock. + // mmtk_disable_collection() can transiently fail (e.g. a GC is in progress or has already + // been triggered by another thread), so retry until it succeeds. We block for GC and try + // again after the next GC pause ends. while (!mmtk_disable_collection()) { - rb_thread_check_ints(); + rb_ractor_t *cr = GET_RACTOR(); + rb_mmtk_block_for_gc((MMTk_VMMutatorThread)cr); } return; }) @@ -3562,9 +3563,10 @@ rb_gc_impl_shutdown_call_finalizer(void *objspace_ptr) // Disable GC like the default GC does. // mmtk_disable_collection() can transiently fail (e.g. a GC is in progress), so retry // until it succeeds. See the comment in rb_gc_impl_gc_disable() about why - // rb_thread_check_ints() is needed here. + // we wait for the next GC here while (!mmtk_disable_collection()) { - rb_thread_check_ints(); + rb_ractor_t *cr = GET_RACTOR(); + rb_mmtk_block_for_gc((MMTk_VMMutatorThread)cr); } // Running data/file finalizers on exit, the MMTk style. diff --git a/internal/gc.h b/internal/gc.h index 201b5a5676342f..939817c366cc5a 100644 --- a/internal/gc.h +++ b/internal/gc.h @@ -175,10 +175,10 @@ struct rb_gc_object_metadata_entry { * necessary. */ #define DURING_GC_COULD_MALLOC_REGION_START() \ assert(rb_during_gc()); \ - VALUE _already_disabled = rb_gc_disable_no_rest() + VALUE _already_disabled = rb_gc_during_gc_could_malloc_region_start() #define DURING_GC_COULD_MALLOC_REGION_END() \ - if (_already_disabled == Qfalse) rb_gc_enable() + rb_gc_during_gc_could_malloc_region_end(_already_disabled) /* gc.c */ RUBY_ATTR_MALLOC void *ruby_mimmalloc(size_t size); @@ -278,6 +278,10 @@ void rb_gc_update_values(long n, VALUE *values); const char *rb_gc_active_gc_name(void); int rb_gc_modular_gc_loaded_p(void); +/* Defined in gc.c; used by object_tracing.c */ +bool rb_gc_during_gc_could_malloc_region_start(); +void rb_gc_during_gc_could_malloc_region_end(bool already_disabled); + RUBY_SYMBOL_EXPORT_END static inline VALUE diff --git a/internal/mmtk_support.h b/internal/mmtk_support.h index 67973c4326c5eb..c8c3bb7197e44b 100644 --- a/internal/mmtk_support.h +++ b/internal/mmtk_support.h @@ -178,6 +178,9 @@ void rb_mmtk_gc_probe_slowpath(bool enter); // xmalloc accounting void rb_mmtk_xmalloc_increase_body(size_t new_size, size_t old_size); +// Block for GC +void rb_mmtk_block_for_gc(MMTk_VMMutatorThread tls); + // Commandline options parsing void rb_mmtk_pre_process_opts(int argc, char **argv); void rb_mmtk_post_process_opts(const char *arg); diff --git a/mmtk_support.c b/mmtk_support.c index c58216ae8a0625..192280083236d0 100644 --- a/mmtk_support.c +++ b/mmtk_support.c @@ -1802,7 +1802,7 @@ rb_mmtk_block_for_gc_internal(void *unused) RUBY_DEBUG_LOG("GC finished."); } -static void +void rb_mmtk_block_for_gc(MMTk_VMMutatorThread tls) { rb_mmtk_assert_mutator(); From b0aa06bb24c1ec1c468f4914991bc03c3b13cd91 Mon Sep 17 00:00:00 2001 From: Kunshan Wang Date: Thu, 23 Jul 2026 17:01:30 +0800 Subject: [PATCH 6/8] Disable tests for enabling/disabling GC --- test/.excludes-mmtk/TestGc.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/.excludes-mmtk/TestGc.rb b/test/.excludes-mmtk/TestGc.rb index cbab458b90ad90..2a8e8fb98eba86 100644 --- a/test/.excludes-mmtk/TestGc.rb +++ b/test/.excludes-mmtk/TestGc.rb @@ -1,9 +1,11 @@ +exclude(:test_enable_disable, "testing behaviour specific to default GC") exclude(:test_expand_heap, "testing behaviour specific to default GC") exclude(:test_gc_config_disable_major, "testing behaviour specific to default GC") exclude(:test_gc_config_disable_major_gc_start_always_works, "testing behaviour specific to default GC") exclude(:test_gc_config_full_mark_by_default, "testing behaviour specific to default GC") exclude(:test_gc_config_invalid_args, "testing behaviour specific to default GC") exclude(:test_gc_config_setting_returns_updated_config_hash, "testing behaviour specific to default GC") +exclude(:test_gc_disabled_start, "testing behaviour specific to default GC") exclude(:test_gc_internals, "testing behaviour specific to default GC") exclude(:test_gc_parameter, "testing behaviour specific to default GC") exclude(:test_gc_parameter_init_slots, "testing behaviour specific to default GC") From 202939df956fea552b06922abd248a3160bbd545 Mon Sep 17 00:00:00 2001 From: Kunshan Wang Date: Fri, 24 Jul 2026 15:24:06 +0800 Subject: [PATCH 7/8] Implement GC disable/enable counter at binding side --- gc.c | 2 +- gc/default/default.c | 29 +++------- internal/mmtk_support.h | 5 ++ mmtk_support.c | 88 +++++++++++++++++++++++++++++ test/.excludes-mmtk/TestGc.rb | 3 +- test/.excludes-mmtk/TestObjSpace.rb | 1 + vm.c | 37 +++++++++++- 7 files changed, 138 insertions(+), 27 deletions(-) diff --git a/gc.c b/gc.c index 61022368d0a7af..0d9a97c8285c3a 100644 --- a/gc.c +++ b/gc.c @@ -5908,7 +5908,7 @@ rb_gc_during_gc_could_malloc_region_start() void rb_gc_during_gc_could_malloc_region_end(bool already_disabled) { // Do nothing when using MMTk. - WHEN_NOT_USING_MMTK({ + WHEN_USING_MMTK({ return; }) diff --git a/gc/default/default.c b/gc/default/default.c index 73961ca67e74b9..095811aed31d54 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -1,5 +1,4 @@ #include "ruby/internal/config.h" -#include "vm_core.h" #include @@ -1636,7 +1635,7 @@ bool rb_gc_impl_gc_enabled_p(void *objspace_ptr) { WHEN_USING_MMTK({ - return mmtk_is_collection_enabled(); + return rb_mmtk_is_collection_enabled(); }) rb_objspace_t *objspace = objspace_ptr; @@ -1647,7 +1646,7 @@ void rb_gc_impl_gc_enable(void *objspace_ptr) { WHEN_USING_MMTK({ - mmtk_enable_collection(); + rb_mmtk_enable_collection(); return; }) @@ -1660,13 +1659,7 @@ void rb_gc_impl_gc_disable(void *objspace_ptr, bool finish_current_gc) { WHEN_USING_MMTK({ - // mmtk_disable_collection() can transiently fail (e.g. a GC is in progress or has already - // been triggered by another thread), so retry until it succeeds. We block for GC and try - // again after the next GC pause ends. - while (!mmtk_disable_collection()) { - rb_ractor_t *cr = GET_RACTOR(); - rb_mmtk_block_for_gc((MMTk_VMMutatorThread)cr); - } + rb_mmtk_disable_collection(); return; }) @@ -3561,13 +3554,7 @@ rb_gc_impl_shutdown_call_finalizer(void *objspace_ptr) rb_gc_set_obj_free_on_exit_started(); // Disable GC like the default GC does. - // mmtk_disable_collection() can transiently fail (e.g. a GC is in progress), so retry - // until it succeeds. See the comment in rb_gc_impl_gc_disable() about why - // we wait for the next GC here - while (!mmtk_disable_collection()) { - rb_ractor_t *cr = GET_RACTOR(); - rb_mmtk_block_for_gc((MMTk_VMMutatorThread)cr); - } + rb_mmtk_disable_collection(); // Running data/file finalizers on exit, the MMTk style. // When using MMTk, we maintain a list of obj_free candidates in the Rust code, @@ -7555,10 +7542,10 @@ rb_gc_impl_start(void *objspace_ptr, bool full_mark, bool immediate_mark, bool i rb_objspace_t *objspace = objspace_ptr; #if USE_MMTK if (rb_mmtk_enabled_p()) { - // Note: GC.start will initiates garbage collection even if manually disabled. - // Therefore, we need to force GC. - // We do a full-heap GC if full_mark is true. In StickyImmix this may or may not trigger defragmentation. - // There is currently no way to force a defragmentation GC. + // Note: In CRuby, manual invocations of GC.start will still initiate garbage collection, + // but in MMTk, disabling GC means GC cannot be triggered in any way. + // The following statement may not necessarily trigger GC. + // TODO: Find a way to reconcile them. mmtk_handle_user_collection_request(GET_THREAD(), true, full_mark); gc_finalize_deferred(objspace); diff --git a/internal/mmtk_support.h b/internal/mmtk_support.h index c8c3bb7197e44b..ee48625ea18cfc 100644 --- a/internal/mmtk_support.h +++ b/internal/mmtk_support.h @@ -178,6 +178,11 @@ void rb_mmtk_gc_probe_slowpath(bool enter); // xmalloc accounting void rb_mmtk_xmalloc_increase_body(size_t new_size, size_t old_size); +// Disabling / enabling GC +void rb_mmtk_disable_collection(); +void rb_mmtk_enable_collection(); +bool rb_mmtk_is_collection_enabled(); + // Block for GC void rb_mmtk_block_for_gc(MMTk_VMMutatorThread tls); diff --git a/mmtk_support.c b/mmtk_support.c index 192280083236d0..8a503112357a67 100644 --- a/mmtk_support.c +++ b/mmtk_support.c @@ -112,11 +112,19 @@ struct rb_mmtk_xmalloc_accounting{ size_t malloc_total; } rb_mmtk_xmalloc_accounting_t; +// States for disabling GC +struct RubyMMTKGCDisablingState { + // This mutex only protects the `gc_is_disabled` field. + pthread_mutex_t mutex; + bool gc_is_disabled; +}; + struct RubyMMTKGlobal { pthread_mutex_t mutex; pthread_cond_t cond_world_stopped; pthread_cond_t cond_world_started; rb_atomic_t mutator_blocking_count; + struct RubyMMTKGCDisablingState gc_disabling_state; unsigned int fork_hook_vm_lock_lev; bool world_stopped; size_t start_the_world_count; @@ -125,6 +133,10 @@ struct RubyMMTKGlobal { .cond_world_stopped = PTHREAD_COND_INITIALIZER, .cond_world_started = PTHREAD_COND_INITIALIZER, .mutator_blocking_count = 0, + .gc_disabling_state = (struct RubyMMTKGCDisablingState) { + .mutex = PTHREAD_MUTEX_INITIALIZER, + .gc_is_disabled = false, + }, .fork_hook_vm_lock_lev = 0, .world_stopped = false, .start_the_world_count = 0, @@ -1802,6 +1814,82 @@ rb_mmtk_block_for_gc_internal(void *unused) RUBY_DEBUG_LOG("GC finished."); } +//////////////////////////////////////////////////////////////////////////////// +// Disabling / enabling GC +//////////////////////////////////////////////////////////////////////////////// + +void +rb_mmtk_disable_collection() +{ + // Only mutator threads can disable GC. + rb_mmtk_assert_mutator(); + + struct RubyMMTKGCDisablingState *state = &rb_mmtk_global.gc_disabling_state; + + for (;;) { + pthread_mutex_lock(&state->mutex); + bool gc_is_disabled = state->gc_is_disabled; + if (!gc_is_disabled) { + // GC is not disabled, yet. We try once to disable it. + bool successful = mmtk_disable_collection(); + if (successful) { + // If it is successful, we have disabled GC. We set the state value. + state->gc_is_disabled = true; + gc_is_disabled = true; + } + } + pthread_mutex_unlock(&state->mutex); + + if (gc_is_disabled) { + // Either GC has already been disabled before this call, + // or we successfully disabled GC. We can return. + return; + } else { + // Otherwise, another thread must have triggered GC, and GC will start soon. + // We block until the next GC finishes and try again. + rb_mmtk_block_for_gc((MMTk_VMMutatorThread)GET_RACTOR()); + } + } +} + +void +rb_mmtk_enable_collection() +{ + // Only mutator threads can enable GC. + rb_mmtk_assert_mutator(); + + struct RubyMMTKGCDisablingState *state = &rb_mmtk_global.gc_disabling_state; + + pthread_mutex_lock(&state->mutex); + bool gc_is_disabled = state->gc_is_disabled; + if (gc_is_disabled) { + // GC is already disabled. We enable GC. This is non-blocking. + mmtk_enable_collection(); + state->gc_is_disabled = false; + } + pthread_mutex_unlock(&state->mutex); + +} + +bool +rb_mmtk_is_collection_enabled() +{ + // Only mutator threads can enable GC. + rb_mmtk_assert_mutator(); + + struct RubyMMTKGCDisablingState *state = &rb_mmtk_global.gc_disabling_state; + + pthread_mutex_lock(&state->mutex); + bool gc_is_disabled = state->gc_is_disabled; + pthread_mutex_unlock(&state->mutex); + + return !gc_is_disabled; +} + +//////////////////////////////////////////////////////////////////////////////// +// Blocking for GC +//////////////////////////////////////////////////////////////////////////////// + void rb_mmtk_block_for_gc(MMTk_VMMutatorThread tls) { diff --git a/test/.excludes-mmtk/TestGc.rb b/test/.excludes-mmtk/TestGc.rb index 2a8e8fb98eba86..dc5ad485ae162f 100644 --- a/test/.excludes-mmtk/TestGc.rb +++ b/test/.excludes-mmtk/TestGc.rb @@ -1,11 +1,10 @@ -exclude(:test_enable_disable, "testing behaviour specific to default GC") exclude(:test_expand_heap, "testing behaviour specific to default GC") exclude(:test_gc_config_disable_major, "testing behaviour specific to default GC") exclude(:test_gc_config_disable_major_gc_start_always_works, "testing behaviour specific to default GC") exclude(:test_gc_config_full_mark_by_default, "testing behaviour specific to default GC") exclude(:test_gc_config_invalid_args, "testing behaviour specific to default GC") exclude(:test_gc_config_setting_returns_updated_config_hash, "testing behaviour specific to default GC") -exclude(:test_gc_disabled_start, "testing behaviour specific to default GC") +exclude(:test_gc_disabled_start, "When GC is disabled on MMTk, GC cannot be triggered in any way.") exclude(:test_gc_internals, "testing behaviour specific to default GC") exclude(:test_gc_parameter, "testing behaviour specific to default GC") exclude(:test_gc_parameter_init_slots, "testing behaviour specific to default GC") diff --git a/test/.excludes-mmtk/TestObjSpace.rb b/test/.excludes-mmtk/TestObjSpace.rb index d31576436ef6e6..0e2b621c33fe84 100644 --- a/test/.excludes-mmtk/TestObjSpace.rb +++ b/test/.excludes-mmtk/TestObjSpace.rb @@ -4,3 +4,4 @@ exclude(:test_memsize_of, "testing behaviour specific to default GC") exclude(:test_memsize_of_root_shared_string, "testing behaviour specific to default GC") exclude(:test_dump_objects_dumps_page_slot_sizes, "testing behaviour specific to default GC") + diff --git a/vm.c b/vm.c index 02ad5a45ca29b5..35814a094f4a1d 100644 --- a/vm.c +++ b/vm.c @@ -4612,7 +4612,27 @@ Init_VM(void) #if USE_MMTK if (rb_mmtk_enabled_p()) { // Now that the VM says it's time to enable GC, we enable GC for MMTk, too. + + // We don't use rb_bug just to be safe. See Init_BareVM. + + // Assert MMTk-level raw state. + if (mmtk_is_collection_enabled()) { + fprintf(stderr, "ERROR: GC is enabled at the MMTk level, but should still be disabled.\n"); + abort(); + } + + // We enable GC at the MMTk level. mmtk_enable_collection(); + + // Assert both MMTk-level raw state and binding-level wrapped state. + if (!mmtk_is_collection_enabled()) { + fprintf(stderr, "ERROR: GC is disabled at the MMTk level, but should be enabled now.\n"); + abort(); + } + if (!rb_mmtk_is_collection_enabled()) { + fprintf(stderr, "ERROR: GC is disabled at the binding level, but should be enabled now.\n"); + abort(); + } } #endif @@ -4674,6 +4694,7 @@ Init_BareVM(void) // When creating a ractor, it will bind mutator. // We pass NULL as the tls because `Collection::spawn_gc_thread` in the mmtk-ruby binding does not use it anyway. mmtk_initialize_collection(NULL); + // mmtk_initialize_collection() leaves collection enabled; explicitly disable it here so // it stays off until Init_VM() re-enables it once the VM is fully bootstrapped. This is // the only thread in the process at this point, and no GC has had a chance to run yet @@ -4686,9 +4707,19 @@ Init_BareVM(void) // abnormal VM state (see the comment in bug_report_file() in error.c), and at this point // in bootstrap the main thread's execution context and ractor aren't set up yet -- so // reporting via rb_bug() here risks turning a clean diagnostic into a mystery crash. - if (!mmtk_disable_collection()) { - fprintf(stderr, "[FATAL] mmtk_disable_collection() failed right after " - "mmtk_initialize_collection()\n"); + + // Assert MMTk-level raw state. + if (!mmtk_is_collection_enabled()) { + fprintf(stderr, "ERROR: GC is disabled at the MMTk level, but should be enabled.\n"); + abort(); + } + + // We disable GC at the MMTk level. + mmtk_disable_collection(); + + // Assert MMTk-level raw state. + if (mmtk_is_collection_enabled()) { + fprintf(stderr, "ERROR: GC is still enabled at the MMTk level, but should be disabled now.\n"); abort(); } }) From 544e422b86529883f100013f1f583487373254d7 Mon Sep 17 00:00:00 2001 From: Kunshan Wang Date: Fri, 24 Jul 2026 15:38:53 +0800 Subject: [PATCH 8/8] Regenerate mmtk.h --- internal/mmtk.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/mmtk.h b/internal/mmtk.h index 0bc9a30e58c6eb..941b6100526cb5 100644 --- a/internal/mmtk.h +++ b/internal/mmtk.h @@ -220,9 +220,9 @@ void mmtk_prepare_to_fork(void); void mmtk_after_fork(MMTk_VMThread tls); -int32_t mmtk_enable_collection(void); +bool mmtk_enable_collection(void); -int32_t mmtk_disable_collection(void); +bool mmtk_disable_collection(void); bool mmtk_is_collection_enabled(void);