Skip to content

Commit d85c640

Browse files
Fix performance of fiber pool stack tests. (ruby#16570)
1 parent 5ffecb8 commit d85c640

File tree

5 files changed

+155
-35
lines changed

5 files changed

+155
-35
lines changed

cont.c

Lines changed: 109 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ extern int madvise(caddr_t, size_t, int);
4242
#include "id_table.h"
4343
#include "ractor_core.h"
4444

45-
static const int DEBUG = 0;
45+
enum {
46+
DEBUG = 0,
47+
DEBUG_EXPAND = 0,
48+
DEBUG_ACQUIRE = 0,
49+
};
4650

4751
#define RB_PAGE_SIZE (pagesize)
4852
#define RB_PAGE_MASK (~(RB_PAGE_SIZE - 1))
@@ -62,11 +66,11 @@ static VALUE rb_cFiberPool;
6266
// Defined in `coroutine/$arch/Context.h`:
6367
#ifdef COROUTINE_LIMITED_ADDRESS_SPACE
6468
#define FIBER_POOL_ALLOCATION_FREE
65-
#define FIBER_POOL_INITIAL_SIZE 8
66-
#define FIBER_POOL_ALLOCATION_MAXIMUM_SIZE 32
69+
#define FIBER_POOL_MINIMUM_COUNT 8
70+
#define FIBER_POOL_MAXIMUM_ALLOCATIONS 32
6771
#else
68-
#define FIBER_POOL_INITIAL_SIZE 32
69-
#define FIBER_POOL_ALLOCATION_MAXIMUM_SIZE 1024
72+
#define FIBER_POOL_MINIMUM_COUNT 32
73+
#define FIBER_POOL_MAXIMUM_ALLOCATIONS 1024
7074
#endif
7175
#ifdef RB_EXPERIMENTAL_FIBER_POOL
7276
#define FIBER_POOL_ALLOCATION_FREE
@@ -189,7 +193,11 @@ struct fiber_pool {
189193
size_t count;
190194

191195
// The initial number of stacks to allocate.
192-
size_t initial_count;
196+
size_t minimum_count;
197+
198+
// If positive, total stacks in this pool cannot exceed this (shared pool only:
199+
// set via RUBY_SHARED_FIBER_POOL_MAXIMUM_COUNT). Expansion fails with errno EAGAIN.
200+
size_t maximum_count;
193201

194202
// Whether to madvise(free) the stack or not.
195203
// If this value is set to 1, the stack will be madvise(free)ed
@@ -470,7 +478,7 @@ fiber_pool_allocate_memory(size_t * count, size_t stride)
470478
// the system would allow (e.g. overcommit * physical memory + swap), we
471479
// divide count by two and try again. This condition should only be
472480
// encountered in edge cases, but we handle it here gracefully.
473-
while (*count > 1) {
481+
while (*count) {
474482
#if defined(_WIN32)
475483
void * base = VirtualAlloc(0, (*count)*stride, MEM_COMMIT, PAGE_READWRITE);
476484

@@ -518,11 +526,28 @@ fiber_pool_allocate_memory(size_t * count, size_t stride)
518526
static struct fiber_pool_allocation *
519527
fiber_pool_expand(struct fiber_pool * fiber_pool, size_t count)
520528
{
529+
if (count == 0) {
530+
errno = EAGAIN;
531+
return NULL;
532+
}
533+
521534
STACK_GROW_DIR_DETECTION;
522535

523536
size_t size = fiber_pool->size;
524537
size_t stride = size + RB_PAGE_SIZE;
525538

539+
// If the maximum number of stacks is set, and we have reached it, return NULL.
540+
if (fiber_pool->maximum_count > 0) {
541+
if (fiber_pool->count >= fiber_pool->maximum_count) {
542+
errno = EAGAIN;
543+
return NULL;
544+
}
545+
size_t remaining = fiber_pool->maximum_count - fiber_pool->count;
546+
if (count > remaining) {
547+
count = remaining;
548+
}
549+
}
550+
526551
// Allocate metadata before mmap: ruby_xmalloc (RB_ALLOC) raises on failure and
527552
// must not run after base is mapped, or the region would leak.
528553
struct fiber_pool_allocation * allocation = RB_ALLOC(struct fiber_pool_allocation);
@@ -548,7 +573,7 @@ fiber_pool_expand(struct fiber_pool * fiber_pool, size_t count)
548573
#endif
549574
allocation->pool = fiber_pool;
550575

551-
if (DEBUG) {
576+
if (DEBUG_EXPAND) {
552577
fprintf(stderr, "fiber_pool_expand(%"PRIuSIZE"): %p, %"PRIuSIZE"/%"PRIuSIZE" x [%"PRIuSIZE":%"PRIuSIZE"]\n",
553578
count, (void*)fiber_pool, fiber_pool->used, fiber_pool->count, size, fiber_pool->vm_stack_size);
554579
}
@@ -613,22 +638,24 @@ fiber_pool_expand(struct fiber_pool * fiber_pool, size_t count)
613638
// Initialize the specified fiber pool with the given number of stacks.
614639
// @param vm_stack_size The size of the vm stack to allocate.
615640
static void
616-
fiber_pool_initialize(struct fiber_pool * fiber_pool, size_t size, size_t count, size_t vm_stack_size)
641+
fiber_pool_initialize(struct fiber_pool * fiber_pool, size_t size, size_t minimum_count, size_t maximum_count, size_t vm_stack_size)
617642
{
618643
VM_ASSERT(vm_stack_size < size);
619644

620645
fiber_pool->allocations = NULL;
621646
fiber_pool->vacancies = NULL;
622647
fiber_pool->size = ((size / RB_PAGE_SIZE) + 1) * RB_PAGE_SIZE;
623648
fiber_pool->count = 0;
624-
fiber_pool->initial_count = count;
649+
fiber_pool->minimum_count = minimum_count;
650+
fiber_pool->maximum_count = maximum_count;
625651
fiber_pool->free_stacks = 1;
626652
fiber_pool->used = 0;
627-
628653
fiber_pool->vm_stack_size = vm_stack_size;
629654

630-
if (RB_UNLIKELY(!fiber_pool_expand(fiber_pool, count))) {
631-
rb_raise(rb_eFiberError, "can't allocate initial fiber stacks (%"PRIuSIZE" x %"PRIuSIZE" bytes): %s", count, fiber_pool->size, strerror(errno));
655+
if (fiber_pool->minimum_count > 0) {
656+
if (RB_UNLIKELY(!fiber_pool_expand(fiber_pool, fiber_pool->minimum_count))) {
657+
rb_raise(rb_eFiberError, "can't allocate initial fiber stacks (%"PRIuSIZE" x %"PRIuSIZE" bytes): %s", fiber_pool->minimum_count, fiber_pool->size, strerror(errno));
658+
}
632659
}
633660
}
634661

@@ -678,15 +705,30 @@ fiber_pool_allocation_free(struct fiber_pool_allocation * allocation)
678705
#endif
679706

680707
// Number of stacks to request when expanding the pool (clamped to min/max).
681-
static inline size_t
708+
static size_t
682709
fiber_pool_stack_expand_count(const struct fiber_pool *pool)
683710
{
684-
const size_t maximum = FIBER_POOL_ALLOCATION_MAXIMUM_SIZE;
685-
const size_t minimum = pool->initial_count;
711+
const size_t maximum_allocations = FIBER_POOL_MAXIMUM_ALLOCATIONS;
712+
const size_t minimum_count = FIBER_POOL_MINIMUM_COUNT;
686713

714+
// We are going try and double the number of stacks in the pool:
687715
size_t count = pool->count;
688-
if (count > maximum) count = maximum;
689-
if (count < minimum) count = minimum;
716+
if (count > maximum_allocations) count = maximum_allocations;
717+
if (count < minimum_count) count = minimum_count;
718+
719+
// If we have a maximum count, we need to clamp the number of stacks to the maximum:
720+
if (pool->maximum_count > 0) {
721+
if (pool->count >= pool->maximum_count) {
722+
// No expansion is possible:
723+
return 0;
724+
}
725+
726+
// Otherwise, compute the number of stacks we can allocate to bring us to the maximum:
727+
size_t remaining = pool->maximum_count - pool->count;
728+
if (count > remaining) {
729+
count = remaining;
730+
}
731+
}
690732

691733
return count;
692734
}
@@ -698,15 +740,15 @@ fiber_pool_stack_acquire_expand(struct fiber_pool *fiber_pool)
698740
{
699741
size_t count = fiber_pool_stack_expand_count(fiber_pool);
700742

701-
if (DEBUG) fprintf(stderr, "fiber_pool_stack_acquire: expanding fiber pool by %"PRIuSIZE" stacks\n", count);
743+
if (DEBUG_ACQUIRE) fprintf(stderr, "fiber_pool_stack_acquire: expanding fiber pool by %"PRIuSIZE" stacks\n", count);
702744

703745
struct fiber_pool_vacancy *vacancy = NULL;
704746

705747
if (RB_LIKELY(fiber_pool_expand(fiber_pool, count))) {
706748
return fiber_pool_vacancy_pop(fiber_pool);
707749
}
708750
else {
709-
if (DEBUG) fprintf(stderr, "fiber_pool_stack_acquire: expand failed (%s), collecting garbage\n", strerror(errno));
751+
if (DEBUG_ACQUIRE) fprintf(stderr, "fiber_pool_stack_acquire: expand failed (%s), collecting garbage\n", strerror(errno));
710752

711753
rb_gc();
712754

@@ -716,6 +758,9 @@ fiber_pool_stack_acquire_expand(struct fiber_pool *fiber_pool)
716758
return vacancy;
717759
}
718760

761+
// Recompute count as gc may have freed up some allocations:
762+
count = fiber_pool_stack_expand_count(fiber_pool);
763+
719764
// Try to expand the fiber pool again:
720765
if (RB_LIKELY(fiber_pool_expand(fiber_pool, count))) {
721766
return fiber_pool_vacancy_pop(fiber_pool);
@@ -3526,7 +3571,7 @@ rb_fiber_pool_initialize(int argc, VALUE* argv, VALUE self)
35263571

35273572
TypedData_Get_Struct(self, struct fiber_pool, &FiberPoolDataType, fiber_pool);
35283573

3529-
fiber_pool_initialize(fiber_pool, NUM2SIZET(size), NUM2SIZET(count), NUM2SIZET(vm_stack_size));
3574+
fiber_pool_initialize(fiber_pool, NUM2SIZET(size), NUM2SIZET(count), 0, NUM2SIZET(vm_stack_size));
35303575

35313576
return self;
35323577
}
@@ -3545,6 +3590,46 @@ rb_fiber_pool_initialize(int argc, VALUE* argv, VALUE self)
35453590
* fiber.resume #=> FiberError: dead fiber called
35463591
*/
35473592

3593+
static size_t
3594+
shared_fiber_pool_minimum_count()
3595+
{
3596+
size_t minimum_count = FIBER_POOL_MINIMUM_COUNT;
3597+
3598+
const char *minimum_count_env = getenv("RUBY_SHARED_FIBER_POOL_MINIMUM_COUNT");
3599+
if (minimum_count_env && minimum_count_env[0]) {
3600+
char *end;
3601+
unsigned long value = strtoul(minimum_count_env, &end, 10);
3602+
if (end != minimum_count_env && *end == '\0') {
3603+
minimum_count = (size_t)value;
3604+
}
3605+
else {
3606+
rb_warn("invalid RUBY_SHARED_FIBER_POOL_MINIMUM_COUNT=%s (expected a non-negative integer)", minimum_count_env);
3607+
}
3608+
}
3609+
3610+
return minimum_count;
3611+
}
3612+
3613+
static size_t
3614+
shared_fiber_pool_maximum_count()
3615+
{
3616+
size_t maximum_count = 0;
3617+
3618+
const char *maximum_count_env = getenv("RUBY_SHARED_FIBER_POOL_MAXIMUM_COUNT");
3619+
if (maximum_count_env && maximum_count_env[0]) {
3620+
char *end;
3621+
unsigned long value = strtoul(maximum_count_env, &end, 10);
3622+
if (end != maximum_count_env && *end == '\0') {
3623+
maximum_count = (size_t)value;
3624+
}
3625+
else {
3626+
rb_warn("invalid RUBY_SHARED_FIBER_POOL_MAXIMUM_COUNT=%s (expected a non-negative integer)", maximum_count_env);
3627+
}
3628+
}
3629+
3630+
return maximum_count;
3631+
}
3632+
35483633
void
35493634
Init_Cont(void)
35503635
{
@@ -3562,7 +3647,9 @@ Init_Cont(void)
35623647
#endif
35633648
SET_MACHINE_STACK_END(&th->ec->machine.stack_end);
35643649

3565-
fiber_pool_initialize(&shared_fiber_pool, stack_size, FIBER_POOL_INITIAL_SIZE, vm_stack_size);
3650+
size_t minimum_count = shared_fiber_pool_minimum_count();
3651+
size_t maximum_count = shared_fiber_pool_maximum_count();
3652+
fiber_pool_initialize(&shared_fiber_pool, stack_size, minimum_count, maximum_count, vm_stack_size);
35663653

35673654
fiber_initialize_keywords[0] = rb_intern_const("blocking");
35683655
fiber_initialize_keywords[1] = rb_intern_const("pool");

man/erb.1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.\"Ruby is copyrighted by Yukihiro Matsumoto <matz@netlab.jp>.
2-
.Dd February 24, 2026
2+
.Dd March 27, 2026
33
.Dt ERB 1 "Ruby Programmer's Reference Guide"
44
.Os UNIX
55
.Sh NAME

man/goruby.1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.\"Ruby is copyrighted by Yukihiro Matsumoto <matz@netlab.jp>.
2-
.Dd February 24, 2026
2+
.Dd March 27, 2026
33
.Dt GORUBY 1 "Ruby Programmer's Reference Guide"
44
.Os UNIX
55
.Sh NAME

man/ruby.1

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
.\"Ruby is copyrighted by Yukihiro Matsumoto <matz@netlab.jp>.
2-
.Dd February 24, 2026
2+
.Dd March 27, 2026
33
.Dt RUBY 1 "Ruby Programmer's Reference Guide"
44
.Os UNIX
55
.Sh NAME
@@ -700,6 +700,16 @@ Frees stacks of pooled fibers, if set to 1.
700700
Do not free the stacks if set to 0.
701701
Introduced in Ruby 2.7, default: 1 (no growth)
702702
.Pp
703+
.It Ev RUBY_SHARED_FIBER_POOL_MAXIMUM_COUNT
704+
If set to a non-negative integer, the shared fiber pool cannot allocate more
705+
than that many stacks; further fiber creation may fail with
706+
.Va FiberError .
707+
Unset or 0 means no explicit cap (subject to process limits).
708+
.Pp
709+
.It Ev RUBY_SHARED_FIBER_POOL_MINIMUM_COUNT
710+
Initial and minimum growth chunk size for the shared fiber pool (stacks).
711+
Unset uses the implementation default.
712+
.Pp
703713
.El
704714
.Sh STACK SIZE ENVIRONMENT
705715
Stack size environment variables are implementation-dependent and

test/ruby/test_fiber.rb

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -508,20 +508,43 @@ def test_machine_stack_gc
508508
end
509509

510510
def test_fiber_pool_stack_acquire_failure
511-
omit "cannot determine max_map_count" unless File.exist?("/proc/sys/vm/max_map_count")
512-
# On these platforms, excessive memory usage can cause the test to fail unexpectedly.
513-
omit "not supported on IBM platforms" if RUBY_PLATFORM =~ /s390x|powerpc/
514-
omit "not supported with YJIT" if defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
515-
omit "not supported with ZJIT" if defined?(RubyVM::ZJIT) && RubyVM::ZJIT.enabled?
516-
517-
assert_separately([], <<~RUBY, timeout: 120)
518-
max_map_count = File.read("/proc/sys/vm/max_map_count").to_i
511+
environment = {
512+
"RUBY_SHARED_FIBER_POOL_MINIMUM_COUNT" => "0",
513+
"RUBY_SHARED_FIBER_POOL_MAXIMUM_COUNT" => "128"
514+
}
515+
516+
# This program requires, effectively, at most one fiber stack, since the fiber immediately becomes unreachable.
517+
assert_separately([environment], <<~RUBY, timeout: 30)
519518
GC.disable
519+
count_before = GC.count
520+
521+
# Create more fibers than the pool can handle (but they become immediately unreachable):
520522
assert_nothing_raised do
521-
(max_map_count + 10).times do
522-
Fiber.new { Fiber.yield }.resume
523+
256.times do
524+
Fiber.new{Fiber.yield}.resume
525+
end
526+
end
527+
528+
# Major GC should have happened at least once:
529+
assert_operator(GC.count, :>, count_before)
530+
RUBY
531+
end
532+
533+
def test_fiber_pool_stack_acquire_failure_at_maximum_count
534+
environment = {
535+
"RUBY_SHARED_FIBER_POOL_MAXIMUM_COUNT" => "128"
536+
}
537+
538+
assert_separately([environment], <<~RUBY, timeout: 30)
539+
GC.disable
540+
fibers = []
541+
assert_raise(FiberError) do
542+
loop do
543+
Fiber.new{fibers << Fiber.current; Fiber.yield}.resume
544+
raise "expected FiberError before this" if fibers.size > 128
523545
end
524546
end
547+
assert_operator fibers.size, :>=, 128
525548
RUBY
526549
end
527550
end

0 commit comments

Comments
 (0)