mirror of
https://github.com/ruby/ruby.git
synced 2026-10-29 00:46:26 +08:00
Enable incremental marking when the process has > 1 objspace
Previously, major GC incremental marking was disabled (even on the main Ractor) when more than 1 Ractor was running. By changing the WB implementation, we now have it back.
This commit is contained in:
1 parent
4133daa130
commit
24be314b45
11 files changed
+780
-54
Notes:
git
2026-09-24 17:55:16 +00:00
No files matched your search
@@ -0,0 +1,30 @@
|
||||
#include "ruby.h"
|
||||
#include "internal/gc.h"
|
||||
|
||||
/* GC.disable continues the cycle in progress (rb_gc_disable calls rb_gc_impl_gc_rest), so it
|
||||
* cannot guard a window that has to stay mid-mark. These expose the other variant: it suppresses
|
||||
* collection for the calling Ractor's objspace alone and leaves an incremental cycle running.
|
||||
* Used by the ractor-local incremental marking tests. */
|
||||
|
||||
static VALUE
|
||||
gc_local_disable_no_rest(VALUE self)
|
||||
{
|
||||
return rb_gc_local_disable_no_rest();
|
||||
}
|
||||
|
||||
static VALUE
|
||||
gc_local_enable(VALUE self)
|
||||
{
|
||||
return rb_gc_local_enable();
|
||||
}
|
||||
|
||||
void
|
||||
Init_disable(void)
|
||||
{
|
||||
rb_ext_ractor_safe(true);
|
||||
|
||||
VALUE mBug = rb_define_module("Bug");
|
||||
VALUE mGC = rb_define_module_under(mBug, "GC");
|
||||
rb_define_singleton_method(mGC, "local_disable_no_rest", gc_local_disable_no_rest, 0);
|
||||
rb_define_singleton_method(mGC, "local_enable", gc_local_enable, 0);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: false
|
||||
require_relative "../../auto_ext.rb"
|
||||
auto_ext(inc: true)
|
||||
@@ -0,0 +1,3 @@
|
||||
# frozen_string_literal: false
|
||||
require_relative "../../auto_ext.rb"
|
||||
auto_ext(inc: true)
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "ruby.h"
|
||||
#include "internal/gc.h"
|
||||
|
||||
struct wb_box {
|
||||
VALUE child;
|
||||
};
|
||||
|
||||
static void
|
||||
wb_box_mark(void *ptr)
|
||||
{
|
||||
struct wb_box *box = ptr;
|
||||
rb_gc_mark(box->child);
|
||||
}
|
||||
|
||||
static size_t
|
||||
wb_box_memsize(const void *ptr)
|
||||
{
|
||||
return ptr ? sizeof(struct wb_box) : 0;
|
||||
}
|
||||
|
||||
static const rb_data_type_t wb_box_type = {
|
||||
"Bug::GC::WriteBarrier::Box",
|
||||
{wb_box_mark, RUBY_TYPED_DEFAULT_FREE, wb_box_memsize,},
|
||||
NULL, NULL,
|
||||
RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FROZEN_SHAREABLE,
|
||||
};
|
||||
|
||||
static VALUE
|
||||
wb_box_alloc(VALUE klass)
|
||||
{
|
||||
struct wb_box *box;
|
||||
VALUE obj = TypedData_Make_Struct(klass, struct wb_box, &wb_box_type, box);
|
||||
box->child = Qnil;
|
||||
return obj;
|
||||
}
|
||||
|
||||
/* No rb_check_frozen: the point of this box is to keep storing after
|
||||
* Ractor.make_shareable froze it, so a test can build a shareable -> unshareable edge
|
||||
* across objspaces. */
|
||||
static VALUE
|
||||
wb_box_store(VALUE self, VALUE child)
|
||||
{
|
||||
struct wb_box *box;
|
||||
TypedData_Get_Struct(self, struct wb_box, &wb_box_type, box);
|
||||
RB_OBJ_WRITE(self, &box->child, child);
|
||||
return child;
|
||||
}
|
||||
|
||||
static VALUE
|
||||
wb_box_child(VALUE self)
|
||||
{
|
||||
struct wb_box *box;
|
||||
TypedData_Get_Struct(self, struct wb_box, &wb_box_type, box);
|
||||
return box->child;
|
||||
}
|
||||
|
||||
static VALUE
|
||||
wb_remember(VALUE self, VALUE obj)
|
||||
{
|
||||
rb_gc_writebarrier_remember(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
void
|
||||
Init_writebarrier(void)
|
||||
{
|
||||
rb_ext_ractor_safe(true);
|
||||
|
||||
VALUE mBug = rb_define_module("Bug");
|
||||
VALUE mGC = rb_define_module_under(mBug, "GC");
|
||||
VALUE mWB = rb_define_module_under(mGC, "WriteBarrier");
|
||||
rb_define_singleton_method(mWB, "remember", wb_remember, 1);
|
||||
|
||||
VALUE cBox = rb_define_class_under(mWB, "Box", rb_cObject);
|
||||
rb_define_alloc_func(cBox, wb_box_alloc);
|
||||
rb_define_method(cBox, "store", wb_box_store, 1);
|
||||
rb_define_method(cBox, "child", wb_box_child, 0);
|
||||
}
|
||||
@@ -235,6 +235,13 @@ rb_gc_event_hook(VALUE obj, rb_event_flag_t event)
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Nonzero once the cleanup walk has stashed an objspace, which is the only case where a
|
||||
* thread needs one other than its own Ractor's. One unsynchronised load gates it,
|
||||
* keeping the per-reference marking path and the malloc paths clear of the lookups
|
||||
* below. It is never unwound, so a stale zero only costs a resolution that predates the
|
||||
* stash. */
|
||||
static rb_atomic_t gc_objspace_override_count;
|
||||
|
||||
/* VM destruct's free-at-exit walk can free the thread and Ractor structs first, so
|
||||
* resolving through the current Ractor would use freed memory; return the objspace
|
||||
* stashed before the walk started. */
|
||||
@@ -243,11 +250,19 @@ void
|
||||
rb_gc_stash_cleanup_objspace(void)
|
||||
{
|
||||
GET_VM()->gc.cleanup_objspace = rb_gc_get_objspace();
|
||||
/* Never unwound, as the VM is going away: from here every resolution takes the slow
|
||||
* path, which is where the stashed objspace is returned from. */
|
||||
RUBY_ATOMIC_INC(gc_objspace_override_count);
|
||||
}
|
||||
|
||||
static inline void *
|
||||
gc_current_objspace_of(rb_ractor_t *const cr)
|
||||
/* Out of line to keep the rare arms out of RB_GC_MARK_OR_TRAVERSE's inline expansions. */
|
||||
NOINLINE(static void *gc_current_objspace_slow(rb_ractor_t *const cr));
|
||||
|
||||
static void *
|
||||
gc_current_objspace_slow(rb_ractor_t *const cr)
|
||||
{
|
||||
/* Checked before anything reads cr or the EC: the cleanup walk is the case where
|
||||
* both may already be freed (see rb_gc_stash_cleanup_objspace). */
|
||||
if (RB_UNLIKELY(ruby_vm_during_cleanup) && GET_VM()->gc.cleanup_objspace) {
|
||||
return GET_VM()->gc.cleanup_objspace;
|
||||
}
|
||||
@@ -261,6 +276,16 @@ gc_current_objspace_of(rb_ractor_t *const cr)
|
||||
return cr->objspace;
|
||||
}
|
||||
|
||||
static inline void *
|
||||
gc_current_objspace_of(rb_ractor_t *const cr)
|
||||
{
|
||||
if (RB_LIKELY(cr != NULL && gc_objspace_override_count == 0)) {
|
||||
RUBY_ASSERT(cr->objspace != NULL);
|
||||
return cr->objspace;
|
||||
}
|
||||
return gc_current_objspace_slow(cr);
|
||||
}
|
||||
|
||||
void *
|
||||
rb_gc_get_objspace(void)
|
||||
{
|
||||
|
||||
+120
-47
@@ -4242,17 +4242,32 @@ gc_finalize_deferred_register(rb_objspace_t *objspace)
|
||||
|
||||
static int pop_mark_stack(mark_stack_t *stack, VALUE *data);
|
||||
|
||||
/* Throw away an unfinished incremental mark and leave the objspace in gc_mode_none. The
|
||||
* mark bits the partial mark set stay behind, so the caller must clear them before the
|
||||
* heap is collected again. */
|
||||
static void
|
||||
gc_abort_incremental_marking(rb_objspace_t *objspace)
|
||||
{
|
||||
GC_ASSERT(is_incremental_marking(objspace));
|
||||
|
||||
VALUE obj;
|
||||
while (pop_mark_stack(&objspace->mark_stack, &obj));
|
||||
|
||||
/* gc_grey records the weak references it greys for gc_marks_finish to resolve; this
|
||||
* cycle never reaches it, and the entries would outlive their objects. */
|
||||
rb_darray_clear(objspace->weak_references);
|
||||
|
||||
objspace->flags.during_incremental_marking = FALSE;
|
||||
gc_mode_set(objspace, gc_mode_none);
|
||||
}
|
||||
|
||||
static void
|
||||
gc_abort(void *objspace_ptr)
|
||||
{
|
||||
rb_objspace_t *objspace = objspace_ptr;
|
||||
|
||||
if (is_incremental_marking(objspace)) {
|
||||
/* Remove all objects from the mark stack. */
|
||||
VALUE obj;
|
||||
while (pop_mark_stack(&objspace->mark_stack, &obj));
|
||||
|
||||
objspace->flags.during_incremental_marking = FALSE;
|
||||
gc_abort_incremental_marking(objspace);
|
||||
}
|
||||
|
||||
if (is_lazy_sweeping(objspace)) {
|
||||
@@ -6879,6 +6894,10 @@ check_color_i(const VALUE child, void *ptr)
|
||||
struct verify_internal_consistency_struct *data = (struct verify_internal_consistency_struct *)ptr;
|
||||
const VALUE parent = data->parent;
|
||||
|
||||
/* This cycle never marks a foreign child (gc_skip_foreign_object_p) and the write
|
||||
* barrier is a no-op across objspaces, so its colour says nothing here. */
|
||||
if (GET_HEAP_OBJSPACE(child) != data->objspace) return;
|
||||
|
||||
if (!RVALUE_WB_UNPROTECTED(data->objspace, parent) && RVALUE_WHITE_P(data->objspace, child)) {
|
||||
fprintf(stderr, "verify_internal_consistency_reachable_i: WB miss (B->W) - %s -> %s\n",
|
||||
rb_obj_info(parent), rb_obj_info(child));
|
||||
@@ -7524,7 +7543,13 @@ gc_marks_finish(rb_objspace_t *objspace)
|
||||
* neither (another objspace may hold them). Running after the full walk makes the
|
||||
* pin count a retention metric: an upper bound on the garbage only a global GC can
|
||||
* reclaim. A global GC's exact mark does not pin. (The allrefs comparison of
|
||||
* RGENGC_CHECK_MODE >= 4 does not model these pins; it reports false positives.) */
|
||||
* RGENGC_CHECK_MODE >= 4 does not model these pins; it reports false positives.)
|
||||
*
|
||||
* Running it here rather than with the other roots is also what lets an incremental
|
||||
* mark run while other objspaces exist. The write barrier bails out on a
|
||||
* cross-objspace edge, so a store made between two mark steps leaves nothing behind
|
||||
* but a shareable or shref bit; scanning those bitmaps after the last step picks up
|
||||
* every bit set during the cycle, which a scan at gc_marks_start would miss. */
|
||||
objspace->last_cycle_pinned = 0;
|
||||
if (!rb_gc_single_objspace_p() && !objspace->flags.during_global_gc) {
|
||||
objspace->last_cycle_pinned = 1;
|
||||
@@ -8215,6 +8240,7 @@ gc_bitmaps_clear(rb_objspace_t *objspace, rb_heap_t *heap, bool clear_shref)
|
||||
|
||||
NOINLINE(static void gc_writebarrier_generational(VALUE a, VALUE b, rb_objspace_t *objspace));
|
||||
|
||||
/* Precondition: `a` and `b` live in `objspace`. */
|
||||
static void
|
||||
gc_writebarrier_generational(VALUE a, VALUE b, rb_objspace_t *objspace)
|
||||
{
|
||||
@@ -8251,6 +8277,7 @@ gc_mark_from(rb_objspace_t *objspace, VALUE obj, VALUE parent)
|
||||
|
||||
NOINLINE(static void gc_writebarrier_incremental(VALUE a, VALUE b, rb_objspace_t *objspace));
|
||||
|
||||
/* Precondition: `a` and `b` live in `objspace`. */
|
||||
static void
|
||||
gc_writebarrier_incremental(VALUE a, VALUE b, rb_objspace_t *objspace)
|
||||
{
|
||||
@@ -8303,13 +8330,15 @@ rb_gc_impl_writebarrier(void *objspace_ptr, VALUE a, VALUE b)
|
||||
}
|
||||
}
|
||||
|
||||
retry:
|
||||
if (!is_incremental_marking(objspace)) {
|
||||
/* The generational barrier covers old->young edges within one objspace only; a
|
||||
* foreign a or b has age bits another objspace mutates, unsafe to read, so check
|
||||
* locality first when multi-Ractor (a foreign a is shareable and the shref above
|
||||
* already keeps b alive). With a single Ractor nothing is foreign. */
|
||||
if ((rb_gc_multi_ractor_p() &&
|
||||
/* The generational barrier covers old->young edges within one objspace only.
|
||||
* NOTE: we shouldn't even check the age of `a` or `b` if they are in another
|
||||
* objspace, so check locality first. The test is rb_gc_ever_multi_ractor_p, not
|
||||
* rb_gc_multi_ractor_p: a foreign objspace exists before the process is
|
||||
* multi-Ractor (rb_gc_objspace_alloc runs while the creator is still the only
|
||||
* Ractor) and outlives the return to one (a fork parks the others in
|
||||
* zombie_objspaces). */
|
||||
if ((rb_gc_ever_multi_ractor_p() &&
|
||||
(GET_HEAP_OBJSPACE(a) != objspace || GET_HEAP_OBJSPACE(b) != objspace)) ||
|
||||
!RVALUE_OLD_P(objspace, a) || RVALUE_OLD_P(objspace, b)) {
|
||||
// do nothing
|
||||
@@ -8319,17 +8348,15 @@ rb_gc_impl_writebarrier(void *objspace_ptr, VALUE a, VALUE b)
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* Slow path, no lock: incremental marking only runs while the process has a single
|
||||
* objspace, so the owning Ractor's GVL already serializes this barrier against its
|
||||
* own GC. */
|
||||
if (is_incremental_marking(objspace)) {
|
||||
gc_writebarrier_incremental(a, b, objspace);
|
||||
// Shareable objects from different object spaces are kept alive by shareable bits
|
||||
if (rb_gc_ever_multi_ractor_p() &&
|
||||
(GET_HEAP_OBJSPACE(a) != objspace || GET_HEAP_OBJSPACE(b) != objspace)) {
|
||||
// do nothing
|
||||
}
|
||||
else {
|
||||
goto retry;
|
||||
gc_writebarrier_incremental(a, b, objspace);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
void
|
||||
@@ -8338,6 +8365,7 @@ rb_gc_impl_obj_became_shareable(void *objspace_ptr, VALUE obj)
|
||||
/* An object becomes shareable on its owner thread, so this page update is
|
||||
* single-writer. */
|
||||
struct heap_page *page = GET_HEAP_PAGE(obj);
|
||||
|
||||
if (_MARKED_IN_BITMAP(page->shareable_bits, page, obj)) return;
|
||||
gc_page_add_shareable(page, obj);
|
||||
|
||||
@@ -8409,23 +8437,26 @@ rb_gc_impl_active_gc_name(void)
|
||||
return "default";
|
||||
}
|
||||
|
||||
/* NOTE: `obj` doesn't necessarily live in `objspace_ptr`, as `objspace_ptr` is just that
|
||||
* of the current Ractor. */
|
||||
void
|
||||
rb_gc_impl_writebarrier_remember(void *objspace_ptr, VALUE obj)
|
||||
{
|
||||
|
||||
rb_objspace_t *objspace = objspace_ptr;
|
||||
|
||||
gc_report(1, objspace, "rb_gc_writebarrier_remember: %s\n", rb_obj_info(obj));
|
||||
|
||||
/* No lock, for the same reason as rb_gc_impl_writebarrier: remembering is an atomic
|
||||
* bitmap set, and the incremental branch only runs with a single objspace, where the
|
||||
* Ractor's GVL serializes it against its own GC. */
|
||||
if (is_incremental_marking(objspace)) {
|
||||
if (RVALUE_BLACK_P(objspace, obj)) {
|
||||
gc_grey(objspace, obj);
|
||||
// Shareable objects from other object spaces don't need to be put on the remembered set
|
||||
// and are only collected during global GC, so not while incremental marking.
|
||||
if (RB_LIKELY(!rb_gc_ever_multi_ractor_p() || GET_HEAP_OBJSPACE(obj) == objspace)) {
|
||||
gc_report(1, objspace, "rb_gc_writebarrier_remember: %s\n", rb_obj_info(obj));
|
||||
if (is_incremental_marking(objspace)) {
|
||||
if (RVALUE_BLACK_P(objspace, obj)) {
|
||||
gc_grey(objspace, obj);
|
||||
}
|
||||
}
|
||||
else if (RVALUE_OLD_P(objspace, obj)) {
|
||||
rgengc_remember(objspace, obj);
|
||||
}
|
||||
}
|
||||
else if (RVALUE_OLD_P(objspace, obj)) {
|
||||
rgengc_remember(objspace, obj);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8783,11 +8814,7 @@ gc_start_body(rb_objspace_t *objspace, unsigned int reason, bool allow_global)
|
||||
|
||||
if (objspace->flags.dont_incremental ||
|
||||
reason & GPR_FLAG_IMMEDIATE_MARK ||
|
||||
ruby_gc_stressful ||
|
||||
/* No incremental marking while multiple objspaces exist: between steps another
|
||||
* Ractor can create and share objects behind this objspace's already-scanned
|
||||
* roots. */
|
||||
!rb_gc_single_objspace_p()) {
|
||||
ruby_gc_stressful) {
|
||||
objspace->flags.during_incremental_marking = FALSE;
|
||||
}
|
||||
else {
|
||||
@@ -9486,8 +9513,6 @@ gc_start_global(rb_objspace_t *driver, unsigned int reason, bool compact, bool a
|
||||
gc_event_hook(driver, RUBY_INTERNAL_EVENT_GC_START);
|
||||
gc_prof_timer_start(driver);
|
||||
|
||||
GC_ASSERT(is_mark_stack_empty(&driver->mark_stack));
|
||||
|
||||
gc_global_snapshot_objspaces();
|
||||
|
||||
/* Mark every objspace as in a global GC before step 3 settles the lazy sweeps: the
|
||||
@@ -9515,9 +9540,14 @@ gc_start_global(rb_objspace_t *driver, unsigned int reason, bool compact, bool a
|
||||
* thread runs every objspace's phases. */
|
||||
for (size_t i = 0; i < global_objspace->global_gc.n_objspaces; i++) {
|
||||
rb_objspace_t *objspace = global_objspace->global_gc.objspaces[i];
|
||||
/* No objspace can be mid-incremental-mark here: that only runs single-objspace
|
||||
* and vm_insert_ractor0 settles it on the transition. Clearing flags in step 5
|
||||
* under a live gray stack would break the owner's GC state machine. */
|
||||
/* The barrier can stop a Ractor between the steps of its own incremental mark, and
|
||||
* nothing else finishes another objspace's mark. Drop the partial mark rather
|
||||
* than clear its flags in step 5 under a live gray stack: the owner would resume
|
||||
* with a mark stack into a heap this GC has since re-marked and swept. Nothing is
|
||||
* lost, the unified mark below redoes the work. */
|
||||
if (is_incremental_marking(objspace)) {
|
||||
gc_abort_incremental_marking(objspace);
|
||||
}
|
||||
GC_ASSERT(!is_incremental_marking(objspace));
|
||||
GC_ASSERT(is_mark_stack_empty(&objspace->mark_stack));
|
||||
rb_gc_initialize_vm_context(&objspace->vm_context);
|
||||
@@ -9538,6 +9568,7 @@ gc_start_global(rb_objspace_t *driver, unsigned int reason, bool compact, bool a
|
||||
objspace->rgengc.uncollectible_wb_unprotected_objects = 0;
|
||||
objspace->rgengc.old_objects = 0;
|
||||
objspace->rgengc.last_major_gc = objspace->profile.count;
|
||||
objspace->rgengc.need_major_gc = GPR_FLAG_NONE;
|
||||
objspace->marked_slots = 0;
|
||||
for (int h = 0; h < HEAP_COUNT; h++) {
|
||||
rb_heap_t *heap = &heaps[h];
|
||||
@@ -9771,6 +9802,38 @@ absorb_finalizer_i(st_data_t key, st_data_t val, st_data_t data)
|
||||
return ST_CONTINUE;
|
||||
}
|
||||
|
||||
static void
|
||||
gc_make_mid_mark_objspace_absorbable(rb_objspace_t *src)
|
||||
{
|
||||
rb_objspace_t *objspace = src;
|
||||
for (int h = 0; h < HEAP_COUNT; h++) {
|
||||
struct heap_page *page = NULL;
|
||||
ccan_list_for_each(&heaps[h].pages, page, page_node) {
|
||||
short stride = page->slot_size;
|
||||
uintptr_t p = (uintptr_t)page->start;
|
||||
uintptr_t pend = p + page->total_slots * stride;
|
||||
|
||||
for (; p < pend; p += stride) {
|
||||
VALUE vp = (VALUE)p;
|
||||
asan_unpoisoning_object(vp) {
|
||||
switch (RB_BUILTIN_TYPE(vp)) {
|
||||
case T_NONE:
|
||||
case T_ZOMBIE:
|
||||
break;
|
||||
default:
|
||||
RVALUE_AGE_RESET(vp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
gc_bitmaps_clear(objspace, &heaps[h], false);
|
||||
}
|
||||
objspace->rgengc.old_objects = 0;
|
||||
objspace->rgengc.uncollectible_wb_unprotected_objects = 0;
|
||||
objspace->marked_slots = 0;
|
||||
}
|
||||
|
||||
/* Merge a dead Ractor's objspace into dst under the VM lock. src has no owner thread and
|
||||
* dst is the calling thread's own objspace (join/value) or main with everyone stopped
|
||||
* (global GC), so single-writer holds throughout. Pages move whole (their bits describe
|
||||
@@ -9788,16 +9851,21 @@ objspace_absorb(rb_objspace_t *dst, rb_objspace_t *src)
|
||||
|
||||
/* Settle dst first: adding pages under a walking lazy-sweep cursor, or into a
|
||||
* half-marked incremental heap, would sweep the merged pages with src's stale mark
|
||||
* bits and free live objects. (Normally settled already: vm_insert_ractor0's settle
|
||||
* means no objspace is incremental while a zombie waits to be absorbed.) */
|
||||
* bits and free live objects. */
|
||||
gc_rest(dst);
|
||||
|
||||
/* Settle src: no lazy sweep and no in-progress allocation page. */
|
||||
/* Settle src: no incremental marking, lazy sweep or in-progress allocation page. */
|
||||
{
|
||||
rb_objspace_t *objspace = src;
|
||||
during_gc = TRUE;
|
||||
gc_sweep_rest(objspace);
|
||||
during_gc = FALSE;
|
||||
/* A zombie parked by fork (rb_ractor_terminate_atfork retires the objspace
|
||||
* without the retire GC) can be mid-incremental-mark, and that mark cannot be
|
||||
* finished. Its owner's threads did not survive the fork, so mark_roots would
|
||||
* scan the absorbing Ractor's roots instead of src's. Drop the cycle. */
|
||||
if (is_incremental_marking(objspace)) {
|
||||
gc_abort_incremental_marking(objspace);
|
||||
gc_make_mid_mark_objspace_absorbable(objspace);
|
||||
}
|
||||
gc_rest(src); // if mid-sweep
|
||||
heap_alloc_state_clear(objspace);
|
||||
/* gc_sweep_finish leaves swept pages "pooled" for a coming incremental mark; src
|
||||
* never runs one (it is about to be merged), so return them to its free list now,
|
||||
@@ -9958,7 +10026,9 @@ objspace_absorb(rb_objspace_t *dst, rb_objspace_t *src)
|
||||
dst->shareable_objects += src->shareable_objects;
|
||||
|
||||
/* Merged pages carry src's mark/age state, so dst rebuilds its view at the next
|
||||
* collection. */
|
||||
* collection. Note that this can be worked around by calling `GC.config(rgengc_allow_full_mark: false)`,
|
||||
* so the absorbed heap should be in a state where a minor GC would also work correctly.
|
||||
*/
|
||||
dst->rgengc.need_major_gc |= GPR_FLAG_MAJOR_BY_FORCE;
|
||||
|
||||
/* src's outstanding malloc pressure moves with the xmalloc'd buffers. Later frees are
|
||||
@@ -10040,6 +10110,9 @@ rb_gc_impl_start(void *objspace_ptr, bool full_mark, bool immediate_mark, bool i
|
||||
* collector that reclaims shareable and cross-objspace garbage. It stops the world,
|
||||
* so auto_compact is honoured here too (mirroring full mark x autocompact locally). */
|
||||
if (!rb_gc_single_objspace_p() && (reason & GPR_FLAG_FULL_MARK)) {
|
||||
/* A mid-cycle driver is settled by gc_start_global itself: it aborts the partial
|
||||
* mark and finishes the lazy sweep, so the dead slots are T_NONE before the
|
||||
* unified conservative root scan. */
|
||||
gc_start_global(objspace, reason, compact || ruby_enable_autocompact, false);
|
||||
}
|
||||
else {
|
||||
|
||||
+3
-1
@@ -273,6 +273,9 @@ void rb_gc_mark_values(long n, const VALUE *values);
|
||||
void rb_gc_mark_vm_stack_values(long n, const VALUE *values);
|
||||
void rb_gc_update_values(long n, VALUE *values);
|
||||
void rb_gc_mark_set_no_pin(st_table *);
|
||||
/* Exercised by the bundled -test-/gc/writebarrier extension, so it must be visible
|
||||
* outside the ruby binary. */
|
||||
void rb_gc_writebarrier_remember(VALUE obj);
|
||||
void rb_gc_update_set_refs(st_table *);
|
||||
|
||||
#if USE_MODULAR_GC
|
||||
@@ -303,7 +306,6 @@ rb_obj_atomic_write(
|
||||
RBIMPL_CAST(rb_obj_atomic_write((VALUE)(old), (VALUE *)(slot), (VALUE)(young), __FILE__, __LINE__))
|
||||
|
||||
int rb_ec_stack_check(struct rb_execution_context_struct *ec);
|
||||
void rb_gc_writebarrier_remember(VALUE obj);
|
||||
void rb_gc_obj_became_shareable(VALUE obj);
|
||||
bool rb_gc_multi_objspace_p(void);
|
||||
bool rb_gc_obj_foreign_p(VALUE obj);
|
||||
|
||||
@@ -548,9 +548,9 @@ vm_insert_ractor0(rb_vm_t *vm, rb_ractor_t *r, bool single_ractor_mode)
|
||||
RUBY_DEBUG_LOG("r:%"PRI_SERIALT_PREFIX"u ractor.cnt:%u++", r->pub.id, vm->ractor.cnt);
|
||||
VM_ASSERT(single_ractor_mode || RB_VM_LOCKED_P());
|
||||
|
||||
/* Incremental marking only runs in a single-objspace world, and nothing later can
|
||||
* finish another objspace's partial mark, so end any cycle in progress before a
|
||||
* second Ractor becomes visible. */
|
||||
/* End main's cycle before a second Ractor becomes visible: the collection was
|
||||
* planned for a single-objspace world (a local GC is a whole-world GC there and may
|
||||
* free shareable objects), so it must not straddle the transition. */
|
||||
if (vm->ractor.cnt == 1) {
|
||||
rb_gc_rest();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
require 'timeout'
|
||||
|
||||
# Shared by the ractor-local incremental marking tests in test_ractor.rb, which run
|
||||
# inside assert_ractor subprocesses and load this through require_relative:.
|
||||
#
|
||||
# A few hundred rounds is typical; this bound means need_major_by stopped working. Each
|
||||
# round runs a minor GC, so keep the bound low enough that it trips (and reports what the
|
||||
# counters were doing) well before the test's own timeout fires.
|
||||
MAX_MAJOR_SETUP_ROUNDS = 2_000
|
||||
# Draining a freshly started major measures ~5_000 allocations.
|
||||
MAX_DRAIN_ALLOCATIONS = 2_000_000
|
||||
RACTOR_WAIT_TIMEOUT = 30
|
||||
|
||||
MARK_HEAP_ROUNDS = 5
|
||||
MARK_HEAP_OBJECTS_PER_ROUND = 20_000
|
||||
MARK_STEP_MIN_SLOTS = 100
|
||||
|
||||
# Leave this Ractor's objspace in the middle of an incremental major mark and return the
|
||||
# GC state, so the caller can assert it really got there.
|
||||
def start_local_incremental_major
|
||||
EnvUtil.without_gc do
|
||||
_retained = request_local_major_gc
|
||||
GC.start(full_mark: false, immediate_mark: false)
|
||||
GC.latest_gc_info(:state)
|
||||
end
|
||||
end
|
||||
|
||||
# Build a working set worth marking incrementally, for a caller that is about to call
|
||||
# start_local_incremental_major. The caller has to hold on to the return value, it is only
|
||||
# worth marking for as long as it stays reachable.
|
||||
def retain_for_incremental_mark(rounds: MARK_HEAP_ROUNDS, per_round: MARK_HEAP_OBJECTS_PER_ROUND)
|
||||
EnvUtil.without_gc do
|
||||
Array.new(rounds) do
|
||||
kept = []
|
||||
(2 * per_round).times { |i| object = Object.new; kept << object if i.even? }
|
||||
# Sweeping each round separately is what leaves the partially filled pages behind.
|
||||
# One sweep at the end would find the whole set laid out in one contiguous run.
|
||||
GC.start(full_mark: false)
|
||||
kept
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Allocate until this objspace asks for a major GC, leaving nothing in progress. Returns
|
||||
# the allocated objects so a caller can keep them alive across what it does next.
|
||||
def request_local_major_gc
|
||||
objects = []
|
||||
rounds = 0
|
||||
until GC.latest_gc_info(:need_major_by)
|
||||
if (rounds += 1) > MAX_MAJOR_SETUP_ROUNDS
|
||||
raise "no major GC requested after #{rounds} rounds " \
|
||||
"(old_objects=#{GC.stat(:old_objects)}/#{GC.stat(:old_objects_limit)}, " \
|
||||
"oldmalloc=#{GC.stat(:oldmalloc_increase_bytes)}/#{GC.stat(:oldmalloc_increase_bytes_limit)})"
|
||||
end
|
||||
objects.append(100.times.map { '*' })
|
||||
GC.start(full_mark: false)
|
||||
end
|
||||
objects
|
||||
end
|
||||
|
||||
# Drive the cycle in progress to completion with allocation. Returns the first state
|
||||
# observed after :marking, the final one, and how many incremental mark steps the drain
|
||||
# saw.
|
||||
def drain_incremental_cycle
|
||||
first_change = nil
|
||||
mark_steps = 0
|
||||
marked = GC.stat(:heap_marked_slots)
|
||||
allocations = 0
|
||||
loop do
|
||||
if (allocations += 1) > MAX_DRAIN_ALLOCATIONS
|
||||
raise "incremental cycle stuck in #{GC.latest_gc_info(:state)} after #{allocations} " \
|
||||
"allocations (free_slots=#{GC.stat(:heap_free_slots)}, " \
|
||||
"marked_slots=#{GC.stat(:heap_marked_slots)})"
|
||||
end
|
||||
Object.new
|
||||
state = GC.latest_gc_info(:state)
|
||||
marked, previously_marked = GC.stat(:heap_marked_slots), marked
|
||||
mark_steps += 1 if marked - previously_marked > MARK_STEP_MIN_SLOTS
|
||||
first_change ||= state unless state == :marking
|
||||
return [first_change, state, mark_steps] if state == :none
|
||||
end
|
||||
end
|
||||
|
||||
def finish_incremental_major
|
||||
_first_change, final, _mark_steps = drain_incremental_cycle
|
||||
final
|
||||
end
|
||||
|
||||
# Push the mark in progress through at least `steps` incremental steps without finishing
|
||||
# it, so a caller can hand off a mark that is provably in flight and has begun draining
|
||||
# its roots. Raises rather than returning if the cycle ends first: a caller asking for
|
||||
# this wanted a mid-mark state, and silently getting a finished one would leave whatever
|
||||
# it does next testing nothing.
|
||||
def advance_incremental_mark(steps: 2)
|
||||
marked = GC.stat(:heap_marked_slots)
|
||||
seen = 0
|
||||
allocations = 0
|
||||
while seen < steps
|
||||
if (allocations += 1) > MAX_DRAIN_ALLOCATIONS
|
||||
raise "only #{seen} of #{steps} mark steps after #{allocations} allocations " \
|
||||
"(marked_slots=#{GC.stat(:heap_marked_slots)})"
|
||||
end
|
||||
Object.new
|
||||
state = GC.latest_gc_info(:state)
|
||||
unless state == :marking
|
||||
raise "cycle reached #{state} after #{seen} of #{steps} mark steps: the working " \
|
||||
"set was too small to keep the mark going"
|
||||
end
|
||||
marked, previously_marked = GC.stat(:heap_marked_slots), marked
|
||||
seen += 1 if marked - previously_marked > MARK_STEP_MIN_SLOTS
|
||||
end
|
||||
GC.latest_gc_info(:state)
|
||||
end
|
||||
|
||||
# Hold off collecting this Ractor's objspace for the duration of the block without
|
||||
# ending the cycle in progress (which is what GC.disable would do). Incremental marking
|
||||
# still advances on page exhaustion, what this rules out is a collection started behind
|
||||
# the caller's back by the allocation or malloc accounting inside the block.
|
||||
def without_local_gc
|
||||
already_disabled = Bug::GC.local_disable_no_rest
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
Bug::GC.local_enable unless already_disabled
|
||||
end
|
||||
end
|
||||
|
||||
# Non-blocking "did the peer die while we waited" check. Ractor.select re-raises the
|
||||
# peer's exception as Ractor::RemoteError, which is the answer we want to report.
|
||||
def peer_death(peer)
|
||||
result = Ractor.select(peer, timeout: 0.01)
|
||||
result && "terminated early with #{result.last.inspect}"
|
||||
rescue Ractor::RemoteError, Ractor::Error => e
|
||||
"terminated early: #{(e.cause || e).message}"
|
||||
end
|
||||
|
||||
# Bounded Ractor.receive. None of these tests send nil, so a nil return is the timeout.
|
||||
# Waiting happens in slices with a liveness check between them: a peer that died gets
|
||||
# reported in well under a second, while a peer that is merely slow (these handshakes
|
||||
# wait on a full incremental-major setup) still gets the whole timeout.
|
||||
def receive_from(peer = nil, timeout: RACTOR_WAIT_TIMEOUT)
|
||||
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
||||
slice = peer ? 0.25 : timeout
|
||||
loop do
|
||||
message = Ractor.receive(timeout: slice)
|
||||
return message unless message.nil?
|
||||
if peer and (death = peer_death(peer))
|
||||
raise "#{peer.inspect} #{death}"
|
||||
end
|
||||
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
||||
end
|
||||
raise "no message received within #{timeout}s"
|
||||
end
|
||||
|
||||
# Bounded Ractor#value. Ractor.select reports termination and re-raises the peer's
|
||||
# exception as Ractor::RemoteError, so a dead peer explains itself instead of hanging.
|
||||
def value_of(ractor, timeout: RACTOR_WAIT_TIMEOUT)
|
||||
result = Ractor.select(ractor, timeout: timeout)
|
||||
raise "#{ractor.inspect} did not terminate within #{timeout}s" if result.nil?
|
||||
result.last
|
||||
end
|
||||
|
||||
# Bound an operation whose own blocking behaviour is under test
|
||||
def with_wait_bound(what, timeout: RACTOR_WAIT_TIMEOUT)
|
||||
Timeout.timeout(timeout, nil, "#{what} did not finish within #{timeout}s") { yield }
|
||||
end
|
||||
|
||||
def wait_until_single_ractor(timeout: RACTOR_WAIT_TIMEOUT)
|
||||
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
||||
while Ractor.count != 1
|
||||
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
||||
raise "#{Ractor.count} Ractors still alive after #{timeout}s"
|
||||
end
|
||||
Thread.pass
|
||||
end
|
||||
end
|
||||
|
||||
# fork, and leave the child through exit! on every path. The child inherits
|
||||
# assert_separately's at_exit, which writes a result token into the pipe the parent test
|
||||
# is parsing, so an exception escaping the block would post a second <error> block and
|
||||
# garble the report. Put the failure on stderr instead, which assert_separately already
|
||||
# treats as a failure, and leave without running at_exit.
|
||||
def fork_child
|
||||
fork do
|
||||
begin
|
||||
yield
|
||||
exit!(0)
|
||||
rescue SystemExit => e
|
||||
exit!(e.status)
|
||||
rescue Exception => e
|
||||
STDERR.puts(e.full_message(highlight: false))
|
||||
exit!(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Bounded Process.wait2
|
||||
def wait_for_pid(pid, timeout: RACTOR_WAIT_TIMEOUT)
|
||||
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
||||
loop do
|
||||
if (waited = Process.waitpid2(pid, Process::WNOHANG))
|
||||
return waited.last
|
||||
end
|
||||
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
||||
Process.kill(:KILL, pid)
|
||||
Process.waitpid(pid)
|
||||
raise "forked child did not exit within #{timeout}s"
|
||||
end
|
||||
sleep 0.05
|
||||
end
|
||||
end
|
||||
@@ -1238,4 +1238,305 @@ class TestRactor < Test::Unit::TestCase
|
||||
assert_equal true, Ractor.new { own = Object.new; own.singleton_class.attached_object.equal?(own) }.value
|
||||
RUBY
|
||||
end
|
||||
|
||||
# The ractor-local GC tests below run through assert_separately rather than
|
||||
# assert_ractor: -W0 silences the experimental-Ractor warning on its own, so the
|
||||
# child does not need assert_ractor's warm-up Ractor, whose objspace would linger
|
||||
# as a zombie and perturb what these tests measure.
|
||||
RACTOR_GC_HELPER = File.expand_path('ractor_gc_helper.rb', __dir__)
|
||||
|
||||
def test_incremental_marking_in_main_and_non_main_ractors
|
||||
omit 'incremental marking gate lives in the default GC' unless GC.config[:implementation] == 'default'
|
||||
assert_separately(%W[-W0 -r-test-/gc/disable -r#{RACTOR_GC_HELPER}], <<~'RUBY', timeout: 120)
|
||||
_blocker = Ractor.new { Ractor.receive }
|
||||
|
||||
assert_equal :marking, start_local_incremental_major
|
||||
|
||||
# Creating a Ractor that is not the first one must leave the creator's mark running:
|
||||
# vm_insert_ractor0 rests only on the 1 -> 2 transition, which _blocker consumed.
|
||||
# (test_first_ractor_new_settles_creator_mid_mark covers the other side.) The local
|
||||
# disable rules out a collection started behind the assertion's back by the malloc
|
||||
# accounting of Ractor creation; unlike GC.disable it leaves the cycle running.
|
||||
slots_before = GC.stat(:total_allocated_objects)
|
||||
state_after_new = without_local_gc do
|
||||
Ractor.new { Ractor.receive }
|
||||
GC.latest_gc_info(:state)
|
||||
end
|
||||
creator_slots = GC.stat(:total_allocated_objects) - slots_before
|
||||
assert_equal :marking, state_after_new
|
||||
# Incremental marking still advances on page exhaustion inside that window, so the
|
||||
# assertion also needs Ractor creation to cost its creator far less than draining a
|
||||
# mark (~5_000 slots; measured 13). Fail on the premise if that ever changes.
|
||||
assert_operator creator_slots, :<, 500
|
||||
|
||||
GC.start
|
||||
assert_equal :none, GC.latest_gc_info(:state)
|
||||
GC.verify_internal_consistency
|
||||
|
||||
entered, first_change, final, mark_steps, retained = value_of(Ractor.new do
|
||||
working_set = retain_for_incremental_mark
|
||||
child_entered = start_local_incremental_major
|
||||
child_first_change, child_final, child_mark_steps = drain_incremental_cycle
|
||||
[child_entered, child_first_change, child_final, child_mark_steps, working_set.sum(&:size)]
|
||||
end)
|
||||
assert_equal :marking, entered
|
||||
assert_includes [:sweeping, :none], first_change
|
||||
assert_equal :none, final
|
||||
# The states above are reached whether or not the mark was incremental: a fresh Ractor
|
||||
# objspace holds so little that its major marks everything in one step. The working set
|
||||
# is what gives the mark enough to do to be worth stepping, so assert it really was
|
||||
# stepped, and that the Ractor still held the whole set while that happened.
|
||||
assert_equal MARK_HEAP_ROUNDS * MARK_HEAP_OBJECTS_PER_ROUND, retained
|
||||
assert_operator mark_steps, :>=, 2, 'non-main Ractor major mark was not incremental'
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_global_gc_supersedes_ractor_local_incremental_mark
|
||||
omit 'incremental marking gate lives in the default GC' unless GC.config[:implementation] == 'default'
|
||||
assert_separately(%W[-W0 -r#{RACTOR_GC_HELPER}], <<~'RUBY', timeout: 120)
|
||||
child = Ractor.new do
|
||||
# Two WeakMaps, one of which goes unreachable, so the aborted mark has weak
|
||||
# references to drop: marking a WeakMap queues it into objspace->weak_references,
|
||||
# and that is the darray gc_abort_incremental_marking clears. Ruby cannot say
|
||||
# whether the mark reached either map before the global GC cut it short, so this
|
||||
# is best-effort coverage; the assertion below only demands that the map still
|
||||
# reachable afterwards resolves its key.
|
||||
weak_map = ObjectSpace::WeakMap.new
|
||||
key = Object.new
|
||||
weak_map[key] = :live
|
||||
doomed = ObjectSpace::WeakMap.new
|
||||
doomed[key] = 1
|
||||
|
||||
# A working set large enough that the mark has real work left, and a couple of
|
||||
# steps spent on it, so what the global GC below interrupts is provably a mark in
|
||||
# flight rather than one that already finished on its own.
|
||||
working_set = retain_for_incremental_mark
|
||||
start_local_incremental_major
|
||||
Ractor.main << advance_incremental_mark
|
||||
doomed = nil
|
||||
receive_from
|
||||
after_global = GC.latest_gc_info(:state)
|
||||
alive = weak_map[key]
|
||||
GC.verify_internal_consistency
|
||||
GC.start(full_mark: false)
|
||||
again = start_local_incremental_major
|
||||
[after_global, alive, again, working_set.sum(&:size)]
|
||||
end
|
||||
|
||||
assert_equal :marking, receive_from(child)
|
||||
GC.start
|
||||
child << :continue
|
||||
|
||||
after_global, alive, again, retained = value_of(child)
|
||||
assert_equal :none, after_global
|
||||
assert_equal :live, alive
|
||||
assert_equal :marking, again
|
||||
assert_equal MARK_HEAP_ROUNDS * MARK_HEAP_OBJECTS_PER_ROUND, retained
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_global_gc_clears_pending_major_request
|
||||
omit 'incremental marking gate lives in the default GC' unless GC.config[:implementation] == 'default'
|
||||
assert_separately(%W[-W0 -r#{RACTOR_GC_HELPER}], <<~'RUBY', timeout: 120)
|
||||
_blocker = Ractor.new { Ractor.receive }
|
||||
|
||||
EnvUtil.without_gc { request_local_major_gc }
|
||||
assert_not_nil GC.latest_gc_info(:need_major_by)
|
||||
|
||||
# gc_start_global clears every objspace's need_major_gc in its step 5, and nothing
|
||||
# in this test can put a flag back: the only writer of GPR_FLAG_MAJOR_BY_FORCE is
|
||||
# objspace_absorb, which needs a join or an orphaned zombie objspace to merge
|
||||
# (_blocker never terminates and no other Ractor is created), and the sweep's own
|
||||
# GPR_FLAG_MAJOR_BY_NOFREE is ruled out by the last_major_gc step 5 just set.
|
||||
GC.start
|
||||
assert_nil GC.latest_gc_info(:need_major_by)
|
||||
assert_equal :none, GC.latest_gc_info(:state)
|
||||
GC.verify_internal_consistency
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_fork_absorbs_mid_mark_zombie
|
||||
omit 'incremental marking gate lives in the default GC' unless GC.config[:implementation] == 'default'
|
||||
omit 'fork is not supported' unless Process.respond_to?(:fork)
|
||||
assert_separately(%W[-W0 -r#{RACTOR_GC_HELPER}], <<~'RUBY', timeout: 120)
|
||||
r = Ractor.new do
|
||||
Ractor.main << start_local_incremental_major
|
||||
Ractor.receive
|
||||
end
|
||||
assert_equal :marking, receive_from(r)
|
||||
|
||||
pid = fork_child do
|
||||
# Only main survives the fork, so rb_ractor_terminate_atfork has already retired
|
||||
# r and set its legacy value to nil (ractor_sync_terminate_atfork). The join
|
||||
# must reach the absorb at once rather than wait for a thread that is not there;
|
||||
# checking the value pins that down, since an error raised before
|
||||
# rb_gc_objspace_absorb_into_current would leave the merge below untested.
|
||||
value = with_wait_bound('value of a Ractor that did not survive the fork') { r.value }
|
||||
raise "Ractor#value after fork returned #{value.inspect}, not nil" unless value.nil?
|
||||
GC.verify_internal_consistency
|
||||
# r's pages were merged carrying the mark bits of a mark that never finished, so
|
||||
# drive the merged heap with minors only: a major would re-derive those bits and
|
||||
# hide a stale one. gc_start_body honours the config over the absorb's own
|
||||
# need_major_gc |= GPR_FLAG_MAJOR_BY_FORCE, which is what makes that possible.
|
||||
GC.config(rgengc_allow_full_mark: false)
|
||||
counts = [GC.stat(:count), GC.stat(:major_gc_count)]
|
||||
400_000.times { Object.new }
|
||||
raise "no collection ran after absorb" unless GC.stat(:count) > counts[0]
|
||||
raise "a major collection ran with full marks disabled" unless GC.stat(:major_gc_count) == counts[1]
|
||||
GC.verify_internal_consistency
|
||||
end
|
||||
assert_predicate wait_for_pid(pid), :success?
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_first_ractor_new_settles_creator_mid_mark
|
||||
omit 'incremental marking gate lives in the default GC' unless GC.config[:implementation] == 'default'
|
||||
assert_separately(%W[-W0 -r#{RACTOR_GC_HELPER}], <<~'RUBY', timeout: 120)
|
||||
wait_until_single_ractor
|
||||
|
||||
assert_equal :marking, start_local_incremental_major
|
||||
Ractor.new { Ractor.receive }
|
||||
assert_equal :none, GC.latest_gc_info(:state)
|
||||
assert_equal :marking, start_local_incremental_major
|
||||
GC.verify_internal_consistency
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_ractor_join_absorbs_zombie_while_joiner_mid_mark
|
||||
omit 'incremental marking gate lives in the default GC' unless GC.config[:implementation] == 'default'
|
||||
assert_separately(%W[-W0 -r#{RACTOR_GC_HELPER}], <<~'RUBY', timeout: 120)
|
||||
zombie = Ractor.new { :done }
|
||||
|
||||
assert_equal :marking, start_local_incremental_major
|
||||
# The join is the operation under test (it absorbs the zombie's objspace into a
|
||||
# mid-mark joiner), so it is bounded in place rather than through Ractor.select,
|
||||
# which would reach the value through that same path.
|
||||
assert_equal :done, with_wait_bound('zombie join') { zombie.value }
|
||||
assert_equal :none, GC.latest_gc_info(:state)
|
||||
GC.verify_internal_consistency
|
||||
assert_equal :marking, start_local_incremental_major
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_incremental_major_with_zombie_objspace
|
||||
omit 'incremental marking gate lives in the default GC' unless GC.config[:implementation] == 'default'
|
||||
assert_separately(%W[-W0 -r#{RACTOR_GC_HELPER}], <<~'RUBY', timeout: 120)
|
||||
zombie = Ractor.new { 1000.times { Object.new } }
|
||||
with_wait_bound('zombie join') { zombie.join }
|
||||
wait_until_single_ractor
|
||||
|
||||
assert_equal :marking, start_local_incremental_major
|
||||
assert_equal :none, finish_incremental_major
|
||||
GC.verify_internal_consistency
|
||||
assert_equal :marking, start_local_incremental_major
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_concurrent_incremental_marks_with_shareable_exchange
|
||||
omit 'incremental marking gate lives in the default GC' unless GC.config[:implementation] == 'default'
|
||||
assert_separately(%W[-W0 -r#{RACTOR_GC_HELPER}], <<~'RUBY', timeout: 120)
|
||||
worker = Ractor.new do
|
||||
results = []
|
||||
3.times do
|
||||
entered = start_local_incremental_major
|
||||
Ractor.main << :ready
|
||||
inbound = receive_from
|
||||
Ractor.main << Ractor.make_shareable([inbound])
|
||||
count_before = GC.stat(:count)
|
||||
results << [entered, finish_incremental_major, GC.stat(:count) - count_before]
|
||||
end
|
||||
GC.verify_internal_consistency
|
||||
results
|
||||
end
|
||||
|
||||
main_results = []
|
||||
3.times do
|
||||
entered = start_local_incremental_major
|
||||
assert_equal :ready, receive_from(worker)
|
||||
worker << Ractor.make_shareable([:payload])
|
||||
receive_from(worker)
|
||||
count_before = GC.stat(:count)
|
||||
main_results << [entered, finish_incremental_major, GC.stat(:count) - count_before]
|
||||
end
|
||||
|
||||
(value_of(worker) + main_results).each do |entered, final, extra_cycles|
|
||||
assert_equal :marking, entered
|
||||
assert_equal :none, final
|
||||
# finish_incremental_major drives the cycle it was handed to :none and stops there.
|
||||
# A second cycle means the drain outran the handshake and the pair is no longer
|
||||
# exchanging shareables across two marks that overlap, which is the whole premise.
|
||||
assert_equal 0, extra_cycles, 'the drain started another cycle: the marks did not overlap'
|
||||
end
|
||||
GC.verify_internal_consistency
|
||||
RUBY
|
||||
end
|
||||
|
||||
def test_writebarrier_foreign_objects_during_incremental_mark
|
||||
omit 'incremental marking gate lives in the default GC' unless GC.config[:implementation] == 'default'
|
||||
assert_separately(%W[-W0 -r-test-/gc/writebarrier -r#{RACTOR_GC_HELPER}], <<~'RUBY', timeout: 120)
|
||||
box = Bug::GC::WriteBarrier::Box.new
|
||||
|
||||
Ractor.make_shareable(box)
|
||||
|
||||
worker = Ractor.new(box) do |box|
|
||||
results = []
|
||||
2.times do
|
||||
entered = start_local_incremental_major
|
||||
Ractor.main << :ready
|
||||
inbound = receive_from
|
||||
|
||||
retained = Array.new(200) { [] }
|
||||
200.times { |i| retained[i] << inbound }
|
||||
retained = nil
|
||||
|
||||
# Local parent holding a foreign shareable child: re-greying it mid-mark must
|
||||
# re-traverse it without touching the foreign child's GC state.
|
||||
holder = [inbound]
|
||||
Bug::GC::WriteBarrier.remember(holder)
|
||||
|
||||
mine = Object.new
|
||||
box.store(mine)
|
||||
mine_id = mine.object_id
|
||||
mine = nil
|
||||
# box is foreign to this Ractor, so this must be a no-op rather than a crash.
|
||||
Bug::GC::WriteBarrier.remember(box)
|
||||
|
||||
Ractor.main << Ractor.make_shareable([:reply, mine_id])
|
||||
results << [entered, finish_incremental_major, box.child.object_id == mine_id, holder[0]]
|
||||
end
|
||||
GC.verify_internal_consistency
|
||||
results
|
||||
end
|
||||
|
||||
main_retained = Array.new(200) { [] }
|
||||
2.times do
|
||||
entered = start_local_incremental_major
|
||||
assert_equal :ready, receive_from(worker)
|
||||
worker << Ractor.make_shareable([:payload])
|
||||
reply = receive_from(worker)
|
||||
200.times { |i| main_retained[i] << reply }
|
||||
# Local parents now hold a foreign shareable child; the barrier's multi-Ractor
|
||||
# bail-out leaves that edge to the shareable bits, so re-greying the parents
|
||||
# mid-mark must re-traverse them safely.
|
||||
Bug::GC::WriteBarrier.remember(main_retained)
|
||||
# reply is foreign to the main Ractor, so this must be a no-op.
|
||||
Bug::GC::WriteBarrier.remember(reply)
|
||||
assert_equal :marking, entered
|
||||
assert_equal :none, finish_incremental_major
|
||||
assert_equal :reply, reply.first
|
||||
end
|
||||
|
||||
value_of(worker).each do |entered, final, child_kept, held|
|
||||
assert_equal :marking, entered
|
||||
assert_equal :none, final
|
||||
# mine was stored into a shareable box mid-mark from the objspace that owns it.
|
||||
# The barrier bails out on that edge, so nothing greys mine; it survives the
|
||||
# worker's mark only because the store set a shref bit that pinned_roots_mark
|
||||
# picks up in gc_marks_finish.
|
||||
assert child_kept, 'an unshareable stored into a shareable mid-mark was collected'
|
||||
assert_equal [:payload], held
|
||||
end
|
||||
GC.verify_internal_consistency
|
||||
RUBY
|
||||
end
|
||||
end
|
||||
Generated
+1
-1
@@ -2314,8 +2314,8 @@ unsafe extern "C" {
|
||||
buff_size: usize,
|
||||
obj: VALUE,
|
||||
) -> *const ::std::os::raw::c_char;
|
||||
pub fn rb_ec_stack_check(ec: *mut rb_execution_context_struct) -> ::std::os::raw::c_int;
|
||||
pub fn rb_gc_writebarrier_remember(obj: VALUE);
|
||||
pub fn rb_ec_stack_check(ec: *mut rb_execution_context_struct) -> ::std::os::raw::c_int;
|
||||
pub fn rb_id_table_lookup(
|
||||
tbl: *mut rb_id_table,
|
||||
id: ID,
|
||||
|
||||
Reference in new issue
Block a user