summaryrefslogtreecommitdiff
path: root/test/-ext-/thread/test_instrumentation_api.rb
diff options
context:
space:
mode:
Diffstat (limited to 'test/-ext-/thread/test_instrumentation_api.rb')
-rw-r--r--test/-ext-/thread/test_instrumentation_api.rb301
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