Skip to content

[pull] master from ruby:master - #1334

Merged
pull[bot] merged 12 commits into
turkdevops:masterfrom
ruby:master
Aug 21, 2026
Merged

[pull] master from ruby:master#1334
pull[bot] merged 12 commits into
turkdevops:masterfrom
ruby:master

Conversation

@pull

@pull pull Bot commented Aug 21, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

mame and others added 12 commits August 21, 2026 16:50
Coverage.peek_result rebuilt the nested branch coverage hash on every
call, and hashing its array keys (Array#hash via the recursion guard)
dominated the cost.  Build { base_key => { target_key => counter_index } }
once per file and cache it; each peek dups it and fills in the counters.

40k branch sites: 40 ms -> 3.5 ms per peek.  The key arrays are now frozen
and shared between results.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Likewise for method coverage: walking me_set and cme2counter and hashing
the array keys on every peek dominated the cost.  Maintain
{ path => { key => me or [me, ...] } } incrementally (both hashes are
append-only) and fill in the counts from cme2counter on each peek.
Extract rb_coverage_method_data_of() from thread.c for that.

10k methods: 24 ms -> 0.7 ms per peek.  rack's test suite with SimpleCov
per-test tracking (lines+branches+methods): 30 s -> 8.4 s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A Ractor whose block ends while one of its own threads is still running took
a full second to terminate: rb_thread_terminate_all() sleeps for a second per
round and relies on the last sub-thread to wake it, but that wakeup was sent
only when the Ractor's main thread already had THREAD_KILLED, which
thread_start_func_2 sets after rb_thread_terminate_all() returns.  For the
main Ractor rb_ec_cleanup() sets it beforehand, which is why only Ractors
were affected.

Setting THREAD_KILLED earlier is not an option here: thread_sched_switch()
reads it as to_dead, promising the M:N scheduler that the coroutine is never
resumed, and this thread still parks and resumes inside the wait.  So mark
the wait itself instead: rb_thread_terminate_all() sets threads.terminating
before it waits, and the exiting thread wakes `main` on that.  Nothing clears
it afterwards, because no thread of that Ractor runs again; the Ractor that
survives a fork gets it reset along with the rest of the thread set.
…f net-http-persistent

Bundler's metadata transport was the last consumer of the vendored
net-http-persistent. Hand out configured Gem::Net::HTTP connections from
a small per-host pool compatible with Gem::Request's checkout/checkin
interface, so metadata requests share RubyGems' request execution with
.gem downloads. As a side effect, https_proxy is now honored for https
sources, and a configured :no_proxy no longer falls back to the
environment proxy.

ruby/rubygems@b6499513b9

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nothing references them since Bundler metadata fetching moved to
Gem::Request. connection_pool was only vendored as a dependency of
net-http-persistent, so it goes away too, along with their automatiek
entries and patches.

ruby/rubygems@eaf49e1329

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…l state

The before hook skips before it saves the previous connection pool client
and UI, but the after hook restored those unconditionally, leaving
Gem::Request::ConnectionPools.client as nil for every later example. Only
restore what was captured, matching the guard Artifice.deactivate uses.

ruby/rubygems@4cf080cb0a

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test reads GC.stat and GC.stat_heap separately and asserts that the
per-heap values sum to the totals.  Both come from the same counters
(objspace_live_slots() is the sum of the per-heap live slots, and so on), so
they only disagree when something allocated or freed between the two reads:

  TestGc#test_stat_heap_constraints [test/ruby/test_gc.rb:298]:
  <243163> expected but was
  <243164>.

Running the pair in a loop with a background allocating thread reproduces it,
7 mismatches in 200,000 rounds; reading the pair again leaves 0.  Disabling
GC does not help (7 in 200,000): the reads disagree over an allocation, not
over a collection.

So retry the test once instead of leaving it flaky.  An accounting bug fails
the retry too, so this keeps what the test checks.

Seen on YJIT macOS with --yjit-call-threshold=1:
https://github.com/ruby/ruby/actions/runs/32472080033/job/96740787745
ZJIT: infer_types early exit if no back traversals

Function::infer_types repeatedly loops over all blocks and instructions
in Reverse Post Order (RPO), progressively updating the types. The loop
continues until a fixpoint is reached (no further type changes will
occur).

This patch improves performance by adding an early exit if no back edge
traversals have occurred. If there were no back traversals the fixpoint
has already been reached, and continuing to loop will not change the
result.

Note, I'm considering an edge that links a block back to itself as a
back edge. The block can loop with different arguments, causing further
type propagation.

This patch reduces compile_hir_time from ~960ms to ~880ms.

Before:

    % for i in $(seq 0 5); do WARMUP_ITRS=0 MIN_BENCH_ITRS=2 MIN_BENCH_TIME=0 ./ruby --zjit --zjit-stats ruby-bench/benchmarks/lobsters/benchmark.rb 2>&1 | grep compile_hir_time; done
    compile_hir_time: 949ms
    compile_hir_time: 962ms
    compile_hir_time: 975ms
    compile_hir_time: 976ms
    compile_hir_time: 952ms
    compile_hir_time: 951ms

After:

    % for i in $(seq 0 5); do WARMUP_ITRS=0 MIN_BENCH_ITRS=2 MIN_BENCH_TIME=0 ./ruby --zjit --zjit-stats ruby-bench/benchmarks/lobsters/benchmark.rb 2>&1 | grep compile_hir_time; done
    compile_hir_time: 866ms
    compile_hir_time: 881ms
    compile_hir_time: 881ms
    compile_hir_time: 894ms
    compile_hir_time: 897ms
    compile_hir_time: 862ms
Ractor::Port#receive, Ractor.receive and Ractor.select gain a `timeout:`
keyword and return nil when it passes.

The wait itself stays where it is: rb_ractor_sched_wait() keeps parking the
thread in the thread scheduler, so an M:N thread still hands its native
thread back instead of becoming dedicated.  How the deadline is taken
depends on which kind of thread waits:

A dedicated native thread parks on its own condvar, so it takes the deadline
there, the way native_cond_sleep() does.  Nothing else is involved: the
condvar it waits on is the one a send already signals.

An M:N thread has no condvar of its own, so its deadline is armed on the
timer thread as a timeout-only wheel entry, and the timer thread wakes it
through thread_sched_to_ready_common(), which is exactly how
rb_ractor_sched_wakeup() wakes it for a send.  That gives such a waiter two
wakers, so rb_ractor_sched_wakeup() now takes an armed timeout back before
waking, skips a thread that a fired timeout already made runnable, and bumps
the scheduler event serial so a timeout that has not fired cannot wake it a
second time.

`timeout: 0` is a poll: it never blocks, converts nothing and reads no clock,
so it costs what a receive of a waiting message costs.  It still delivers the
incoming queue first, so a message another thread just sent is not missed.

A timeout that expires leaves the waiter on the Ractor's waiter list, as a
spurious wakeup does, and ractor_wait_receive() still delivers pending
messages before it reports the timeout, so a message that arrived just before
the deadline is not lost.

Both kinds of wait exist on every pthread platform, including builds without
the timer wheel (USE_MN_THREADS == 0), where every thread is dedicated.  On
win32 the wait is a condvar wait, which takes the timeout directly.
Prepending a module to a module that already has includers backfills an
origin iclass into each includer's ancestor chain using rb_prepend_module.

The backfilled iclass is never added to a subclasses list, so
rb_clear_method_cache can't reach it when one of the module's methods is
later redefined.

Calling super through a call site that has cached the backfilled iclass
calls the old entry, even though it should be redefined.

```ruby
module M; def foo; :m; end; end
class D; include M; end
M.prepend(Module.new { def foo; super; end })
D.new.foo                          # prime the super cache
M.send(:define_method, :foo) { :hooked }
D.new.foo                          # => got :m, expected :hooked
```

This commit makes sure that the backfilled iclass is registered in the
subclasses list.
@pull pull Bot locked and limited conversation to collaborators Aug 21, 2026
@pull pull Bot added the ⤵️ pull label Aug 21, 2026
@pull
pull Bot merged commit c59c4d7 into turkdevops:master Aug 21, 2026
0 of 2 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants