diff --git a/NEWS.md b/NEWS.md
index 8f9dd8900d6ef1..3e1111cecfe318 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -188,6 +188,7 @@ They are still available on rubygems.org and can be installed with
* error_highlight 0.7.2
* io-console 0.9.2
* 0.8.2 to [v0.9.0][io-console-v0.9.0], [v0.9.1][io-console-v0.9.1], [v0.9.2][io-console-v0.9.2]
+* io-wait 999.999.999
* ipaddr 1.2.9
* 1.2.8 to [v1.2.9][ipaddr-v1.2.9]
* json 3.0.2
@@ -387,6 +388,28 @@ A lot of work has gone into making Ractors more stable, performant, and usable.
* `ObjectSpace.define_finalizer` on another Ractor's object raises
`Ractor::IsolationError`.
+* `Ractor#monitor` now sends an Array naming the Ractor and what happened to
+ it, `[ractor, :exited]` or `[ractor, :aborted]`, where it used to send the
+ bare Symbol `:exited` or `:aborted`. Several Ractors can then report to one
+ port and the receiver still knows which one finished. The Array is built for
+ the receiving Ractor, so watching many Ractors leaves no shareable objects
+ behind.
+
+ r = Ractor.new { :ok }
+ r.monitor(port = Ractor::Port.new)
+ port.receive #=> [r, :exited]
+
+ One port can therefore watch a whole group, which is all a supervisor
+ needs:
+
+ workers.each { |r| r.monitor port }
+
+ until workers.empty?
+ r, status = port.receive
+ workers.delete(r)
+ workers << restart(r) if status == :aborted
+ end
+
### M:N thread scheduler
* The scheduler scales with the number of waiters and of Ractors, where it
diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb
index 4d1b8d924ba8d2..4158556c06bc07 100644
--- a/bootstraptest/test_ractor.rb
+++ b/bootstraptest/test_ractor.rb
@@ -2710,7 +2710,7 @@ def initialize(a)
## Ractor#monitor
-# monitor port returns `:exited` when the monitering Ractor terminated.
+# monitor port returns [ractor, :exited] when the monitering Ractor terminated.
assert_equal 'true', %q{
r = Ractor.new do
Ractor.main << :ok1
@@ -2719,10 +2719,10 @@ def initialize(a)
r.monitor port = Ractor::Port.new
Ractor.receive # :ok1
- port.receive == :exited
+ port.receive == [r, :exited]
}
-# monitor port returns `:exited` even if the monitoring Ractor was terminated.
+# monitor port returns [ractor, :exited] even if the monitoring Ractor was terminated.
assert_equal 'true', %q{
r = Ractor.new do
:ok
@@ -2731,7 +2731,7 @@ def initialize(a)
r.join # wait for r's terminateion
r.monitor port = Ractor::Port.new
- port.receive == :exited
+ port.receive == [r, :exited]
}
# monitor returns false if the monitoring Ractor was terminated.
@@ -2745,7 +2745,7 @@ def initialize(a)
r.monitor Ractor::Port.new
}
-# monitor port returns `:aborted` when the monitering Ractor is aborted.
+# monitor port returns [ractor, :aborted] when the monitering Ractor is aborted.
assert_equal 'true', %q{
r = Ractor.new do
Ractor.main << :ok1
@@ -2754,10 +2754,10 @@ def initialize(a)
r.monitor port = Ractor::Port.new
Ractor.receive # :ok1
- port.receive == :aborted
+ port.receive == [r, :aborted]
}
-# monitor port returns `:aborted` even if the monitoring Ractor was aborted.
+# monitor port returns [ractor, :aborted] even if the monitoring Ractor was aborted.
assert_equal 'true', %q{
r = Ractor.new do
raise 'ok'
@@ -2770,7 +2770,7 @@ def initialize(a)
end
r.monitor port = Ractor::Port.new
- port.receive == :aborted
+ port.receive == [r, :aborted]
}
assert_equal 'ok', %q{
diff --git a/ext/erb/escape/escape.c b/ext/erb/escape/escape.c
index 66cb77ed332a7d..1268aa7b34e020 100644
--- a/ext/erb/escape/escape.c
+++ b/ext/erb/escape/escape.c
@@ -172,11 +172,93 @@ find_next_match_neon(search_state *search)
// uint64_t >>= 64 is undefined behaviour
RUBY_ASSERT(trailing_zeros < 64);
search->matches_bitmap >>= trailing_zeros;
- search->cstr += trailing_zeros / 4;
+ search->cstr += trailing_zeros;
RUBY_ASSERT(search->cstr <= search->end);
return true;
}
+// This 16-byte lookup table is indexed into by using the
+// low nibble of each input byte.
+// Note: index 0 is intentionally set to a character that will not match
+// the NULL byte.
+static const uint8x16_t escape_char_by_low_nibble = {
+ '\'', 0, '"', 0,
+ 0, 0, '&', '\'',
+ 0, 0, 0, 0,
+ '<', 0, '>', 0,
+};
+
+static inline uint8x16_t
+neon_escape_matches(const uint8x16_t bytes)
+{
+ // An example to demonstrate how this works. The goal is to get a uint8x16_t
+ // with each lane to equal 0xFF if the corresponding byte in 'bytes' needs
+ // to be escaped, or 0x00 otherwise.
+ //
+ // To keep things very simple, I'm going to assume a vector of length 6, in
+ // reality, the vector would be 16 bytes wide.
+ //
+ // Assume the string is: "
"
+ // Converted to integers:
+ // [0x3c 0x62 0x72 0x20 0x2f 0x3e]
+ //
+ // Next, we mask off the top nibble so we are left only with the low nibble
+ // of each byte. We do this by AND'ing each byte with 0x0F.
+ //
+ // The result:
+ // [0x0c 0x02 0x02 0x00 0x0f 0x0e]
+ //
+ // Now, we use these low nibbles as indexes into the
+ // escape_char_by_low_nibble array and find the full byte
+ // value we expect to match in the input.
+ //
+ // The result:
+ // [0x3c 0x22 0x22 0x27 0x00 0x3e]
+ //
+ // Finally, we compare the bytes we expect with the actual input bytes.
+ //
+ // The result:
+ // [0xFF 0x00 0x00 0x00 0x00 0xFF]
+ const uint8x16_t low_nibbles = vandq_u8(bytes, vdupq_n_u8(0x0F));
+ const uint8x16_t looked_up = vqtbl1q_u8(escape_char_by_low_nibble, low_nibbles);
+ return vceqq_u8(looked_up, bytes);
+}
+
+static inline uint64_t
+neon_matches_to_bitmap16(const uint8x16_t matches)
+{
+ static const uint8x16_t bit_mask = {
+ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
+ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
+ };
+
+ uint8x16_t folded = vandq_u8(matches, bit_mask);
+ folded = vpaddq_u8(folded, folded);
+ folded = vpaddq_u8(folded, folded);
+ folded = vpaddq_u8(folded, folded);
+
+ return vgetq_lane_u16(vreinterpretq_u16_u8(folded), 0);
+}
+
+static inline uint64_t
+neon_matches_to_bitmap64(const uint8x16_t m0, const uint8x16_t m1, const uint8x16_t m2, const uint8x16_t m3)
+{
+ static const uint8x16_t bit_mask = {
+ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
+ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
+ };
+
+ const uint8x16_t t0 = vandq_u8(m0, bit_mask);
+ const uint8x16_t t1 = vandq_u8(m1, bit_mask);
+ const uint8x16_t t2 = vandq_u8(m2, bit_mask);
+ const uint8x16_t t3 = vandq_u8(m3, bit_mask);
+
+ uint8x16_t folded = vpaddq_u8(vpaddq_u8(t0, t1), vpaddq_u8(t2, t3));
+ folded = vpaddq_u8(folded, folded);
+
+ return vgetq_lane_u64(vreinterpretq_u64_u8(folded), 0);
+}
+
static inline bool
find_next_neon(search_state *search)
{
@@ -184,30 +266,34 @@ find_next_neon(search_state *search)
return find_next_match_neon(search);
}
- const uint8x16_t single_quote = vdupq_n_u8('\'');
- const uint8x16_t double_quote = vdupq_n_u8('"');
- const uint8x16_t ampersand = vdupq_n_u8('&');
- const uint8x16_t lt = vdupq_n_u8('<');
- const uint8x16_t gt = vdupq_n_u8('>');
+ while ((size_t)(search->end - search->cstr) >= sizeof(uint8x16x4_t)) {
+ const uint8x16_t bytes0 = vld1q_u8(search->cstr + 0);
+ const uint8x16_t bytes1 = vld1q_u8(search->cstr + 16);
+ const uint8x16_t bytes2 = vld1q_u8(search->cstr + 32);
+ const uint8x16_t bytes3 = vld1q_u8(search->cstr + 48);
- while ((size_t)(search->end - search->cstr) >= sizeof(uint8x16_t)) {
- const uint8x16_t bytes = vld1q_u8(search->cstr);
- const uint8x16_t match1 = vceqq_u8(bytes, single_quote);
- const uint8x16_t match2 = vceqq_u8(bytes, double_quote);
- const uint8x16_t match3 = vceqq_u8(bytes, ampersand);
- const uint8x16_t match4 = vceqq_u8(bytes, lt);
- const uint8x16_t match5 = vceqq_u8(bytes, gt);
+ const uint8x16_t m0 = neon_escape_matches(bytes0);
+ const uint8x16_t m1 = neon_escape_matches(bytes1);
+ const uint8x16_t m2 = neon_escape_matches(bytes2);
+ const uint8x16_t m3 = neon_escape_matches(bytes3);
- const uint8x16_t mask1 = vorrq_u8(match1, match2);
- const uint8x16_t mask2 = vorrq_u8(match3, match4);
- const uint8x16_t mask3 = vorrq_u8(mask1, match5);
- const uint8x16_t matches = vorrq_u8(mask2, mask3);
+ const uint64_t bitmap = neon_matches_to_bitmap64(m0, m1, m2, m3);
- const uint8x8_t res = vshrn_n_u16(vreinterpretq_u16_u8(matches), 4);
- const uint64_t bitmap = vget_lane_u64(vreinterpret_u64_u8(res), 0);
+ if (bitmap) {
+ search->matches_bitmap = bitmap;
+ return find_next_match_neon(search);
+ }
+
+ search->cstr += 64;
+ }
+
+ while ((size_t)(search->end - search->cstr) >= sizeof(uint8x16_t)) {
+ const uint8x16_t bytes = vld1q_u8(search->cstr);
+ const uint8x16_t matches = neon_escape_matches(bytes);
+ const uint64_t bitmap = neon_matches_to_bitmap16(matches);
if (bitmap) {
- search->matches_bitmap = bitmap & 0x8888888888888888ull;
+ search->matches_bitmap = bitmap;
return find_next_match_neon(search);
}
search->cstr += sizeof(uint8x16_t);
diff --git a/ext/io/wait/depend b/ext/io/wait/depend
deleted file mode 100644
index 82111bdb888124..00000000000000
--- a/ext/io/wait/depend
+++ /dev/null
@@ -1,3 +0,0 @@
-# AUTOGENERATED DEPENDENCIES START
-wait.o: wait.c
-# AUTOGENERATED DEPENDENCIES END
diff --git a/ext/io/wait/extconf.rb b/ext/io/wait/extconf.rb
deleted file mode 100644
index 00c455a45c8f6f..00000000000000
--- a/ext/io/wait/extconf.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: false
-require 'mkmf'
-
-create_makefile("io/wait")
diff --git a/ext/io/wait/io-wait.gemspec b/ext/io/wait/io-wait.gemspec
index c1c6172589efc5..147de511ba5153 100644
--- a/ext/io/wait/io-wait.gemspec
+++ b/ext/io/wait/io-wait.gemspec
@@ -1,4 +1,4 @@
-_VERSION = "0.4.0"
+_VERSION = "999.999.999"
Gem::Specification.new do |spec|
spec.name = "io-wait"
@@ -6,20 +6,18 @@ Gem::Specification.new do |spec|
spec.authors = ["Nobu Nakada", "Charles Oliver Nutter"]
spec.email = ["nobu@ruby-lang.org", "headius@headius.com"]
- spec.summary = %q{Waits until IO is readable or writable without blocking.}
- spec.description = %q{Waits until IO is readable or writable without blocking.}
+ spec.summary = %q{Deprecated: All functionality ships with Ruby 3.2 and higher.}
+ spec.description = %q{Deprecated: All functionality ships with Ruby 3.2 and higher.}
spec.homepage = "https://github.com/ruby/io-wait"
spec.licenses = ["Ruby", "BSD-2-Clause"]
- spec.required_ruby_version = Gem::Requirement.new(">= 3.2")
+ spec.required_ruby_version = Gem::Requirement.new(">= 4.1")
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = spec.homepage
- jruby = true if Gem::Platform.new('java') =~ spec.platform or RUBY_ENGINE == 'jruby'
dir, gemspec = File.split(__FILE__)
excludes = [
- *%w[:^/.git* :^/Gemfile* :^/Rakefile* :^/bin/ :^/test/ :^/rakelib/ :^*.java],
- *(jruby ? %w[:^/ext/io] : %w[:^/ext/java]),
+ *%w[:^/.git* :^/Gemfile* :^/Rakefile* :^/bin/ :^/test/ :^/rakelib/],
":(exclude,literal,top)#{gemspec}"
]
files = IO.popen(%w[git ls-files -z --] + excludes, chdir: dir, &:read).split("\x0")
@@ -28,12 +26,4 @@ Gem::Specification.new do |spec|
spec.bindir = "exe"
spec.executables = []
spec.require_paths = ["lib"]
-
- if jruby
- spec.platform = 'java'
- spec.files << "lib/io/wait.jar"
- spec.require_paths += ["ext/java/lib"]
- else
- spec.extensions = %w[ext/io/wait/extconf.rb]
- end
end
diff --git a/ext/io/wait/wait.c b/ext/io/wait/wait.c
deleted file mode 100644
index f7575191fedf2d..00000000000000
--- a/ext/io/wait/wait.c
+++ /dev/null
@@ -1,23 +0,0 @@
-/* -*- c-file-style: "ruby"; indent-tabs-mode: t -*- */
-/**********************************************************************
-
- io/wait.c -
-
- $Author$
- created at: Tue Aug 28 09:08:06 JST 2001
-
- All the files in this distribution are covered under the Ruby's
- license (see the file COPYING).
-
-**********************************************************************/
-
-#include "ruby.h" /* abi_version */
-
-/*
- * IO wait methods are built in ruby now, just for backward compatibility.
- */
-
-void
-Init_wait(void)
-{
-}
diff --git a/gc/default/default.c b/gc/default/default.c
index 0eae80af6f403f..776eb818cbc7b7 100644
--- a/gc/default/default.c
+++ b/gc/default/default.c
@@ -158,6 +158,10 @@ rb_hrtime_sub(rb_hrtime_t a, rb_hrtime_t b)
#ifndef GC_HEAP_FREE_SLOTS
#define GC_HEAP_FREE_SLOTS 4096
#endif
+#ifndef GC_RACTOR_HEAP_INIT_BYTES
+/* 0 is resolved at boot to the smallest size that works. */
+#define GC_RACTOR_HEAP_INIT_BYTES 0
+#endif
#ifndef GC_HEAP_GROWTH_FACTOR
#define GC_HEAP_GROWTH_FACTOR 1.8
#endif
@@ -250,6 +254,7 @@ static RB_THREAD_LOCAL_SPECIFIER int malloc_increase_local;
typedef struct {
size_t heap_init_bytes;
+ size_t ractor_heap_init_bytes;
size_t heap_free_slots;
double growth_factor;
size_t growth_max_bytes;
@@ -271,6 +276,7 @@ typedef struct {
static ruby_gc_params_t gc_params = {
GC_HEAP_INIT_BYTES,
+ GC_RACTOR_HEAP_INIT_BYTES,
GC_HEAP_FREE_SLOTS,
GC_HEAP_GROWTH_FACTOR,
GC_HEAP_GROWTH_MAX_BYTES,
@@ -933,6 +939,14 @@ static const size_t pool_slot_sizes[HEAP_COUNT] = {
#undef SLOT
};
+/* An init size below one slot in the largest heap never forces that heap's first
+ * page, and allocating there then fails with "cannot create a new page after GC". */
+static inline size_t
+heap_init_bytes_min(void)
+{
+ return pool_slot_sizes[HEAP_COUNT - 1];
+}
+
/* Precomputed reciprocals for fast slot index calculation.
* For slot size d: reciprocal = ceil(2^48 / d).
* Then offset / d == (uint32_t)((offset * reciprocal) >> 48)
@@ -2140,6 +2154,15 @@ heap_page_add_free_region(rb_objspace_t *objspace, struct heap_page *page, VALUE
gc_report(3, objspace, "heap_page_add_free_region: %p\n", (void *)obj);
}
+/* The initial size is per objspace, so a Ractor's own gets a smaller one than
+ * main's rather than paying main's again. */
+static inline size_t
+objspace_heap_init_bytes(const rb_objspace_t *objspace)
+{
+ return objspace == global_objspace->main_objspace
+ ? gc_params.heap_init_bytes : gc_params.ractor_heap_init_bytes;
+}
+
static void
heap_allocatable_bytes_expand(rb_objspace_t *objspace,
rb_heap_t *heap, size_t free_slots, size_t total_slots, size_t slot_size)
@@ -2151,7 +2174,7 @@ heap_allocatable_bytes_expand(rb_objspace_t *objspace,
target_total_slots = (size_t)(total_slots * gc_params.growth_factor);
}
else if (total_slots == 0) {
- target_total_slots = gc_params.heap_init_bytes / slot_size;
+ target_total_slots = objspace_heap_init_bytes(objspace) / slot_size;
}
else {
/* Find `f' where free_slots = f * total_slots * goal_ratio
@@ -2977,7 +3000,7 @@ heap_prepare(rb_objspace_t *objspace, rb_heap_t *heap)
{
GC_ASSERT(heap->free_pages == NULL);
- if (heap->total_slots < gc_params.heap_init_bytes / heap->slot_size &&
+ if (heap->total_slots < objspace_heap_init_bytes(objspace) / heap->slot_size &&
heap->sweeping_page == NULL) {
heap_page_allocate_and_initialize_force(objspace, heap);
GC_ASSERT(heap->free_pages != NULL);
@@ -5139,7 +5162,7 @@ gc_sweep_finish_heap(rb_objspace_t *objspace, rb_heap_t *heap)
size_t total_slots = heap->total_slots;
size_t swept_slots = heap->freed_slots + heap->empty_slots;
- size_t init_slots = gc_params.heap_init_bytes / heap->slot_size;
+ size_t init_slots = objspace_heap_init_bytes(objspace) / heap->slot_size;
size_t min_free_slots = (size_t)(MAX(total_slots, init_slots) * gc_params.heap_free_slots_min_ratio);
if (swept_slots < min_free_slots &&
@@ -5258,8 +5281,9 @@ gc_sweep_step(rb_objspace_t *objspace, rb_heap_t *heap)
heap->sweeping_page = ccan_list_next(&heap->pages, sweep_page, page_node);
- if (free_slots == sweep_page->total_slots) {
- /* There are no living objects, so move this page to the global empty pages. */
+ if (free_slots == sweep_page->total_slots && heap->total_pages > 1) {
+ /* There are no living objects, so move this page to the global empty pages.
+ * The last one stays: nothing grows a heap that has no pages at all. */
heap_unlink_page(objspace, heap, sweep_page);
sweep_page->start = 0;
@@ -7164,7 +7188,10 @@ gc_marks_finish(rb_objspace_t *objspace)
#endif
{
- const unsigned long ractor_cnt = rb_gc_vm_ractor_count();
+ /* Only this objspace's own Ractor allocates from it. The main objspace
+ * keeps the VM-wide count it has used since before per-Ractor GC. */
+ const unsigned long ractor_cnt = objspace == global_objspace->main_objspace
+ ? rb_gc_vm_ractor_count() : 1;
const unsigned long r_mul = ractor_cnt > 8 ? 8 : ractor_cnt; // upto 8
size_t total_slots = objspace_available_slots(objspace);
@@ -7182,7 +7209,7 @@ gc_marks_finish(rb_objspace_t *objspace)
/* Setup freeable slots. */
size_t total_init_slots = 0;
for (int i = 0; i < HEAP_COUNT; i++) {
- total_init_slots += (gc_params.heap_init_bytes / heaps[i].slot_size) * r_mul;
+ total_init_slots += (objspace_heap_init_bytes(objspace) / heaps[i].slot_size) * r_mul;
}
if (max_free_slots < total_init_slots) {
@@ -10184,17 +10211,51 @@ rb_gc_impl_gc_count(void *objspace_ptr)
return objspace->profile.count;
}
-static VALUE
-gc_info_decode(rb_objspace_t *objspace, const VALUE hash_or_key, const unsigned int orig_flags)
+/* Filled by setup_gc_latest_gc_info_symbols() at boot, not on first use. */
+static VALUE sym_major_by, sym_gc_by, sym_immediate_sweep, sym_have_finalizer, sym_state, sym_need_major_by;
+static VALUE sym_nofree, sym_oldgen, sym_shady, sym_force, sym_stress;
+#if RGENGC_ESTIMATE_OLDMALLOC
+static VALUE sym_oldmalloc;
+#endif
+static VALUE sym_newobj, sym_malloc, sym_method, sym_capi;
+static VALUE sym_none, sym_marking, sym_sweeping;
+static VALUE sym_weak_references_count;
+
+static void
+setup_gc_latest_gc_info_symbols(void)
{
- static VALUE sym_major_by = Qnil, sym_gc_by, sym_immediate_sweep, sym_have_finalizer, sym_state, sym_need_major_by;
- static VALUE sym_nofree, sym_oldgen, sym_shady, sym_force, sym_stress;
+#define S(s) sym_##s = ID2SYM(rb_intern_const(#s))
+ S(major_by);
+ S(gc_by);
+ S(immediate_sweep);
+ S(have_finalizer);
+ S(state);
+ S(need_major_by);
+
+ S(stress);
+ S(nofree);
+ S(oldgen);
+ S(shady);
+ S(force);
#if RGENGC_ESTIMATE_OLDMALLOC
- static VALUE sym_oldmalloc;
+ S(oldmalloc);
#endif
- static VALUE sym_newobj, sym_malloc, sym_method, sym_capi;
- static VALUE sym_none, sym_marking, sym_sweeping;
- static VALUE sym_weak_references_count;
+ S(newobj);
+ S(malloc);
+ S(method);
+ S(capi);
+
+ S(none);
+ S(marking);
+ S(sweeping);
+
+ S(weak_references_count);
+#undef S
+}
+
+static VALUE
+gc_info_decode(rb_objspace_t *objspace, const VALUE hash_or_key, const unsigned int orig_flags)
+{
VALUE hash = Qnil, key = Qnil;
VALUE major_by, need_major_by;
unsigned int flags = orig_flags ? orig_flags : objspace->profile.latest_gc_info;
@@ -10209,36 +10270,6 @@ gc_info_decode(rb_objspace_t *objspace, const VALUE hash_or_key, const unsigned
rb_bug("gc_info_decode: non-hash or symbol given");
}
- if (NIL_P(sym_major_by)) {
-#define S(s) sym_##s = ID2SYM(rb_intern_const(#s))
- S(major_by);
- S(gc_by);
- S(immediate_sweep);
- S(have_finalizer);
- S(state);
- S(need_major_by);
-
- S(stress);
- S(nofree);
- S(oldgen);
- S(shady);
- S(force);
-#if RGENGC_ESTIMATE_OLDMALLOC
- S(oldmalloc);
-#endif
- S(newobj);
- S(malloc);
- S(method);
- S(capi);
-
- S(none);
- S(marking);
- S(sweeping);
-
- S(weak_references_count);
-#undef S
- }
-
#define SET(name, attr) \
if (key == sym_##name) \
return (attr); \
@@ -10362,56 +10393,54 @@ static VALUE gc_stat_symbols[gc_stat_sym_last];
static void
setup_gc_stat_symbols(void)
{
- if (gc_stat_symbols[0] == 0) {
#define S(s) gc_stat_symbols[gc_stat_sym_##s] = ID2SYM(rb_intern_const(#s))
- S(count);
- S(time);
- S(marking_time),
- S(sweeping_time),
- S(heap_allocated_pages);
- S(heap_empty_pages);
- S(heap_allocatable_bytes);
- S(heap_available_slots);
- S(heap_live_slots);
- S(heap_free_slots);
- S(heap_final_slots);
- S(heap_marked_slots);
- S(heap_eden_pages);
- S(total_allocated_pages);
- S(total_freed_pages);
- S(total_allocated_objects);
- S(total_freed_objects);
- S(total_malloc_bytes);
- S(total_free_bytes);
- S(malloc_increase_bytes);
- S(malloc_increase_bytes_limit);
- S(minor_gc_count);
- S(major_gc_count);
- S(compact_count);
- S(read_barrier_faults);
- S(total_moved_objects);
- S(remembered_wb_unprotected_objects);
- S(remembered_wb_unprotected_objects_limit);
- S(old_objects);
- S(old_objects_limit);
+ S(count);
+ S(time);
+ S(marking_time),
+ S(sweeping_time),
+ S(heap_allocated_pages);
+ S(heap_empty_pages);
+ S(heap_allocatable_bytes);
+ S(heap_available_slots);
+ S(heap_live_slots);
+ S(heap_free_slots);
+ S(heap_final_slots);
+ S(heap_marked_slots);
+ S(heap_eden_pages);
+ S(total_allocated_pages);
+ S(total_freed_pages);
+ S(total_allocated_objects);
+ S(total_freed_objects);
+ S(total_malloc_bytes);
+ S(total_free_bytes);
+ S(malloc_increase_bytes);
+ S(malloc_increase_bytes_limit);
+ S(minor_gc_count);
+ S(major_gc_count);
+ S(compact_count);
+ S(read_barrier_faults);
+ S(total_moved_objects);
+ S(remembered_wb_unprotected_objects);
+ S(remembered_wb_unprotected_objects_limit);
+ S(old_objects);
+ S(old_objects_limit);
#if RGENGC_ESTIMATE_OLDMALLOC
- S(oldmalloc_increase_bytes);
- S(oldmalloc_increase_bytes_limit);
+ S(oldmalloc_increase_bytes);
+ S(oldmalloc_increase_bytes_limit);
#endif
#if RGENGC_PROFILE
- S(total_generated_normal_object_count);
- S(total_generated_shady_object_count);
- S(total_shade_operation_count);
- S(total_promoted_count);
- S(total_remembered_normal_object_count);
- S(total_remembered_shady_object_count);
+ S(total_generated_normal_object_count);
+ S(total_generated_shady_object_count);
+ S(total_shade_operation_count);
+ S(total_promoted_count);
+ S(total_remembered_normal_object_count);
+ S(total_remembered_shady_object_count);
#endif /* RGENGC_PROFILE */
- S(page_pool_arenas);
- S(page_pool_arenas_freed);
- S(page_pool_total_pages);
- S(page_pool_discarded_pages);
+ S(page_pool_arenas);
+ S(page_pool_arenas_freed);
+ S(page_pool_total_pages);
+ S(page_pool_discarded_pages);
#undef S
- }
}
static uint64_t
@@ -10428,8 +10457,6 @@ rb_gc_impl_stat(void *objspace_ptr, VALUE hash_or_sym)
rb_objspace_t *objspace = objspace_ptr;
VALUE hash = Qnil, key = Qnil;
- setup_gc_stat_symbols();
-
malloc_increase_local_flush(objspace);
if (RB_TYPE_P(hash_or_sym, T_HASH)) {
@@ -10552,22 +10579,20 @@ static VALUE gc_stat_heap_symbols[gc_stat_heap_sym_last];
static void
setup_gc_stat_heap_symbols(void)
{
- if (gc_stat_heap_symbols[0] == 0) {
#define S(s) gc_stat_heap_symbols[gc_stat_heap_sym_##s] = ID2SYM(rb_intern_const(#s))
- S(slot_size);
- S(heap_live_slots);
- S(heap_free_slots);
- S(heap_final_slots);
- S(heap_eden_pages);
- S(heap_eden_slots);
- S(heap_allocatable_slots);
- S(total_allocated_pages);
- S(force_major_gc_count);
- S(force_incremental_marking_finish_count);
- S(total_allocated_objects);
- S(total_freed_objects);
+ S(slot_size);
+ S(heap_live_slots);
+ S(heap_free_slots);
+ S(heap_final_slots);
+ S(heap_eden_pages);
+ S(heap_eden_slots);
+ S(heap_allocatable_slots);
+ S(total_allocated_pages);
+ S(force_major_gc_count);
+ S(force_incremental_marking_finish_count);
+ S(total_allocated_objects);
+ S(total_freed_objects);
#undef S
- }
}
static VALUE
@@ -10606,8 +10631,6 @@ rb_gc_impl_stat_heap(void *objspace_ptr, VALUE heap_name, VALUE hash_or_sym)
{
rb_objspace_t *objspace = objspace_ptr;
- setup_gc_stat_heap_symbols();
-
if (NIL_P(heap_name)) {
if (!RB_TYPE_P(hash_or_sym, T_HASH)) {
rb_bug("non-hash given");
@@ -10850,7 +10873,10 @@ rb_gc_impl_set_params(void *objspace_ptr)
rb_objspace_t *objspace = objspace_ptr;
get_envparam_size("RUBY_GC_HEAP_FREE_SLOTS", &gc_params.heap_free_slots, 0);
- get_envparam_size("RUBY_GC_HEAP_INIT_BYTES", &gc_params.heap_init_bytes, 0);
+ get_envparam_size("RUBY_GC_HEAP_INIT_BYTES", &gc_params.heap_init_bytes,
+ heap_init_bytes_min() - 1);
+ get_envparam_size("RUBY_GC_RACTOR_HEAP_INIT_BYTES", &gc_params.ractor_heap_init_bytes,
+ heap_init_bytes_min() - 1);
get_envparam_double("RUBY_GC_HEAP_GROWTH_FACTOR", &gc_params.growth_factor, 1.0, 0.0, FALSE);
get_envparam_size ("RUBY_GC_HEAP_GROWTH_MAX_BYTES", &gc_params.growth_max_bytes, 0);
@@ -12697,6 +12723,8 @@ rb_gc_impl_objspace_init(void *objspace_ptr)
heap_page_alloc_use_mmap = INIT_HEAP_PAGE_ALLOC_USE_MMAP;
#endif
gc_params.heap_init_bytes = GC_HEAP_INIT_BYTES;
+ gc_params.ractor_heap_init_bytes = GC_RACTOR_HEAP_INIT_BYTES ? GC_RACTOR_HEAP_INIT_BYTES
+ : heap_init_bytes_min();
}
// GC.measure_total_time= sets the caller's objspace only; a new Ractor's follows
// its creator's, which is the objspace running this init (main starts it on).
@@ -12721,6 +12749,13 @@ rb_gc_impl_objspace_init(void *objspace_ptr)
void
rb_gc_impl_init(void)
{
+ /* Fill the symbol tables here, where no other ractor exists yet: they used to
+ * be filled on first use, guarded by their own first element, so a second
+ * ractor could see a half-filled table and GC.stat raised on the rest. */
+ setup_gc_stat_symbols();
+ setup_gc_stat_heap_symbols();
+ setup_gc_latest_gc_info_symbols();
+
VALUE gc_constants = rb_hash_new();
rb_hash_aset(gc_constants, ID2SYM(rb_intern("DEBUG")), GC_DEBUG ? Qtrue : Qfalse);
/* Minimum slot size that fits a standard RVALUE */
diff --git a/io.c b/io.c
index d446991c281577..83ecb651bef351 100644
--- a/io.c
+++ b/io.c
@@ -16070,4 +16070,17 @@ Init_IO(void)
sym_wait_writable = ID2SYM(rb_intern_const("wait_writable"));
}
+static void init_builtin_io(void);
+#define Init_builtin_io init_builtin_io
#include "io.rbinc"
+#undef Init_builtin_io
+
+void
+Init_builtin_io(void)
+{
+ init_builtin_io();
+
+ /* Init_IO is called earlier than `loaded_features` is initialized */
+ rb_provide("io/wait.rb");
+ rb_provide("io/wait.so");
+}
diff --git a/ractor.rb b/ractor.rb
index 48afd71a56fba0..bea2480798ffeb 100644
--- a/ractor.rb
+++ b/ractor.rb
@@ -314,38 +314,48 @@ def self.count
def self.select(*ports, timeout: nil)
raise ArgumentError, 'specify at least one Ractor::Port or Ractor' if ports.empty?
- monitors = {} # Ractor::Port => Ractor
-
- ports = ports.map do |arg|
- case arg
- when Ractor
- port = Ractor::Port.new
- monitors[port] = arg
- arg.monitor port
- port
- when Ractor::Port
- arg
- else
- raise ArgumentError, "should be Ractor::Port or Ractor"
- end
- end
+ monitored = []
+ others = []
+ mp = nil
begin
- result = __builtin_ractor_select_internal(ports, timeout)
- return nil if result.nil? # timed out
+ ports.each do |arg|
+ case arg
+ when Ractor
+ monitored << arg
+ when Ractor::Port
+ others << arg
+ else
+ raise ArgumentError, "should be Ractor::Port or Ractor"
+ end
+ end
- result_port, obj = result
+ # One port for every watched ractor rather than one each: the exit token
+ # names the ractor, so there is nothing to look the result up in.
+ unless monitored.empty?
+ mp = Ractor::Port.new
+ monitored.each { |r| r.monitor mp }
+ end
- if r = monitors[result_port]
- [r, r.value]
+ if others.empty?
+ # Nothing but ractors: one port to wait on, so no selector is built.
+ token = mp.receive(timeout: timeout)
+ return nil if token.nil?
else
- [result_port, obj]
+ result = __builtin_ractor_select_internal(mp ? [mp, *others] : others, timeout)
+ return nil if result.nil?
+
+ port, obj = result
+ return [port, obj] unless port.equal?(mp)
+ token = obj
end
+
+ r = token[0]
+ [r, r.value]
ensure
- # close all ports for join
- monitors.each do |port, r|
- r.unmonitor port
- port.close
+ if mp
+ monitored.each { |r| r.unmonitor mp }
+ mp.close
end
end
end
@@ -599,7 +609,7 @@ def join
port = Port.new
self.monitor port
- if port.receive == :aborted
+ if port.receive[1] == :aborted
__builtin_ractor_value
end
@@ -632,23 +642,28 @@ def value
# call-seq:
# ractor.monitor(port) -> true or false
#
- # Registers the port as a monitoring port for this ractor. When the ractor terminates,
- # the port receives a Symbol object.
+ # Registers the port as a monitoring port for this ractor. When the ractor
+ # terminates, the port receives an Array naming the ractor and what happened to
+ # it, so that several ractors can report to one port.
+ #
+ # * [ractor, :exited] if the ractor terminated without an unhandled
+ # exception.
+ # * [ractor, :aborted] if it terminated by one.
#
- # * +:exited+ is sent if the ractor terminates without an unhandled exception.
- # * +:aborted+ is sent if the ractor terminates by an unhandled exception.
+ # The Array is built for the receiving ractor, so watching many ractors does not
+ # leave shareable objects behind.
#
# Returns +true+ if the monitor was registered (the ractor is still running).
# Returns +false+ if the ractor had already terminated; in that case the
- # termination message (+:exited+ or +:aborted+) is sent to the port immediately.
+ # termination message is sent to the port immediately.
#
# r = Ractor.new{ some_task() }
# r.monitor(port = Ractor::Port.new)
- # port.receive #=> :exited and r is terminated
+ # port.receive #=> [r, :exited]
#
# r = Ractor.new{ raise "foo" }
# r.monitor(port = Ractor::Port.new)
- # port.receive #=> :aborted and r is terminated by the RuntimeError "foo"
+ # port.receive #=> [r, :aborted]
#
def monitor port
__builtin_ractor_monitor(port)
diff --git a/ractor_sync.c b/ractor_sync.c
index fd19b6b3f39cd4..45c319d9c1c1c4 100644
--- a/ractor_sync.c
+++ b/ractor_sync.c
@@ -16,6 +16,7 @@ static VALUE rb_cRactorPort;
static VALUE ractor_receive(rb_execution_context_t *ec, const struct ractor_port *rp, const rb_hrtime_t *end);
static VALUE ractor_send(rb_execution_context_t *ec, const struct ractor_port *rp, VALUE obj, VALUE move);
static struct ractor_basket *ractor_basket_new_ref(VALUE shareable);
+static struct ractor_basket *ractor_basket_new_exit(VALUE sender, VALUE token);
static void ractor_send_basket(rb_execution_context_t *ec, const struct ractor_port *rp, struct ractor_basket *b, bool raise_on_error);
static void ractor_add_port(rb_ractor_t *r, st_data_t id);
@@ -235,6 +236,10 @@ enum ractor_basket_type {
basket_type_ref,
basket_type_copy,
basket_type_move,
+ /* A ractor's exit token. The pair it becomes is built on the receiving side,
+ * so a terminating ractor adds no shareable object to the vm, and building it
+ * needs neither a tag nor a courier -- which the sender has no stack for. */
+ basket_type_exit,
};
struct ractor_basket {
@@ -291,6 +296,9 @@ ractor_basket_mark(const struct ractor_basket *b)
/* Marshaled bytes are off-heap and hold nothing to mark. */
rb_gc_mark(b->p.v);
}
+
+ /* An exit token names a ractor that may be reachable from nothing else. */
+ rb_gc_mark(b->sender);
}
static void
@@ -770,10 +778,12 @@ ractor_mark_monitors(rb_ractor_t *r)
}
}
+/* Paired with the ractor it is about when it is received, so that many ractors
+ * can report to one port. */
static VALUE
-ractor_exit_token(bool exc)
+ractor_exit_token(const rb_ractor_t *r)
{
- if (exc) {
+ if (r->sync.legacy_exc) {
RUBY_DEBUG_LOG("aborted");
return ID2SYM(idAborted);
}
@@ -807,7 +817,7 @@ ractor_monitor(rb_execution_context_t *ec, VALUE self, VALUE port)
if (terminated) {
SIZED_FREE(rm);
- ractor_port_send(ec, port, ractor_exit_token(r->sync.legacy_exc), Qfalse);
+ ractor_send_basket(ec, rp, ractor_basket_new_exit(self, ractor_exit_token(r)), false);
return Qfalse;
}
@@ -867,14 +877,14 @@ ractor_notify_exit(rb_execution_context_t *ec, rb_ractor_t *cr, VALUE legacy, bo
static void
ractor_send_exit_tokens(rb_execution_context_t *ec, rb_ractor_t *cr)
{
- VALUE token = ractor_exit_token(cr->sync.legacy_exc);
+ VALUE token = ractor_exit_token(cr);
struct ractor_monitor *rm, *nxt;
ccan_list_for_each_safe(&cr->sync.monitors, rm, nxt, node)
{
RUBY_DEBUG_LOG("port:%u@r%u", (unsigned int)ractor_port_id(&rm->port), (unsigned int)rb_ractor_id(rm->port.r));
- ractor_send_basket(ec, &rm->port, ractor_basket_new_ref(token), false);
+ ractor_send_basket(ec, &rm->port, ractor_basket_new_exit(cr->pub.self, token), false);
ccan_list_del(&rm->node);
SIZED_FREE(rm);
@@ -1208,6 +1218,9 @@ ractor_basket_value(struct ractor_basket *b)
switch (b->type) {
case basket_type_ref:
break;
+ case basket_type_exit:
+ /* Allocated here, in the receiving ractor: a copy, not a shared object. */
+ return rb_ary_new_from_args(2, b->sender, b->p.v);
case basket_type_copy: {
/* An off-heap copy courier rebuilds exactly like a move one; only the sources
* differ (still alive here, already shells there). */
@@ -1347,6 +1360,7 @@ basket_type_name(enum ractor_basket_type type)
case basket_type_ref: return "ref";
case basket_type_copy: return "copy";
case basket_type_move: return "move";
+ case basket_type_exit: return "exit";
}
VM_ASSERT(0);
return NULL;
@@ -1676,6 +1690,20 @@ ractor_basket_new_ref(VALUE shareable)
return b;
}
+/* sender is the ractor the token is about; both it and the token are shareable,
+ * so nothing is copied until the receiver builds the pair. */
+static struct ractor_basket *
+ractor_basket_new_exit(VALUE sender, VALUE token)
+{
+ struct ractor_basket *b = ractor_basket_alloc();
+
+ b->type = basket_type_exit;
+ b->sender = sender;
+ b->p.v = token;
+
+ return b;
+}
+
static VALUE
ractor_send0(rb_execution_context_t *ec, const struct ractor_port *rp, VALUE obj, VALUE move, bool raise_on_error)
{
diff --git a/spec/ruby/core/kernel/require_spec.rb b/spec/ruby/core/kernel/require_spec.rb
index 1c223c6aa3d8ae..078e97209cec58 100644
--- a/spec/ruby/core/kernel/require_spec.rb
+++ b/spec/ruby/core/kernel/require_spec.rb
@@ -22,7 +22,7 @@
provided += %w[set pathname]
end
ruby_version_is "4.1" do
- provided += %w[monitor]
+ provided += %w[monitor io/wait]
end
out = ruby_exe("puts $LOADED_FEATURES", options: '--disable-gems --disable-did-you-mean')
diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb
index 44273d19183698..97b1d8b4f498c1 100644
--- a/test/ruby/test_ractor.rb
+++ b/test/ruby/test_ractor.rb
@@ -751,7 +751,7 @@ def test_unmonitor_does_not_remove_other_ractors_monitor
b = Ractor.new(target) do |t|
t.monitor(p = Ractor::Port.new)
Ractor.main << :ready
- p.receive
+ p.receive == [t, :exited]
end
Ractor.receive # b's monitor is registered
@@ -765,7 +765,7 @@ def test_unmonitor_does_not_remove_other_ractors_monitor
end
assert_equal :ok, a.value
- assert_equal :exited, b.value
+ assert_equal true, b.value
RUBY
end