diff options
Diffstat (limited to 'test/-ext-/thread/test_instrumentation_api.rb')
-rw-r--r-- | test/-ext-/thread/test_instrumentation_api.rb | 301 |
1 files changed, 256 insertions, 45 deletions
diff --git a/test/-ext-/thread/test_instrumentation_api.rb b/test/-ext-/thread/test_instrumentation_api.rb index fe91c942c7..9a3b67fa10 100644 --- a/test/-ext-/thread/test_instrumentation_api.rb +++ b/test/-ext-/thread/test_instrumentation_api.rb @@ -1,78 +1,289 @@ # frozen_string_literal: false require 'envutil' +require_relative "helper" class TestThreadInstrumentation < Test::Unit::TestCase + include ThreadInstrumentation::TestHelper + def setup - pend("TODO: No windows support yet") if /mswin|mingw|bccwin/ =~ RUBY_PLATFORM + pend("No windows support") if /mswin|mingw|bccwin/ =~ RUBY_PLATFORM + + require '-test-/thread/instrumentation' + + cleanup_threads + end + + def teardown + return if /mswin|mingw|bccwin/ =~ RUBY_PLATFORM + Bug::ThreadInstrumentation.unregister_callback + cleanup_threads end THREADS_COUNT = 3 - def test_thread_instrumentation - require '-test-/thread/instrumentation' - Bug::ThreadInstrumentation.reset_counters - Bug::ThreadInstrumentation::register_callback + 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 - begin - threads = threaded_cpu_work + def test_muti_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) - counters = Bug::ThreadInstrumentation.counters - counters.each do |c| - assert_predicate c, :nonzero?, "Call counters: #{counters.inspect}" + end + + threads.each do |thread| + timeline = timeline_for(thread, full_timeline) + assert_consistent_timeline(timeline) + assert 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_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 - assert_equal THREADS_COUNT, counters.first - assert_in_delta THREADS_COUNT, counters.last, 1 # It's possible that a thread didn't execute its EXIT hook yet. - ensure - Bug::ThreadInstrumentation::unregister_callback + 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_thread_instrumentation_fork_safe - skip "No fork()" unless Process.respond_to?(:fork) + def test_blocking_on_ractor + assert_ractor(<<-"RUBY", require_relative: "helper", require: "-test-/thread/instrumentation") + include ThreadInstrumentation::TestHelper - require '-test-/thread/instrumentation' - Bug::ThreadInstrumentation::register_callback - - read_pipe, write_pipe = IO.pipe - - begin - pid = fork do - Bug::ThreadInstrumentation.reset_counters - threads = threaded_cpu_work - write_pipe.write(Marshal.dump(threads.map(&:status))) - write_pipe.write(Marshal.dump(Bug::ThreadInstrumentation.counters)) - write_pipe.close - exit!(0) + 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 - write_pipe.close - _, status = Process.wait2(pid) - assert_predicate status, :success? + 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 - thread_statuses = Marshal.load(read_pipe) - assert_equal [false] * THREADS_COUNT, thread_statuses + def test_thread_instrumentation_fork_safe + skip "No fork()" unless Process.respond_to?(:fork) - counters = Marshal.load(read_pipe) - read_pipe.close - counters.each do |c| - assert_predicate c, :nonzero?, "Call counters: #{counters.inspect}" + thread_statuses = full_timeline = nil + IO.popen("-") do |read_pipe| + if read_pipe + thread_statuses = Marshal.load(read_pipe) + full_timeline = Marshal.load(read_pipe) + else + threads = threaded_cpu_bound_work.each(&:join) + Marshal.dump(threads.map(&:status), 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 THREADS_COUNT, counters.first - assert_in_delta THREADS_COUNT, counters.last, 1 # It's possible that a thread didn't execute its EXIT hook yet. - ensure - Bug::ThreadInstrumentation::unregister_callback + assert_equal [false] * THREADS_COUNT, thread_statuses + 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 - require '-test-/thread/instrumentation' assert Bug::ThreadInstrumentation::register_and_unregister_callbacks end private - def threaded_cpu_work - THREADS_COUNT.times.map { Thread.new { 100.times { |i| i + i } } }.each(&:join) + def fib(n = 30) + return n if n <= 1 + fib(n-1) + fib(n-2) + end + + 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 threaded_cpu_bound_work(duration = 0.5) + THREADS_COUNT.times.map do + Thread.new do + cpu_bound_work(duration) + end + end + end + + 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 |