Skip to content

Commit 09b76c8

Browse files
Merge pull request #521 from eightbitraptor/mvh-ractor-harness-improvements
Improve Ractor harness summary output
2 parents 38436ff + 77667b9 commit 09b76c8

14 files changed

Lines changed: 524 additions & 29 deletions

‎.github/workflows/test.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ on:
66
pull_request:
77
schedule:
88
- cron: "00 15 * * *"
9+
env:
10+
BUNDLE_VERSION: system
911
jobs:
1012
test:
1113
runs-on: ubuntu-latest

‎harness-ractor/harness.rb‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@ def run_benchmark(num_itrs_hint, ractor_args: [], &block)
7373
time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - before
7474
time_ms = (1000 * time).to_i
7575
itr_str = "%-3s %4s %6s" % ["#{rs}", "##{num_itrs}:", "#{time_ms}ms"]
76-
stats[rs] << time_ms
76+
stats[rs] << time
7777
puts itr_str
7878
end
7979
end
80-
return_results([], stats.values.flatten)
80+
return_results([], stats.values.flatten, bench_by_ractors: stats)
8181
end
8282

8383
# NOTE: we use `ractor_deep_dup` instead of `Ractor.make_shareable(copy: true)` for the case of

‎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,

‎lib/ractor_breakdown.rb‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# frozen_string_literal: true
2+
3+
module RactorBreakdown
4+
KEY_SEP = "\x00"
5+
6+
Result = Struct.new(:bench_data, :groups)
7+
8+
module_function
9+
10+
def data_key(base_name, count)
11+
"#{base_name}#{KEY_SEP}#{count}"
12+
end
13+
14+
def base_name(data_key)
15+
data_key.split(KEY_SEP, 2).first
16+
end
17+
18+
def expand(bench_data)
19+
groups = {}
20+
new_data = {}
21+
22+
bench_data.each do |exe, benchmarks|
23+
new_data[exe] = {}
24+
benchmarks.each do |name, blob|
25+
breakdown = blob.is_a?(Hash) && blob['bench_by_ractors']
26+
unless breakdown
27+
new_data[exe][name] = blob
28+
next
29+
end
30+
31+
counts = breakdown.keys.map { |c| Integer(c) }.sort
32+
groups[name] ||= counts.map { |c| [data_key(name, c), c] }
33+
34+
counts.each do |count|
35+
key = data_key(name, count)
36+
new_data[exe][key] = per_count_blob(blob, breakdown, count)
37+
end
38+
end
39+
end
40+
41+
Result.new(new_data, groups.to_a)
42+
end
43+
44+
def per_count_blob(blob, breakdown, count)
45+
per_count = blob.reject { |k, _| k == 'bench_by_ractors' || k == 'bench' }
46+
per_count['bench'] = breakdown[count.to_s]
47+
per_count['warmup'] = []
48+
per_count
49+
end
50+
end

‎lib/results_table_builder.rb‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
require_relative '../misc/stats'
2+
require_relative 'row_layout'
23
require 'yaml'
34

45
class ResultsTableBuilder
56
SECONDS_TO_MS = 1000.0
67
BYTES_TO_MIB = 1024.0 * 1024.0
78

8-
def initialize(executable_names:, bench_data:, include_rss: false, include_pvalue: false, zjit_stats: [])
9+
attr_reader :bench_names
10+
11+
def initialize(executable_names:, bench_data:, include_rss: false, include_pvalue: false, zjit_stats: [], row_layout: FlatRowLayout.new)
912
@executable_names = executable_names
1013
@bench_data = bench_data
1114
@include_rss = include_rss
@@ -15,6 +18,7 @@ def initialize(executable_names:, bench_data:, include_rss: false, include_pvalu
1518
@rss_has_samples = @include_rss && detect_rss_samples(bench_data)
1619
@base_name = executable_names.first
1720
@other_names = executable_names[1..]
21+
@row_layout = row_layout
1822
@bench_names = compute_bench_names
1923
end
2024

@@ -26,11 +30,10 @@ def build
2630
table = [build_header]
2731
format = build_format
2832

29-
@bench_names.each do |bench_name|
30-
next unless has_complete_data?(bench_name)
33+
@row_layout.entries(@bench_names).each do |entry|
34+
next unless has_complete_data?(entry.data_key)
3135

32-
row = build_row(bench_name)
33-
table << row
36+
table << (entry.label_cells + build_stat_cells(entry.data_key))
3437
end
3538

3639
gc_table = build_gc_summary_table
@@ -45,7 +48,7 @@ def has_complete_data?(bench_name)
4548
end
4649

4750
def build_header
48-
header = ["bench"]
51+
header = ["bench", *@row_layout.extra_header_columns]
4952

5053
@executable_names.each do |name|
5154
header << "#{name} (ms)"
@@ -74,7 +77,7 @@ def build_header
7477
end
7578

7679
def build_format
77-
format = ["%s"]
80+
format = ["%s", *@row_layout.extra_format_columns]
7881

7982
@executable_names.each do |_name|
8083
format << "%s"
@@ -135,7 +138,7 @@ def build_gc_summary_format(gc_table)
135138
Array.new(gc_table.first.size, "%s")
136139
end
137140

138-
def build_row(bench_name)
141+
def build_stat_cells(bench_name)
139142
t0s = extract_first_iteration_times(bench_name)
140143
times_no_warmup = extract_benchmark_times(bench_name)
141144
rsss = extract_rss_values(bench_name)
@@ -153,7 +156,7 @@ def build_row(bench_name)
153156
[stat, extract_zjit_stat(bench_name, stat)]
154157
end
155158

156-
row = [bench_name]
159+
row = []
157160
build_base_columns(row, base_t, base_rss_cell, zjit_stat_values, 0)
158161
build_comparison_columns(row, other_ts, other_rss_cells, zjit_stat_values)
159162
build_ratio_columns(row, base_t0, other_t0s, base_t, other_ts)
@@ -438,7 +441,7 @@ def sort_benchmarks(bench_names, metadata)
438441
end
439442

440443
def category_priority(bench_name, metadata)
441-
category = metadata.dig(bench_name, 'category') || 'other'
444+
category = metadata.dig(@row_layout.base_name(bench_name), 'category') || 'other'
442445
case category
443446
when 'headline' then 0
444447
when 'micro' then 2

0 commit comments

Comments
 (0)