@@ -103,6 +103,32 @@ def GCStats.global_gc_attributed?(*) = false
103103 puts "workload_ran=#{workload_ran}"
104104 RUBY
105105
106+ MEM_WORKLOAD_BODY = <<~'RUBY'
107+ run_benchmark(2) do |count|
108+ workers = count.times.map do |worker|
109+ Ractor.new(worker) do |worker_id|
110+ measure_worker_gc do
111+ 20_000.times { Object.new }
112+ GC.start(full_mark: true, immediate_sweep: true)
113+ worker_id
114+ end
115+ end
116+ end
117+ workers.each_with_index do |worker, worker_id|
118+ _, sample = worker.value
119+ record_worker_gc(worker_id, sample)
120+ end
121+ nil
122+ end
123+ RUBY
124+
125+ MEM_UNRECORDED_BODY = <<~'RUBY'
126+ run_benchmark(2) do |count|
127+ count.times.map { Ractor.new { 10_000.times { Object.new } } }.each(&:value)
128+ nil
129+ end
130+ RUBY
131+
106132 before do
107133 @explicit_target = !ENV [ 'RACTOR_GC_TEST_RUBY' ] . nil?
108134 @ruby = ENV [ 'RACTOR_GC_TEST_RUBY' ] || RbConfig . ruby
@@ -132,7 +158,7 @@ def GCStats.global_gc_attributed?(*) = false
132158 end
133159 end
134160
135- def run_workload ( body )
161+ def run_workload ( body , harness : 'harness-ractor' )
136162 Dir . mktmpdir do |dir |
137163 result_path = File . join ( dir , 'results.json' )
138164 script = File . join ( dir , 'workload.rb' )
@@ -146,7 +172,7 @@ def run_workload(body)
146172 'MIN_BENCH_TIME' => '0' ,
147173 'RESULT_JSON_PATH' => result_path
148174 )
149- stdout , stderr , status = Open3 . capture3 ( env , @ruby , "-I#{ File . join ( ROOT , ' harness-ractor' ) } " , script , chdir : ROOT )
175+ stdout , stderr , status = Open3 . capture3 ( env , @ruby , "-I#{ File . join ( ROOT , harness ) } " , script , chdir : ROOT )
150176 yield stdout , stderr , status , result_path
151177 end
152178 end
@@ -294,6 +320,70 @@ def run_workload(body)
294320 end
295321 end
296322
323+ it 'collects per-worker GC samples from a ractor memory scenario' do
324+ run_workload ( MEM_WORKLOAD_BODY , harness : 'harness-ractor-mem' ) do |stdout , stderr , status , result_path |
325+ assert status . success? , "workload failed:\n #{ stdout } \n #{ stderr } "
326+
327+ data = JSON . parse ( File . read ( result_path ) )
328+ assert_equal 'ractor-local-workload' , data [ 'gc_scope' ]
329+ assert_equal 'global' , data [ 'ractor_mem_settle' ]
330+ assert_equal %w[ 1 2 ] , data [ 'bench_by_ractors' ] . keys . sort
331+ assert_equal %w[ 1 2 ] , data [ 'gc_by_ractors' ] . keys . sort
332+
333+ data [ 'gc_by_ractors' ] . each do |count , group |
334+ assert_equal 2 , data [ 'bench_by_ractors' ] [ count ] . length , "count #{ count } timing samples"
335+ assert_equal 2 , group [ 'gc_worker_samples' ] . length , "count #{ count } measured iterations"
336+ assert_equal 2 , group [ 'gc_controller_samples' ] . length , "count #{ count } controller samples"
337+ group [ 'gc_worker_samples' ] . each_with_index do |workers , i |
338+ assert_equal ( 0 ...count . to_i ) . to_a , workers . map { |w | w [ 'worker_index' ] } , 'spawn-index order'
339+ workers . each { |w | assert_operator w [ 'gc_count' ] , :> , 0 , 'every worker records GC activity' }
340+ assert_equal workers . sum { |w | w [ 'gc_count' ] } , group [ 'gc_count_bench' ] [ i ]
341+ assert_in_delta workers . sum { |w | w [ 'gc_total_time_ns' ] } / 1_000_000.0 , group [ 'gc_total_time_bench' ] [ i ] , 1e-9
342+ end
343+ end
344+ end
345+ end
346+
347+ it 'fails a ractor memory scenario that does not record its worker GC samples' do
348+ run_workload ( MEM_UNRECORDED_BODY , harness : 'harness-ractor-mem' ) do |stdout , stderr , status , result_path |
349+ refute status . success? , "expected an unrecorded scenario to fail:\n #{ stdout } \n #{ stderr } "
350+ assert_includes stderr , 'scenario recorded worker GC samples [] for 1 ractors'
351+ refute File . exist? ( result_path ) , 'no results file may be written for a failed benchmark'
352+ end
353+ end
354+
355+ it 'reports every worker of each ractor memory benchmark' do
356+ %w[ ractor-dead-set ractor-idle-garbage ractor-msg-backlog ] . each do |bench |
357+ Dir . mktmpdir do |dir |
358+ result_path = File . join ( dir , 'results.json' )
359+ env = CLEAN_ENV . merge (
360+ 'RUBY_BENCH_RACTOR_GC' => '1' ,
361+ 'RUBY_BENCH_RACTORS' => '1,2' ,
362+ 'MIN_BENCH_ITRS' => '1' ,
363+ 'MAX_BENCH_ITRS' => '1' ,
364+ 'RACTOR_MEM_SETTLE_SLEEP' => '0' ,
365+ 'RACTOR_DEAD_SET_ITEMS' => '5000' ,
366+ 'RACTOR_IDLE_GARBAGE_ITEMS' => '5000' ,
367+ 'RACTOR_BACKLOG_MESSAGES' => '500' ,
368+ 'RACTOR_BACKLOG_GATE_SLEEP' => '0.05' ,
369+ 'RESULT_JSON_PATH' => result_path
370+ )
371+ script = File . join ( ROOT , 'benchmarks' , bench , 'benchmark.rb' )
372+ stdout , stderr , status = Open3 . capture3 ( env , @ruby , "-I#{ File . join ( ROOT , 'harness-ractor-mem' ) } " , script , chdir : ROOT )
373+ assert status . success? , "#{ bench } failed:\n #{ stdout } \n #{ stderr } "
374+
375+ data = JSON . parse ( File . read ( result_path ) )
376+ assert_equal %w[ 1 2 ] , data [ 'gc_by_ractors' ] . keys . sort , bench
377+ data [ 'gc_by_ractors' ] . each do |count , group |
378+ assert_equal 1 , group [ 'gc_controller_samples' ] . length , "#{ bench } count #{ count } controller samples"
379+ workers = group [ 'gc_worker_samples' ] . fetch ( 0 )
380+ assert_equal ( 0 ...count . to_i ) . to_a , workers . map { |w | w [ 'worker_index' ] } , "#{ bench } count #{ count } workers"
381+ workers . each { |w | assert_kind_of Integer , w [ 'gc_count' ] , "#{ bench } count #{ count } worker gc_count" }
382+ end
383+ end
384+ end
385+ end
386+
297387 it 'propagates a worker failure without writing partial or dummy results' do
298388 run_workload ( FAILING_WORKLOAD_BODY ) do |stdout , stderr , status , result_path |
299389 refute status . success? , "expected worker failure to fail the run:\n #{ stdout } \n #{ stderr } "
0 commit comments