Skip to content

Commit 70aba1f

Browse files
Introduce a GC harness.
1 parent fde3e58 commit 70aba1f

5 files changed

Lines changed: 200 additions & 9 deletions

File tree

harness-gc/harness.rb

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
require_relative "../harness/harness-common"
2+
3+
WARMUP_ITRS = Integer(ENV.fetch('WARMUP_ITRS', 15))
4+
MIN_BENCH_ITRS = Integer(ENV.fetch('MIN_BENCH_ITRS', 10))
5+
MIN_BENCH_TIME = Integer(ENV.fetch('MIN_BENCH_TIME', 10))
6+
7+
puts RUBY_DESCRIPTION
8+
9+
def realtime
10+
r0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
11+
yield
12+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - r0
13+
end
14+
15+
def gc_stat_heap_snapshot
16+
return {} unless GC.respond_to?(:stat_heap)
17+
GC.stat_heap
18+
end
19+
20+
def gc_stat_heap_delta(before, after)
21+
delta = {}
22+
after.each do |heap_idx, after_stats|
23+
before_stats = before[heap_idx] || {}
24+
heap_delta = {}
25+
after_stats.each do |key, val|
26+
next unless val.is_a?(Numeric) && before_stats.key?(key)
27+
heap_delta[key] = val - before_stats[key]
28+
end
29+
delta[heap_idx] = heap_delta unless heap_delta.empty?
30+
end
31+
delta
32+
end
33+
34+
def run_benchmark(_num_itrs_hint, **, &block)
35+
times = []
36+
marking_times = []
37+
sweeping_times = []
38+
gc_counts = []
39+
gc_heap_deltas = []
40+
total_time = 0
41+
num_itrs = 0
42+
43+
has_marking = GC.stat.key?(:marking_time)
44+
has_sweeping = GC.stat.key?(:sweeping_time)
45+
46+
header = "itr: time"
47+
header << " marking" if has_marking
48+
header << " sweeping" if has_sweeping
49+
header << " gc_count"
50+
puts header
51+
52+
begin
53+
gc_before = GC.stat
54+
heap_before = gc_stat_heap_snapshot
55+
56+
time = realtime(&block)
57+
num_itrs += 1
58+
59+
gc_after = GC.stat
60+
heap_after = gc_stat_heap_snapshot
61+
62+
time_ms = (1000 * time).to_i
63+
mark_delta = has_marking ? gc_after[:marking_time] - gc_before[:marking_time] : 0
64+
sweep_delta = has_sweeping ? gc_after[:sweeping_time] - gc_before[:sweeping_time] : 0
65+
count_delta = gc_after[:count] - gc_before[:count]
66+
67+
itr_str = "%4s %6s" % ["##{num_itrs}:", "#{time_ms}ms"]
68+
itr_str << " %9.1fms" % mark_delta if has_marking
69+
itr_str << " %9.1fms" % sweep_delta if has_sweeping
70+
itr_str << " %9d" % count_delta
71+
puts itr_str
72+
73+
times << time
74+
marking_times << mark_delta
75+
sweeping_times << sweep_delta
76+
gc_counts << count_delta
77+
gc_heap_deltas << gc_stat_heap_delta(heap_before, heap_after)
78+
total_time += time
79+
end until num_itrs >= WARMUP_ITRS + MIN_BENCH_ITRS and total_time >= MIN_BENCH_TIME
80+
81+
warmup_range = 0...WARMUP_ITRS
82+
bench_range = WARMUP_ITRS..-1
83+
84+
extra = {}
85+
extra["gc_marking_time_warmup"] = marking_times[warmup_range]
86+
extra["gc_marking_time_bench"] = marking_times[bench_range]
87+
extra["gc_sweeping_time_warmup"] = sweeping_times[warmup_range]
88+
extra["gc_sweeping_time_bench"] = sweeping_times[bench_range]
89+
extra["gc_count_warmup"] = gc_counts[warmup_range]
90+
extra["gc_count_bench"] = gc_counts[bench_range]
91+
extra["gc_stat_heap_deltas"] = gc_heap_deltas[bench_range]
92+
93+
return_results(times[warmup_range], times[bench_range], **extra)
94+
95+
non_warmups = times[bench_range]
96+
if non_warmups.size > 1
97+
non_warmups_ms = ((non_warmups.sum / non_warmups.size) * 1000.0).to_i
98+
puts "Average of last #{non_warmups.size}, non-warmup iters: #{non_warmups_ms}ms"
99+
100+
if has_marking
101+
mark_bench = marking_times[bench_range]
102+
avg_mark = mark_bench.sum / mark_bench.size
103+
puts "Average marking time: %.1fms" % avg_mark
104+
end
105+
106+
if has_sweeping
107+
sweep_bench = sweeping_times[bench_range]
108+
avg_sweep = sweep_bench.sum / sweep_bench.size
109+
puts "Average sweeping time: %.1fms" % avg_sweep
110+
end
111+
end
112+
end

harness/harness-common.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,12 @@ def get_maxrss
135135
yb_env_var = ENV.fetch("RESULT_JSON_PATH", default_path)
136136
YB_OUTPUT_FILE = File.expand_path yb_env_var
137137

138-
def return_results(warmup_iterations, bench_iterations)
138+
def return_results(warmup_iterations, bench_iterations, **extra)
139139
ruby_bench_results = {
140140
"RUBY_DESCRIPTION" => RUBY_DESCRIPTION,
141141
"warmup" => warmup_iterations,
142142
"bench" => bench_iterations,
143+
**extra,
143144
}
144145

145146
# Collect JIT stats before loading any additional code.

lib/benchmark_runner.rb

Lines changed: 5 additions & 1 deletion
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)
51+
def build_output_text(ruby_descriptions, table, format, bench_failures, include_rss: false, include_gc: false)
5252
base_name, *other_names = ruby_descriptions.keys
5353

5454
output_str = +""
@@ -68,6 +68,10 @@ def build_output_text(ruby_descriptions, table, format, bench_failures, include_
6868
if include_rss
6969
output_str << "- RSS #{base_name}/#{name}: ratio of #{base_name}/#{name} RSS. Higher is better for #{name}. Above 1 means lower memory usage.\n"
7070
end
71+
if include_gc
72+
output_str << "- mark #{base_name}/#{name}: ratio of GC marking time. Higher is better for #{name}. Above 1 represents faster marking.\n"
73+
output_str << "- sweep #{base_name}/#{name}: ratio of GC sweeping time. Higher is better for #{name}. Above 1 represents faster sweeping.\n"
74+
end
7175
end
7276
output_str << "- ***: p < 0.001, **: p < 0.01, *: p < 0.05 (Welch's t-test)\n"
7377
end

lib/benchmark_runner/cli.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def run
109109
BenchmarkRunner.write_csv(output_path, ruby_descriptions, table)
110110

111111
# Save the output in a text file that we can easily refer to
112-
output_str = BenchmarkRunner.build_output_text(ruby_descriptions, table, format, bench_failures, include_rss: args.rss)
112+
output_str = BenchmarkRunner.build_output_text(ruby_descriptions, table, format, bench_failures, include_rss: args.rss, include_gc: builder.include_gc?)
113113
out_txt_path = output_path + ".txt"
114114
File.open(out_txt_path, "w") { |f| f.write output_str }
115115

lib/results_table_builder.rb

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@ def initialize(executable_names:, bench_data:, include_rss: false, include_pvalu
1010
@bench_data = bench_data
1111
@include_rss = include_rss
1212
@include_pvalue = include_pvalue
13+
@include_gc = detect_gc_data(bench_data)
1314
@base_name = executable_names.first
1415
@other_names = executable_names[1..]
1516
@bench_names = compute_bench_names
1617
end
1718

19+
def include_gc?
20+
@include_gc
21+
end
22+
1823
def build
1924
table = [build_header]
2025
format = build_format
@@ -41,6 +46,10 @@ def build_header
4146
@executable_names.each do |name|
4247
header << "#{name} (ms)"
4348
header << "RSS (MiB)" if @include_rss
49+
if @include_gc
50+
header << "#{name} mark (ms)"
51+
header << "#{name} sweep (ms)"
52+
end
4453
end
4554

4655
@other_names.each do |name|
@@ -60,6 +69,13 @@ def build_header
6069
end
6170
end
6271

72+
if @include_gc
73+
@other_names.each do |name|
74+
header << "mark #{@base_name}/#{name}"
75+
header << "sweep #{@base_name}/#{name}"
76+
end
77+
end
78+
6379
header
6480
end
6581

@@ -69,6 +85,10 @@ def build_format
6985
@executable_names.each do |_name|
7086
format << "%s"
7187
format << "%.1f" if @include_rss
88+
if @include_gc
89+
format << "%s"
90+
format << "%s"
91+
end
7292
end
7393

7494
@other_names.each do |_name|
@@ -88,6 +108,13 @@ def build_format
88108
end
89109
end
90110

111+
if @include_gc
112+
@other_names.each do |_name|
113+
format << "%s"
114+
format << "%s"
115+
end
116+
end
117+
91118
format
92119
end
93120

@@ -100,28 +127,45 @@ def build_row(bench_name)
100127
base_t, *other_ts = times_no_warmup
101128
base_rss, *other_rsss = rsss
102129

130+
if @include_gc
131+
marking_times = extract_gc_times(bench_name, 'gc_marking_time_bench')
132+
sweeping_times = extract_gc_times(bench_name, 'gc_sweeping_time_bench')
133+
base_mark, *other_marks = marking_times
134+
base_sweep, *other_sweeps = sweeping_times
135+
end
136+
103137
row = [bench_name]
104-
build_base_columns(row, base_t, base_rss)
105-
build_comparison_columns(row, other_ts, other_rsss)
138+
build_base_columns(row, base_t, base_rss, base_mark, base_sweep)
139+
build_comparison_columns(row, other_ts, other_rsss, other_marks, other_sweeps)
106140
build_ratio_columns(row, base_t0, other_t0s, base_t, other_ts)
107141
build_rss_ratio_columns(row, base_rss, other_rsss)
142+
build_gc_ratio_columns(row, base_mark, other_marks, base_sweep, other_sweeps)
108143

109144
row
110145
end
111146

112-
def build_base_columns(row, base_t, base_rss)
147+
def build_base_columns(row, base_t, base_rss, base_mark, base_sweep)
113148
row << format_time_with_stddev(base_t)
114149
row << base_rss if @include_rss
150+
if @include_gc
151+
row << format_time_with_stddev(base_mark)
152+
row << format_time_with_stddev(base_sweep)
153+
end
115154
end
116155

117-
def build_comparison_columns(row, other_ts, other_rsss)
118-
other_ts.zip(other_rsss).each do |other_t, other_rss|
156+
def build_comparison_columns(row, other_ts, other_rsss, other_marks, other_sweeps)
157+
other_ts.each_with_index do |other_t, i|
119158
row << format_time_with_stddev(other_t)
120-
row << other_rss if @include_rss
159+
row << other_rsss[i] if @include_rss
160+
if @include_gc
161+
row << format_time_with_stddev(other_marks[i])
162+
row << format_time_with_stddev(other_sweeps[i])
163+
end
121164
end
122165
end
123166

124167
def format_time_with_stddev(values)
168+
return "N/A" if values.nil? || values.empty?
125169
"%.1f ± %.1f%%" % [mean(values), stddev_percent(values)]
126170
end
127171

@@ -147,6 +191,26 @@ def build_rss_ratio_columns(row, base_rss, other_rsss)
147191
end
148192
end
149193

194+
def build_gc_ratio_columns(row, base_mark, other_marks, base_sweep, other_sweeps)
195+
return unless @include_gc
196+
197+
(other_marks || []).each do |other_mark|
198+
row << gc_ratio(base_mark, other_mark)
199+
end
200+
(other_sweeps || []).each do |other_sweep|
201+
row << gc_ratio(base_sweep, other_sweep)
202+
end
203+
end
204+
205+
def gc_ratio(base, other)
206+
if base.nil? || base.empty? || other.nil? || other.empty? ||
207+
mean(other) == 0.0
208+
return "N/A"
209+
end
210+
pval = Stats.welch_p_value(base, other)
211+
format_ratio(mean(base) / mean(other), pval)
212+
end
213+
150214
def format_ratio(ratio, pval)
151215
sym = significance_symbol(pval)
152216
formatted = "%.3f" % ratio
@@ -211,6 +275,16 @@ def extract_rss_values(bench_name)
211275
end
212276
end
213277

278+
def extract_gc_times(bench_name, key)
279+
@executable_names.map do |name|
280+
bench_data_for(name, bench_name)[key] || []
281+
end
282+
end
283+
284+
def detect_gc_data(bench_data)
285+
bench_data.values.any? { |benchmarks| benchmarks.values.any? { |d| d.is_a?(Hash) && d.key?('gc_marking_time_bench') } }
286+
end
287+
214288
def bench_data_for(name, bench_name)
215289
@bench_data[name][bench_name]
216290
end

0 commit comments

Comments
 (0)