Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9ebb977
Page pool: allow OS to free page and arena memory when only 1 objspace
luke-gruber Aug 13, 2026
3c66369
thread: wake the timer thread when an enqueued ractor has no server
ko1 Aug 13, 2026
6f176c8
thread: do not publish an M:N thread before its native thread exists
ko1 Aug 13, 2026
4e4e3de
thread: leave RB_WAITFD_PRI waits to the blocking path
ko1 Aug 13, 2026
78f2945
thread: do not clobber a sleeper's status in thread_sched_wait_events
ko1 Aug 13, 2026
bd2b9be
thread: RB_NOGVL_UBF_ASYNC_SAFE does not apply to the sentinel ubfs
ko1 Aug 13, 2026
33124c7
thread: clean up the M:N scheduler's remaining fork residue
ko1 Aug 13, 2026
dce74eb
thread: free a shared native thread's resources when it retires
ko1 Aug 13, 2026
582b3ef
thread: harden the M:N scheduler's latent overflow and error paths
ko1 Aug 13, 2026
f3456db
thread: M:N scheduler cleanups
ko1 Aug 13, 2026
477fd8a
thread: let timed fd waits ride the M:N scheduler
ko1 Aug 14, 2026
75d5302
Get main Ractor to not take VM lock during local GC marking when JIT …
luke-gruber Aug 14, 2026
70a0bfa
[ruby/digest] Reapply "Add BLAKE3 support in Digest"
nobu Aug 14, 2026
4490ced
[ruby/digest] Don't use _Atomic in C99 mode
byroot Aug 14, 2026
2febd4b
[ruby/digest] Pass config macros via command line
nobu Aug 14, 2026
f0edab2
ZJIT: Put stack map details behind `#if USE_ZJIT`
XrXr Aug 14, 2026
1b0c22c
hash.c: compact ar_table on freeze
byroot Aug 14, 2026
65a071a
GC: fix page pool advised_count underflow on 64KiB-page systems
ko1 Aug 14, 2026
3349f41
Filter TracePoint events in test_trace_optimized_methods to current file
XrXr Aug 14, 2026
6cc5c92
ZJIT: Skip fiddle memory leak test when --zjit-call-threshold=1
XrXr Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions bootstraptest/test_thread.rb
Original file line number Diff line number Diff line change
Expand Up @@ -571,3 +571,118 @@ def inspect
got == 'x' ? 'ok' : got.inspect
end.value
}

# A ractor made runnable while every shared native thread is dedicated to a
# blocking region must still be served: ractor_sched_enq has to wake the timer
# thread, whose untimed sleep otherwise never ends. [Bug #21504]
assert_equal 'ok', %q{
lockpath = "mn_enq_wake_#{$$}.lock"
flagpath = "mn_enq_wake_#{$$}.flag"
begin
File.write(lockpath, "")
lock = File.open(lockpath, "r+")
lock.flock(File::LOCK_EX)
r = Ractor.new(lockpath, flagpath) do |lockpath, flagpath|
f = File.open(lockpath, "r+")
t = Thread.new do
sleep 0.2 # let the Ractor.receive below park first
# Stay off the scheduler until the timer thread is in its untimed sleep;
# blocking right away would be repaired by the pending 10ms timeout.
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
nil while Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0 < 0.05
f.flock(File::LOCK_EX) # the last shared native thread goes dedicated
end
msg = Ractor.receive
File.write(flagpath, "")
t.join
msg
end
sleep 1 # r is parked, its flock thread is dedicated, the timer sleeps untimed
r.send(:ok)
served = false
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
until served || Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
served = File.exist?(flagpath)
sleep 0.05
end
lock.flock(File::LOCK_UN)
served ? r.value.to_s : 'the enqueued ractor was never served'
ensure
File.unlink(lockpath) rescue nil
File.unlink(flagpath) rescue nil
end
}

# Creating a thread when no native thread can be spawned must fail cleanly:
# the thread must not be published to the scheduler before its native thread
# exists, or an existing shared thread runs it to death concurrently with the
# creator's failure path (living-set removal races its own).
assert_equal 'ok', %q{
can_limit = begin
Process.setrlimit(:NPROC, Process.getrlimit(:NPROC)[0])
true
rescue StandardError, NotImplementedError
false
end
if !can_limit
'ok' # cannot make thread creation fail on this platform; nothing to test
else
warm = 2.times.map { Ractor.new { nil until Ractor.receive == :quit } }
sleep 0.3 # the pool now has shared native threads parked for the warm ractors
Process.setrlimit(:NPROC, 1)
# RLIMIT_NPROC binds neither root (CI containers) nor macOS threads;
# probe that thread creation actually fails before asserting on it.
limited = begin
Thread.new {}.join
false
rescue ThreadError
true
end
result =
if !limited
'ok'
else
errs = 0
20.times do
begin
Ractor.new { :born }
rescue ThreadError
errs += 1
end
end
sleep 0.5 # a wrongly-published thread would be served and die about now
errs == 20 ? 'ok' : "#{errs} of 20 raised"
end
warm.each { |r| r.send(:quit) }
warm.each(&:value)
GC.start
result
end
}

# An M:N thread's sleep must survive a spurious wakeup: an interrupt that
# handle_interrupt defers wakes the sleeper, whose status must stay
# THREAD_STOPPED so that sleep_hrtime sleeps the remaining time, as it does
# on a dedicated native thread.
assert_equal 'ok', %q{
Ractor.new do
elapsed = nil
th = Thread.new do
Thread.handle_interrupt(RuntimeError => :never) do
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
sleep 1.0
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
end
end
sleep 0.3
begin th.raise(RuntimeError, "deferred"); rescue RuntimeError; end
begin th.join; rescue RuntimeError; end
if elapsed.nil?
'the sleeper died inside handle_interrupt :never'
elsif elapsed >= 0.9
'ok'
else
"slept only %.2fs of 1.0s" % elapsed
end
end.value
}
49 changes: 4 additions & 45 deletions cont.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,6 @@
#include <sys/mman.h>
#endif

// On Solaris, madvise() is NOT declared for SUS (XPG4v2) or later,
// but MADV_* macros are defined when __EXTENSIONS__ is defined.
#ifdef NEED_MADVICE_PROTOTYPE_USING_CADDR_T
#include <sys/types.h>
extern int madvise(caddr_t, size_t, int);
#endif

#include COROUTINE_H

#include "eval_intern.h"
Expand All @@ -34,6 +27,7 @@ extern int madvise(caddr_t, size_t, int);
#include "internal/gc.h"
#include "internal/proc.h"
#include "internal/sanitizers.h"
#include "internal/vm_map.h"
#include "internal/warnings.h"
#include "ruby/fiber/scheduler.h"
#include "yjit.h"
Expand Down Expand Up @@ -501,12 +495,7 @@ fiber_pool_allocate_memory(size_t * count, size_t stride)
}
else {
ruby_annotate_mmap(base, mmap_size, "Ruby:fiber_pool_allocate_memory");
#if defined(MADV_FREE_REUSE)
// On Mac MADV_FREE_REUSE is necessary for the task_info api
// to keep the accounting accurate as possible when a page is marked as reusable
// it can possibly not occurring at first call thus re-iterating if necessary.
while (madvise(base, mmap_size, MADV_FREE_REUSE) == -1 && errno == EAGAIN);
#endif
rb_vm_map_reuse(base, mmap_size);
return base;
}
#endif
Expand Down Expand Up @@ -820,7 +809,7 @@ fiber_pool_stack_acquire(struct fiber_pool * fiber_pool)
}

// We advise the operating system that the stack memory pages are no longer being used.
// This introduce some performance overhead but allows system to relaim memory when there is pressure.
// This introduces some performance overhead but allows the system to reclaim memory when there is pressure.
static inline void
fiber_pool_stack_free(struct fiber_pool_stack * stack)
{
Expand All @@ -843,37 +832,7 @@ fiber_pool_stack_free(struct fiber_pool_stack * stack)
// In addition, it's actually slightly desirable to not do anything here,
// but that results in higher memory usage.

#ifdef __wasi__
// WebAssembly doesn't support madvise, so we just don't do anything.
#elif VM_CHECK_MODE > 0 && defined(MADV_DONTNEED)
if (!advice) advice = MADV_DONTNEED;
// This immediately discards the pages and the memory is reset to zero.
madvise(base, size, advice);
#elif defined(MADV_FREE_REUSABLE)
if (!advice) advice = MADV_FREE_REUSABLE;
// Darwin / macOS / iOS.
// Acknowledge the kernel down to the task info api we make this
// page reusable for future use.
// As for MADV_FREE_REUSABLE below we ensure in the rare occasions the task was not
// completed at the time of the call to re-iterate.
while (madvise(base, size, advice) == -1 && errno == EAGAIN);
#elif defined(MADV_FREE)
if (!advice) advice = MADV_FREE;
// Recent Linux.
madvise(base, size, advice);
#elif defined(MADV_DONTNEED)
if (!advice) advice = MADV_DONTNEED;
// Old Linux.
madvise(base, size, advice);
#elif defined(POSIX_MADV_DONTNEED)
if (!advice) advice = POSIX_MADV_DONTNEED;
// Solaris?
posix_madvise(base, size, advice);
#elif defined(_WIN32)
VirtualAlloc(base, size, MEM_RESET, PAGE_READWRITE);
// Not available in all versions of Windows.
//DiscardVirtualMemory(base, size);
#endif
rb_vm_map_reusable_lazy(base, size, advice);

#if defined(COROUTINE_SANITIZE_ADDRESS)
__asan_poison_memory_region(fiber_pool_stack_poison_base(stack), fiber_pool_stack_poison_size(stack));
Expand Down
Loading