570 Commits
Author SHA1 Message Date
Peter Zhu 16cf9129cc [ruby/mmtk] Register pinned root objects in PinningRegistry
We need to register objects pinned from the root in PinningRegistry for
rb_gc_impl_pinned_p to work properly.

https://github.com/ruby/mmtk/commit/6e8ad61924
2026-10-28 03:46:43 +00:00
Luke Gruber 29264f878a Fix rb_gc_ever_multi_ractor_p to simply read the latch variable
In single-ractor mode, we don't want foreign_object_p checks to actually
run (there can be no foreign objspaces). This foreign_object_p check return
false if there was never a (non-main) Ractor created. Previously,
rb_gc_ever_multi_ractor_p() was needlessly complex. This change should
speed up marking and WB implementations in single-ractor mode.
2026-10-26 14:29:57 -04:00
Matt Valentine-House e648c81dce Clamp oldmalloc_increase at zero instead of snapshotting it
Since e631daf17b, gc_sweep_finish calls
gc_malloc_counters_snapshot_free_at_last_gc on both oldcounters and
counters.

That is correct for counters, because gc_reset_malloc_info takes the
baselines at every GC. However for oldcounters, baselines are only taken
at a major GC, so every minor sweep forgot the frees it had just made
but keeps the allocation and oldmalloc_increase grows with allocation
volume.

This means a program churning short-lived malloc'd buffers hit the
oldmalloc limit every few minors.

This commit goes back to having a single oldmalloc_increase counter,
clamped at 0, reset by a major and checked at the end of a minor sweep.

I used the following script to measure the impact of this change.

```ruby
def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)

GC.start
before = GC.stat
start = now

2_000_000.times { "x" * 4096 }

after = GC.stat
elapsed = now - start

majors = after[:major_gc_count] - before[:major_gc_count]
minors = after[:minor_gc_count] - before[:minor_gc_count]

puts [
  format("%-60s", RUBY_DESCRIPTION[0, 60]),
  "majors=#{majors}",
  "minors=#{minors}",
  "oldmalloc_inc=#{after[:oldmalloc_increase_bytes]}",
  "limit=#{after[:oldmalloc_increase_bytes_limit]}",
  format("%.2fs", elapsed),
].join(" ")
```

And observed the following results:

4.0.7:
    majors=0 minors=614 oldmalloc_inc=3343152 limit=16777216 0.46s

Previous master:
    majors=53 minors=191 oldmalloc_inc=63220807 limit=134217728 0.65s

This commit:
    majors=1 minors=243 oldmalloc_inc=20829148 limit=16777216 0.64s
2026-10-23 18:51:09 +01:00
Matt Valentine-House 132d22571b Add RUBY_ATOMIC_SIZE_LOAD
The size_t atomics had add, sub, cas, exchange and fetch_add but no
load, so the GC read its atomic size_t counters through the VALUE load
with a cast.

This commit adds RUBY_ATOMIC_SIZE_LOAD and rbimpl_atomic_size_load,
following rbimpl_atomic_load, and uses it for the deferred free count.
2026-10-23 18:51:09 +01:00
Matt Valentine-House cc3776ba11 GC: Move zombie page accounting into the default GC
The zombie_objspaces table in gc.c kept a page count for each entry and
a running total. gc.c asked the GC for the counts through
rb_gc_impl_zombie_heap_page_count, and the default GC read the total
back through rb_gc_vm_zombie_total_pages and
rb_gc_vm_refresh_zombie_pages. Only the default GC uses the numbers, so
they crossed the GC API twice for nothing.

In this commit we replace both page count functions with one hook,
rb_gc_impl_objspace_zombify, which gc.c calls when it parks an objspace
in the zombie table. The default GC stores the floored page count in the
objspace and keeps the total in the global objspace. objspace_absorb
subtracts it again, and each global cycle re-measures the zombies in its
snapshot. A nonzero zombie_pages also marks an objspace as a zombie, so
the verifier no longer needs rb_gc_vm_zombie_objspace_p.

The writers keep the same locks as before: the VM lock for zombify and
absorb, the barrier for the refresh. With RGENGC_CHECK_MODE we assert at
each global snapshot that zombie_total_pages equals the sum of
zombie_pages over the enumerated objspaces.
2026-10-23 18:03:37 +01:00
Matt Valentine-House 01fe91202c 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.
2026-10-23 17:05:54 +01:00
Luke Gruber 7a39a7a957 Fix retire GC for Ractors holding on to empty pages
There was a condition that only a heap with > 1 page can move an empty
page from free_pages to empty_pages. This was even true during a retire
GC (Ractor#join), so running a retire GC couldn't release these actually
empty pages back to the page pool. This meant that when a terminated Ractor
got absorbed, 768KB (12 heap pages) came with it, even if those pages were
empty (this assumes the Ractor had a page for every heap).

Since Ractors can now have 0 pages and the main global GC trigger is
zombie pages (and not retired Ractors), we need to count retired Ractors
with 0 pages towards this zombie pages limit. For now, the floor is
HEAP_COUNT (12 pages), but this can be changed if it's too high.
2026-10-22 17:28:30 -04:00
Yusuke EndohandClaude Opus 5.5 6b60cb45f9 Unpoison zombies before clearing FL_FINALIZE at shutdown
A Ractor's zombies with finalizers are poisoned and moved to the main
objspace when the Ractor terminates.  If the process exits before they
are finalized, rb_gc_impl_shutdown_call_finalizer_i touches them and
ASAN reports use-after-poison.

```
8.times.map do
  Ractor.new do
    10.times do
      4.times { ObjectSpace.define_finalizer(Object.new, proc { sleep 0.0005 }) }
      GC.start
      sleep 0.0001
    end
  end
end.each(&:join)
#=> ERROR: AddressSanitizer: use-after-poison (sometimes)
```

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 15:59:41 +09:00
Matt Valentine-House 37561263fc ZJIT: Make the write barrier marking flag per-objspace
`zjit_during_incremental_marking` was one process-wide global, written
by `gc_set_incremental_marking` on every transition of any objspace.
Since 24be314b45, two objspaces can run incremental marks at the same
time, so when one finished its mark it wrote zero while another was
still marking.

`gen_write_barrier` skips `rb_gc_writebarrier` when the receiver is young,
not shareable, and the word reads zero, so JIT code in the still-marking
objspace skipped the barrier.

A white object stored into a young black receiver was never marked, the
sweep freed it, and the next major crashed with

```
[BUG] try to mark T_NONE object (parent: T_ARRAY/Array).
```

This bug can be reproduced in a few seconds with

```
RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR=0.5 ./ruby --zjit test.rb, where test.rb is:
```

```
Ractor.new {
  loop {
    GC.start(full_mark: true, immediate_mark: false, global: false)
    GC.start(global: false)
  }
}

loop {
  GC.start(full_mark: true, immediate_mark: false, global: false)
  Array.new(2_000) { [nil] }.each { |b|
    200.times { Object.new }
    sleep 0.0002
    b[0] = Object.new
  }
}
```

Limiting the oldobject limit factor ensures we keep running majors which
makes this bug easier to trigger, and the array is pre-sized because the
inlined ArrayAset side-exits on an empty array.

This commit moves zjit_incremental_marking into rb_objspace_t and makes
it a bool, so each Ractor can signal to zjit when it's incrementally
marking. Because this is now directly addressable per objspace we've
also removed the incremental marking flag.
2026-10-19 13:46:08 +01:00
Peter Zhu 18ec10c7f8 [ruby/mmtk] Increase LLVM inline-threshold to 5000
Compared to the default of 225, this gives noticeable performance
improvements.

For the following benchmark:

    objs = []
    2_000_000.times { objs << Object.new }

We can see this branch is about 10% faster:

    Benchmark 1: branch
      Time (mean ± σ):     193.0 ms ±   3.3 ms    [User: 207.0 ms, System: 79.2 ms]
      Range (min … max):   189.1 ms … 203.1 ms    15 runs

    Benchmark 2: master
      Time (mean ± σ):     214.9 ms ±   4.9 ms    [User: 245.2 ms, System: 79.5 ms]
      Range (min … max):   206.4 ms … 220.4 ms    13 runs

    Summary
      branch ran
        1.11 ± 0.03 times faster than master

https://github.com/ruby/mmtk/commit/b736a1f92b
2026-10-19 11:29:48 +00:00
hawthbot 5538d4dfbf wbcheck: update for current GC API and fix startup
- Implement the gc_impl.h functions added since wbcheck was last updated
  (single-objspace stubs, as in mmtk)
- Create the object table in objspace_init, since st_init_numtable needs
  an objspace to allocate from
- Report during_global_gc_p outside the mutator phase: snapshots and GCs
  run behind rb_gc_vm_barrier()
- Drop the internal header includes (only RCLASS_IS_ROOT was used), so
  wbcheck builds like the other modular GCs without the generated id.h
2026-10-15 12:34:53 -07:00
Kunshan Wang d037ba85ef [ruby/mmtk] Update mmtk-core and add non_moving_root feature
A recent update in mmtk-core put non-moving roots behind a cargo feature
"non_moving_root".  CRuby has been using non-moving roots for
conservative stack scanning. We explicitly enable this feature in
Cargo.toml.

https://github.com/ruby/mmtk/commit/ee8c7d2323
2026-10-14 07:21:56 +00:00
Peter Zhu 47dddf7800 [ruby/mmtk] Directly call functions in C binding
This commit removes RubyUpcalls and instead directly calls functions in
the C binding from Rust. This also allows LTO to inline C calls from Rust.

https://github.com/ruby/mmtk/commit/0f5a049a2d
2026-10-13 11:27:03 +00:00
Peter Zhu 83fdd0da95 [ruby/mmtk] Build with LTO when able
https://github.com/ruby/mmtk/commit/2dbb8005aa
2026-10-13 04:14:24 +00:00
Peter Zhu 9e8bf7356b [ruby/mmtk] Resize objects to optimal size when moving
https://github.com/ruby/mmtk/commit/6d83b67d9a
2026-10-12 09:45:50 +00:00
Hiroshi SHIBATAandClaude Opus 5.5 4ef08fc130 Use rb_len_t for finalizer counts and tables in the GCs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:45:25 +09:00
Hiroshi SHIBATAandClaude Opus 5 1fe11aeb66 Use rb_len_t for lengths in gc.c and ractor.c
The courier's hash size is an entry count rather than a String or Array length, so it stays `long`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 14:00:15 +09:00
Hiroshi SHIBATAandClaude Opus 5 cc3ba11012 Use rb_len_t in the C API for lengths and indices
Once rb_len_t widens this changes the `long *` output parameters of rb_range_beg_len(), rb_str_subpos(), rb_arithmetic_sequence_beg_len_step(), ruby_enc_find_basename() and ruby_enc_find_extname(), so extensions passing `long *` to them will need to be updated. The extension guides list these signatures verbatim and follow along.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-09 14:00:15 +09:00
Max Bernstein e4672be48d ZJIT: Inline write barrier check in LIR (#18641)
For `WriteBarrier a, b`, inline the fast-path of the write barrier that covers the majority of cases and doesn't require any actual barrier:

* `a` isn't old
* `a` isn't shareable
* not incremental marking

Removes the vast majority of barrier C calls.

Lobsters before:

```
Top-20 calls to C functions from JIT code (69.2% of total 50,814,557):
                  rb_hash_aref: 5,770,278 (11.4%)
  rb_vm_opt_send_without_block: 5,428,040 (10.7%)
            rb_gc_writebarrier: 3,304,955 ( 6.5%)
...
```

After: completely gone from top-20. Total C calls drop from ~50.8MM to ~47.6MM.

On a microbenchmark such as `setivar_object` or `setivar_young`, it takes per-iteration time from ~41ms to ~36ms (roughly -12%).

```c
void
rb_gc_impl_writebarrier(void *objspace_ptr, VALUE a, VALUE b)
{
    rb_objspace_t *objspace = objspace_ptr;

#if RGENGC_CHECK_MODE
    if (SPECIAL_CONST_P(a)) rb_bug("rb_gc_writebarrier: a is special const: %"PRIxVALUE, a);
    if (SPECIAL_CONST_P(b)) rb_bug("rb_gc_writebarrier: b is special const: %"PRIxVALUE, b);
#else
    RBIMPL_ASSERT_OR_ASSUME(!SPECIAL_CONST_P(a));
    RBIMPL_ASSERT_OR_ASSUME(!SPECIAL_CONST_P(b));
#endif

    GC_ASSERT(!during_gc);
    GC_ASSERT(RB_BUILTIN_TYPE(a) != T_NONE);
    GC_ASSERT(RB_BUILTIN_TYPE(a) != T_MOVED);
    GC_ASSERT(RB_BUILTIN_TYPE(a) != T_ZOMBIE);

    /* A shareable object now references an unshareable one: record b as a shref so its
     * owner's local GC roots it (the parent may live in another objspace, untraversed
     * there).  Only b's owner stores this, on its own page: a plain store suffices. */
    if (RB_UNLIKELY(RB_FL_TEST_RAW(a, RUBY_FL_SHAREABLE)) &&
            !RB_FL_TEST_RAW(b, RUBY_FL_SHAREABLE)) {
        struct heap_page *bpage = GET_HEAP_PAGE(b);
        if (!_MARKED_IN_BITMAP(bpage->shref_bits, bpage, b)) {
            _MARK_IN_BITMAP(bpage->shref_bits, bpage, b);
            bpage->flags.has_shref_objects = TRUE;
        }
    }

  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() &&
                (GET_HEAP_OBJSPACE(a) != objspace || GET_HEAP_OBJSPACE(b) != objspace)) ||
                !RVALUE_OLD_P(objspace, a) || RVALUE_OLD_P(objspace, b)) {
            // do nothing
        }
        else {
            gc_writebarrier_generational(a, b, objspace);
        }
    }
    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);
        }
        else {
            goto retry;
        }
    }
    return;
}
```
2026-10-08 11:13:52 -04:00
Hiroshi SHIBATAandClaude Opus 5.5 957caeef23 Drop support for Darwin on PowerPC
The last Mac OS X release for PowerPC Macs is 10.5, and nothing else runs this code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:04:09 +09:00
Luke Gruber f0439cdfa7 Remove VM lock from Ractor.make_shareable()
Ractor.make_shareable could previously take the VM lock and then call
rb_funcall(id_freeze), which is not correct to do. Create a new
rb_objspace_reachable_objects_from_local() function that works the same as the
non-local version except doesn't lock. The callback given to it must skip foreign
objects (classes/modules, etc.) as running mark_children could traverse data
structures that could be mutated out from under it.

The reachable_objects_from_local callback must skip foreign objects altogether
instead of checking their shareability because foreign objects that aren't
shareable could be collected or have their page unmapped due to RLGC. Of course,
foreign edges should be shareable anyway. This was a bug that existed in the
locking version as well. Unfortunately, that means we can't check that T_DATA
objects don't refer to unshareables across object spaces while making them
shareable.

The alternative is to check and risk a crash, and raise if we see these illegal
edges.
2026-10-06 18:31:56 -04:00
Luke Gruber df0fa43f53 Add marking context to speed up marking in multi-Ractor
This makes a bigger difference on arm64, where GET_EC() is not inlined.
Calling this when marking every reference is expensive. It also makes a
bigger difference with Ractors because in single-ractor, the main ractor
can be retrieved through a global variable instead of through the EC.

The context now holds the objspace, derived from the Ractor. The context is
passed to other internal marking functions as an argument so they don't need
to rederive it.

Custom ruby-bench GC benchmark: gc_balloon.
Arm64 MacOS, ruby_ctl = without this commit, ruby_exp = with it:

| ractors |     ctl (ms) |     exp (ms) | exp 1st itr | ctl/exp |
| ------: | -----------: | -----------: | ----------: | ------: |
|       0 | 332.3 ± 0.3% | 328.5 ± 0.3% |       1.011 |   1.011 |
|       1 | 373.4 ± 1.0% | 336.7 ± 2.0% |       1.101 |   1.109 |
|       2 | 375.4 ± 1.0% | 341.3 ± 0.3% |       1.115 |   1.100 |
|       4 | 385.5 ± 1.3% | 344.9 ± 0.5% |       1.134 |   1.118 |
|       6 | 386.4 ± 0.6% | 362.0 ± 1.0% |       1.073 |   1.067 |
|       8 | 461.8 ± 1.1% | 421.9 ± 1.0% |       1.091 |   1.095 |

The difference between the two can be accounted for in the marking time (not shown).

Linux x86, same gc_balloon benchmark:

| ractors |     ctl (ms) |     exp (ms) | exp 1st itr | ctl/exp |
| ------: | -----------: | -----------: | ----------: | ------: |
|       0 | 443.4 ± 7.7% | 435.6 ± 6.7% |       1.045 |   1.018 |
|       1 | 489.0 ± 2.8% | 457.0 ± 1.9% |       1.093 |   1.070 |
|       2 | 509.5 ± 0.9% | 470.2 ± 0.4% |       1.073 |   1.084 |
|       4 | 528.5 ± 1.4% | 496.5 ± 0.6% |       1.093 |   1.065 |
|       6 | 547.3 ± 1.8% | 509.0 ± 0.8% |       1.049 |   1.075 |
|       8 | 704.9 ± 7.1% | 645.1 ± 7.1% |       1.100 |   1.093 |

Single-ractor marking time isn't affected as much.

Linux x86, gcbench benchmark:

|   bench |     ctl (ms) |     exp (ms) | exp 1st itr | ctl/exp |
| ------: | -----------: | -----------: | ----------: | ------: |
| gcbench | 264.5 ± 0.8% | 262.7 ± 0.9% |       0.988 |   1.007 |
2026-10-05 15:32:58 -04:00
Matt Valentine-House ee4013b90f Count global GC cycles per Ractor in GC.stat
GC.stat(:global_gc_count) now reports the stop-the-world global cycles
the current Ractor started, instead of mirroring the process-wide count,
so the values can be summed across Ractors. GC.stat(:global_gc_count,
scope: :global) gives the process total, including Ractors that have
since exited.

A global cycle no longer counts as a major GC on the Ractor that started
it, so count == minor_gc_count + major_gc_count + global_gc_count.
2026-10-05 19:08:14 +01:00
Peter Zhu c68a89facf [ruby/mmtk] Make StickyImmix default GC plan
https://github.com/ruby/mmtk/commit/537eae4e0a
2026-10-05 11:20:16 +00:00
Peter Zhu 9b8b0ae443 [ruby/mmtk] Implement mmtk_is_pinned
https://github.com/ruby/mmtk/commit/dedb6123d6
2026-10-03 08:20:45 +00:00
Peter Zhu 2192dca200 [ruby/mmtk] Fix rustsrcs in extconf.rb
https://github.com/ruby/mmtk/commit/7e9aadd29e
2026-10-02 07:11:53 +00:00
Luke Gruber a320eeff00 Remove redundant check in rgengc_check_relation
WB-unprotected objects are always young. This invariant is maintained throughout the code base.
When WB-unprotecting an object, the object gets de-aged.
2026-10-01 17:59:23 -04:00
Matt Valentine-House e131720215 Use pthread_sigmask in the GC read barrier signal handler
sigprocmask() is unspecified in multithreaded processes; on macOS it
left SIGSEGV/SIGBUS blocked on the faulting thread, so a second
read-barrier fault was never delivered and the process hung on the
faulting instruction.  [Bug #22389]
2026-09-30 12:47:52 +01:00
Matt Valentine-House 6141c568b6 Unprotect all objspaces before a global compaction updates references
Match the per-objspace ordering of gc_compact_finish (unprotect, then
update) across objspaces, so the update pass never faults on a foreign
protected page and abandons that page's moves.
2026-09-30 12:47:52 +01:00
Kunshan Wang 64a4a372d7 [ruby/mmtk] Update mmtk-core and remove LOS workaround
There was a bug in mmtk-core that caused `is_live()` and
`is_reachable()` to return wrong results for objects in the large object
space.  The function `object_survives_current_gc` was introduced to
workaround the bug.

The bug has been fixed in mmtk-core.  We bump the mmtk-core dependency
revision, and remove the workaround.  We now simply use
`object.is_reachable()` to test if an object will survive the current
GC.

See: https://github.com/mmtk/mmtk-core/pull/1594

https://github.com/ruby/mmtk/commit/8676718e7d
2026-09-29 07:26:07 +00:00
Peter Zhu 6eab8427a8 Force postmortem GC to always run
Currently, if GC is disabled, postmortem GC is not ran when a Ractor dies.
This can cause the object space to have issues because we need to run the
postmortem GC. This commit will guarantee that it runs.
2026-09-26 22:02:26 +09:00
Hiroshi SHIBATAandClaude Opus 5.5 047cd2fd27 Remove the _aligned_malloc prototype for old Visual C++
malloc.h, which win32.h includes, declares it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 12:42:26 +09:00
Matt Valentine-House bca1314ef7 GC: verify consistency of Ractor local registered addresses.
Now that registrations are Ractor-scoped, a store of another Ractor's
unshareable object through a registered address is a use-after-free
waiting for the owner's next GC.

This commit introduces a method to catch it in debug and ASAN builds by
recording each VALUE at registration time so that when
gc_verify_internal_consistency walks the registration list it can flag
live, non-shareable values that are owned by a Ractor other than the
registrant.
2026-09-25 19:24:10 +01:00
Matt Valentine-House 2ad6b99a98 GC: scope rb_gc_register_address to the calling Ractor
The registered_globals list made every Ractor's GC scan every slot under
a shared lock. This commit splits it into per-Ractor lists.

We also maintain a global registry that references every Ractor that has
ever registered so that unregistering from a Ractor other than the one
that registered can look up the owning list.

When a Ractor is joined, the dead Ractor's slots are moved to the
joining Ractor's heap before the objspace merge happens.

This change tightens up the requirements for values stored in a
registered address.

Previously, Every Ractor's local GC scanned the shared list. So if a
slot held an unshareable object living in a different Ractor's heap,
that object was always kept alive regardless.

After this change, only the registering Ractor's GC scans its list. So if a
slot holds another Ractor's unshareable object, the owning Ractor's
local GC cannot see that reference, so it can collect the object while
the registered slot still points at it.

Storing such an object was already a Ractor containment violation. It
was just hidden by the shared scan.

[Feature #22277]
2026-09-25 19:24:10 +01:00
Matt Valentine-House 60c206b683 Add global scope to GC.stat 2026-09-25 12:18:01 +01:00
Matt Valentine-House cfced4edb2 Fix overlapping GC CPU timing during global compaction 2026-09-25 12:18:01 +01:00
Luke Gruber 24be314b45 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.
2026-09-24 13:54:45 -04:00
Peter Zhu 4a71c4936c [ruby/mmtk] Bump to Rust 2024 edition
https://github.com/ruby/mmtk/commit/2801d95aa4
2026-09-23 10:00:32 +00:00
Peter Zhu eb08b74558 [ruby/mmtk] Keep track of pending pages in RubyHeapTrigger
We need to keep track of pending pages in RubyHeapTrigger otherwise a large
allocation may cause infinite number of GCs to be ran since it will always
appear that we have enough space in the heap (and thus the heap won't grow)
but still not have enough for the allocation.

https://github.com/ruby/mmtk/commit/dd8034050d
2026-09-21 12:01:48 +00:00
Luke Gruber 9f6f03ecae Make sure VM lock is acquired during GC when single_objspace_p()
Otherwise we can get assertion failures like:

```
TestGc#test_finalizer_not_run_with_vm_lock [/tmp/_actions-runner-working-dir/ruby/ruby/src/test/ruby/test_gc.rb:1055]:
  pid 225354 killed by SIGABRT (signal 6)
  | ../src/vm_sync.c:40: Assertion Failed: RUBY_ASSERT_vm_locking_with_barrier:vm_locked(vm)
  |
  | -- Threading information ---------------------------------------------------
  | Total ractor count: 1
  | Ruby thread count for this ractor: 2
  |
  | -- C level backtrace information -------------------------------------------
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(rb_print_backtrace+0x10) [0x2aa3ad260f8] ../src/vm_dump.c:1113
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(rb_vm_bugreport) ../src/vm_dump.c:1479
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(rb_assert_failure_detail+0x13c) [0x2aa3aaba034] ../src/error.c:1258
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(rb_assert_failure+0x16) [0x2aa3aaba066] ../src/error.c:1234
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(rb_current_execution_context+0x0) [0x2aa3ad2728a] ../src/vm_sync.c:40
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(vm_lock_assertable_p) ../src/vm_sync.c:23
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(RUBY_ASSERT_vm_locking_with_barrier) ../src/vm_sync.c:38
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(RB_SPECIAL_CONST_P+0x0) [0x2aa3ac4dab6] ../src/string.c:622
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(RB_FL_ABLE) ../src/include/ruby/internal/fl_type.h:383
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(RB_FL_TEST) ../src/include/ruby/internal/fl_type.h:432
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(rb_gc_free_fstring) ../src/string.c:624
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(gc_obj_free+0x0) [0x2aa3aaf239a] ../src/gc/default/default.c:5139
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(gc_sweep_plane) ../src/gc/default/default.c:5140
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(gc_sweep_page) ../src/gc/default/default.c:5208
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(gc_sweep_step+0x194) [0x2aa3aaf37b4] ../src/gc/default/default.c:5628
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(gc_sweep+0xce) [0x2aa3aaf3c1e] ../src/gc/default/default.c:5943
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(gc_start_body+0x48a) [0x2aa3aaf945a] ../src/gc/default/default.c:8860
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(gc_start+0x12) [0x2aa3aaf9a72] ../src/gc/default/default.c:8879
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(heap_prepare) ../src/gc/default/default.c:3083
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(heap_next_free_page) ../src/gc/default/default.c:3300
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(newobj_refill) ../src/gc/default/default.c:3446
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(RB_SPECIAL_CONST_P+0x0) [0x2aa3aafff82] ../src/gc/default/default.c:3468
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(RB_BUILTIN_TYPE) ../src/include/ruby/internal/value_type.h:184
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(newobj_init) ../src/gc/default/default.c:3145
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(rb_gc_impl_new_obj) ../src/gc/default/default.c:3552
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(newobj_body+0x26) [0x2aa3ab00c00] ../src/gc.c:1149
  | /tmp/_actions-runner-working-dir/ruby/ruby/build/ruby(rb_newobj) ../src/gc.c:1187
```
2026-09-18 17:53:28 -04:00
Luke Gruber fb33630a57 Get main Ractor to not take VM lock during local GC sweeping
Sweeping unshareables generally doesn't modify global VM data structures. The exception
is rb_free_generic_ivar, which modifies the generic_fields_tbl. However, that table has
its own mutex protecting it.
2026-09-18 10:39:49 -04:00
Peter Zhu 66c1d1970e [ruby/mmtk] Always use fixed heap for NoGC
Ruby and CPU heap doesn't make any sense when using the NoGC plan.

https://github.com/ruby/mmtk/commit/85f97bb0d9
2026-09-18 12:11:32 +00:00
Luke Gruber c55886f946 Remove VM lock acquire during finalize_list and run_final
Each objspace has its own finalizer list since RLGC landed so there aren't
concurrency issues here. This used to be a global table that all Ractors
shared. This affects only the default GC.
2026-09-17 23:15:20 -04:00
Luke Gruber e755b92a3f Call dfree on non-thread-safe T_DATA with the VM barrier
When running multiple Ractors, we can't call dfree of a non-thread-safe
T_DATA while other Ractors are running or doing GC work. If there are multiple
Ractors, we save the T_DATA object's `dfree` and `data` pointer in an entry. Once
the entry buffer is full per-Ractor, we publish it to the global list atomically.
Once the threshold for these deferred T_DATAs is reached (right now ~65K), the next
Ractor to run interrupts calls the `dfree` functions for these objects under the VM
barrier. It does not do any other GC work during this postponed job.

If a global GC runs, it also calls the `dfree` functions for all these objects (it drains
the deferred-free queue). When only 1 Ractor is running (`rb_gc_single_objspace_p()`),
these objects are freed normally and aren't added to any queue.

NOTE
----
Embedded T_DATA objects need to be special-cased because their `data` points to inside the
object's slot itself, which we reuse right away now. For non-thread-safe T_DATA that is
embeddable, we need to create it as unembedded - even without Ractors.
2026-09-17 22:02:06 -04:00
Koichi SasadaandClaude Opus 5 6ca2b168f3 gc: give RUBY_GC_HEAP_INIT_BYTES a lower bound too
The initial heap size is a slot count target, bytes / slot_size for each
heap, so below one slot in the largest heap heap_prepare never forces that
heap's first page.  RUBY_GC_RACTOR_HEAP_INIT_BYTES rejects such values;
RUBY_GC_HEAP_INIT_BYTES accepts anything above zero, and dies partway
through boot:

    $ RUBY_GC_HEAP_INIT_BYTES=512 ./ruby -e 'puts "ok"'
    encdb.so: [BUG] cannot create a new page after major GC

Give it the same bound.  Too small a value is now ignored with a warning
under -w, as one that overflows or does not parse already is:

    RUBY_GC_HEAP_INIT_BYTES=512 (default value: 2621440) is ignored
    because it must be greater than 1023.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 02:57:58 +09:00
Koichi SasadaandClaude Opus 5 51f811afb0 gc: count only the Ractor that allocates from an objspace
gc_marks_finish keeps at least heap_free_slots * r_mul slots free, where
r_mul was the number of Ractors allocating from this objspace, capped at
8.  Per-Ractor GC replaced that count with the VM-wide one, so every
Ractor's objspace now asks for free slots in proportion to how many
Ractors the whole VM has, although only its own Ractor allocates from it.

The floor can then exceed the heap it applies to.  A Ractor started with
the smallest initial heap holds about 7,200 slots; with one other Ractor
in the VM the floor is 4096 * 2, so no sweep can ever leave enough behind
and every collection escalates to a major one:

    1,000,000 allocations on one Ractor, all garbage

                        collections   major   reason
    before                      409     102   nofree
    after                       617       1   oldgen

Lowering RUBY_GC_HEAP_FREE_SLOTS to 1024 has the same effect, which is
what identified the floor.  The major collections free nothing: they cost
8% on a Ractor that allocates nothing but garbage and nothing once one
holds a live set, and the heap does not shrink.  Memory is unchanged
either way, at 298 KB per Ractor.

The main objspace keeps the VM-wide count it has used since before
per-Ractor GC.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 02:57:58 +09:00
Koichi SasadaandClaude Opus 5 a22d46c446 gc: keep the last page of a heap that has been used
A page with nothing live on it leaves its heap for the global empty pages,
and may be returned to the system from there.  When it is the heap's only
page the heap is left with none, and nothing grows a heap in that state:
gc_sweep_finish_heap's growth path asks for total_slots > 0, and
heap_prepare forces a first page only when it is not already sweeping.  An
allocation in that heap then reaches "cannot create a new page after GC".

It takes a heap whose page empties completely while another heap is being
swept, so a Ractor allocating a mix of sizes finds it and one allocating a
single size does not:

    Warning[:experimental] = false
    r = Ractor.new {
      20_000.times { |i| [Array.new(i % 300), "x" * (i % 2000), {a: 1}] }
    }
    r.value

Keep the page instead.  A heap that has been used once holds at least one,
which is what the rest of the code already assumes.  Recovering afterwards
in heap_prepare was measured too and holds the same 224-231 KB per Ractor,
but this keeps the invariant rather than repairing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 02:57:58 +09:00
Koichi SasadaandClaude Fable 5.1 abac51a1ce gc: give a Ractor's own objspace a smaller initial heap size
heap_init_bytes is a slot count target applied per objspace, bytes divided
by each heap's slot size: heap_prepare fills a heap up to it before the
first collection, gc_sweep_finish_heap puts a floor under the free slots a
sweep should leave, and gc_marks_finish sums it across the heaps as the cap
on how much may stay free, which is what keeps empty pages around.  With
one shared objspace this 2.5 MB was paid once.  With per-Ractor GC every
Ractor's own objspace pays it again, and that is the steady heap a Ractor
holds: a Ractor that allocates 40,000 objects and discards them keeps
4.3 MB, where Ruby 4.0 kept 71 KB.

Add ractor_heap_init_bytes (RUBY_GC_RACTOR_HEAP_INIT_BYTES) and use it for
every objspace but the main one.  Its default, 0, is resolved at boot to
the smallest size that works: one slot in the largest heap.  Below that,
heap_prepare never forces a heap's first page and allocating there fails
with "cannot create a new page after GC", so the environment variable
rejects smaller values the way the other size parameters reject theirs.

Measured with 64 Ractors, each allocating 40,000 objects and then
discarding them, reported after a GC so that it is the heap floor rather
than live data:

    ruby                    kept per Ractor   GCs/Ractor    wall
    4.0.6                          70.9 KB        147.1   271 ms
    master                       4,310.3 KB          1.2   264 ms
    this commit                    302.2 KB         24.5   195 ms

Nothing between the minimum and one page's worth is worth choosing: from
1 KB to 16 KB the GC behaves identically (409 collections, 102 of them
major) and only the memory grows, 302 KB to 857 KB.  The 102 major
collections are all MAJOR_BY_NOFREE, and they stop at 64 KB, which is
HEAP_PAGE_SIZE: below one page's worth of slots, a sweep can never leave
enough free slots behind.  Removing them costs three times the memory
(910 KB) and buys 8% on a Ractor that allocates nothing but garbage, and
nothing at all once a Ractor holds a live set: with 200,000 objects live
the minimum runs 56 collections against 45 and takes 1% longer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 02:57:58 +09:00
Koichi SasadaandClaude Opus 5 ab51d15bc3 GC: fill the stat symbol tables at boot, not on first use
setup_gc_stat_symbols(), setup_gc_stat_heap_symbols() and the symbols
gc_info_decode() decodes were each filled on first use and guarded by
their own first element.  That element is also the first one written, so
a second ractor arriving while the first was still filling saw the guard
already set and skipped the setup, leaving the later entries at 0:

    Warning[:experimental] = false
    port = Ractor::Port.new
    256.times { Ractor.new(port) { |p|
      begin
        GC.stat(:heap_allocated_pages); p << :ok
      rescue ArgumentError => e
        p << e.message
      end
      nil
    } }
    p 256.times.map { port.receive }.reject { _1 == :ok }
    #=> ["unknown key: heap_allocated_pages"]

Reproduces about once per ten runs here; calling GC.stat once on the main
ractor first makes it go away, which is what pointed at the guard.

Fill all three tables from rb_gc_impl_init(), where no other ractor
exists yet, and drop the guards.  gc_info_decode()'s function-local
statics move to file scope so the new setup function can reach them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 02:57:58 +09:00
Peter Zhu 7a824bdf92 [ruby/mmtk] Implement support for StickyImmix
We can see some nice performance gains across the board with StickyImmix
support:

--------------  -------------  ------------  ----------------  ------------  -------------------  -----------------  ---------------------
bench              Immix (ms)     RSS (MiB)  StickyImmix (ms)     RSS (MiB)  StickyImmix 1st itr  Immix/StickyImmix  RSS Immix/StickyImmix
activerecord     205.1 ± 1.4%  127.8 ± 0.0%      126.1 ± 3.8%  150.3 ± 0.6%                1.790              1.626                  0.850
chunky-png       409.2 ± 1.6%   90.2 ± 0.1%      378.7 ± 1.0%  119.0 ± 0.4%                1.046              1.081                  0.758
erubi-rails     1028.3 ± 1.0%  168.5 ± 0.0%      549.3 ± 0.8%  194.1 ± 0.0%                1.879              1.872                  0.868
hexapdf         1014.9 ± 2.3%  534.7 ± 2.3%      885.5 ± 0.7%  743.4 ± 2.4%                1.075              1.146                  0.719
liquid-c          35.1 ± 2.9%   93.5 ± 0.1%       25.1 ± 5.2%  107.6 ± 0.3%                1.502              1.399                  0.869
liquid-compile    88.3 ± 3.4%   91.4 ± 0.1%       31.2 ± 3.9%  110.1 ± 0.3%                2.707              2.826                  0.830
liquid-il        228.9 ± 1.7%   77.8 ± 0.1%      168.2 ± 0.7%   93.2 ± 0.0%                1.347              1.361                  0.835
liquid-render     71.2 ± 2.0%   93.8 ± 0.1%       59.2 ± 2.4%  109.8 ± 0.2%                1.197              1.203                  0.854
lobsters         425.8 ± 0.7%  387.6 ± 0.8%      368.6 ± 3.0%  514.6 ± 0.4%                1.238              1.155                  0.753
mail             115.0 ± 1.7%  104.8 ± 0.0%       62.7 ± 2.2%  110.9 ± 0.1%                1.927              1.835                  0.945
psych-load      1618.2 ± 1.1%   86.2 ± 0.0%      993.7 ± 0.6%  101.2 ± 0.0%                1.648              1.628                  0.852
railsbench      1058.4 ± 0.4%  190.8 ± 0.0%      775.2 ± 0.5%  216.5 ± 0.0%                1.384              1.365                  0.881
rubocop           84.3 ± 3.9%  169.5 ± 1.5%       74.6 ± 5.1%  202.4 ± 1.1%                1.381              1.130                  0.837
ruby-lsp         106.8 ± 4.0%  126.8 ± 0.3%       75.2 ± 5.7%  151.1 ± 0.9%                1.377              1.420                  0.839
sequel            54.2 ± 3.0%   81.1 ± 0.1%       27.7 ± 4.2%   92.5 ± 0.3%                2.065              1.953                  0.876
shipit           667.4 ± 2.7%  240.0 ± 0.0%      630.6 ± 0.8%  301.3 ± 0.3%                1.097              1.058                  0.796
--------------  -------------  ------------  ----------------  ------------  -------------------  -----------------  ---------------------

https://github.com/ruby/mmtk/commit/24f507513f
2026-09-12 00:32:56 +00:00