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