serialbench 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/benchmark.yml +181 -30
- data/.github/workflows/ci.yml +3 -3
- data/.github/workflows/docker.yml +272 -0
- data/.github/workflows/rake.yml +15 -0
- data/.github/workflows/release.yml +25 -0
- data/Gemfile +6 -30
- data/README.adoc +381 -415
- data/Rakefile +0 -55
- data/config/benchmarks/full.yml +29 -0
- data/config/benchmarks/short.yml +26 -0
- data/config/environments/asdf-ruby-3.2.yml +8 -0
- data/config/environments/asdf-ruby-3.3.yml +8 -0
- data/config/environments/docker-ruby-3.0.yml +9 -0
- data/config/environments/docker-ruby-3.1.yml +9 -0
- data/config/environments/docker-ruby-3.2.yml +9 -0
- data/config/environments/docker-ruby-3.3.yml +9 -0
- data/config/environments/docker-ruby-3.4.yml +9 -0
- data/docker/Dockerfile.alpine +33 -0
- data/docker/Dockerfile.ubuntu +32 -0
- data/docker/README.md +214 -0
- data/exe/serialbench +1 -1
- data/lib/serialbench/benchmark_runner.rb +270 -350
- data/lib/serialbench/cli/base_cli.rb +51 -0
- data/lib/serialbench/cli/benchmark_cli.rb +380 -0
- data/lib/serialbench/cli/environment_cli.rb +181 -0
- data/lib/serialbench/cli/resultset_cli.rb +215 -0
- data/lib/serialbench/cli/ruby_build_cli.rb +238 -0
- data/lib/serialbench/cli.rb +59 -410
- data/lib/serialbench/config_manager.rb +140 -0
- data/lib/serialbench/models/benchmark_config.rb +63 -0
- data/lib/serialbench/models/benchmark_result.rb +45 -0
- data/lib/serialbench/models/environment_config.rb +71 -0
- data/lib/serialbench/models/platform.rb +59 -0
- data/lib/serialbench/models/result.rb +53 -0
- data/lib/serialbench/models/result_set.rb +71 -0
- data/lib/serialbench/models/result_store.rb +108 -0
- data/lib/serialbench/models.rb +54 -0
- data/lib/serialbench/ruby_build_manager.rb +153 -0
- data/lib/serialbench/runners/asdf_runner.rb +296 -0
- data/lib/serialbench/runners/base.rb +32 -0
- data/lib/serialbench/runners/docker_runner.rb +142 -0
- data/lib/serialbench/serializers/base_serializer.rb +8 -16
- data/lib/serialbench/serializers/json/base_json_serializer.rb +4 -4
- data/lib/serialbench/serializers/json/json_serializer.rb +0 -2
- data/lib/serialbench/serializers/json/oj_serializer.rb +0 -2
- data/lib/serialbench/serializers/json/rapidjson_serializer.rb +50 -0
- data/lib/serialbench/serializers/json/yajl_serializer.rb +6 -4
- data/lib/serialbench/serializers/toml/base_toml_serializer.rb +5 -3
- data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +0 -2
- data/lib/serialbench/serializers/toml/tomlib_serializer.rb +0 -2
- data/lib/serialbench/serializers/toml/tomlrb_serializer.rb +56 -0
- data/lib/serialbench/serializers/xml/base_xml_serializer.rb +4 -9
- data/lib/serialbench/serializers/xml/libxml_serializer.rb +0 -2
- data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +21 -5
- data/lib/serialbench/serializers/xml/oga_serializer.rb +0 -2
- data/lib/serialbench/serializers/xml/ox_serializer.rb +0 -2
- data/lib/serialbench/serializers/xml/rexml_serializer.rb +32 -4
- data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +59 -0
- data/lib/serialbench/serializers/yaml/psych_serializer.rb +54 -0
- data/lib/serialbench/serializers/yaml/syck_serializer.rb +102 -0
- data/lib/serialbench/serializers.rb +34 -6
- data/lib/serialbench/site_generator.rb +105 -0
- data/lib/serialbench/templates/assets/css/benchmark_report.css +535 -0
- data/lib/serialbench/templates/assets/css/format_based.css +526 -0
- data/lib/serialbench/templates/assets/css/themes.css +588 -0
- data/lib/serialbench/templates/assets/js/chart_helpers.js +381 -0
- data/lib/serialbench/templates/assets/js/dashboard.js +796 -0
- data/lib/serialbench/templates/assets/js/navigation.js +142 -0
- data/lib/serialbench/templates/base.liquid +49 -0
- data/lib/serialbench/templates/format_based.liquid +279 -0
- data/lib/serialbench/templates/partials/chart_section.liquid +4 -0
- data/lib/serialbench/version.rb +1 -1
- data/lib/serialbench.rb +2 -31
- data/serialbench.gemspec +28 -17
- metadata +192 -55
- data/lib/serialbench/chart_generator.rb +0 -821
- data/lib/serialbench/result_formatter.rb +0 -182
- data/lib/serialbench/result_merger.rb +0 -1201
- data/lib/serialbench/serializers/xml/base_parser.rb +0 -69
- data/lib/serialbench/serializers/xml/libxml_parser.rb +0 -98
- data/lib/serialbench/serializers/xml/nokogiri_parser.rb +0 -111
- data/lib/serialbench/serializers/xml/oga_parser.rb +0 -85
- data/lib/serialbench/serializers/xml/ox_parser.rb +0 -64
- data/lib/serialbench/serializers/xml/rexml_parser.rb +0 -129
@@ -1,821 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Serialbench
|
4
|
-
class ChartGenerator
|
5
|
-
COLORS = {
|
6
|
-
ox: '#2E8B57', # Sea Green
|
7
|
-
nokogiri: '#4169E1', # Royal Blue
|
8
|
-
libxml: '#DC143C', # Crimson
|
9
|
-
oga: '#FF8C00', # Dark Orange
|
10
|
-
rexml: '#9932CC' # Dark Orchid
|
11
|
-
}.freeze
|
12
|
-
|
13
|
-
def initialize(results)
|
14
|
-
@results = results
|
15
|
-
end
|
16
|
-
|
17
|
-
def generate_all_charts(output_dir = 'results/charts')
|
18
|
-
FileUtils.mkdir_p(output_dir)
|
19
|
-
|
20
|
-
charts = {
|
21
|
-
'dom_parsing_performance.svg' => generate_dom_parsing_chart,
|
22
|
-
'sax_parsing_performance.svg' => generate_sax_parsing_chart,
|
23
|
-
'xml_generation_performance.svg' => generate_xml_generation_chart,
|
24
|
-
'memory_usage_comparison.svg' => generate_memory_usage_chart,
|
25
|
-
'performance_overview.svg' => generate_overview_chart
|
26
|
-
}
|
27
|
-
|
28
|
-
charts.each do |filename, svg_content|
|
29
|
-
File.write(File.join(output_dir, filename), svg_content)
|
30
|
-
end
|
31
|
-
|
32
|
-
charts.keys
|
33
|
-
end
|
34
|
-
|
35
|
-
def generate_multi_version_charts(output_dir = 'results/charts')
|
36
|
-
FileUtils.mkdir_p(output_dir)
|
37
|
-
|
38
|
-
charts = {
|
39
|
-
'ruby_version_comparison.svg' => generate_ruby_version_comparison_chart,
|
40
|
-
'multi_version_overview.svg' => generate_multi_version_overview_chart,
|
41
|
-
'dom_parsing_performance.svg' => generate_multi_version_dom_chart,
|
42
|
-
'sax_parsing_performance.svg' => generate_multi_version_sax_chart,
|
43
|
-
'xml_generation_performance.svg' => generate_multi_version_generation_chart
|
44
|
-
}
|
45
|
-
|
46
|
-
charts.each do |filename, svg_content|
|
47
|
-
File.write(File.join(output_dir, filename), svg_content)
|
48
|
-
end
|
49
|
-
|
50
|
-
charts.keys
|
51
|
-
end
|
52
|
-
|
53
|
-
def generate_dom_parsing_chart
|
54
|
-
data = extract_performance_data(@results[:dom_parsing])
|
55
|
-
create_bar_chart(
|
56
|
-
title: 'DOM parsing performance comparison',
|
57
|
-
subtitle: 'Time per iteration (lower is better)',
|
58
|
-
data: data,
|
59
|
-
y_label: 'Time (milliseconds)',
|
60
|
-
value_formatter: ->(v) { "#{(v * 1000).round(2)}ms" }
|
61
|
-
)
|
62
|
-
end
|
63
|
-
|
64
|
-
def generate_sax_parsing_chart
|
65
|
-
data = extract_performance_data(@results[:sax_parsing])
|
66
|
-
create_bar_chart(
|
67
|
-
title: 'SAX parsing performance comparison',
|
68
|
-
subtitle: 'Time per iteration (lower is better)',
|
69
|
-
data: data,
|
70
|
-
y_label: 'Time (milliseconds)',
|
71
|
-
value_formatter: ->(v) { "#{(v * 1000).round(2)}ms" }
|
72
|
-
)
|
73
|
-
end
|
74
|
-
|
75
|
-
def generate_xml_generation_chart
|
76
|
-
data = extract_performance_data(@results[:xml_generation])
|
77
|
-
create_bar_chart(
|
78
|
-
title: 'XML generation performance comparison',
|
79
|
-
subtitle: 'Time per iteration (lower is better)',
|
80
|
-
data: data,
|
81
|
-
y_label: 'Time (milliseconds)',
|
82
|
-
value_formatter: ->(v) { "#{(v * 1000).round(2)}ms" }
|
83
|
-
)
|
84
|
-
end
|
85
|
-
|
86
|
-
def generate_memory_usage_chart
|
87
|
-
return create_empty_chart('Memory usage data not available') unless @results[:memory_usage]
|
88
|
-
|
89
|
-
data = extract_memory_data(@results[:memory_usage])
|
90
|
-
create_bar_chart(
|
91
|
-
title: 'Memory usage comparison',
|
92
|
-
subtitle: 'Allocated memory (lower is better)',
|
93
|
-
data: data,
|
94
|
-
y_label: 'Memory (MB)',
|
95
|
-
value_formatter: ->(v) { "#{(v / 1024.0 / 1024.0).round(2)}MB" }
|
96
|
-
)
|
97
|
-
end
|
98
|
-
|
99
|
-
def generate_overview_chart
|
100
|
-
# Create a radar chart showing relative performance across all metrics
|
101
|
-
create_radar_chart
|
102
|
-
end
|
103
|
-
|
104
|
-
private
|
105
|
-
|
106
|
-
def extract_performance_data(benchmark_results)
|
107
|
-
return {} unless benchmark_results
|
108
|
-
|
109
|
-
data = {}
|
110
|
-
benchmark_results.each do |size, parsers|
|
111
|
-
data[size] = {}
|
112
|
-
parsers.each do |parser, results|
|
113
|
-
next if results[:error]
|
114
|
-
|
115
|
-
data[size][parser] = results[:time_per_iteration]
|
116
|
-
end
|
117
|
-
end
|
118
|
-
data
|
119
|
-
end
|
120
|
-
|
121
|
-
def extract_memory_data(memory_results)
|
122
|
-
return {} unless memory_results
|
123
|
-
|
124
|
-
data = {}
|
125
|
-
memory_results.each do |size, parsers|
|
126
|
-
data[size] = {}
|
127
|
-
parsers.each do |parser, results|
|
128
|
-
next if results[:error]
|
129
|
-
|
130
|
-
data[size][parser] = results[:allocated_memory]
|
131
|
-
end
|
132
|
-
end
|
133
|
-
data
|
134
|
-
end
|
135
|
-
|
136
|
-
def create_bar_chart(title:, subtitle:, data:, y_label:, value_formatter:)
|
137
|
-
return create_empty_chart('No data available') if data.empty?
|
138
|
-
|
139
|
-
width = 800
|
140
|
-
height = 600
|
141
|
-
margin = { top: 80, right: 50, bottom: 100, left: 80 }
|
142
|
-
chart_width = width - margin[:left] - margin[:right]
|
143
|
-
chart_height = height - margin[:top] - margin[:bottom]
|
144
|
-
|
145
|
-
# Prepare data for charting
|
146
|
-
sizes = data.keys
|
147
|
-
parsers = data.values.flat_map(&:keys).uniq
|
148
|
-
max_value = data.values.flat_map(&:values).compact.max || 1
|
149
|
-
|
150
|
-
# Calculate bar dimensions
|
151
|
-
group_width = chart_width / sizes.length.to_f
|
152
|
-
bar_width = (group_width * 0.8) / parsers.length.to_f
|
153
|
-
bar_spacing = group_width * 0.1
|
154
|
-
|
155
|
-
<<~SVG
|
156
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
157
|
-
<svg width="#{width}" height="#{height}" xmlns="http://www.w3.org/2000/svg">
|
158
|
-
<defs>
|
159
|
-
<style>
|
160
|
-
.chart-title { font-family: Arial, sans-serif; font-size: 20px; font-weight: bold; text-anchor: middle; }
|
161
|
-
.chart-subtitle { font-family: Arial, sans-serif; font-size: 14px; text-anchor: middle; fill: #666; }
|
162
|
-
.axis-label { font-family: Arial, sans-serif; font-size: 12px; text-anchor: middle; }
|
163
|
-
.tick-label { font-family: Arial, sans-serif; font-size: 10px; text-anchor: middle; }
|
164
|
-
.legend-text { font-family: Arial, sans-serif; font-size: 11px; }
|
165
|
-
.bar { stroke: none; }
|
166
|
-
.bar:hover { opacity: 0.8; }
|
167
|
-
.grid-line { stroke: #e0e0e0; stroke-width: 1; }
|
168
|
-
.axis-line { stroke: #333; stroke-width: 2; }
|
169
|
-
</style>
|
170
|
-
</defs>
|
171
|
-
|
172
|
-
<!-- Background -->
|
173
|
-
<rect width="#{width}" height="#{height}" fill="white"/>
|
174
|
-
|
175
|
-
<!-- Title -->
|
176
|
-
<text x="#{width / 2}" y="30" class="chart-title">#{title}</text>
|
177
|
-
<text x="#{width / 2}" y="50" class="chart-subtitle">#{subtitle}</text>
|
178
|
-
|
179
|
-
<!-- Chart area -->
|
180
|
-
<g transform="translate(#{margin[:left]}, #{margin[:top]})">
|
181
|
-
<!-- Grid lines -->
|
182
|
-
#{generate_grid_lines(chart_height, max_value, 5)}
|
183
|
-
|
184
|
-
<!-- Bars -->
|
185
|
-
#{generate_bars(data, sizes, parsers, chart_height, max_value, group_width, bar_width, bar_spacing)}
|
186
|
-
|
187
|
-
<!-- Axes -->
|
188
|
-
<line x1="0" y1="#{chart_height}" x2="#{chart_width}" y2="#{chart_height}" class="axis-line"/>
|
189
|
-
<line x1="0" y1="0" x2="0" y2="#{chart_height}" class="axis-line"/>
|
190
|
-
|
191
|
-
<!-- Y-axis labels -->
|
192
|
-
#{generate_y_axis_labels(chart_height, max_value, 5, value_formatter)}
|
193
|
-
|
194
|
-
<!-- X-axis labels -->
|
195
|
-
#{generate_x_axis_labels(sizes, chart_width, chart_height)}
|
196
|
-
|
197
|
-
<!-- Y-axis title -->
|
198
|
-
<text x="-40" y="#{chart_height / 2}" class="axis-label" transform="rotate(-90, -40, #{chart_height / 2})">#{y_label}</text>
|
199
|
-
</g>
|
200
|
-
|
201
|
-
<!-- Legend -->
|
202
|
-
#{generate_legend(parsers, width, height, margin)}
|
203
|
-
</svg>
|
204
|
-
SVG
|
205
|
-
end
|
206
|
-
|
207
|
-
def generate_grid_lines(chart_height, max_value, num_ticks)
|
208
|
-
lines = []
|
209
|
-
(0..num_ticks).each do |i|
|
210
|
-
y = chart_height - (i * chart_height / num_ticks.to_f)
|
211
|
-
lines << %(<line x1="0" y1="#{y}" x2="100%" y2="#{y}" class="grid-line"/>)
|
212
|
-
end
|
213
|
-
lines.join("\n")
|
214
|
-
end
|
215
|
-
|
216
|
-
def generate_bars(data, sizes, parsers, chart_height, max_value, group_width, bar_width, bar_spacing)
|
217
|
-
bars = []
|
218
|
-
|
219
|
-
sizes.each_with_index do |size, size_index|
|
220
|
-
group_x = size_index * group_width + bar_spacing / 2
|
221
|
-
|
222
|
-
parsers.each_with_index do |parser, parser_index|
|
223
|
-
value = data[size][parser] || 0
|
224
|
-
next if value == 0
|
225
|
-
|
226
|
-
bar_height = (value / max_value.to_f) * chart_height
|
227
|
-
bar_x = group_x + parser_index * bar_width
|
228
|
-
bar_y = chart_height - bar_height
|
229
|
-
|
230
|
-
color = COLORS[parser.to_sym] || '#999999'
|
231
|
-
|
232
|
-
bars << <<~BAR
|
233
|
-
<rect x="#{bar_x}" y="#{bar_y}" width="#{bar_width}" height="#{bar_height}"
|
234
|
-
fill="#{color}" class="bar">
|
235
|
-
<title>#{parser} (#{size}): #{value}</title>
|
236
|
-
</rect>
|
237
|
-
BAR
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
|
-
bars.join("\n")
|
242
|
-
end
|
243
|
-
|
244
|
-
def generate_y_axis_labels(chart_height, max_value, num_ticks, value_formatter)
|
245
|
-
labels = []
|
246
|
-
(0..num_ticks).each do |i|
|
247
|
-
value = (i * max_value / num_ticks.to_f)
|
248
|
-
y = chart_height - (i * chart_height / num_ticks.to_f)
|
249
|
-
formatted_value = value_formatter.call(value)
|
250
|
-
labels << %(<text x="-10" y="#{y + 4}" class="tick-label" text-anchor="end">#{formatted_value}</text>)
|
251
|
-
end
|
252
|
-
labels.join("\n")
|
253
|
-
end
|
254
|
-
|
255
|
-
def generate_x_axis_labels(sizes, chart_width, chart_height)
|
256
|
-
labels = []
|
257
|
-
sizes.each_with_index do |size, index|
|
258
|
-
x = (index + 0.5) * (chart_width / sizes.length.to_f)
|
259
|
-
labels << %(<text x="#{x}" y="#{chart_height + 20}" class="tick-label">#{size.to_s.capitalize}</text>)
|
260
|
-
end
|
261
|
-
labels.join("\n")
|
262
|
-
end
|
263
|
-
|
264
|
-
def generate_legend(parsers, width, height, margin)
|
265
|
-
legend_x = width - margin[:right] - 150
|
266
|
-
legend_y = margin[:top] + 20
|
267
|
-
|
268
|
-
legend_items = []
|
269
|
-
parsers.each_with_index do |parser, index|
|
270
|
-
y = legend_y + index * 20
|
271
|
-
color = COLORS[parser.to_sym] || '#999999'
|
272
|
-
|
273
|
-
legend_items << <<~ITEM
|
274
|
-
<rect x="#{legend_x}" y="#{y - 8}" width="12" height="12" fill="#{color}"/>
|
275
|
-
<text x="#{legend_x + 18}" y="#{y + 2}" class="legend-text">#{parser.capitalize}</text>
|
276
|
-
ITEM
|
277
|
-
end
|
278
|
-
|
279
|
-
legend_items.join("\n")
|
280
|
-
end
|
281
|
-
|
282
|
-
def create_radar_chart
|
283
|
-
# Simplified radar chart for overview
|
284
|
-
parsers = @results[:dom_parsing]&.values&.first&.keys || []
|
285
|
-
return create_empty_chart('No data for overview') if parsers.empty?
|
286
|
-
|
287
|
-
width = 600
|
288
|
-
height = 600
|
289
|
-
center_x = width / 2
|
290
|
-
center_y = height / 2
|
291
|
-
radius = 200
|
292
|
-
|
293
|
-
# Calculate relative scores for each parser across all metrics
|
294
|
-
scores = calculate_relative_scores(parsers)
|
295
|
-
|
296
|
-
<<~SVG
|
297
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
298
|
-
<svg width="#{width}" height="#{height}" xmlns="http://www.w3.org/2000/svg">
|
299
|
-
<defs>
|
300
|
-
<style>
|
301
|
-
.chart-title { font-family: Arial, sans-serif; font-size: 18px; font-weight: bold; text-anchor: middle; }
|
302
|
-
.radar-grid { stroke: #ddd; stroke-width: 1; fill: none; }
|
303
|
-
.radar-line { stroke-width: 2; fill: none; opacity: 0.8; }
|
304
|
-
.radar-area { opacity: 0.3; }
|
305
|
-
.axis-label { font-family: Arial, sans-serif; font-size: 12px; text-anchor: middle; }
|
306
|
-
</style>
|
307
|
-
</defs>
|
308
|
-
|
309
|
-
<!-- Background -->
|
310
|
-
<rect width="#{width}" height="#{height}" fill="white"/>
|
311
|
-
|
312
|
-
<!-- Title -->
|
313
|
-
<text x="#{center_x}" y="30" class="chart-title">Performance overview (relative scores)</text>
|
314
|
-
|
315
|
-
<!-- Radar grid -->
|
316
|
-
#{generate_radar_grid(center_x, center_y, radius)}
|
317
|
-
|
318
|
-
<!-- Radar areas for each parser -->
|
319
|
-
#{generate_radar_areas(scores, center_x, center_y, radius)}
|
320
|
-
|
321
|
-
<!-- Axis labels -->
|
322
|
-
#{generate_radar_labels(center_x, center_y, radius)}
|
323
|
-
</svg>
|
324
|
-
SVG
|
325
|
-
end
|
326
|
-
|
327
|
-
def generate_radar_grid(center_x, center_y, radius)
|
328
|
-
grid = []
|
329
|
-
|
330
|
-
# Concentric circles
|
331
|
-
(1..5).each do |i|
|
332
|
-
r = radius * i / 5.0
|
333
|
-
grid << %(<circle cx="#{center_x}" cy="#{center_y}" r="#{r}" class="radar-grid"/>)
|
334
|
-
end
|
335
|
-
|
336
|
-
# Radial lines
|
337
|
-
metrics = ['DOM Parse', 'SAX Parse', 'XML Gen', 'Memory']
|
338
|
-
metrics.each_with_index do |_, index|
|
339
|
-
angle = (index * 2 * Math::PI / metrics.length) - Math::PI / 2
|
340
|
-
x = center_x + radius * Math.cos(angle)
|
341
|
-
y = center_y + radius * Math.sin(angle)
|
342
|
-
grid << %(<line x1="#{center_x}" y1="#{center_y}" x2="#{x}" y2="#{y}" class="radar-grid"/>)
|
343
|
-
end
|
344
|
-
|
345
|
-
grid.join("\n")
|
346
|
-
end
|
347
|
-
|
348
|
-
def generate_radar_areas(scores, center_x, center_y, radius)
|
349
|
-
areas = []
|
350
|
-
|
351
|
-
scores.each do |parser, parser_scores|
|
352
|
-
color = COLORS[parser.to_sym] || '#999999'
|
353
|
-
points = []
|
354
|
-
|
355
|
-
parser_scores.each_with_index do |score, index|
|
356
|
-
angle = (index * 2 * Math::PI / parser_scores.length) - Math::PI / 2
|
357
|
-
r = radius * score
|
358
|
-
x = center_x + r * Math.cos(angle)
|
359
|
-
y = center_y + r * Math.sin(angle)
|
360
|
-
points << "#{x},#{y}"
|
361
|
-
end
|
362
|
-
|
363
|
-
areas << <<~AREA
|
364
|
-
<polygon points="#{points.join(' ')}" fill="#{color}" class="radar-area"/>
|
365
|
-
<polygon points="#{points.join(' ')}" stroke="#{color}" class="radar-line"/>
|
366
|
-
AREA
|
367
|
-
end
|
368
|
-
|
369
|
-
areas.join("\n")
|
370
|
-
end
|
371
|
-
|
372
|
-
def generate_radar_labels(center_x, center_y, radius)
|
373
|
-
labels = []
|
374
|
-
metrics = ['DOM parsing', 'SAX parsing', 'XML generation', 'Memory efficiency']
|
375
|
-
|
376
|
-
metrics.each_with_index do |metric, index|
|
377
|
-
angle = (index * 2 * Math::PI / metrics.length) - Math::PI / 2
|
378
|
-
x = center_x + (radius + 30) * Math.cos(angle)
|
379
|
-
y = center_y + (radius + 30) * Math.sin(angle)
|
380
|
-
labels << %(<text x="#{x}" y="#{y}" class="axis-label">#{metric}</text>)
|
381
|
-
end
|
382
|
-
|
383
|
-
labels.join("\n")
|
384
|
-
end
|
385
|
-
|
386
|
-
def calculate_relative_scores(parsers)
|
387
|
-
scores = {}
|
388
|
-
|
389
|
-
parsers.each do |parser|
|
390
|
-
# Calculate relative performance scores (higher is better)
|
391
|
-
dom_score = calculate_performance_score(parser, @results[:dom_parsing])
|
392
|
-
sax_score = calculate_performance_score(parser, @results[:sax_parsing])
|
393
|
-
gen_score = calculate_performance_score(parser, @results[:xml_generation])
|
394
|
-
mem_score = calculate_memory_score(parser, @results[:memory_usage])
|
395
|
-
|
396
|
-
scores[parser] = [dom_score, sax_score, gen_score, mem_score]
|
397
|
-
end
|
398
|
-
|
399
|
-
scores
|
400
|
-
end
|
401
|
-
|
402
|
-
def calculate_performance_score(parser, results)
|
403
|
-
return 0.5 unless results
|
404
|
-
|
405
|
-
# Average performance across all test sizes (inverted - lower time = higher score)
|
406
|
-
times = results.values.map { |r| r[parser]&.[](:time_per_iteration) }.compact
|
407
|
-
return 0.5 if times.empty?
|
408
|
-
|
409
|
-
avg_time = times.sum / times.length.to_f
|
410
|
-
all_times = results.values.flat_map { |r| r.values.map { |v| v[:time_per_iteration] } }.compact
|
411
|
-
max_time = all_times.max
|
412
|
-
|
413
|
-
return 0.5 if max_time.nil? || max_time == 0
|
414
|
-
|
415
|
-
# Invert and normalize (faster = higher score)
|
416
|
-
1.0 - (avg_time / max_time)
|
417
|
-
end
|
418
|
-
|
419
|
-
def calculate_memory_score(parser, results)
|
420
|
-
return 0.5 unless results
|
421
|
-
|
422
|
-
# Average memory usage across all test sizes (inverted - lower memory = higher score)
|
423
|
-
memories = results.values.map { |r| r[parser]&.[](:allocated_memory) }.compact
|
424
|
-
return 0.5 if memories.empty?
|
425
|
-
|
426
|
-
avg_memory = memories.sum / memories.length.to_f
|
427
|
-
all_memories = results.values.flat_map { |r| r.values.map { |v| v[:allocated_memory] } }.compact
|
428
|
-
max_memory = all_memories.max
|
429
|
-
|
430
|
-
return 0.5 if max_memory.nil? || max_memory == 0
|
431
|
-
|
432
|
-
# Invert and normalize (less memory = higher score)
|
433
|
-
1.0 - (avg_memory / max_memory)
|
434
|
-
end
|
435
|
-
|
436
|
-
def generate_ruby_version_comparison_chart
|
437
|
-
return create_empty_chart('Multi-version data not available') unless @results[:ruby_versions]
|
438
|
-
|
439
|
-
ruby_versions = @results[:ruby_versions].keys.sort
|
440
|
-
|
441
|
-
# Create a line chart showing performance trends across Ruby versions
|
442
|
-
create_version_trend_chart(
|
443
|
-
title: 'Performance trends across Ruby versions',
|
444
|
-
subtitle: 'Average DOM parsing performance by Ruby version',
|
445
|
-
ruby_versions: ruby_versions,
|
446
|
-
data_extractor: ->(version_data) { extract_average_dom_performance(version_data) }
|
447
|
-
)
|
448
|
-
end
|
449
|
-
|
450
|
-
def generate_multi_version_overview_chart
|
451
|
-
return create_empty_chart('Multi-version data not available') unless @results[:ruby_versions]
|
452
|
-
|
453
|
-
# Create a comprehensive overview showing all parsers across all Ruby versions
|
454
|
-
create_multi_version_heatmap
|
455
|
-
end
|
456
|
-
|
457
|
-
def generate_multi_version_dom_chart
|
458
|
-
return create_empty_chart('Multi-version data not available') unless @results[:ruby_versions]
|
459
|
-
|
460
|
-
create_multi_version_category_chart('DOM parsing', :dom_parsing)
|
461
|
-
end
|
462
|
-
|
463
|
-
def generate_multi_version_sax_chart
|
464
|
-
return create_empty_chart('Multi-version data not available') unless @results[:ruby_versions]
|
465
|
-
|
466
|
-
create_multi_version_category_chart('SAX parsing', :sax_parsing)
|
467
|
-
end
|
468
|
-
|
469
|
-
def generate_multi_version_generation_chart
|
470
|
-
return create_empty_chart('Multi-version data not available') unless @results[:ruby_versions]
|
471
|
-
|
472
|
-
create_multi_version_category_chart('XML generation', :xml_generation)
|
473
|
-
end
|
474
|
-
|
475
|
-
def create_version_trend_chart(title:, subtitle:, ruby_versions:, data_extractor:)
|
476
|
-
width = 800
|
477
|
-
height = 600
|
478
|
-
margin = { top: 80, right: 150, bottom: 100, left: 80 }
|
479
|
-
chart_width = width - margin[:left] - margin[:right]
|
480
|
-
chart_height = height - margin[:top] - margin[:bottom]
|
481
|
-
|
482
|
-
# Extract data for all parsers across versions
|
483
|
-
parser_data = {}
|
484
|
-
ruby_versions.each do |version|
|
485
|
-
version_data = @results[:ruby_versions][version]
|
486
|
-
performance = data_extractor.call(version_data)
|
487
|
-
|
488
|
-
performance.each do |parser, value|
|
489
|
-
parser_data[parser] ||= []
|
490
|
-
parser_data[parser] << { version: version, value: value }
|
491
|
-
end
|
492
|
-
end
|
493
|
-
|
494
|
-
return create_empty_chart('No trend data available') if parser_data.empty?
|
495
|
-
|
496
|
-
max_value = parser_data.values.flat_map { |data| data.map { |d| d[:value] } }.max || 1
|
497
|
-
|
498
|
-
<<~SVG
|
499
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
500
|
-
<svg width="#{width}" height="#{height}" xmlns="http://www.w3.org/2000/svg">
|
501
|
-
<defs>
|
502
|
-
<style>
|
503
|
-
.chart-title { font-family: Arial, sans-serif; font-size: 20px; font-weight: bold; text-anchor: middle; }
|
504
|
-
.chart-subtitle { font-family: Arial, sans-serif; font-size: 14px; text-anchor: middle; fill: #666; }
|
505
|
-
.axis-label { font-family: Arial, sans-serif; font-size: 12px; text-anchor: middle; }
|
506
|
-
.tick-label { font-family: Arial, sans-serif; font-size: 10px; text-anchor: middle; }
|
507
|
-
.legend-text { font-family: Arial, sans-serif; font-size: 11px; }
|
508
|
-
.trend-line { stroke-width: 3; fill: none; }
|
509
|
-
.trend-point { stroke-width: 2; fill: white; }
|
510
|
-
.grid-line { stroke: #e0e0e0; stroke-width: 1; }
|
511
|
-
.axis-line { stroke: #333; stroke-width: 2; }
|
512
|
-
</style>
|
513
|
-
</defs>
|
514
|
-
|
515
|
-
<!-- Background -->
|
516
|
-
<rect width="#{width}" height="#{height}" fill="white"/>
|
517
|
-
|
518
|
-
<!-- Title -->
|
519
|
-
<text x="#{width / 2}" y="30" class="chart-title">#{title}</text>
|
520
|
-
<text x="#{width / 2}" y="50" class="chart-subtitle">#{subtitle}</text>
|
521
|
-
|
522
|
-
<!-- Chart area -->
|
523
|
-
<g transform="translate(#{margin[:left]}, #{margin[:top]})">
|
524
|
-
<!-- Grid lines -->
|
525
|
-
#{generate_trend_grid_lines(chart_width, chart_height, ruby_versions.length, max_value)}
|
526
|
-
|
527
|
-
<!-- Trend lines -->
|
528
|
-
#{generate_trend_lines(parser_data, ruby_versions, chart_width, chart_height, max_value)}
|
529
|
-
|
530
|
-
<!-- Axes -->
|
531
|
-
<line x1="0" y1="#{chart_height}" x2="#{chart_width}" y2="#{chart_height}" class="axis-line"/>
|
532
|
-
<line x1="0" y1="0" x2="0" y2="#{chart_height}" class="axis-line"/>
|
533
|
-
|
534
|
-
<!-- Axis labels -->
|
535
|
-
#{generate_trend_x_labels(ruby_versions, chart_width, chart_height)}
|
536
|
-
#{generate_trend_y_labels(chart_height, max_value)}
|
537
|
-
|
538
|
-
<!-- Axis titles -->
|
539
|
-
<text x="#{chart_width / 2}" y="#{chart_height + 50}" class="axis-label">Ruby version</text>
|
540
|
-
<text x="-50" y="#{chart_height / 2}" class="axis-label" transform="rotate(-90, -50, #{chart_height / 2})">Time (ms)</text>
|
541
|
-
</g>
|
542
|
-
|
543
|
-
<!-- Legend -->
|
544
|
-
#{generate_trend_legend(parser_data.keys, width, margin)}
|
545
|
-
</svg>
|
546
|
-
SVG
|
547
|
-
end
|
548
|
-
|
549
|
-
def create_multi_version_heatmap
|
550
|
-
ruby_versions = @results[:ruby_versions].keys.sort
|
551
|
-
parsers = extract_all_parsers_from_versions
|
552
|
-
|
553
|
-
width = 800
|
554
|
-
height = 600
|
555
|
-
margin = { top: 80, right: 50, bottom: 100, left: 100 }
|
556
|
-
|
557
|
-
<<~SVG
|
558
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
559
|
-
<svg width="#{width}" height="#{height}" xmlns="http://www.w3.org/2000/svg">
|
560
|
-
<defs>
|
561
|
-
<style>
|
562
|
-
.chart-title { font-family: Arial, sans-serif; font-size: 20px; font-weight: bold; text-anchor: middle; }
|
563
|
-
.axis-label { font-family: Arial, sans-serif; font-size: 12px; text-anchor: middle; }
|
564
|
-
.heatmap-cell { stroke: white; stroke-width: 2; }
|
565
|
-
.heatmap-text { font-family: Arial, sans-serif; font-size: 10px; text-anchor: middle; fill: white; }
|
566
|
-
</style>
|
567
|
-
</defs>
|
568
|
-
|
569
|
-
<!-- Background -->
|
570
|
-
<rect width="#{width}" height="#{height}" fill="white"/>
|
571
|
-
|
572
|
-
<!-- Title -->
|
573
|
-
<text x="#{width / 2}" y="30" class="chart-title">Multi-version performance heatmap</text>
|
574
|
-
<text x="#{width / 2}" y="50" class="chart-subtitle">Relative performance across Ruby versions (darker = better)</text>
|
575
|
-
|
576
|
-
<!-- Heatmap -->
|
577
|
-
<g transform="translate(#{margin[:left]}, #{margin[:top]})">
|
578
|
-
#{generate_heatmap_cells(ruby_versions, parsers, width - margin[:left] - margin[:right], height - margin[:top] - margin[:bottom])}
|
579
|
-
#{generate_heatmap_labels(ruby_versions, parsers, width - margin[:left] - margin[:right], height - margin[:top] - margin[:bottom])}
|
580
|
-
</g>
|
581
|
-
</svg>
|
582
|
-
SVG
|
583
|
-
end
|
584
|
-
|
585
|
-
def create_multi_version_category_chart(category_name, category_key)
|
586
|
-
ruby_versions = @results[:ruby_versions].keys.sort
|
587
|
-
|
588
|
-
# Aggregate data across versions for this category
|
589
|
-
aggregated_data = {}
|
590
|
-
ruby_versions.each do |version|
|
591
|
-
version_data = @results[:ruby_versions][version]
|
592
|
-
category_data = version_data[category_key]
|
593
|
-
next unless category_data
|
594
|
-
|
595
|
-
category_data.each do |size, parsers|
|
596
|
-
aggregated_data[size] ||= {}
|
597
|
-
parsers.each do |parser, results|
|
598
|
-
next if results[:error]
|
599
|
-
|
600
|
-
aggregated_data[size]["#{parser}_#{version}"] = results[:time_per_iteration]
|
601
|
-
end
|
602
|
-
end
|
603
|
-
end
|
604
|
-
|
605
|
-
create_bar_chart(
|
606
|
-
title: "#{category_name} performance across Ruby versions",
|
607
|
-
subtitle: 'Time per iteration by Ruby version (lower is better)',
|
608
|
-
data: aggregated_data,
|
609
|
-
y_label: 'Time (milliseconds)',
|
610
|
-
value_formatter: ->(v) { "#{(v * 1000).round(2)}ms" }
|
611
|
-
)
|
612
|
-
end
|
613
|
-
|
614
|
-
def extract_average_dom_performance(version_data)
|
615
|
-
return {} unless version_data[:dom_parsing]
|
616
|
-
|
617
|
-
averages = {}
|
618
|
-
version_data[:dom_parsing].each do |size, parsers|
|
619
|
-
parsers.each do |parser, results|
|
620
|
-
next if results[:error]
|
621
|
-
|
622
|
-
averages[parser] ||= []
|
623
|
-
averages[parser] << results[:time_per_iteration]
|
624
|
-
end
|
625
|
-
end
|
626
|
-
|
627
|
-
# Calculate averages
|
628
|
-
averages.transform_values { |times| times.sum / times.length.to_f }
|
629
|
-
end
|
630
|
-
|
631
|
-
def extract_all_parsers_from_versions
|
632
|
-
parsers = []
|
633
|
-
@results[:ruby_versions].each_value do |version_data|
|
634
|
-
version_data[:dom_parsing]&.each_value do |size_data|
|
635
|
-
parsers.concat(size_data.keys)
|
636
|
-
end
|
637
|
-
end
|
638
|
-
parsers.uniq
|
639
|
-
end
|
640
|
-
|
641
|
-
def generate_trend_grid_lines(chart_width, chart_height, num_x_ticks, max_value)
|
642
|
-
lines = []
|
643
|
-
|
644
|
-
# Horizontal grid lines
|
645
|
-
(0..5).each do |i|
|
646
|
-
y = chart_height - (i * chart_height / 5.0)
|
647
|
-
lines << %(<line x1="0" y1="#{y}" x2="#{chart_width}" y2="#{y}" class="grid-line"/>)
|
648
|
-
end
|
649
|
-
|
650
|
-
# Vertical grid lines
|
651
|
-
(0...num_x_ticks).each do |i|
|
652
|
-
x = i * chart_width / (num_x_ticks - 1).to_f
|
653
|
-
lines << %(<line x1="#{x}" y1="0" x2="#{x}" y2="#{chart_height}" class="grid-line"/>)
|
654
|
-
end
|
655
|
-
|
656
|
-
lines.join("\n")
|
657
|
-
end
|
658
|
-
|
659
|
-
def generate_trend_lines(parser_data, ruby_versions, chart_width, chart_height, max_value)
|
660
|
-
lines = []
|
661
|
-
|
662
|
-
parser_data.each do |parser, data|
|
663
|
-
color = COLORS[parser.to_sym] || '#999999'
|
664
|
-
points = []
|
665
|
-
|
666
|
-
data.each do |point|
|
667
|
-
version_index = ruby_versions.index(point[:version])
|
668
|
-
next unless version_index
|
669
|
-
|
670
|
-
x = version_index * chart_width / (ruby_versions.length - 1).to_f
|
671
|
-
y = chart_height - (point[:value] / max_value * chart_height)
|
672
|
-
points << "#{x},#{y}"
|
673
|
-
end
|
674
|
-
|
675
|
-
next if points.empty?
|
676
|
-
|
677
|
-
lines << %(<polyline points="#{points.join(' ')}" stroke="#{color}" class="trend-line"/>)
|
678
|
-
|
679
|
-
# Add points
|
680
|
-
points.each do |point|
|
681
|
-
x, y = point.split(',').map(&:to_f)
|
682
|
-
lines << %(<circle cx="#{x}" cy="#{y}" r="4" stroke="#{color}" class="trend-point"/>)
|
683
|
-
end
|
684
|
-
end
|
685
|
-
|
686
|
-
lines.join("\n")
|
687
|
-
end
|
688
|
-
|
689
|
-
def generate_trend_x_labels(ruby_versions, chart_width, chart_height)
|
690
|
-
labels = []
|
691
|
-
ruby_versions.each_with_index do |version, index|
|
692
|
-
x = index * chart_width / (ruby_versions.length - 1).to_f
|
693
|
-
labels << %(<text x="#{x}" y="#{chart_height + 20}" class="tick-label">Ruby #{version}</text>)
|
694
|
-
end
|
695
|
-
labels.join("\n")
|
696
|
-
end
|
697
|
-
|
698
|
-
def generate_trend_y_labels(chart_height, max_value)
|
699
|
-
labels = []
|
700
|
-
(0..5).each do |i|
|
701
|
-
value = i * max_value / 5.0
|
702
|
-
y = chart_height - (i * chart_height / 5.0)
|
703
|
-
formatted_value = "#{(value * 1000).round(1)}ms"
|
704
|
-
labels << %(<text x="-10" y="#{y + 4}" class="tick-label" text-anchor="end">#{formatted_value}</text>)
|
705
|
-
end
|
706
|
-
labels.join("\n")
|
707
|
-
end
|
708
|
-
|
709
|
-
def generate_trend_legend(parsers, width, margin)
|
710
|
-
legend_x = width - margin[:right] - 120
|
711
|
-
legend_y = margin[:top] + 20
|
712
|
-
|
713
|
-
legend_items = []
|
714
|
-
parsers.each_with_index do |parser, index|
|
715
|
-
y = legend_y + index * 20
|
716
|
-
color = COLORS[parser.to_sym] || '#999999'
|
717
|
-
|
718
|
-
legend_items << <<~ITEM
|
719
|
-
<line x1="#{legend_x}" y1="#{y}" x2="#{legend_x + 20}" y2="#{y}" stroke="#{color}" stroke-width="3"/>
|
720
|
-
<text x="#{legend_x + 25}" y="#{y + 4}" class="legend-text">#{parser.capitalize}</text>
|
721
|
-
ITEM
|
722
|
-
end
|
723
|
-
|
724
|
-
legend_items.join("\n")
|
725
|
-
end
|
726
|
-
|
727
|
-
def generate_heatmap_cells(ruby_versions, parsers, width, height)
|
728
|
-
cell_width = width / ruby_versions.length.to_f
|
729
|
-
cell_height = height / parsers.length.to_f
|
730
|
-
|
731
|
-
cells = []
|
732
|
-
ruby_versions.each_with_index do |version, v_index|
|
733
|
-
parsers.each_with_index do |parser, p_index|
|
734
|
-
x = v_index * cell_width
|
735
|
-
y = p_index * cell_height
|
736
|
-
|
737
|
-
# Calculate performance score for this parser/version combination
|
738
|
-
score = calculate_heatmap_score(version, parser)
|
739
|
-
color_intensity = (score * 255).to_i
|
740
|
-
color = "rgb(#{255 - color_intensity}, #{255 - color_intensity}, 255)"
|
741
|
-
|
742
|
-
cells << %(<rect x="#{x}" y="#{y}" width="#{cell_width}" height="#{cell_height}" fill="#{color}" class="heatmap-cell"/>)
|
743
|
-
end
|
744
|
-
end
|
745
|
-
|
746
|
-
cells.join("\n")
|
747
|
-
end
|
748
|
-
|
749
|
-
def generate_heatmap_labels(ruby_versions, parsers, width, height)
|
750
|
-
cell_width = width / ruby_versions.length.to_f
|
751
|
-
cell_height = height / parsers.length.to_f
|
752
|
-
|
753
|
-
labels = []
|
754
|
-
|
755
|
-
# Version labels (top)
|
756
|
-
ruby_versions.each_with_index do |version, index|
|
757
|
-
x = index * cell_width + cell_width / 2
|
758
|
-
labels << %(<text x="#{x}" y="-10" class="axis-label">Ruby #{version}</text>)
|
759
|
-
end
|
760
|
-
|
761
|
-
# Parser labels (left)
|
762
|
-
parsers.each_with_index do |parser, index|
|
763
|
-
y = index * cell_height + cell_height / 2
|
764
|
-
labels << %(<text x="-10" y="#{y}" class="axis-label" text-anchor="end">#{parser.capitalize}</text>)
|
765
|
-
end
|
766
|
-
|
767
|
-
labels.join("\n")
|
768
|
-
end
|
769
|
-
|
770
|
-
def calculate_heatmap_score(version, parser)
|
771
|
-
version_data = @results[:ruby_versions][version]
|
772
|
-
return 0.5 unless version_data && version_data[:dom_parsing]
|
773
|
-
|
774
|
-
# Calculate average performance for this parser in this version
|
775
|
-
times = []
|
776
|
-
version_data[:dom_parsing].each_value do |size_data|
|
777
|
-
next unless size_data[parser] && !size_data[parser][:error]
|
778
|
-
|
779
|
-
times << size_data[parser][:time_per_iteration]
|
780
|
-
end
|
781
|
-
|
782
|
-
return 0.5 if times.empty?
|
783
|
-
|
784
|
-
avg_time = times.sum / times.length.to_f
|
785
|
-
|
786
|
-
# Get all times across all versions for normalization
|
787
|
-
all_times = []
|
788
|
-
@results[:ruby_versions].each_value do |vd|
|
789
|
-
next unless vd[:dom_parsing]
|
790
|
-
|
791
|
-
vd[:dom_parsing].each_value do |sd|
|
792
|
-
sd.each_value do |pd|
|
793
|
-
next if pd[:error]
|
794
|
-
|
795
|
-
all_times << pd[:time_per_iteration]
|
796
|
-
end
|
797
|
-
end
|
798
|
-
end
|
799
|
-
|
800
|
-
return 0.5 if all_times.empty?
|
801
|
-
|
802
|
-
max_time = all_times.max
|
803
|
-
min_time = all_times.min
|
804
|
-
|
805
|
-
return 0.5 if max_time == min_time
|
806
|
-
|
807
|
-
# Normalize (lower time = higher score)
|
808
|
-
1.0 - (avg_time - min_time) / (max_time - min_time)
|
809
|
-
end
|
810
|
-
|
811
|
-
def create_empty_chart(message)
|
812
|
-
<<~SVG
|
813
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
814
|
-
<svg width="400" height="200" xmlns="http://www.w3.org/2000/svg">
|
815
|
-
<rect width="400" height="200" fill="white" stroke="#ddd"/>
|
816
|
-
<text x="200" y="100" text-anchor="middle" font-family="Arial" font-size="14" fill="#666">#{message}</text>
|
817
|
-
</svg>
|
818
|
-
SVG
|
819
|
-
end
|
820
|
-
end
|
821
|
-
end
|