Skip to content

Commit 20bdebb

Browse files
Split summary table by harness
1 parent 3875527 commit 20bdebb

6 files changed

Lines changed: 196 additions & 17 deletions

File tree

lib/benchmark_runner.rb

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def write_csv(output_path, ruby_descriptions, table)
4848
end
4949

5050
# Build output text string with metadata, table, and legend
51-
def build_output_text(ruby_descriptions, table, format, bench_failures, include_rss: false, include_gc: false, include_pvalue: false, gc_table: nil, gc_format: nil)
51+
def build_output_text(ruby_descriptions, table, format, bench_failures, include_rss: false, include_gc: false, include_pvalue: false, gc_table: nil, gc_format: nil, sections: nil)
5252
base_name, *other_names = ruby_descriptions.keys
5353

5454
output_str = +""
@@ -58,11 +58,17 @@ def build_output_text(ruby_descriptions, table, format, bench_failures, include_
5858
end
5959

6060
output_str << "\n"
61-
output_str << TableFormatter.new(table, format, bench_failures).to_s + "\n"
62-
63-
if include_gc && gc_table && gc_format
64-
output_str << "GC summary:\n"
65-
output_str << TableFormatter.new(gc_table, gc_format, {}).to_s + "\n"
61+
sections ||= [{ table: table, format: format, failures: bench_failures, include_gc: include_gc, gc_table: gc_table, gc_format: gc_format }]
62+
has_gc_summary = sections.any? { |section| section[:include_gc] && section[:gc_table] }
63+
sections.each do |section|
64+
title = section[:title]
65+
output_str << "#{title}:\n" if title
66+
output_str << TableFormatter.new(section[:table], section[:format], section.fetch(:failures, {})).to_s + "\n"
67+
68+
if section[:include_gc] && section[:gc_table] && section[:gc_format]
69+
output_str << (title ? "GC summary (#{title}):\n" : "GC summary:\n")
70+
output_str << TableFormatter.new(section[:gc_table], section[:gc_format], {}).to_s + "\n"
71+
end
6672
end
6773

6874
unless other_names.empty?
@@ -74,7 +80,7 @@ def build_output_text(ruby_descriptions, table, format, bench_failures, include_
7480
output_str << "- RSS #{base_name}/#{name}: ratio of #{base_name}/#{name} RSS. Higher is better for #{name}. Above 1 means lower memory usage.\n"
7581
end
7682
end
77-
if include_gc && gc_table
83+
if has_gc_summary
7884
output_str << "- GC summary compares #{base_name} → comparison. Ratio columns are #{base_name}/comparison; above 1 means the comparison spent less GC time.\n"
7985
output_str << "- mark/iter ratio and sweep/iter ratio compare total GC phase time per benchmark iteration, so they include both per-GC cost and GC frequency changes.\n"
8086
output_str << "- mark/GC ratio and sweep/GC ratio compare average phase time per GC, isolating whether each GC became cheaper or more expensive.\n"

lib/benchmark_runner/cli.rb

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
require_relative '../benchmark_runner'
77
require_relative '../benchmark_suite'
88
require_relative '../results_table_builder'
9+
require_relative '../ractor_breakdown'
10+
require_relative '../row_layout'
911

1012
module BenchmarkRunner
1113
class CLI
@@ -63,6 +65,9 @@ def run
6365
bench_start_time = Time.now.to_f
6466
bench_data = {}
6567
bench_failures = {}
68+
bench_harnesses = suite.benchmarks.each_with_object({}) do |entry, h|
69+
h[entry.name] = suite.harness_for(entry.name)
70+
end
6671

6772
if args.interleave
6873
args.executables.each_key { |name| bench_data[name] = {} }
@@ -104,7 +109,7 @@ def run
104109

105110
puts
106111

107-
# Build results table
112+
# Build the results table
108113
builder = ResultsTableBuilder.new(
109114
executable_names: ruby_descriptions.keys,
110115
bench_data: bench_data,
@@ -125,7 +130,8 @@ def run
125130
BenchmarkRunner.write_csv(output_path, ruby_descriptions, table)
126131

127132
# Save the output in a text file that we can easily refer to
128-
output_str = BenchmarkRunner.build_output_text(ruby_descriptions, table, format, bench_failures, include_rss: args.rss, include_gc: builder.include_gc?, include_pvalue: args.pvalue, gc_table: gc_table, gc_format: gc_format)
133+
output_sections = build_output_sections(ruby_descriptions.keys, bench_data, bench_harnesses, bench_failures)
134+
output_str = BenchmarkRunner.build_output_text(ruby_descriptions, table, format, bench_failures, include_rss: args.rss, include_gc: builder.include_gc?, include_pvalue: args.pvalue, gc_table: gc_table, gc_format: gc_format, sections: output_sections)
129135
out_txt_path = output_path + ".txt"
130136
File.open(out_txt_path, "w") { |f| f.write output_str }
131137

@@ -149,5 +155,81 @@ def run
149155
exit(1)
150156
end
151157
end
158+
159+
private
160+
161+
def build_output_sections(executable_names, bench_data, bench_harnesses, bench_failures)
162+
ordered_names = sorted_benchmark_names(executable_names, bench_data)
163+
failed_names = bench_failures.values.flat_map(&:keys).uniq
164+
ordered_names.concat(failed_names.reject { |name| ordered_names.include?(name) })
165+
166+
names_by_harness = {}
167+
ordered_names.each do |bench_name|
168+
harness = bench_harnesses.fetch(bench_name, args.harness)
169+
names_by_harness[harness] ||= []
170+
names_by_harness[harness] << bench_name
171+
end
172+
173+
show_titles = names_by_harness.size > 1
174+
names_by_harness.map do |harness, names|
175+
section = build_output_section(executable_names, bench_data, bench_failures, harness, names)
176+
section[:title] = nil unless show_titles
177+
section
178+
end
179+
end
180+
181+
def build_output_section(executable_names, bench_data, bench_failures, harness, bench_names)
182+
section_data = slice_bench_data(bench_data, bench_names)
183+
breakdown = RactorBreakdown.expand(section_data)
184+
use_ractor_layout = harness == BenchmarkSuite::RACTOR_HARNESS && !breakdown.groups.empty?
185+
layout = use_ractor_layout ? RactorRowLayout.new(groups: breakdown.groups) : FlatRowLayout.new
186+
display_data = use_ractor_layout ? breakdown.bench_data : section_data
187+
188+
builder = ResultsTableBuilder.new(
189+
executable_names: executable_names,
190+
bench_data: display_data,
191+
include_rss: args.rss,
192+
include_pvalue: args.pvalue,
193+
zjit_stats: args.zjit_stats,
194+
row_layout: layout
195+
)
196+
table, format, gc_table, gc_format = builder.build
197+
198+
{
199+
title: harness,
200+
table: table,
201+
format: format,
202+
failures: slice_failures(bench_failures, bench_names),
203+
include_gc: builder.include_gc?,
204+
gc_table: gc_table,
205+
gc_format: gc_format,
206+
}
207+
end
208+
209+
def sorted_benchmark_names(executable_names, bench_data)
210+
builder = ResultsTableBuilder.new(
211+
executable_names: executable_names,
212+
bench_data: bench_data,
213+
include_rss: args.rss,
214+
include_pvalue: args.pvalue,
215+
zjit_stats: args.zjit_stats
216+
)
217+
builder.bench_names
218+
end
219+
220+
def slice_bench_data(bench_data, bench_names)
221+
wanted = bench_names.each_with_object({}) { |name, h| h[name] = true }
222+
bench_data.each_with_object({}) do |(executable, benchmarks), sliced|
223+
sliced[executable] = benchmarks.select { |name, _data| wanted[name] }
224+
end
225+
end
226+
227+
def slice_failures(bench_failures, bench_names)
228+
wanted = bench_names.each_with_object({}) { |name, h| h[name] = true }
229+
bench_failures.each_with_object({}) do |(executable, failures), sliced|
230+
selected = failures.select { |name, _failure| wanted[name] }
231+
sliced[executable] = selected unless selected.empty?
232+
end
233+
end
152234
end
153235
end

lib/benchmark_suite.rb

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ def benchmarks
3838
@benchmarks ||= discover_benchmarks
3939
end
4040

41+
def harness_for(benchmark_name)
42+
benchmark_harness_for(benchmark_name)
43+
end
44+
4145
# Run a single benchmark entry on a single executable.
4246
# Returns { name:, data: } on success, { name:, failure: } on error.
4347
def run_benchmark(entry, ruby:, ruby_description:)
@@ -50,19 +54,21 @@ def run_benchmark(entry, ruby:, ruby_description:)
5054

5155
# Clear project-level Bundler environment so benchmarks run in a clean context.
5256
# Benchmarks that need Bundler (e.g., railsbench) set up their own via use_gemfile.
57+
benchmark_harness = benchmark_harness_for(entry.name)
58+
5359
result = if defined?(Bundler)
5460
Bundler.with_unbundled_env do
55-
run_single_benchmark(entry.script_path, result_json_path, ruby, cmd_prefix, env, entry.name, quiet: quiet)
61+
run_single_benchmark(entry.script_path, result_json_path, ruby, cmd_prefix, env, benchmark_harness, quiet: quiet)
5662
end
5763
else
58-
run_single_benchmark(entry.script_path, result_json_path, ruby, cmd_prefix, env, entry.name, quiet: quiet)
64+
run_single_benchmark(entry.script_path, result_json_path, ruby, cmd_prefix, env, benchmark_harness, quiet: quiet)
5965
end
6066

6167
if result[:success]
62-
{ name: entry.name, data: process_benchmark_result(result_json_path, result[:command], delete_file: !caller_json_path) }
68+
{ name: entry.name, data: process_benchmark_result(result_json_path, result[:command], delete_file: !caller_json_path), harness: benchmark_harness }
6369
else
6470
FileUtils.rm_f(result_json_path) unless caller_json_path
65-
{ name: entry.name, failure: result[:status].exitstatus }
71+
{ name: entry.name, failure: result[:status].exitstatus, harness: benchmark_harness }
6672
end
6773
end
6874

@@ -145,7 +151,7 @@ def filter_entries(entries, categories:, name_filters:, excludes:, directory_map
145151
entries.select { |entry| filter.match?(entry.name) }
146152
end
147153

148-
def run_single_benchmark(script_path, result_json_path, ruby, cmd_prefix, env, benchmark_name, quiet: false)
154+
def run_single_benchmark(script_path, result_json_path, ruby, cmd_prefix, env, benchmark_harness, quiet: false)
149155
# Fix for jruby/jruby#7394 in JRuby 9.4.2.0
150156
script_path = File.expand_path(script_path)
151157

@@ -154,9 +160,6 @@ def run_single_benchmark(script_path, result_json_path, ruby, cmd_prefix, env, b
154160
original_result_json_path = ENV["RESULT_JSON_PATH"]
155161
ENV["RESULT_JSON_PATH"] = result_json_path
156162

157-
# Use per-benchmark default_harness if set, otherwise use global harness
158-
benchmark_harness = benchmark_harness_for(benchmark_name)
159-
160163
# Set up the benchmarking command
161164
cmd = cmd_prefix + [
162165
*ruby,

test/benchmark_runner_cli_test.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,41 @@ def create_args(overrides = {})
8484
end
8585
end
8686

87+
describe '#build_output_sections' do
88+
it 'splits text summary tables by harness in sorted benchmark order' do
89+
args = create_args
90+
cli = BenchmarkRunner::CLI.new(args)
91+
bench_data = {
92+
'ruby' => {
93+
'fib' => {
94+
'warmup' => [],
95+
'bench' => [0.1],
96+
'rss' => 10 * 1024 * 1024
97+
},
98+
'symbol-name-ractor' => {
99+
'warmup' => [],
100+
'bench' => [1.0, 2.0],
101+
'bench_by_ractors' => { '0' => [1.0], '2' => [2.0] },
102+
'rss' => 10 * 1024 * 1024
103+
}
104+
}
105+
}
106+
bench_harnesses = {
107+
'fib' => 'harness',
108+
'symbol-name-ractor' => 'harness-ractor'
109+
}
110+
111+
sections = cli.send(:build_output_sections, ['ruby'], bench_data, bench_harnesses, {})
112+
113+
assert_equal ['harness-ractor', 'harness'], sections.map { |section| section[:title] }
114+
assert_equal ['bench', 'ractors', 'ruby (ms)'], sections[0][:table][0]
115+
assert_equal ['symbol-name-ractor', '0', '1000.0 ± 0.0%'], sections[0][:table][1]
116+
assert_equal ['', '2', '2000.0 ± 0.0%'], sections[0][:table][2]
117+
assert_equal ['bench', 'ruby (ms)'], sections[1][:table][0]
118+
assert_equal ['fib', '100.0 ± 0.0%'], sections[1][:table][1]
119+
end
120+
end
121+
87122
describe '#run integration test' do
88123
it 'runs a simple benchmark end-to-end and produces all output files' do
89124
Dir.mktmpdir do |tmpdir|

test/benchmark_runner_test.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,57 @@
496496
assert_includes result, '100.0'
497497
end
498498

499+
it 'prints multiple table sections when provided' do
500+
ruby_descriptions = { 'ruby' => 'ruby 3.3.0' }
501+
sections = [
502+
{
503+
title: 'harness',
504+
table: [['bench', 'ruby (ms)'], ['fib', '100.0']],
505+
format: ['%s', '%.1f'],
506+
failures: {},
507+
include_gc: false,
508+
},
509+
{
510+
title: 'harness-ractor',
511+
table: [['bench', 'ractors', 'ruby (ms)'], ['symbol-name-ractor', '0', '200.0']],
512+
format: ['%s', '%s', '%.1f'],
513+
failures: {},
514+
include_gc: false,
515+
}
516+
]
517+
518+
result = BenchmarkRunner.build_output_text(
519+
ruby_descriptions, sections.first[:table], sections.first[:format], {}, sections: sections
520+
)
521+
522+
assert_includes result, "harness:\n"
523+
assert_includes result, "harness-ractor:\n"
524+
assert_includes result, 'fib'
525+
assert_includes result, 'symbol-name-ractor'
526+
end
527+
528+
it 'labels GC summaries by section title' do
529+
ruby_descriptions = { 'ruby' => 'ruby 3.3.0', 'exp' => 'ruby 3.3.0 exp' }
530+
sections = [
531+
{
532+
title: 'harness-gc',
533+
table: [['bench', 'ruby (ms)', 'exp (ms)'], ['gcbench', '100.0', '90.0']],
534+
format: ['%s', '%.1f', '%.1f'],
535+
failures: {},
536+
include_gc: true,
537+
gc_table: [['bench', 'mark/iter ratio'], ['gcbench', '1.100']],
538+
gc_format: ['%s', '%s'],
539+
}
540+
]
541+
542+
result = BenchmarkRunner.build_output_text(
543+
ruby_descriptions, sections.first[:table], sections.first[:format], {}, sections: sections
544+
)
545+
546+
assert_includes result, "GC summary (harness-gc):\n"
547+
assert_includes result, '- GC summary compares ruby → comparison.'
548+
end
549+
499550
it 'omits legend when no other executables' do
500551
ruby_descriptions = { 'ruby' => 'ruby 3.3.0' }
501552
table = [['bench', 'ruby (ms)'], ['fib', '100.0']]

test/benchmark_suite_test.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,7 @@
569569
assert_includes result[:data], 'warmup'
570570
assert_includes result[:data], 'bench'
571571
assert_includes result[:data], 'rss'
572+
assert_equal 'harness', result[:harness]
572573
assert_nil result[:failure]
573574
end
574575

@@ -592,6 +593,7 @@
592593
assert_equal 'failing', result[:name]
593594
assert_nil result[:data]
594595
assert_equal 1, result[:failure]
596+
assert_equal 'harness', result[:harness]
595597
end
596598

597599
it 'produces same data as run for the same benchmark' do

0 commit comments

Comments
 (0)