summaryrefslogtreecommitdiff
path: root/test/-ext-/thread
diff options
context:
space:
mode:
Diffstat (limited to 'test/-ext-/thread')
-rw-r--r--test/-ext-/thread/helper.rb51
-rw-r--r--test/-ext-/thread/test_instrumentation_api.rb274
-rw-r--r--test/-ext-/thread/test_lock_native_thread.rb50
3 files changed, 337 insertions, 38 deletions
diff --git a/test/-ext-/thread/helper.rb b/test/-ext-/thread/helper.rb
new file mode 100644
index 0000000000..3ea2057d15
--- /dev/null
+++ b/test/-ext-/thread/helper.rb
@@ -0,0 +1,51 @@
+module ThreadInstrumentation
+ module TestHelper
+ private
+
+ def record
+ Bug::ThreadInstrumentation.register_callback(!ENV["GVL_DEBUG"])
+ yield
+ ensure
+ timeline = Bug::ThreadInstrumentation.unregister_callback
+ if $!
+ raise
+ else
+ return timeline
+ end
+ end
+
+ def timeline_for(thread, timeline)
+ timeline.select { |t, _| t == thread }.map(&:last)
+ end
+
+ def assert_consistent_timeline(events)
+ refute_predicate events, :empty?
+
+ previous_event = nil
+ events.each do |event|
+ refute_equal :exited, previous_event, "`exited` must be the final event: #{events.inspect}"
+ case event
+ when :started
+ assert_nil previous_event, "`started` must be the first event: #{events.inspect}"
+ when :ready
+ unless previous_event.nil?
+ assert %i(started suspended).include?(previous_event), "`ready` must be preceded by `started` or `suspended`: #{events.inspect}"
+ end
+ when :resumed
+ unless previous_event.nil?
+ assert_equal :ready, previous_event, "`resumed` must be preceded by `ready`: #{events.inspect}"
+ end
+ when :suspended
+ unless previous_event.nil?
+ assert_equal :resumed, previous_event, "`suspended` must be preceded by `resumed`: #{events.inspect}"
+ end
+ when :exited
+ unless previous_event.nil?
+ assert %i(resumed suspended).include?(previous_event), "`exited` must be preceded by `resumed` or `suspended`: #{events.inspect}"
+ end
+ end
+ previous_event = event
+ end
+ end
+ end
+end
diff --git a/test/-ext-/thread/test_instrumentation_api.rb b/test/-ext-/thread/test_instrumentation_api.rb
index dd620e7380..663e41be53 100644
--- a/test/-ext-/thread/test_instrumentation_api.rb
+++ b/test/-ext-/thread/test_instrumentation_api.rb
@@ -1,91 +1,289 @@
# frozen_string_literal: false
require 'envutil'
+require_relative "helper"
class TestThreadInstrumentation < Test::Unit::TestCase
+ include ThreadInstrumentation::TestHelper
+
def setup
pend("No windows support") if /mswin|mingw|bccwin/ =~ RUBY_PLATFORM
require '-test-/thread/instrumentation'
- Thread.list.each do |thread|
- if thread != Thread.current
- thread.kill
- thread.join rescue nil
- end
- end
- assert_equal [Thread.current], Thread.list
-
- Bug::ThreadInstrumentation.reset_counters
- Bug::ThreadInstrumentation::register_callback
+ cleanup_threads
end
def teardown
return if /mswin|mingw|bccwin/ =~ RUBY_PLATFORM
- Bug::ThreadInstrumentation::unregister_callback
+ Bug::ThreadInstrumentation.unregister_callback
+ cleanup_threads
end
THREADS_COUNT = 3
- def test_thread_instrumentation
- threads = threaded_cpu_work
- assert_equal [false] * THREADS_COUNT, threads.map(&:status)
- counters = Bug::ThreadInstrumentation.counters
- assert_join_counters(counters)
- assert_global_join_counters(counters)
+ def test_single_thread_timeline
+ thread = nil
+ full_timeline = record do
+ thread = Thread.new { 1 + 1 }
+ thread.join
+ end
+ assert_equal %i(started ready resumed suspended exited), timeline_for(thread, full_timeline)
+ ensure
+ thread&.kill
+ end
+
+ def test_thread_pass_single_thread
+ full_timeline = record do
+ Thread.pass
+ end
+ assert_equal [], timeline_for(Thread.current, full_timeline)
+ end
+
+ def test_thread_pass_multi_thread
+ thread = Thread.new do
+ cpu_bound_work(0.5)
+ end
+
+ full_timeline = record do
+ Thread.pass
+ end
+
+ assert_equal %i(suspended ready resumed), timeline_for(Thread.current, full_timeline)
+ ensure
+ thread&.kill
+ thread&.join
+ end
+
+ def test_multi_thread_timeline
+ threads = nil
+ full_timeline = record do
+ threads = threaded_cpu_bound_work(1.0)
+ results = threads.map(&:value)
+ results.each do |r|
+ refute_equal false, r
+ end
+ assert_equal [false] * THREADS_COUNT, threads.map(&:status)
+ end
+
+ threads.each do |thread|
+ timeline = timeline_for(thread, full_timeline)
+ assert_consistent_timeline(timeline)
+ assert_operator timeline.count(:suspended), :>=, 1, "Expected threads to yield suspended at least once: #{timeline.inspect}"
+ end
+
+ timeline = timeline_for(Thread.current, full_timeline)
+ assert_consistent_timeline(timeline)
+ ensure
+ threads&.each(&:kill)
+ end
+
+ def test_join_suspends # Bug #18900
+ thread = other_thread = nil
+ full_timeline = record do
+ other_thread = Thread.new { sleep 0.3 }
+ thread = Thread.new { other_thread.join }
+ thread.join
+ end
+
+ timeline = timeline_for(thread, full_timeline)
+ assert_consistent_timeline(timeline)
+ assert_equal %i(started ready resumed suspended ready resumed suspended exited), timeline
+ ensure
+ other_thread&.kill
+ thread&.kill
end
- def test_join_counters # Bug #18900
- thr = Thread.new { fib(30) }
- Bug::ThreadInstrumentation.reset_counters
- thr.join
- assert_join_counters(Bug::ThreadInstrumentation.local_counters)
+ def test_io_release_gvl
+ r, w = IO.pipe
+ thread = nil
+ full_timeline = record do
+ thread = Thread.new do
+ w.write("Hello\n")
+ end
+ thread.join
+ end
+
+ timeline = timeline_for(thread, full_timeline)
+ assert_consistent_timeline(timeline)
+ assert_equal %i(started ready resumed suspended ready resumed suspended exited), timeline
+ ensure
+ r&.close
+ w&.close
+ end
+
+ def test_queue_releases_gvl
+ queue1 = Queue.new
+ queue2 = Queue.new
+
+ thread = nil
+
+ full_timeline = record do
+ thread = Thread.new do
+ queue1 << true
+ queue2.pop
+ end
+
+ queue1.pop
+ queue2 << true
+ thread.join
+ end
+
+ timeline = timeline_for(thread, full_timeline)
+ assert_consistent_timeline(timeline)
+ assert_equal %i(started ready resumed suspended ready resumed suspended exited), timeline
+ end
+
+ def test_blocking_on_ractor
+ assert_ractor(<<-"RUBY", require_relative: "helper", require: "-test-/thread/instrumentation")
+ include ThreadInstrumentation::TestHelper
+
+ ractor = Ractor.new {
+ Ractor.receive # wait until woke
+ Thread.current
+ }
+
+ # Wait for the main thread to block, then wake the ractor
+ Thread.new do
+ while Thread.main.status != "sleep"
+ Thread.pass
+ end
+ ractor.send true
+ end
+
+ full_timeline = record do
+ ractor.take
+ end
+
+ timeline = timeline_for(Thread.current, full_timeline)
+ assert_consistent_timeline(timeline)
+ assert_equal %i(suspended ready resumed), timeline
+ RUBY
+ end
+
+ def test_sleeping_inside_ractor
+ assert_ractor(<<-"RUBY", require_relative: "helper", require: "-test-/thread/instrumentation")
+ include ThreadInstrumentation::TestHelper
+
+ thread = nil
+
+ full_timeline = record do
+ thread = Ractor.new{
+ sleep 0.1
+ Thread.current
+ }.take
+ sleep 0.1
+ end
+
+ timeline = timeline_for(thread, full_timeline)
+ assert_consistent_timeline(timeline)
+ assert_equal %i(started ready resumed suspended ready resumed suspended exited), timeline
+ RUBY
+ end
+
+ def test_thread_blocked_forever_on_mutex
+ mutex = Mutex.new
+ mutex.lock
+ thread = nil
+
+ full_timeline = record do
+ thread = Thread.new do
+ mutex.lock
+ end
+ 10.times { Thread.pass }
+ sleep 0.1
+ end
+
+ mutex.unlock
+ thread.join
+
+ timeline = timeline_for(thread, full_timeline)
+ assert_consistent_timeline(timeline)
+ assert_equal %i(started ready resumed suspended), timeline
+ end
+
+ def test_thread_blocked_temporarily_on_mutex
+ mutex = Mutex.new
+ mutex.lock
+ thread = nil
+
+ full_timeline = record do
+ thread = Thread.new do
+ mutex.lock
+ end
+ 10.times { Thread.pass }
+ sleep 0.1
+ mutex.unlock
+ 10.times { Thread.pass }
+ sleep 0.1
+ end
+
+ thread.join
+
+ timeline = timeline_for(thread, full_timeline)
+ assert_consistent_timeline(timeline)
+ assert_equal %i(started ready resumed suspended ready resumed suspended exited), timeline
end
def test_thread_instrumentation_fork_safe
skip "No fork()" unless Process.respond_to?(:fork)
- thread_statuses = counters = nil
+ thread_statuses = full_timeline = nil
IO.popen("-") do |read_pipe|
if read_pipe
thread_statuses = Marshal.load(read_pipe)
- counters = Marshal.load(read_pipe)
+ full_timeline = Marshal.load(read_pipe)
else
- Bug::ThreadInstrumentation.reset_counters
- threads = threaded_cpu_work
+ threads = threaded_cpu_bound_work.each(&:join)
Marshal.dump(threads.map(&:status), STDOUT)
- Marshal.dump(Bug::ThreadInstrumentation.counters, STDOUT)
+ full_timeline = Bug::ThreadInstrumentation.unregister_callback.map { |t, e| [t.to_s, e ] }
+ Marshal.dump(full_timeline, STDOUT)
end
end
assert_predicate $?, :success?
assert_equal [false] * THREADS_COUNT, thread_statuses
- assert_join_counters(counters)
- assert_global_join_counters(counters)
+ thread_names = full_timeline.map(&:first).uniq
+ thread_names.each do |thread_name|
+ assert_consistent_timeline(timeline_for(thread_name, full_timeline))
+ end
end
def test_thread_instrumentation_unregister
- Bug::ThreadInstrumentation::unregister_callback
assert Bug::ThreadInstrumentation::register_and_unregister_callbacks
end
private
- def fib(n = 20)
+ def fib(n = 30)
return n if n <= 1
fib(n-1) + fib(n-2)
end
- def threaded_cpu_work(size = 20)
- THREADS_COUNT.times.map { Thread.new { fib(size) } }.each(&:join)
+ def cpu_bound_work(duration)
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + duration
+ i = 0
+ while deadline > Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ fib(25)
+ i += 1
+ end
+ i > 0 ? i : false
end
- def assert_join_counters(counters)
- counters.each_with_index do |c, i|
- assert_operator c, :>, 0, "Call counters[#{i}]: #{counters.inspect}"
+ def threaded_cpu_bound_work(duration = 0.5)
+ THREADS_COUNT.times.map do
+ Thread.new do
+ cpu_bound_work(duration)
+ end
end
end
- def assert_global_join_counters(counters)
- assert_equal THREADS_COUNT, counters.first
+ def cleanup_threads
+ Thread.list.each do |thread|
+ if thread != Thread.current
+ thread.kill
+ thread.join rescue nil
+ end
+ end
+ assert_equal [Thread.current], Thread.list
end
end
diff --git a/test/-ext-/thread/test_lock_native_thread.rb b/test/-ext-/thread/test_lock_native_thread.rb
new file mode 100644
index 0000000000..8a5ba78838
--- /dev/null
+++ b/test/-ext-/thread/test_lock_native_thread.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: false
+
+require 'envutil'
+
+mn_supported_p = -> do
+ out, *_ = EnvUtil.invoke_ruby([{'RUBY_MN_THREADS' => '1'}, '-v'], '', true)
+ return /\+MN/ =~ out
+end
+
+if mn_supported_p.call
+ # test only on MN threads
+else
+ return
+end
+
+class TestThreadLockNativeThread < Test::Unit::TestCase
+ def test_lock_native_thread
+ assert_separately([{'RUBY_MN_THREADS' => '1'}], <<-RUBY)
+ require '-test-/thread/lock_native_thread'
+
+ Thread.new{
+ assert_equal true, Thread.current.lock_native_thread
+ }.join
+
+ # main thread already has DNT
+ assert_equal false, Thread.current.lock_native_thread
+ RUBY
+ end
+
+ def test_lock_native_thread_tls
+ assert_separately([{'RUBY_MN_THREADS' => '1'}], <<-RUBY)
+ require '-test-/thread/lock_native_thread'
+ tn = 10
+ ln = 1_000
+
+ ts = tn.times.map{|i|
+ Thread.new(i){|i|
+ Thread.current.set_tls i
+ assert_equal true, Thread.current.lock_native_thread
+
+ ln.times{
+ assert_equal i, Thread.current.get_tls
+ Thread.pass
+ }
+ }
+ }
+ ts.each(&:join)
+ RUBY
+ end
+end