diff --git a/.github/workflows/zjit-macos.yml b/.github/workflows/zjit-macos.yml index 224f8cbb556b0a..c99689b00ae77f 100644 --- a/.github/workflows/zjit-macos.yml +++ b/.github/workflows/zjit-macos.yml @@ -93,7 +93,7 @@ jobs: rustup install ${{ matrix.rust_version }} --profile minimal rustup default ${{ matrix.rust_version }} - - uses: taiki-e/install-action@328a871ad8f62ecac78390391f463ccabc974b72 # v2.69.9 + - uses: taiki-e/install-action@7627fb428e65e78e2ec9a24ae5c5bd5f8553f182 # v2.69.10 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/.github/workflows/zjit-ubuntu.yml b/.github/workflows/zjit-ubuntu.yml index 12d33ac67232ef..01c4f293e861a9 100644 --- a/.github/workflows/zjit-ubuntu.yml +++ b/.github/workflows/zjit-ubuntu.yml @@ -119,7 +119,7 @@ jobs: ruby-version: '3.1' bundler: none - - uses: taiki-e/install-action@328a871ad8f62ecac78390391f463ccabc974b72 # v2.69.9 + - uses: taiki-e/install-action@7627fb428e65e78e2ec9a24ae5c5bd5f8553f182 # v2.69.10 with: tool: nextest@0.9 if: ${{ matrix.test_task == 'zjit-check' }} diff --git a/include/ruby/internal/attr/nonstring.h b/include/ruby/internal/attr/nonstring.h index 5ad6ef2a86507e..da2501f7bfe8dd 100644 --- a/include/ruby/internal/attr/nonstring.h +++ b/include/ruby/internal/attr/nonstring.h @@ -27,7 +27,7 @@ # define RBIMPL_ATTR_NONSTRING() __attribute__((nonstring)) # if RBIMPL_COMPILER_SINCE(GCC, 15, 0, 0) # define RBIMPL_ATTR_NONSTRING_ARRAY() RBIMPL_ATTR_NONSTRING() -# elif RBIMPL_COMPILER_SINCE(Clang, 21, 0, 0) +# elif defined(__clang_major__) && __clang_major__ >= 21 # define RBIMPL_ATTR_NONSTRING_ARRAY() RBIMPL_ATTR_NONSTRING() # else # define RBIMPL_ATTR_NONSTRING_ARRAY() /* void */ diff --git a/lib/did_you_mean.rb b/lib/did_you_mean.rb index 74cd1760425510..640d910389764c 100644 --- a/lib/did_you_mean.rb +++ b/lib/did_you_mean.rb @@ -47,9 +47,9 @@ # # Did you mean? :foo # # -# == Disabling +did_you_mean+ +# == Disabling \DidYouMean # -# Occasionally, you may want to disable the +did_you_mean+ gem for e.g. +# Occasionally, you may want to disable the \DidYouMean gem for e.g. # debugging issues in the error object itself. You can disable it entirely by # specifying +--disable-did_you_mean+ option to the +ruby+ command: # diff --git a/template/Makefile.in b/template/Makefile.in index 9ed8705030bc4e..3413b56bb9c092 100644 --- a/template/Makefile.in +++ b/template/Makefile.in @@ -516,17 +516,11 @@ _PREFIXED_SYMBOL = TOKEN_PASTE($(SYMBOL_PREFIX),name) .d.h: @$(ECHO) translating probes $< - $(Q) $(DTRACE) -o $@.tmp -h -C $(INCFLAGS) `echo "$(CPPFLAGS)" | \ - sed -e "s/^/\n/" \ - -e ": loop" \ - -e " s/\n\(\('[^']*'\|[^ ']*\)*\)/\1\n/" \ - -e " /^\(-[DU]\|'-[DU]\).*/P" \ - -e " s/^..*\n/\n/" \ - -e " T" \ - -e " s/\n */\n/" \ - -e "t loop" \ - -e "s/.*//" \ - ` -s $< + $(Q) set x -o $@.tmp -h -C $(INCFLAGS); \ + for flag in $(CPPFLAGS); do case $$flag in -[DU]*) set "$$@" "$$flag";; esac; done; \ + shift; \ + $(Q1:0=:) set -x; \ + $(DTRACE) "$$@" -s $< $(Q) sed -e 's/RUBY_/RUBY_DTRACE_/g' -e 's/PROBES_H_TMP/RUBY_PROBES_H/' -e 's/(char \*/(const char */g' -e 's/, char \*/, const char */g' $@.tmp > $@ $(Q) $(RM) $@.tmp diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index eb5a8ca59e0ee6..00960dffee8d7f 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -84,6 +84,24 @@ def test_dump_strict assert_equal '"World"', "World".to_json(strict: true) end + def test_not_frozen + [ + [[], '[]'], + [{}, '{}'], + ["string", '"string"'], + [:sym, '"sym"'], + [1, '1'], + [1.0, '1.0'], + [true, 'true'], + [false, 'false'], + [nil, 'null'], + ].each do |(obj, exp)| + dumped = dump(obj, strict: true) + assert_equal exp, dumped + refute_predicate dumped, :frozen? + end + end + def test_state_depth_to_json depth = Object.new def depth.to_json(state) @@ -475,25 +493,27 @@ def foo.to_h assert_equal '2', state.indent end - def test_broken_bignum # [ruby-core:38867] - pid = fork do - x = 1 << 64 - x.class.class_eval do - def to_s - end + def test_broken_bignum # [Bug #5173] + bignum = 1 << 64 + bignum_to_s = bignum.to_s + + original_to_s = bignum.class.instance_method(:to_s) + bignum.class.class_eval do + def to_s + nil end - begin - JSON::Ext::Generator::State.new.generate(x) - exit 1 - rescue TypeError - exit 0 + alias_method :to_s, :to_s + end + case RUBY_PLATFORM + when "java" + assert_equal bignum_to_s, JSON.generate(bignum) + else + assert_raise(TypeError) do + JSON.generate(bignum) end end - _, status = Process.waitpid2(pid) - assert status.success? - rescue NotImplementedError - # forking to avoid modifying core class of a parent process and - # introducing race conditions of tests are run in parallel + ensure + bignum.class.define_method(:to_s, original_to_s) if original_to_s end def test_hash_likeness_set_symbol diff --git a/thread.c b/thread.c index 99252168defe3a..b13b06d7e5d94e 100644 --- a/thread.c +++ b/thread.c @@ -170,7 +170,7 @@ static inline void blocking_region_end(rb_thread_t *th, struct rb_blocking_regio #define THREAD_BLOCKING_BEGIN(th) do { \ struct rb_thread_sched * const sched = TH_SCHED(th); \ RB_VM_SAVE_MACHINE_CONTEXT(th); \ - thread_sched_to_waiting((sched), (th)); + thread_sched_to_waiting((sched), (th), true); #define THREAD_BLOCKING_END(th) \ thread_sched_to_running((sched), (th)); \ @@ -194,7 +194,7 @@ static inline void blocking_region_end(rb_thread_t *th, struct rb_blocking_regio /* Important that this is inlined into the macro, and not part of \ * blocking_region_begin - see bug #20493 */ \ RB_VM_SAVE_MACHINE_CONTEXT(th); \ - thread_sched_to_waiting(TH_SCHED(th), th); \ + thread_sched_to_waiting(TH_SCHED(th), th, false); \ exec; \ blocking_region_end(th, &__region); \ }; \ @@ -2092,7 +2092,7 @@ rb_thread_call_with_gvl(void *(*func)(void *), void *data1) int released = blocking_region_begin(th, brb, prev_unblock.func, prev_unblock.arg, FALSE); RUBY_ASSERT_ALWAYS(released); RB_VM_SAVE_MACHINE_CONTEXT(th); - thread_sched_to_waiting(TH_SCHED(th), th); + thread_sched_to_waiting(TH_SCHED(th), th, true); return r; } diff --git a/thread_none.c b/thread_none.c index 1f7492fda878b7..cb844148e1ff27 100644 --- a/thread_none.c +++ b/thread_none.c @@ -26,11 +26,11 @@ thread_sched_to_running(struct rb_thread_sched *sched, rb_thread_t *th) } static void -thread_sched_to_waiting(struct rb_thread_sched *sched, rb_thread_t *th) +thread_sched_to_waiting(struct rb_thread_sched *sched, rb_thread_t *th, bool yield_immediately) { } -#define thread_sched_to_dead thread_sched_to_waiting +#define thread_sched_to_dead(a,b) thread_sched_to_waiting(a,b,true) static void thread_sched_yield(struct rb_thread_sched *sched, rb_thread_t *th) diff --git a/thread_pthread.c b/thread_pthread.c index 91e53dc254b05b..6e3ce8ef871944 100644 --- a/thread_pthread.c +++ b/thread_pthread.c @@ -698,8 +698,12 @@ thread_sched_readyq_contain_p(struct rb_thread_sched *sched, rb_thread_t *th) { rb_thread_t *rth; ccan_list_for_each(&sched->readyq, rth, sched.node.readyq) { - if (rth == th) return true; + if (rth == th) { + VM_ASSERT(th->sched.node.is_ready); + return true; + } } + VM_ASSERT(!th->sched.node.is_ready); return false; } @@ -720,6 +724,8 @@ thread_sched_deq(struct rb_thread_sched *sched) } else { next_th = ccan_list_pop(&sched->readyq, rb_thread_t, sched.node.readyq); + VM_ASSERT(next_th->sched.node.is_ready); + next_th->sched.node.is_ready = false; VM_ASSERT(sched->readyq_cnt > 0); sched->readyq_cnt--; @@ -753,6 +759,7 @@ thread_sched_enq(struct rb_thread_sched *sched, rb_thread_t *ready_th) } ccan_list_add_tail(&sched->readyq, &ready_th->sched.node.readyq); + ready_th->sched.node.is_ready = true; sched->readyq_cnt++; } @@ -836,6 +843,30 @@ thread_sched_wait_running_turn(struct rb_thread_sched *sched, rb_thread_t *th, b VM_ASSERT(th == rb_ec_thread_ptr(rb_current_ec_noinline())); if (th != sched->running) { + // TODO: This optimization should also be made to work for MN_THREADS + if (th->has_dedicated_nt && th == sched->runnable_hot_th && (sched->running == NULL || sched->running->has_dedicated_nt)) { + RUBY_DEBUG_LOG("(nt) stealing: hot-th:%u. running:%u", rb_th_serial(th), rb_th_serial(sched->running)); + + // If there is a thread set to run, move it back to the front of the readyq + if (sched->running != NULL) { + rb_thread_t *running = sched->running; + VM_ASSERT(!thread_sched_readyq_contain_p(sched, running)); + running->sched.node.is_ready = true; + ccan_list_add(&sched->readyq, &running->sched.node.readyq); + sched->readyq_cnt++; + } + + // Pull off the ready queue and start running. + if (th->sched.node.is_ready) { + VM_ASSERT(thread_sched_readyq_contain_p(sched, th)); + ccan_list_del_init(&th->sched.node.readyq); + th->sched.node.is_ready = false; + sched->readyq_cnt--; + } + thread_sched_set_running(sched, th); + rb_ractor_thread_switch(th->ractor, th, false); + } + // already deleted from running threads // VM_ASSERT(!ractor_sched_running_threads_contain_p(th->vm, th)); // need locking @@ -852,6 +883,15 @@ thread_sched_wait_running_turn(struct rb_thread_sched *sched, rb_thread_t *th, b } thread_sched_set_locked(sched, th); + if (sched->runnable_hot_th != NULL && sched->runnable_hot_th_waiting) { + VM_ASSERT(sched->runnable_hot_th != th); + // Give the hot thread a chance to preempt, if it's actively spinning. + // On multicore, this reduces the rate of core-switching. On single-core it + // should mostly be a nop, since the other thread can't be concurrently spinning. + thread_sched_unlock(sched, th); + thread_sched_lock(sched, th); + } + RUBY_DEBUG_LOG("(nt) wakeup %s", sched->running == th ? "success" : "failed"); if (th == sched->running) { rb_ractor_thread_switch(th->ractor, th, false); @@ -900,6 +940,11 @@ thread_sched_wait_running_turn(struct rb_thread_sched *sched, rb_thread_t *th, b thread_sched_add_running_thread(sched, th); } + // Control transfer to the current thread is now complete. The original thread + // cannot steal control at this point. + sched->runnable_hot_th = NULL; + sched->runnable_hot_th_waiting = 0; + // VM_ASSERT(ractor_sched_running_threads_contain_p(th->vm, th)); need locking RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_RESUMED, th); } @@ -936,6 +981,13 @@ thread_sched_to_running_common(struct rb_thread_sched *sched, rb_thread_t *th) static void thread_sched_to_running(struct rb_thread_sched *sched, rb_thread_t *th) { + // We are reading and writing these sched fields without lock cover, but + // there are no correctness issues resulting from stale cache or delayed writeback. + // When it works, this causes the next-scheduled thread to yield the sched lock + // briefly so that we can grab it if we're still spinning (not descheduled yet). + if (sched->runnable_hot_th == th) { + sched->runnable_hot_th_waiting = 1; + } thread_sched_lock(sched, th); { thread_sched_to_running_common(sched, th); @@ -1000,13 +1052,17 @@ thread_sched_to_dead(struct rb_thread_sched *sched, rb_thread_t *th) // // This thread will run dedicated task (th->nt->dedicated++). static void -thread_sched_to_waiting_common(struct rb_thread_sched *sched, rb_thread_t *th) +thread_sched_to_waiting_common(struct rb_thread_sched *sched, rb_thread_t *th, bool yield_immediately) { RUBY_DEBUG_LOG("th:%u DNT:%d", rb_th_serial(th), th->nt->dedicated); RB_INTERNAL_THREAD_HOOK(RUBY_INTERNAL_THREAD_EVENT_SUSPENDED, th); native_thread_dedicated_inc(th->vm, th->ractor, th->nt); + if (!yield_immediately) { + sched->runnable_hot_th = th; + sched->runnable_hot_th_waiting = 0; + } thread_sched_wakeup_next_thread(sched, th, false); } @@ -1014,11 +1070,11 @@ thread_sched_to_waiting_common(struct rb_thread_sched *sched, rb_thread_t *th) // // This thread will run a dedicated task. static void -thread_sched_to_waiting(struct rb_thread_sched *sched, rb_thread_t *th) +thread_sched_to_waiting(struct rb_thread_sched *sched, rb_thread_t *th, bool yield_immediately) { thread_sched_lock(sched, th); { - thread_sched_to_waiting_common(sched, th); + thread_sched_to_waiting_common(sched, th, yield_immediately); } thread_sched_unlock(sched, th); } diff --git a/thread_pthread.h b/thread_pthread.h index cd93182480d7b3..36f2273d5bbe9d 100644 --- a/thread_pthread.h +++ b/thread_pthread.h @@ -56,6 +56,9 @@ struct rb_thread_sched_item { // connected to ractor->threads.sched.reqdyq // locked by ractor->threads.sched.lock struct ccan_list_node readyq; + // Indicates whether thread is on the readyq. + // There is no clear relationship between this and th->status. + bool is_ready; // connected to vm->ractor.sched.timeslice_threads // locked by vm->ractor.sched.lock @@ -128,6 +131,11 @@ struct rb_thread_sched { struct rb_thread_struct *lock_owner; #endif struct rb_thread_struct *running; // running thread or NULL + // Most recently running thread or NULL. If this thread wakes up before the newly running + // thread completes the transfer of control, it can interrupt and resume running. + // The new thread clears this field when it takes control. + struct rb_thread_struct *runnable_hot_th; + int runnable_hot_th_waiting; bool is_running; bool is_running_timeslice; bool enable_mn_threads; diff --git a/thread_win32.c b/thread_win32.c index 5de79751f91c6e..a2ce3b9d155afd 100644 --- a/thread_win32.c +++ b/thread_win32.c @@ -132,18 +132,22 @@ thread_sched_to_running(struct rb_thread_sched *sched, rb_thread_t *th) if (GVL_DEBUG) fprintf(stderr, "gvl acquire (%p): acquire\n", th); } -#define thread_sched_to_dead thread_sched_to_waiting - static void -thread_sched_to_waiting(struct rb_thread_sched *sched, rb_thread_t *th) +thread_sched_to_waiting(struct rb_thread_sched *sched, rb_thread_t *th, bool yield_immediately) { ReleaseMutex(sched->lock); } +static void +thread_sched_to_dead(struct rb_thread_sched *sched, rb_thread_t *th) +{ + thread_sched_to_waiting(sched, th, true); +} + static void thread_sched_yield(struct rb_thread_sched *sched, rb_thread_t *th) { - thread_sched_to_waiting(sched, th); + thread_sched_to_waiting(sched, th, true); native_thread_yield(); thread_sched_to_running(sched, th); }