From 0e0608754890319c18b39ef9ee55fa63ff6de789 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 10 Sep 2026 09:33:55 +0000 Subject: [PATCH 1/4] Do not measure with a per-thread CPU clock under the M:N scheduler assert_linear_performance() picks CLOCK_THREAD_CPUTIME_ID when it is available. That clock belongs to the native thread that reads it, and an M:N thread moves between native threads, so two reads around one measurement can come from different native threads and the elapsed time comes out negative: Expected 0 to be <= -2.2150775009999997. Reproduced with RUBY_MN_THREADS=2, 1500 fork/waitpid pairs per run: the Ruby thread changed native thread 1, 9 and 10 times over three runs, and the clock went backwards 1, 5 and 6 times, by as much as 0.38s. The default, =1 and =-1 never moved the main thread and never went backwards. CLOCK_PROCESS_CPUTIME_ID stayed monotonic across 15 such migrations. Prefer process CPU time when the M:N scheduler is on. This only changes which clock the assertion uses; it does not relax the assertion. Co-Authored-By: Claude Opus 5 (1M context) --- tool/lib/core_assertions.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tool/lib/core_assertions.rb b/tool/lib/core_assertions.rb index 8b229a25486c19..002c367e94b8e7 100644 --- a/tool/lib/core_assertions.rb +++ b/tool/lib/core_assertions.rb @@ -876,10 +876,15 @@ def assert_all_assertions_foreach(msg = nil, *keys, &block) end alias all_assertions_foreach assert_all_assertions_foreach - %w[ + clocks = %w[ CLOCK_THREAD_CPUTIME_ID CLOCK_PROCESS_CPUTIME_ID CLOCK_MONOTONIC - ].find do |c| + ] + # CLOCK_THREAD_CPUTIME_ID is per native thread, and an M:N thread moves + # between native threads, so it can go backwards and yield a negative + # elapsed time. Measure process CPU time there instead. + clocks.shift if RUBY_DESCRIPTION.include?("+MN") + clocks.find do |c| if Process.const_defined?(c) [c.to_sym, Process.const_get(c)].find do |clk| begin From 88208f946589ca709003db97d4949d47ce63d186 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 10 Sep 2026 09:39:33 +0000 Subject: [PATCH 2/4] [DOC] Process.clock_gettime: CLOCK_THREAD_CPUTIME_ID under M:N The clock belongs to the native thread that reads it. An M:N Ruby thread moves between native threads, so two reads taken from one Ruby thread can come from different native threads and the later one can be smaller. Co-Authored-By: Claude Opus 5 (1M context) --- process.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/process.c b/process.c index a94b1b4fced775..a31843f9314e81 100644 --- a/process.c +++ b/process.c @@ -8124,6 +8124,13 @@ ruby_real_ms_time(void) * +:CLOCK_MONOTONIC+, +:CLOCK_PROCESS_CPUTIME_ID+, * and +:CLOCK_THREAD_CPUTIME_ID+ are optional. * + * +:CLOCK_THREAD_CPUTIME_ID+ measures the native thread that reads it, not + * the Ruby thread. Under the M:N thread scheduler (see +RUBY_MN_THREADS+) a + * Ruby thread can move between native threads, so two reads taken from one + * Ruby thread may come from different native threads, and the later read can + * be smaller than the earlier one. To measure elapsed time there, use + * +:CLOCK_PROCESS_CPUTIME_ID+ or +:CLOCK_MONOTONIC+. + * * Certain emulations are used when the given +clock_id+ * is not supported directly: * From 3742091090a3ebc2187d4eca9c8abd141e026306 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 10 Sep 2026 10:00:28 +0000 Subject: [PATCH 3/4] Reinitialize jit_cont_lock in a forked child cont.c takes jit_cont_lock whenever an execution context is created or freed. cont_free() does it from a Ractor reclaiming its own dead root fiber, outside the VM lock, so fork() can land there. The child then inherits the lock held by a thread that no longer exists, and aborts in rb_jit_cont_finish(): [BUG] pthread_mutex_destroy: Device or resource busy (EBUSY) rb_thread_atfork_internal() already reinitializes the other locks another thread may have held at the fork; jit_cont_lock was missing. Co-Authored-By: Claude Opus 5 (1M context) --- thread.c | 1 + 1 file changed, 1 insertion(+) diff --git a/thread.c b/thread.c index b034522ea4adcd..ebaf61d963e075 100644 --- a/thread.c +++ b/thread.c @@ -5272,6 +5272,7 @@ rb_thread_atfork_internal(rb_thread_t *th, void (*atfork)(rb_thread_t *, const r rb_native_mutex_initialize(&th->interrupt_lock); rb_native_mutex_initialize(&vm->once_lock); rb_native_cond_initialize(&vm->once_cond); + rb_jit_cont_init(); // cont.c's jit_cont_lock, likewise rb_gc_zombie_objspaces_atfork(); rb_gc_atfork_global_locks(); rb_generic_fields_lock_atfork(); From 43b07929b0ce955130aeab9ab781ce35d5b0ce46 Mon Sep 17 00:00:00 2001 From: Schneems Date: Thu, 10 Sep 2026 12:36:11 -0500 Subject: [PATCH 4/4] [ruby/syntax_suggest] Fix SYNTAX_SUGGEST_DEBUG The code in the rescue block of the core extension wasn't exercised in test. It calls `$stderr.warn` which is a private method so it would error. This adds a test to exercise that code and fixes the no method error. https://github.com/ruby/syntax_suggest/commit/f28514fe92 --- lib/syntax_suggest/core_ext.rb | 4 +-- .../integration/ruby_command_line_spec.rb | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/syntax_suggest/core_ext.rb b/lib/syntax_suggest/core_ext.rb index ffbc922eedf172..3918c45d1edbab 100644 --- a/lib/syntax_suggest/core_ext.rb +++ b/lib/syntax_suggest/core_ext.rb @@ -33,8 +33,8 @@ def detailed_message(highlight: true, syntax_suggest: true, **kwargs) end rescue => e if ENV["SYNTAX_SUGGEST_DEBUG"] - $stderr.warn(e.message) - $stderr.warn(e.backtrace) + warn(e.message) + warn(e.backtrace) end # Ignore internal errors diff --git a/spec/syntax_suggest/integration/ruby_command_line_spec.rb b/spec/syntax_suggest/integration/ruby_command_line_spec.rb index 02354ceff00eea..761bd760b5d56a 100644 --- a/spec/syntax_suggest/integration/ruby_command_line_spec.rb +++ b/spec/syntax_suggest/integration/ruby_command_line_spec.rb @@ -185,5 +185,34 @@ class Dog expect(out).to include("Invalid break") end end + + it "SYNTAX_SUGGEST_DEBUG reports a rescued internal error instead of masking it" do + Dir.mktmpdir do |dir| + tmpdir = Pathname(dir) + + # Force `SyntaxSuggest.call` to raise so the rescue in `detailed_message` fires + monkeypatch = tmpdir.join("raise_monkeypatch.rb") + monkeypatch.write <<~EOM + require "syntax_suggest/api" + + module SyntaxSuggest + def self.call(*args, **kwargs) + raise "boom from monkeypatch" + end + end + EOM + + script = tmpdir.join("script.rb") + script.write <<~EOM + def lol + puts "haha" + EOM + + out = `SYNTAX_SUGGEST_DEBUG=1 #{ruby} -I#{lib_dir} -rsyntax_suggest -r#{monkeypatch} #{script} 2>&1` + + expect($?.success?).to be_falsey + expect(out).to include("boom from monkeypatch") + end + end end end