Skip to content

Commit c43731e

Browse files
authored
Merge pull request #443 from ruby/rmf-graph
Extract code to render the graph
2 parents ee7230c + 8d0a2c5 commit c43731e

File tree

7 files changed

+218
-55
lines changed

7 files changed

+218
-55
lines changed

.github/workflows/test.yml

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,27 @@ jobs:
1616
if: ${{ github.event_name != 'schedule' || github.repository == 'ruby/ruby-bench' }}
1717
steps:
1818
- uses: actions/checkout@v3
19+
- name: Cache apt packages
20+
uses: actions/cache@v4
21+
with:
22+
path: |
23+
/var/cache/apt/archives
24+
/var/lib/apt/lists
25+
key: ${{ runner.os }}-apt-imagemagick-${{ hashFiles('.github/workflows/test.yml') }}
26+
restore-keys: |
27+
${{ runner.os }}-apt-imagemagick-
28+
- name: Install ImageMagick dependencies
29+
run: |
30+
sudo apt-get update
31+
sudo apt-get install -y --no-install-recommends libmagickwand-dev
1932
- name: Set up Ruby
2033
uses: ruby/setup-ruby@v1
2134
with:
2235
ruby-version: ${{ matrix.ruby }}
2336

2437
- name: Run tests
2538
run: rake test
39+
continue-on-error: ${{ matrix.ruby == 'truffleruby' }}
2640

2741
benchmark-default:
2842
runs-on: ubuntu-latest
@@ -95,16 +109,26 @@ jobs:
95109
if: ${{ github.event_name != 'schedule' || github.repository == 'ruby/ruby-bench' }}
96110
steps:
97111
- uses: actions/checkout@v3
112+
- name: Cache apt packages
113+
uses: actions/cache@v4
114+
with:
115+
path: |
116+
/var/cache/apt/archives
117+
/var/lib/apt/lists
118+
key: ${{ runner.os }}-apt-imagemagick-${{ hashFiles('.github/workflows/test.yml') }}
119+
restore-keys: |
120+
${{ runner.os }}-apt-imagemagick-
121+
- name: Install ImageMagick dependencies
122+
run: |
123+
sudo apt-get update
124+
sudo apt-get install -y --no-install-recommends libmagickwand-dev
98125
- name: Set up Ruby
99126
uses: ruby/setup-ruby@v1
100127
with:
101128
ruby-version: ruby
102129

103130
- name: Test run_benchmarks.rb --graph
104-
run: |
105-
sudo apt-get update
106-
sudo apt-get install -y --no-install-recommends libmagickwand-dev
107-
./run_benchmarks.rb --graph fib
131+
run: ./run_benchmarks.rb --graph fib
108132
env:
109133
WARMUP_ITRS: '1'
110134
MIN_BENCH_ITRS: '1'

lib/benchmark_runner.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ def free_file_no(directory)
1616
end
1717
end
1818

19+
# Render a graph from JSON benchmark data
20+
def render_graph(json_path)
21+
png_path = json_path.sub(/\.json$/, '.png')
22+
require_relative 'graph_renderer'
23+
GraphRenderer.render(json_path, png_path)
24+
end
25+
1926
# Checked system - error or return info if the command fails
2027
def check_call(command, env: {}, raise_error: true, quiet: false)
2128
puts("+ #{command}") unless quiet

lib/graph_renderer.rb

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# frozen_string_literal: true
2+
3+
require_relative '../misc/stats'
4+
require 'json'
5+
begin
6+
require 'gruff'
7+
rescue LoadError
8+
Gem.install('gruff')
9+
gem 'gruff'
10+
require 'gruff'
11+
end
12+
13+
# Renders benchmark data as a graph
14+
class GraphRenderer
15+
DEFAULT_WIDTH = 1600
16+
COLOR_PALETTE = %w[#3285e1 #489d32 #e2c13e #8A6EAF #D1695E].freeze
17+
THEME = {
18+
colors: COLOR_PALETTE,
19+
marker_color: '#dddddd',
20+
font_color: 'black',
21+
background_colors: 'white'
22+
}.freeze
23+
DEFAULT_BOTTOM_MARGIN = 30.0
24+
DEFAULT_LEGEND_MARGIN = 4.0
25+
26+
class << self
27+
def render(json_path, png_path, title_font_size: 16.0, legend_font_size: 12.0, marker_font_size: 10.0)
28+
ruby_descriptions, data, baseline, bench_names = load_benchmark_data(json_path)
29+
30+
graph = Gruff::Bar.new(DEFAULT_WIDTH)
31+
configure_graph(graph, ruby_descriptions, bench_names, title_font_size, legend_font_size, marker_font_size)
32+
33+
ruby_descriptions.each do |ruby, description|
34+
speedups = calculate_speedups(data, baseline, ruby, bench_names)
35+
graph.data "#{ruby}: #{description}", speedups
36+
end
37+
graph.write(png_path)
38+
png_path
39+
end
40+
41+
private
42+
43+
def load_benchmark_data(json_path)
44+
json = JSON.load_file(json_path)
45+
ruby_descriptions = json.fetch("metadata")
46+
data = json.fetch("raw_data")
47+
baseline = ruby_descriptions.first.first
48+
bench_names = data.first.last.keys
49+
50+
[ruby_descriptions, data, baseline, bench_names]
51+
end
52+
53+
def configure_graph(graph, ruby_descriptions, bench_names, title_font_size, legend_font_size, marker_font_size)
54+
graph.title = "Speedup ratio relative to #{ruby_descriptions.keys.first}"
55+
graph.title_font_size = title_font_size
56+
graph.theme = THEME
57+
graph.labels = bench_names.map.with_index { |bench, index| [index, bench] }.to_h
58+
graph.show_labels_for_bar_values = true
59+
graph.bottom_margin = DEFAULT_BOTTOM_MARGIN
60+
graph.legend_margin = DEFAULT_LEGEND_MARGIN
61+
graph.legend_font_size = legend_font_size
62+
graph.marker_font_size = marker_font_size
63+
end
64+
65+
def calculate_speedups(data, baseline, ruby, bench_names)
66+
bench_names.map { |bench|
67+
baseline_times = data.fetch(baseline).fetch(bench).fetch("bench")
68+
times = data.fetch(ruby).fetch(bench).fetch("bench")
69+
Stats.new(baseline_times).mean / Stats.new(times).mean
70+
}
71+
end
72+
end
73+
end

misc/graph.rb

Lines changed: 4 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,9 @@
11
#!/usr/bin/env ruby
22

3-
require_relative 'stats'
4-
require 'json'
5-
begin
6-
require 'gruff'
7-
rescue LoadError
8-
Gem.install('gruff')
9-
gem 'gruff'
10-
require 'gruff'
11-
end
12-
13-
def render_graph(json_path, png_path, title_font_size: 16.0, legend_font_size: 12.0, marker_font_size: 10.0)
14-
json = JSON.load_file(json_path)
15-
ruby_descriptions = json.fetch("metadata")
16-
data = json.fetch("raw_data")
17-
baseline = ruby_descriptions.first.first
18-
bench_names = data.first.last.keys
19-
20-
# ruby_descriptions, bench_names, table
21-
g = Gruff::Bar.new(1600)
22-
g.title = "Speedup ratio relative to #{ruby_descriptions.keys.first}"
23-
g.title_font_size = title_font_size
24-
g.theme = {
25-
colors: %w[#3285e1 #489d32 #e2c13e #8A6EAF #D1695E],
26-
marker_color: '#dddddd',
27-
font_color: 'black',
28-
background_colors: 'white'
29-
}
30-
g.labels = bench_names.map.with_index { |bench, index| [index, bench] }.to_h
31-
g.show_labels_for_bar_values = true
32-
g.bottom_margin = 30.0
33-
g.legend_margin = 4.0
34-
g.legend_font_size = legend_font_size
35-
g.marker_font_size = marker_font_size
36-
37-
ruby_descriptions.each do |ruby, description|
38-
speedups = bench_names.map { |bench|
39-
baseline_times = data.fetch(baseline).fetch(bench).fetch("bench")
40-
times = data.fetch(ruby).fetch(bench).fetch("bench")
41-
Stats.new(baseline_times).mean / Stats.new(times).mean
42-
}
43-
g.data "#{ruby}: #{description}", speedups
44-
end
45-
g.write(png_path)
46-
end
3+
require_relative '../lib/graph_renderer'
474

48-
# This file may be used as a standalone command as well.
49-
if $0 == __FILE__
5+
# Standalone command-line interface for rendering graphs
6+
if __FILE__ == $0
507
require 'optparse'
518

529
args = {}
@@ -68,7 +25,7 @@ def render_graph(json_path, png_path, title_font_size: 16.0, legend_font_size: 1
6825
abort parser.help if json_path.nil?
6926

7027
png_path = json_path.sub(/\.json\z/, '.png')
71-
render_graph(json_path, png_path, **args)
28+
GraphRenderer.render(json_path, png_path, **args)
7229

7330
open = %w[open xdg-open].find { |open| system("which #{open} >/dev/null 2>/dev/null") }
7431
system(open, png_path) if open

run_benchmarks.rb

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,9 @@
126126
puts
127127
puts "Output:"
128128
puts out_json_path
129+
129130
if args.graph
130-
require_relative 'misc/graph'
131-
out_graph_path = output_path + ".png"
132-
render_graph(out_json_path, out_graph_path)
133-
puts out_graph_path
131+
puts BenchmarkRunner.render_graph(out_json_path)
134132
end
135133

136134
if !bench_failures.empty?

test/benchmark_runner_test.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,24 @@
157157
end
158158
end
159159
end
160+
161+
describe '.render_graph' do
162+
it 'delegates to GraphRenderer and returns calculated png_path' do
163+
Dir.mktmpdir do |dir|
164+
json_path = File.join(dir, 'test.json')
165+
expected_png_path = File.join(dir, 'test.png')
166+
167+
json_data = {
168+
metadata: { 'ruby-a' => 'version A' },
169+
raw_data: { 'ruby-a' => { 'bench1' => { 'bench' => [1.0] } } }
170+
}
171+
File.write(json_path, JSON.generate(json_data))
172+
173+
result = BenchmarkRunner.render_graph(json_path)
174+
175+
assert_equal expected_png_path, result
176+
assert File.exist?(expected_png_path)
177+
end
178+
end
179+
end
160180
end

test/graph_renderer_test.rb

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
require_relative 'test_helper'
2+
require_relative '../lib/graph_renderer'
3+
require 'tempfile'
4+
require 'tmpdir'
5+
require 'json'
6+
7+
describe GraphRenderer do
8+
describe '.render' do
9+
it 'creates a PNG file from JSON data' do
10+
Dir.mktmpdir do |dir|
11+
json_path = File.join(dir, 'test.json')
12+
png_path = File.join(dir, 'test.png')
13+
14+
# Create test JSON file with minimal benchmark data
15+
json_data = {
16+
metadata: {
17+
'ruby-a' => 'version A'
18+
},
19+
raw_data: {
20+
'ruby-a' => {
21+
'bench1' => {
22+
'bench' => [1.0, 1.1, 0.9]
23+
}
24+
}
25+
}
26+
}
27+
File.write(json_path, JSON.generate(json_data))
28+
29+
result = GraphRenderer.render(json_path, png_path)
30+
31+
assert_equal png_path, result
32+
assert File.exist?(png_path), 'PNG file should be created'
33+
assert File.size(png_path) > 0, 'PNG file should not be empty'
34+
end
35+
end
36+
37+
it 'returns the png_path' do
38+
Dir.mktmpdir do |dir|
39+
json_path = File.join(dir, 'test.json')
40+
png_path = File.join(dir, 'test.png')
41+
42+
json_data = {
43+
metadata: { 'ruby-a' => 'version A' },
44+
raw_data: { 'ruby-a' => { 'bench1' => { 'bench' => [1.0] } } }
45+
}
46+
File.write(json_path, JSON.generate(json_data))
47+
48+
result = GraphRenderer.render(json_path, png_path)
49+
50+
assert_equal png_path, result
51+
end
52+
end
53+
54+
it 'handles multiple rubies and benchmarks' do
55+
Dir.mktmpdir do |dir|
56+
json_path = File.join(dir, 'test.json')
57+
png_path = File.join(dir, 'test.png')
58+
59+
json_data = {
60+
metadata: {
61+
'ruby-a' => 'version A',
62+
'ruby-b' => 'version B'
63+
},
64+
raw_data: {
65+
'ruby-a' => {
66+
'bench1' => { 'bench' => [1.0, 1.1] },
67+
'bench2' => { 'bench' => [2.0, 2.1] }
68+
},
69+
'ruby-b' => {
70+
'bench1' => { 'bench' => [0.9, 1.0] },
71+
'bench2' => { 'bench' => [1.8, 1.9] }
72+
}
73+
}
74+
}
75+
File.write(json_path, JSON.generate(json_data))
76+
77+
GraphRenderer.render(json_path, png_path)
78+
79+
assert File.exist?(png_path)
80+
assert File.size(png_path) > 0
81+
end
82+
end
83+
end
84+
end

0 commit comments

Comments
 (0)