From 9ca169334003cff95803f36cd9280d1fc42823e8 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 19 Feb 2021 15:46:22 +0000 Subject: [PATCH 1/6] Name the async_cb_thread for easier debugging I found this useful while reviewing datadog/dd-trace-rb#1371 -- we were searching for specs that leaked threads, and saw a "mysterious" thread with no stack that we couldn't exactly point out what created it or why it existed. By setting a name on the callback thread, it becomes immediately obvious when looking at `Thread.list` what this thread is and where it comes from. (Thread naming has been around [since Ruby 2.3](https://github.com/ruby/ruby/blob/v2_3_0/NEWS) which means we can do it safely for every supported version) --- ext/ffi_c/Function.c | 2 ++ spec/ffi/function_spec.rb | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/ext/ffi_c/Function.c b/ext/ffi_c/Function.c index 305f8b180..5bd999583 100644 --- a/ext/ffi_c/Function.c +++ b/ext/ffi_c/Function.c @@ -310,6 +310,8 @@ function_init(VALUE self, VALUE rbFunctionInfo, VALUE rbProc) #if defined(DEFER_ASYNC_CALLBACK) if (async_cb_thread == Qnil) { async_cb_thread = rb_thread_create(async_cb_event, NULL); + // Name thread, for better debugging + rb_funcall(async_cb_thread, rb_intern("name="), 1, rb_str_new2("FFI::Function Callback Dispatcher")); } #endif diff --git a/spec/ffi/function_spec.rb b/spec/ffi/function_spec.rb index dd4d2eaaf..7e84858c2 100644 --- a/spec/ffi/function_spec.rb +++ b/spec/ffi/function_spec.rb @@ -21,6 +21,14 @@ module LibTest expect(fn.call).to eql 5 end + context 'when called with a block' do + it 'creates a thread for dispatching callbacks and sets its name' do + FFI::Function.new(:int, []) { 5 } # Trigger initialization + + expect(Thread.list.map(&:name)).to include("FFI::Function Callback Dispatcher") + end + end + it 'raises an error when passing a wrong signature' do expect { FFI::Function.new([], :int).new { } }.to raise_error TypeError end From 0ac0a4537f2afc670e00339bd3907698c05818e5 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 19 Feb 2021 16:20:06 +0000 Subject: [PATCH 2/6] Skip thread naming spec on TruffleRuby I'll submit this separately. --- spec/ffi/function_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/ffi/function_spec.rb b/spec/ffi/function_spec.rb index 7e84858c2..187d04bed 100644 --- a/spec/ffi/function_spec.rb +++ b/spec/ffi/function_spec.rb @@ -23,6 +23,8 @@ module LibTest context 'when called with a block' do it 'creates a thread for dispatching callbacks and sets its name' do + skip "not yet supported on TruffleRuby" if RUBY_ENGINE == "truffleruby" + FFI::Function.new(:int, []) { 5 } # Trigger initialization expect(Thread.list.map(&:name)).to include("FFI::Function Callback Dispatcher") From db93a49e046647a2537cac3992db0a4cb9bdb5ad Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Fri, 19 Feb 2021 17:52:17 +0000 Subject: [PATCH 3/6] Skip thread naming spec on JRuby I'll take a look at it separately as well. --- spec/ffi/function_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/ffi/function_spec.rb b/spec/ffi/function_spec.rb index 187d04bed..9939942a5 100644 --- a/spec/ffi/function_spec.rb +++ b/spec/ffi/function_spec.rb @@ -24,6 +24,7 @@ module LibTest context 'when called with a block' do it 'creates a thread for dispatching callbacks and sets its name' do skip "not yet supported on TruffleRuby" if RUBY_ENGINE == "truffleruby" + skip "not yet supported on JRuby" if RUBY_ENGINE == 'jruby' FFI::Function.new(:int, []) { 5 } # Trigger initialization From 0d1990bd304bf35ad02581bccb3c4cb5842f33f2 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Sun, 21 Feb 2021 12:29:02 +0000 Subject: [PATCH 4/6] Name callback runner thread for easier debugging Similar to what was done for the dispatcher thread. --- ext/ffi_c/Function.c | 6 ++++-- spec/ffi/async_callback_spec.rb | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/ext/ffi_c/Function.c b/ext/ffi_c/Function.c index 5bd999583..f68d1320f 100644 --- a/ext/ffi_c/Function.c +++ b/ext/ffi_c/Function.c @@ -310,7 +310,7 @@ function_init(VALUE self, VALUE rbFunctionInfo, VALUE rbProc) #if defined(DEFER_ASYNC_CALLBACK) if (async_cb_thread == Qnil) { async_cb_thread = rb_thread_create(async_cb_event, NULL); - // Name thread, for better debugging + /* Name thread, for better debugging */ rb_funcall(async_cb_thread, rb_intern("name="), 1, rb_str_new2("FFI::Function Callback Dispatcher")); } #endif @@ -527,7 +527,9 @@ async_cb_event(void* unused) rb_thread_call_without_gvl(async_cb_wait, &w, async_cb_stop, &w); if (w.cb != NULL) { /* Start up a new ruby thread to run the ruby callback */ - rb_thread_create(async_cb_call, w.cb); + VALUE new_thread = rb_thread_create(async_cb_call, w.cb); + /* Name thread, for better debugging */ + rb_funcall(new_thread, rb_intern("name="), 1, rb_str_new2("FFI::Function Callback Runner")); } } diff --git a/spec/ffi/async_callback_spec.rb b/spec/ffi/async_callback_spec.rb index 3d24369f5..02338e557 100644 --- a/spec/ffi/async_callback_spec.rb +++ b/spec/ffi/async_callback_spec.rb @@ -34,4 +34,15 @@ module LibTest expect(called).to be true expect(v).to eq(0x7fffffff) end + + it "sets the name of the thread that runs the callback" do + skip "not yet supported on TruffleRuby" if RUBY_ENGINE == "truffleruby" + skip "not yet supported on JRuby" if RUBY_ENGINE == "jruby" + + thread_name = nil + + LibTest.testAsyncCallback(proc { thread_name = Thread.current.name }, 0) + + expect(thread_name).to eq("FFI::Function Callback Runner") + end end From df411d08f6e444402bc4950b8622cf6b03ea0c96 Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Sun, 21 Feb 2021 12:56:22 +0000 Subject: [PATCH 5/6] Clarify that TruffleRuby/JRuby do not use a dispatcher thread --- spec/ffi/function_spec.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spec/ffi/function_spec.rb b/spec/ffi/function_spec.rb index 9939942a5..a7d6dc718 100644 --- a/spec/ffi/function_spec.rb +++ b/spec/ffi/function_spec.rb @@ -23,12 +23,10 @@ module LibTest context 'when called with a block' do it 'creates a thread for dispatching callbacks and sets its name' do - skip "not yet supported on TruffleRuby" if RUBY_ENGINE == "truffleruby" - skip "not yet supported on JRuby" if RUBY_ENGINE == 'jruby' - + skip 'this is MRI-specific' if RUBY_ENGINE == 'truffleruby' || RUBY_ENGINE == 'jruby' FFI::Function.new(:int, []) { 5 } # Trigger initialization - expect(Thread.list.map(&:name)).to include("FFI::Function Callback Dispatcher") + expect(Thread.list.map(&:name)).to include('FFI::Function Callback Dispatcher') end end From 7099d9594667297baa8fd3a23a5f610649b4a7fc Mon Sep 17 00:00:00 2001 From: Ivo Anjo Date: Mon, 22 Feb 2021 17:54:10 +0000 Subject: [PATCH 6/6] Fix callback thread spec failing on Windows This was probably failing due to thread naming being a racy operation; by keeping a reference to the thread and checking it after the call finishes, this will hopefully be enough for the name to become available. --- spec/ffi/async_callback_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/ffi/async_callback_spec.rb b/spec/ffi/async_callback_spec.rb index 02338e557..aab54e052 100644 --- a/spec/ffi/async_callback_spec.rb +++ b/spec/ffi/async_callback_spec.rb @@ -39,10 +39,10 @@ module LibTest skip "not yet supported on TruffleRuby" if RUBY_ENGINE == "truffleruby" skip "not yet supported on JRuby" if RUBY_ENGINE == "jruby" - thread_name = nil + callback_runner_thread = nil - LibTest.testAsyncCallback(proc { thread_name = Thread.current.name }, 0) + LibTest.testAsyncCallback(proc { callback_runner_thread = Thread.current }, 0) - expect(thread_name).to eq("FFI::Function Callback Runner") + expect(callback_runner_thread.name).to eq("FFI::Function Callback Runner") end end