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,1201 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'json'
|
4
|
-
require 'fileutils'
|
5
|
-
|
6
|
-
module Serialbench
|
7
|
-
class ResultMerger
|
8
|
-
attr_reader :merged_results
|
9
|
-
|
10
|
-
def initialize(output_dir = 'results')
|
11
|
-
@output_dir = output_dir
|
12
|
-
@charts_dir = File.join(output_dir, 'charts')
|
13
|
-
@reports_dir = File.join(output_dir, 'reports')
|
14
|
-
@assets_dir = File.join(output_dir, 'assets')
|
15
|
-
@merged_results = {
|
16
|
-
environments: {},
|
17
|
-
combined_results: {},
|
18
|
-
metadata: {
|
19
|
-
merged_at: Time.now.iso8601,
|
20
|
-
ruby_versions: [],
|
21
|
-
platforms: []
|
22
|
-
}
|
23
|
-
}
|
24
|
-
end
|
25
|
-
|
26
|
-
# Main report generation method for single benchmark results
|
27
|
-
def generate_all_reports(results)
|
28
|
-
setup_directories
|
29
|
-
|
30
|
-
# Generate CSS
|
31
|
-
generate_css
|
32
|
-
|
33
|
-
# Generate combined HTML report directly
|
34
|
-
html_file = File.join(@reports_dir, 'benchmark_report.html')
|
35
|
-
generate_combined_html_report(results, html_file)
|
36
|
-
|
37
|
-
{
|
38
|
-
html: html_file,
|
39
|
-
css: File.join(@assets_dir, 'css', 'benchmark_report.css')
|
40
|
-
}
|
41
|
-
end
|
42
|
-
|
43
|
-
# Generate standalone HTML report for single benchmark results
|
44
|
-
def generate_combined_html_report(results, html_file)
|
45
|
-
setup_directories
|
46
|
-
html_content = generate_single_benchmark_html(results)
|
47
|
-
File.write(html_file, html_content)
|
48
|
-
end
|
49
|
-
|
50
|
-
def merge_files(json_files)
|
51
|
-
json_files.each do |file_path|
|
52
|
-
unless File.exist?(file_path)
|
53
|
-
puts "Warning: File not found: #{file_path}"
|
54
|
-
next
|
55
|
-
end
|
56
|
-
|
57
|
-
begin
|
58
|
-
data = JSON.parse(File.read(file_path), symbolize_names: true)
|
59
|
-
merge_result_data(data, file_path)
|
60
|
-
rescue JSON::ParserError => e
|
61
|
-
puts "Warning: Invalid JSON in #{file_path}: #{e.message}"
|
62
|
-
next
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
@merged_results
|
67
|
-
end
|
68
|
-
|
69
|
-
def merge_directories(input_dirs, output_dir)
|
70
|
-
FileUtils.mkdir_p(output_dir)
|
71
|
-
|
72
|
-
json_files = []
|
73
|
-
|
74
|
-
input_dirs.each do |dir|
|
75
|
-
unless Dir.exist?(dir)
|
76
|
-
puts "Warning: Directory not found: #{dir}"
|
77
|
-
next
|
78
|
-
end
|
79
|
-
|
80
|
-
# Look for results.json files in subdirectories
|
81
|
-
pattern = File.join(dir, '**/results.json')
|
82
|
-
found_files = Dir.glob(pattern)
|
83
|
-
|
84
|
-
if found_files.empty?
|
85
|
-
# Also check for results.json directly in the directory
|
86
|
-
direct_file = File.join(dir, 'results.json')
|
87
|
-
found_files << direct_file if File.exist?(direct_file)
|
88
|
-
end
|
89
|
-
|
90
|
-
json_files.concat(found_files)
|
91
|
-
end
|
92
|
-
|
93
|
-
raise 'No results.json files found in the specified directories' if json_files.empty?
|
94
|
-
|
95
|
-
puts "Found #{json_files.length} result files to merge:"
|
96
|
-
json_files.each { |file| puts " - #{file}" }
|
97
|
-
|
98
|
-
merge_files(json_files)
|
99
|
-
|
100
|
-
# Save merged results
|
101
|
-
output_file = File.join(output_dir, 'merged_results.json')
|
102
|
-
File.write(output_file, JSON.pretty_generate(@merged_results))
|
103
|
-
|
104
|
-
puts "Merged results saved to: #{output_file}"
|
105
|
-
output_file
|
106
|
-
end
|
107
|
-
|
108
|
-
def generate_github_pages_html(output_dir)
|
109
|
-
FileUtils.mkdir_p(output_dir)
|
110
|
-
|
111
|
-
html_content = generate_combined_html
|
112
|
-
|
113
|
-
# Save as index.html for GitHub Pages
|
114
|
-
index_file = File.join(output_dir, 'index.html')
|
115
|
-
File.write(index_file, html_content)
|
116
|
-
|
117
|
-
# Also save CSS file
|
118
|
-
css_file = File.join(output_dir, 'styles.css')
|
119
|
-
File.write(css_file, generate_css)
|
120
|
-
|
121
|
-
puts "GitHub Pages HTML generated: #{index_file}"
|
122
|
-
puts "CSS file generated: #{css_file}"
|
123
|
-
|
124
|
-
{
|
125
|
-
html: index_file,
|
126
|
-
css: css_file
|
127
|
-
}
|
128
|
-
end
|
129
|
-
|
130
|
-
private
|
131
|
-
|
132
|
-
def merge_result_data(data, source_file)
|
133
|
-
# Extract environment info
|
134
|
-
ruby_version = data[:ruby_version] || data[:environment]&.dig(:ruby_version) || 'unknown'
|
135
|
-
ruby_platform = data[:ruby_platform] || data[:environment]&.dig(:ruby_platform) || 'unknown'
|
136
|
-
|
137
|
-
env_key = "#{ruby_version}_#{ruby_platform}".gsub(/[^a-zA-Z0-9_]/, '_')
|
138
|
-
|
139
|
-
@merged_results[:environments][env_key] = {
|
140
|
-
ruby_version: ruby_version,
|
141
|
-
ruby_platform: ruby_platform,
|
142
|
-
source_file: source_file,
|
143
|
-
timestamp: data[:timestamp],
|
144
|
-
environment: data[:environment]
|
145
|
-
}
|
146
|
-
|
147
|
-
# Track unique Ruby versions and platforms
|
148
|
-
unless @merged_results[:metadata][:ruby_versions].include?(ruby_version)
|
149
|
-
@merged_results[:metadata][:ruby_versions] << ruby_version
|
150
|
-
end
|
151
|
-
unless @merged_results[:metadata][:platforms].include?(ruby_platform)
|
152
|
-
@merged_results[:metadata][:platforms] << ruby_platform
|
153
|
-
end
|
154
|
-
|
155
|
-
# Merge benchmark results
|
156
|
-
%i[parsing generation streaming memory_usage].each do |benchmark_type|
|
157
|
-
next unless data[benchmark_type]
|
158
|
-
|
159
|
-
@merged_results[:combined_results][benchmark_type] ||= {}
|
160
|
-
|
161
|
-
data[benchmark_type].each do |size, size_data|
|
162
|
-
@merged_results[:combined_results][benchmark_type][size] ||= {}
|
163
|
-
|
164
|
-
size_data.each do |format, format_data|
|
165
|
-
@merged_results[:combined_results][benchmark_type][size][format] ||= {}
|
166
|
-
|
167
|
-
format_data.each do |serializer, serializer_data|
|
168
|
-
@merged_results[:combined_results][benchmark_type][size][format][serializer] ||= {}
|
169
|
-
@merged_results[:combined_results][benchmark_type][size][format][serializer][env_key] = serializer_data
|
170
|
-
end
|
171
|
-
end
|
172
|
-
end
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
def generate_combined_html
|
177
|
-
<<~HTML
|
178
|
-
<!DOCTYPE html>
|
179
|
-
<html lang="en">
|
180
|
-
<head>
|
181
|
-
<meta charset="UTF-8">
|
182
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
183
|
-
<title>SerialBench - Multi-Ruby Version Comparison</title>
|
184
|
-
<link rel="stylesheet" href="styles.css">
|
185
|
-
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
186
|
-
</head>
|
187
|
-
<body>
|
188
|
-
<div class="container">
|
189
|
-
<header>
|
190
|
-
<h1>SerialBench - Multi-Ruby Version Comparison</h1>
|
191
|
-
<p class="subtitle">Comprehensive serialization performance benchmarks across Ruby versions</p>
|
192
|
-
<div class="metadata">
|
193
|
-
<p><strong>Generated:</strong> #{@merged_results[:metadata][:merged_at]}</p>
|
194
|
-
<p><strong>Ruby Versions:</strong> #{@merged_results[:metadata][:ruby_versions].join(', ')}</p>
|
195
|
-
<p><strong>Platforms:</strong> #{@merged_results[:metadata][:platforms].join(', ')}</p>
|
196
|
-
</div>
|
197
|
-
</header>
|
198
|
-
|
199
|
-
<nav class="benchmark-nav">
|
200
|
-
<button class="nav-btn active" onclick="showSection('parsing')">Parsing Performance</button>
|
201
|
-
<button class="nav-btn" onclick="showSection('generation')">Generation Performance</button>
|
202
|
-
<button class="nav-btn" onclick="showSection('streaming')">Streaming Performance</button>
|
203
|
-
<button class="nav-btn" onclick="showSection('memory')">Memory Usage</button>
|
204
|
-
<button class="nav-btn" onclick="showSection('environments')">Environment Details</button>
|
205
|
-
</nav>
|
206
|
-
|
207
|
-
#{generate_parsing_section}
|
208
|
-
#{generate_generation_section}
|
209
|
-
#{generate_streaming_section}
|
210
|
-
#{generate_memory_section}
|
211
|
-
#{generate_environments_section}
|
212
|
-
</div>
|
213
|
-
|
214
|
-
<script>
|
215
|
-
#{generate_javascript}
|
216
|
-
</script>
|
217
|
-
</body>
|
218
|
-
</html>
|
219
|
-
HTML
|
220
|
-
end
|
221
|
-
|
222
|
-
def generate_parsing_section
|
223
|
-
unless @merged_results[:combined_results][:parsing]
|
224
|
-
return '<div id="parsing" class="section active"><p>No parsing data available</p></div>'
|
225
|
-
end
|
226
|
-
|
227
|
-
content = '<div id="parsing" class="section active">'
|
228
|
-
content += '<h2>Parsing Performance Comparison</h2>'
|
229
|
-
|
230
|
-
%i[small medium large].each do |size|
|
231
|
-
next unless @merged_results[:combined_results][:parsing][size]
|
232
|
-
|
233
|
-
content += "<h3>#{size.capitalize} Files</h3>"
|
234
|
-
content += '<div class="charts-grid">'
|
235
|
-
|
236
|
-
@merged_results[:combined_results][:parsing][size].each do |format, format_data|
|
237
|
-
content += generate_performance_chart("parsing_#{size}_#{format}", "#{format.upcase} Parsing (#{size})",
|
238
|
-
format_data, 'iterations_per_second')
|
239
|
-
end
|
240
|
-
|
241
|
-
content += '</div>'
|
242
|
-
end
|
243
|
-
|
244
|
-
content += '</div>'
|
245
|
-
end
|
246
|
-
|
247
|
-
def generate_generation_section
|
248
|
-
unless @merged_results[:combined_results][:generation]
|
249
|
-
return '<div id="generation" class="section"><p>No generation data available</p></div>'
|
250
|
-
end
|
251
|
-
|
252
|
-
content = '<div id="generation" class="section">'
|
253
|
-
content += '<h2>Generation Performance Comparison</h2>'
|
254
|
-
|
255
|
-
%i[small medium large].each do |size|
|
256
|
-
next unless @merged_results[:combined_results][:generation][size]
|
257
|
-
|
258
|
-
content += "<h3>#{size.capitalize} Files</h3>"
|
259
|
-
content += '<div class="charts-grid">'
|
260
|
-
|
261
|
-
@merged_results[:combined_results][:generation][size].each do |format, format_data|
|
262
|
-
content += generate_performance_chart("generation_#{size}_#{format}",
|
263
|
-
"#{format.upcase} Generation (#{size})", format_data, 'iterations_per_second')
|
264
|
-
end
|
265
|
-
|
266
|
-
content += '</div>'
|
267
|
-
end
|
268
|
-
|
269
|
-
content += '</div>'
|
270
|
-
end
|
271
|
-
|
272
|
-
def generate_streaming_section
|
273
|
-
unless @merged_results[:combined_results][:streaming]
|
274
|
-
return '<div id="streaming" class="section"><p>No streaming data available</p></div>'
|
275
|
-
end
|
276
|
-
|
277
|
-
content = '<div id="streaming" class="section">'
|
278
|
-
content += '<h2>Streaming Performance Comparison</h2>'
|
279
|
-
|
280
|
-
%i[small medium large].each do |size|
|
281
|
-
next unless @merged_results[:combined_results][:streaming][size]
|
282
|
-
|
283
|
-
content += "<h3>#{size.capitalize} Files</h3>"
|
284
|
-
content += '<div class="charts-grid">'
|
285
|
-
|
286
|
-
@merged_results[:combined_results][:streaming][size].each do |format, format_data|
|
287
|
-
content += generate_performance_chart("streaming_#{size}_#{format}", "#{format.upcase} Streaming (#{size})",
|
288
|
-
format_data, 'iterations_per_second')
|
289
|
-
end
|
290
|
-
|
291
|
-
content += '</div>'
|
292
|
-
end
|
293
|
-
|
294
|
-
content += '</div>'
|
295
|
-
end
|
296
|
-
|
297
|
-
def generate_memory_section
|
298
|
-
unless @merged_results[:combined_results][:memory_usage]
|
299
|
-
return '<div id="memory" class="section"><p>No memory data available</p></div>'
|
300
|
-
end
|
301
|
-
|
302
|
-
content = '<div id="memory" class="section">'
|
303
|
-
content += '<h2>Memory Usage Comparison</h2>'
|
304
|
-
|
305
|
-
%i[small medium large].each do |size|
|
306
|
-
next unless @merged_results[:combined_results][:memory_usage][size]
|
307
|
-
|
308
|
-
content += "<h3>#{size.capitalize} Files</h3>"
|
309
|
-
content += '<div class="charts-grid">'
|
310
|
-
|
311
|
-
@merged_results[:combined_results][:memory_usage][size].each do |format, format_data|
|
312
|
-
content += generate_memory_chart("memory_#{size}_#{format}", "#{format.upcase} Memory Usage (#{size})",
|
313
|
-
format_data)
|
314
|
-
end
|
315
|
-
|
316
|
-
content += '</div>'
|
317
|
-
end
|
318
|
-
|
319
|
-
content += '</div>'
|
320
|
-
end
|
321
|
-
|
322
|
-
def generate_environments_section
|
323
|
-
content = '<div id="environments" class="section">'
|
324
|
-
content += '<h2>Environment Details</h2>'
|
325
|
-
content += '<div class="environments-grid">'
|
326
|
-
|
327
|
-
@merged_results[:environments].each do |env_key, env_data|
|
328
|
-
content += <<~ENV
|
329
|
-
<div class="environment-card">
|
330
|
-
<h3>#{env_data[:ruby_version]} on #{env_data[:ruby_platform]}</h3>
|
331
|
-
<p><strong>Source:</strong> #{File.basename(env_data[:source_file])}</p>
|
332
|
-
<p><strong>Timestamp:</strong> #{env_data[:timestamp]}</p>
|
333
|
-
#{generate_serializer_versions(env_data[:environment])}
|
334
|
-
</div>
|
335
|
-
ENV
|
336
|
-
end
|
337
|
-
|
338
|
-
content += '</div></div>'
|
339
|
-
end
|
340
|
-
|
341
|
-
def generate_serializer_versions(environment)
|
342
|
-
return '' unless environment&.dig(:serializer_versions)
|
343
|
-
|
344
|
-
content = '<div class="serializer-versions">'
|
345
|
-
content += '<h4>Serializer Versions:</h4>'
|
346
|
-
content += '<ul>'
|
347
|
-
|
348
|
-
environment[:serializer_versions].each do |name, version|
|
349
|
-
content += "<li><strong>#{name}:</strong> #{version}</li>"
|
350
|
-
end
|
351
|
-
|
352
|
-
content += '</ul></div>'
|
353
|
-
end
|
354
|
-
|
355
|
-
def generate_performance_chart(chart_id, title, data, metric)
|
356
|
-
# Store chart data for later initialization
|
357
|
-
@chart_initializers ||= []
|
358
|
-
@chart_initializers << "createPerformanceChart('#{chart_id}', '#{title}', #{data.to_json}, '#{metric}');"
|
359
|
-
|
360
|
-
<<~CHART
|
361
|
-
<div class="chart-container">
|
362
|
-
<h4>#{title}</h4>
|
363
|
-
<canvas id="#{chart_id}" width="400" height="300"></canvas>
|
364
|
-
</div>
|
365
|
-
CHART
|
366
|
-
end
|
367
|
-
|
368
|
-
def generate_memory_chart(chart_id, title, data)
|
369
|
-
# Store chart data for later initialization
|
370
|
-
@chart_initializers ||= []
|
371
|
-
@chart_initializers << "createMemoryChart('#{chart_id}', '#{title}', #{data.to_json});"
|
372
|
-
|
373
|
-
<<~CHART
|
374
|
-
<div class="chart-container">
|
375
|
-
<h4>#{title}</h4>
|
376
|
-
<canvas id="#{chart_id}" width="400" height="300"></canvas>
|
377
|
-
</div>
|
378
|
-
CHART
|
379
|
-
end
|
380
|
-
|
381
|
-
def generate_javascript
|
382
|
-
chart_init_code = @chart_initializers ? @chart_initializers.join("\n ") : ''
|
383
|
-
|
384
|
-
<<~JS
|
385
|
-
function showSection(sectionName) {
|
386
|
-
// Hide all sections
|
387
|
-
document.querySelectorAll('.section').forEach(section => {
|
388
|
-
section.classList.remove('active');
|
389
|
-
});
|
390
|
-
|
391
|
-
// Remove active class from all nav buttons
|
392
|
-
document.querySelectorAll('.nav-btn').forEach(btn => {
|
393
|
-
btn.classList.remove('active');
|
394
|
-
});
|
395
|
-
|
396
|
-
// Show selected section
|
397
|
-
document.getElementById(sectionName).classList.add('active');
|
398
|
-
|
399
|
-
// Add active class to clicked button
|
400
|
-
event.target.classList.add('active');
|
401
|
-
}
|
402
|
-
|
403
|
-
function createPerformanceChart(canvasId, title, data, metric) {
|
404
|
-
const ctx = document.getElementById(canvasId).getContext('2d');
|
405
|
-
|
406
|
-
const environments = #{@merged_results[:environments].keys.to_json};
|
407
|
-
const serializers = Object.keys(data);
|
408
|
-
|
409
|
-
const datasets = serializers.map((serializer, index) => {
|
410
|
-
const serializerData = data[serializer];
|
411
|
-
const values = environments.map(env => {
|
412
|
-
const envData = serializerData[env];
|
413
|
-
return envData ? (envData[metric] || 0) : 0;
|
414
|
-
});
|
415
|
-
|
416
|
-
return {
|
417
|
-
label: serializer,
|
418
|
-
data: values,
|
419
|
-
backgroundColor: `hsl(${index * 60}, 70%, 50%)`,
|
420
|
-
borderColor: `hsl(${index * 60}, 70%, 40%)`,
|
421
|
-
borderWidth: 1
|
422
|
-
};
|
423
|
-
});
|
424
|
-
|
425
|
-
const environmentLabels = environments.map(env => {
|
426
|
-
const envData = #{@merged_results[:environments].to_json}[env];
|
427
|
-
return envData.ruby_version + ' (' + envData.ruby_platform + ')';
|
428
|
-
});
|
429
|
-
|
430
|
-
new Chart(ctx, {
|
431
|
-
type: 'bar',
|
432
|
-
data: {
|
433
|
-
labels: environmentLabels,
|
434
|
-
datasets: datasets
|
435
|
-
},
|
436
|
-
options: {
|
437
|
-
responsive: true,
|
438
|
-
plugins: {
|
439
|
-
title: {
|
440
|
-
display: true,
|
441
|
-
text: title
|
442
|
-
},
|
443
|
-
legend: {
|
444
|
-
position: 'top'
|
445
|
-
}
|
446
|
-
},
|
447
|
-
scales: {
|
448
|
-
y: {
|
449
|
-
beginAtZero: true,
|
450
|
-
title: {
|
451
|
-
display: true,
|
452
|
-
text: metric === 'iterations_per_second' ? 'Operations/Second' : 'Time (ms)'
|
453
|
-
}
|
454
|
-
}
|
455
|
-
}
|
456
|
-
}
|
457
|
-
});
|
458
|
-
}
|
459
|
-
|
460
|
-
function createMemoryChart(canvasId, title, data) {
|
461
|
-
const ctx = document.getElementById(canvasId).getContext('2d');
|
462
|
-
|
463
|
-
const environments = #{@merged_results[:environments].keys.to_json};
|
464
|
-
const serializers = Object.keys(data);
|
465
|
-
|
466
|
-
const datasets = serializers.map((serializer, index) => {
|
467
|
-
const serializerData = data[serializer];
|
468
|
-
const values = environments.map(env => {
|
469
|
-
const envData = serializerData[env];
|
470
|
-
return envData ? (envData.allocated_memory / 1024 / 1024) : 0; // Convert to MB
|
471
|
-
});
|
472
|
-
|
473
|
-
return {
|
474
|
-
label: serializer,
|
475
|
-
data: values,
|
476
|
-
backgroundColor: `hsl(${index * 60}, 70%, 50%)`,
|
477
|
-
borderColor: `hsl(${index * 60}, 70%, 40%)`,
|
478
|
-
borderWidth: 1
|
479
|
-
};
|
480
|
-
});
|
481
|
-
|
482
|
-
const environmentLabels = environments.map(env => {
|
483
|
-
const envData = #{@merged_results[:environments].to_json}[env];
|
484
|
-
return envData.ruby_version + ' (' + envData.ruby_platform + ')';
|
485
|
-
});
|
486
|
-
|
487
|
-
new Chart(ctx, {
|
488
|
-
type: 'bar',
|
489
|
-
data: {
|
490
|
-
labels: environmentLabels,
|
491
|
-
datasets: datasets
|
492
|
-
},
|
493
|
-
options: {
|
494
|
-
responsive: true,
|
495
|
-
plugins: {
|
496
|
-
title: {
|
497
|
-
display: true,
|
498
|
-
text: title
|
499
|
-
},
|
500
|
-
legend: {
|
501
|
-
position: 'top'
|
502
|
-
}
|
503
|
-
},
|
504
|
-
scales: {
|
505
|
-
y: {
|
506
|
-
beginAtZero: true,
|
507
|
-
title: {
|
508
|
-
display: true,
|
509
|
-
text: 'Memory Usage (MB)'
|
510
|
-
}
|
511
|
-
}
|
512
|
-
}
|
513
|
-
}
|
514
|
-
});
|
515
|
-
}
|
516
|
-
|
517
|
-
// Initialize all charts when page loads
|
518
|
-
document.addEventListener('DOMContentLoaded', function() {
|
519
|
-
#{chart_init_code}
|
520
|
-
});
|
521
|
-
JS
|
522
|
-
end
|
523
|
-
|
524
|
-
# Generate HTML for single benchmark results (not multi-version)
|
525
|
-
def generate_single_benchmark_html(results)
|
526
|
-
@single_chart_initializers = []
|
527
|
-
|
528
|
-
<<~HTML
|
529
|
-
<!DOCTYPE html>
|
530
|
-
<html lang="en">
|
531
|
-
<head>
|
532
|
-
<meta charset="UTF-8">
|
533
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
534
|
-
<title>SerialBench - Performance Report</title>
|
535
|
-
<link rel="stylesheet" href="../assets/css/benchmark_report.css">
|
536
|
-
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
537
|
-
</head>
|
538
|
-
<body>
|
539
|
-
<div class="container">
|
540
|
-
<header>
|
541
|
-
<h1>SerialBench - Performance Report</h1>
|
542
|
-
<p class="subtitle">Comprehensive serialization performance benchmarks</p>
|
543
|
-
<div class="metadata">
|
544
|
-
<p><strong>Generated:</strong> #{Time.now.strftime('%B %d, %Y at %H:%M')}</p>
|
545
|
-
<p><strong>Ruby Version:</strong> #{results[:environment][:ruby_version]}</p>
|
546
|
-
<p><strong>Platform:</strong> #{results[:environment][:ruby_platform]}</p>
|
547
|
-
</div>
|
548
|
-
</header>
|
549
|
-
|
550
|
-
<nav class="benchmark-nav">
|
551
|
-
<button class="nav-btn active" onclick="showSection('parsing')">Parsing Performance</button>
|
552
|
-
<button class="nav-btn" onclick="showSection('generation')">Generation Performance</button>
|
553
|
-
<button class="nav-btn" onclick="showSection('streaming')">Streaming Performance</button>
|
554
|
-
<button class="nav-btn" onclick="showSection('memory')">Memory Usage</button>
|
555
|
-
<button class="nav-btn" onclick="showSection('summary')">Summary</button>
|
556
|
-
</nav>
|
557
|
-
|
558
|
-
#{generate_single_parsing_section(results)}
|
559
|
-
#{generate_single_generation_section(results)}
|
560
|
-
#{generate_single_streaming_section(results)}
|
561
|
-
#{generate_single_memory_section(results)}
|
562
|
-
#{generate_summary_section(results)}
|
563
|
-
</div>
|
564
|
-
|
565
|
-
<script>
|
566
|
-
#{generate_single_benchmark_javascript}
|
567
|
-
</script>
|
568
|
-
</body>
|
569
|
-
</html>
|
570
|
-
HTML
|
571
|
-
end
|
572
|
-
|
573
|
-
def generate_single_parsing_section(results)
|
574
|
-
parsing_data = results[:parsing]
|
575
|
-
return '<div id="parsing" class="section active"><p>No parsing data available</p></div>' unless parsing_data
|
576
|
-
|
577
|
-
content = '<div id="parsing" class="section active">'
|
578
|
-
content += '<h2>Parsing Performance</h2>'
|
579
|
-
|
580
|
-
%i[small medium large].each do |size|
|
581
|
-
next unless parsing_data[size]
|
582
|
-
|
583
|
-
content += "<h3>#{size.capitalize} Files</h3>"
|
584
|
-
content += '<div class="charts-grid">'
|
585
|
-
|
586
|
-
parsing_data[size].each do |format, format_data|
|
587
|
-
chart_id = "parsing_#{size}_#{format}"
|
588
|
-
content += generate_single_performance_chart(chart_id, "#{format.upcase} Parsing (#{size})", format_data,
|
589
|
-
'iterations_per_second')
|
590
|
-
end
|
591
|
-
|
592
|
-
content += '</div>'
|
593
|
-
end
|
594
|
-
|
595
|
-
content += '</div>'
|
596
|
-
end
|
597
|
-
|
598
|
-
def generate_single_generation_section(results)
|
599
|
-
generation_data = results[:generation]
|
600
|
-
return '<div id="generation" class="section"><p>No generation data available</p></div>' unless generation_data
|
601
|
-
|
602
|
-
content = '<div id="generation" class="section">'
|
603
|
-
content += '<h2>Generation Performance</h2>'
|
604
|
-
|
605
|
-
%i[small medium large].each do |size|
|
606
|
-
next unless generation_data[size]
|
607
|
-
|
608
|
-
content += "<h3>#{size.capitalize} Files</h3>"
|
609
|
-
content += '<div class="charts-grid">'
|
610
|
-
|
611
|
-
generation_data[size].each do |format, format_data|
|
612
|
-
chart_id = "generation_#{size}_#{format}"
|
613
|
-
content += generate_single_performance_chart(chart_id, "#{format.upcase} Generation (#{size})", format_data,
|
614
|
-
'iterations_per_second')
|
615
|
-
end
|
616
|
-
|
617
|
-
content += '</div>'
|
618
|
-
end
|
619
|
-
|
620
|
-
content += '</div>'
|
621
|
-
end
|
622
|
-
|
623
|
-
def generate_single_streaming_section(results)
|
624
|
-
streaming_data = results[:streaming]
|
625
|
-
return '<div id="streaming" class="section"><p>No streaming data available</p></div>' unless streaming_data
|
626
|
-
|
627
|
-
content = '<div id="streaming" class="section">'
|
628
|
-
content += '<h2>Streaming Performance</h2>'
|
629
|
-
|
630
|
-
%i[small medium large].each do |size|
|
631
|
-
next unless streaming_data[size]
|
632
|
-
|
633
|
-
content += "<h3>#{size.capitalize} Files</h3>"
|
634
|
-
content += '<div class="charts-grid">'
|
635
|
-
|
636
|
-
streaming_data[size].each do |format, format_data|
|
637
|
-
chart_id = "streaming_#{size}_#{format}"
|
638
|
-
content += generate_single_performance_chart(chart_id, "#{format.upcase} Streaming (#{size})", format_data,
|
639
|
-
'iterations_per_second')
|
640
|
-
end
|
641
|
-
|
642
|
-
content += '</div>'
|
643
|
-
end
|
644
|
-
|
645
|
-
content += '</div>'
|
646
|
-
end
|
647
|
-
|
648
|
-
def generate_single_memory_section(results)
|
649
|
-
memory_data = results[:memory_usage]
|
650
|
-
return '<div id="memory" class="section"><p>No memory data available</p></div>' unless memory_data
|
651
|
-
|
652
|
-
content = '<div id="memory" class="section">'
|
653
|
-
content += '<h2>Memory Usage</h2>'
|
654
|
-
|
655
|
-
%i[small medium large].each do |size|
|
656
|
-
next unless memory_data[size]
|
657
|
-
|
658
|
-
content += "<h3>#{size.capitalize} Files</h3>"
|
659
|
-
content += '<div class="charts-grid">'
|
660
|
-
|
661
|
-
memory_data[size].each do |format, format_data|
|
662
|
-
chart_id = "memory_#{size}_#{format}"
|
663
|
-
content += generate_single_memory_chart(chart_id, "#{format.upcase} Memory Usage (#{size})", format_data)
|
664
|
-
end
|
665
|
-
|
666
|
-
content += '</div>'
|
667
|
-
end
|
668
|
-
|
669
|
-
content += '</div>'
|
670
|
-
end
|
671
|
-
|
672
|
-
def generate_summary_section(results)
|
673
|
-
content = '<div id="summary" class="section">'
|
674
|
-
content += '<h2>Performance Summary</h2>'
|
675
|
-
content += '<div class="summary-grid">'
|
676
|
-
|
677
|
-
# Generate key findings
|
678
|
-
content += '<div class="summary-card">'
|
679
|
-
content += '<h3>Key Findings</h3>'
|
680
|
-
content += generate_key_findings(results)
|
681
|
-
content += '</div>'
|
682
|
-
|
683
|
-
# Generate recommendations
|
684
|
-
content += '<div class="summary-card">'
|
685
|
-
content += '<h3>Recommendations</h3>'
|
686
|
-
content += generate_recommendations(results)
|
687
|
-
content += '</div>'
|
688
|
-
|
689
|
-
content += '</div></div>'
|
690
|
-
end
|
691
|
-
|
692
|
-
def generate_single_performance_chart(chart_id, title, data, metric)
|
693
|
-
@single_chart_initializers << "createSinglePerformanceChart('#{chart_id}', '#{title}', #{data.to_json}, '#{metric}');"
|
694
|
-
|
695
|
-
<<~CHART
|
696
|
-
<div class="chart-container">
|
697
|
-
<h4>#{title}</h4>
|
698
|
-
<canvas id="#{chart_id}" width="400" height="300"></canvas>
|
699
|
-
</div>
|
700
|
-
CHART
|
701
|
-
end
|
702
|
-
|
703
|
-
def generate_single_memory_chart(chart_id, title, data)
|
704
|
-
@single_chart_initializers << "createSingleMemoryChart('#{chart_id}', '#{title}', #{data.to_json});"
|
705
|
-
|
706
|
-
<<~CHART
|
707
|
-
<div class="chart-container">
|
708
|
-
<h4>#{title}</h4>
|
709
|
-
<canvas id="#{chart_id}" width="400" height="300"></canvas>
|
710
|
-
</div>
|
711
|
-
CHART
|
712
|
-
end
|
713
|
-
|
714
|
-
def generate_single_benchmark_javascript
|
715
|
-
chart_init_code = @single_chart_initializers ? @single_chart_initializers.join("\n ") : ''
|
716
|
-
|
717
|
-
<<~JS
|
718
|
-
function showSection(sectionName) {
|
719
|
-
document.querySelectorAll('.section').forEach(section => {
|
720
|
-
section.classList.remove('active');
|
721
|
-
});
|
722
|
-
|
723
|
-
document.querySelectorAll('.nav-btn').forEach(btn => {
|
724
|
-
btn.classList.remove('active');
|
725
|
-
});
|
726
|
-
|
727
|
-
document.getElementById(sectionName).classList.add('active');
|
728
|
-
event.target.classList.add('active');
|
729
|
-
}
|
730
|
-
|
731
|
-
function createSinglePerformanceChart(canvasId, title, data, metric) {
|
732
|
-
const ctx = document.getElementById(canvasId).getContext('2d');
|
733
|
-
|
734
|
-
const serializers = Object.keys(data);
|
735
|
-
const values = serializers.map(serializer => {
|
736
|
-
const serializerData = data[serializer];
|
737
|
-
return serializerData[metric] || 0;
|
738
|
-
});
|
739
|
-
|
740
|
-
const colors = serializers.map((_, index) => `hsl(${index * 60}, 70%, 50%)`);
|
741
|
-
|
742
|
-
new Chart(ctx, {
|
743
|
-
type: 'bar',
|
744
|
-
data: {
|
745
|
-
labels: serializers,
|
746
|
-
datasets: [{
|
747
|
-
label: metric === 'iterations_per_second' ? 'Operations/Second' : 'Time (ms)',
|
748
|
-
data: values,
|
749
|
-
backgroundColor: colors,
|
750
|
-
borderColor: colors.map(color => color.replace('50%', '40%')),
|
751
|
-
borderWidth: 1
|
752
|
-
}]
|
753
|
-
},
|
754
|
-
options: {
|
755
|
-
responsive: true,
|
756
|
-
plugins: {
|
757
|
-
title: {
|
758
|
-
display: true,
|
759
|
-
text: title
|
760
|
-
},
|
761
|
-
legend: {
|
762
|
-
display: false
|
763
|
-
}
|
764
|
-
},
|
765
|
-
scales: {
|
766
|
-
y: {
|
767
|
-
beginAtZero: true,
|
768
|
-
title: {
|
769
|
-
display: true,
|
770
|
-
text: metric === 'iterations_per_second' ? 'Operations/Second' : 'Time (ms)'
|
771
|
-
}
|
772
|
-
}
|
773
|
-
}
|
774
|
-
}
|
775
|
-
});
|
776
|
-
}
|
777
|
-
|
778
|
-
function createSingleMemoryChart(canvasId, title, data) {
|
779
|
-
const ctx = document.getElementById(canvasId).getContext('2d');
|
780
|
-
|
781
|
-
const serializers = Object.keys(data);
|
782
|
-
const values = serializers.map(serializer => {
|
783
|
-
const serializerData = data[serializer];
|
784
|
-
return serializerData.allocated_memory ? (serializerData.allocated_memory / 1024 / 1024) : 0;
|
785
|
-
});
|
786
|
-
|
787
|
-
const colors = serializers.map((_, index) => `hsl(${index * 60}, 70%, 50%)`);
|
788
|
-
|
789
|
-
new Chart(ctx, {
|
790
|
-
type: 'bar',
|
791
|
-
data: {
|
792
|
-
labels: serializers,
|
793
|
-
datasets: [{
|
794
|
-
label: 'Memory Usage (MB)',
|
795
|
-
data: values,
|
796
|
-
backgroundColor: colors,
|
797
|
-
borderColor: colors.map(color => color.replace('50%', '40%')),
|
798
|
-
borderWidth: 1
|
799
|
-
}]
|
800
|
-
},
|
801
|
-
options: {
|
802
|
-
responsive: true,
|
803
|
-
plugins: {
|
804
|
-
title: {
|
805
|
-
display: true,
|
806
|
-
text: title
|
807
|
-
},
|
808
|
-
legend: {
|
809
|
-
display: false
|
810
|
-
}
|
811
|
-
},
|
812
|
-
scales: {
|
813
|
-
y: {
|
814
|
-
beginAtZero: true,
|
815
|
-
title: {
|
816
|
-
display: true,
|
817
|
-
text: 'Memory Usage (MB)'
|
818
|
-
}
|
819
|
-
}
|
820
|
-
}
|
821
|
-
}
|
822
|
-
});
|
823
|
-
}
|
824
|
-
|
825
|
-
document.addEventListener('DOMContentLoaded', function() {
|
826
|
-
#{chart_init_code}
|
827
|
-
});
|
828
|
-
JS
|
829
|
-
end
|
830
|
-
|
831
|
-
def generate_key_findings(results)
|
832
|
-
findings = []
|
833
|
-
|
834
|
-
# Analyze parsing results
|
835
|
-
if results[:parsing]
|
836
|
-
fastest_parser = find_fastest_serializer(results[:parsing])
|
837
|
-
if fastest_parser
|
838
|
-
findings << "<li><strong>#{fastest_parser[:name].capitalize}</strong> demonstrates superior parsing performance with #{fastest_parser[:performance]} average across all test sizes</li>"
|
839
|
-
end
|
840
|
-
end
|
841
|
-
|
842
|
-
# Analyze generation results
|
843
|
-
if results[:generation]
|
844
|
-
fastest_gen = find_fastest_serializer(results[:generation])
|
845
|
-
if fastest_gen
|
846
|
-
findings << "<li><strong>#{fastest_gen[:name].capitalize}</strong> excels in generation performance with #{fastest_gen[:performance]}</li>"
|
847
|
-
end
|
848
|
-
end
|
849
|
-
|
850
|
-
# Analyze memory usage
|
851
|
-
if results[:memory_usage]
|
852
|
-
most_efficient = find_most_memory_efficient_serializer(results[:memory_usage])
|
853
|
-
if most_efficient
|
854
|
-
findings << "<li><strong>#{most_efficient[:name].capitalize}</strong> shows the best memory efficiency, using #{most_efficient[:memory]} on average</li>"
|
855
|
-
end
|
856
|
-
end
|
857
|
-
|
858
|
-
findings.empty? ? '<p>Analysis pending - benchmark data processing in progress.</p>' : "<ul>#{findings.join("\n")}</ul>"
|
859
|
-
end
|
860
|
-
|
861
|
-
def generate_recommendations(results)
|
862
|
-
recommendations = []
|
863
|
-
|
864
|
-
# Performance recommendations
|
865
|
-
if results[:parsing]
|
866
|
-
fastest = find_fastest_serializer(results[:parsing])
|
867
|
-
if fastest
|
868
|
-
recommendations << "<li><strong>For high-performance applications:</strong> Use #{fastest[:name].capitalize} for optimal parsing speed</li>"
|
869
|
-
end
|
870
|
-
end
|
871
|
-
|
872
|
-
# Memory recommendations
|
873
|
-
if results[:memory_usage]
|
874
|
-
most_efficient = find_most_memory_efficient_serializer(results[:memory_usage])
|
875
|
-
if most_efficient
|
876
|
-
recommendations << "<li><strong>For memory-constrained environments:</strong> #{most_efficient[:name].capitalize} provides the best memory efficiency</li>"
|
877
|
-
end
|
878
|
-
end
|
879
|
-
|
880
|
-
# General recommendations
|
881
|
-
recommendations << '<li><strong>For built-in support:</strong> JSON and REXML require no additional dependencies</li>'
|
882
|
-
recommendations << '<li><strong>For streaming large files:</strong> Consider SAX/streaming parsers when available</li>'
|
883
|
-
|
884
|
-
recommendations.empty? ? '<p>Recommendations require complete benchmark data.</p>' : "<ul>#{recommendations.join("\n")}</ul>"
|
885
|
-
end
|
886
|
-
|
887
|
-
def find_fastest_serializer(category_results)
|
888
|
-
return nil unless category_results && !category_results.empty?
|
889
|
-
|
890
|
-
serializer_averages = {}
|
891
|
-
|
892
|
-
category_results.each do |size, size_data|
|
893
|
-
size_data.each do |format, format_data|
|
894
|
-
format_data.each do |serializer, data|
|
895
|
-
next if data[:error] || !data[:iterations_per_second]
|
896
|
-
|
897
|
-
key = "#{format}/#{serializer}"
|
898
|
-
serializer_averages[key] ||= []
|
899
|
-
serializer_averages[key] << data[:iterations_per_second]
|
900
|
-
end
|
901
|
-
end
|
902
|
-
end
|
903
|
-
|
904
|
-
return nil if serializer_averages.empty?
|
905
|
-
|
906
|
-
fastest = serializer_averages.max_by { |serializer, values| values.sum / values.length.to_f }
|
907
|
-
avg_performance = fastest[1].sum / fastest[1].length.to_f
|
908
|
-
|
909
|
-
{
|
910
|
-
name: fastest[0],
|
911
|
-
performance: "#{avg_performance.round(2)} ops/sec"
|
912
|
-
}
|
913
|
-
end
|
914
|
-
|
915
|
-
def find_most_memory_efficient_serializer(memory_results)
|
916
|
-
return nil unless memory_results && !memory_results.empty?
|
917
|
-
|
918
|
-
serializer_averages = {}
|
919
|
-
|
920
|
-
memory_results.each do |size, size_data|
|
921
|
-
size_data.each do |format, format_data|
|
922
|
-
format_data.each do |serializer, data|
|
923
|
-
next if data[:error] || !data[:allocated_memory]
|
924
|
-
|
925
|
-
key = "#{format}/#{serializer}"
|
926
|
-
serializer_averages[key] ||= []
|
927
|
-
serializer_averages[key] << data[:allocated_memory]
|
928
|
-
end
|
929
|
-
end
|
930
|
-
end
|
931
|
-
|
932
|
-
return nil if serializer_averages.empty?
|
933
|
-
|
934
|
-
most_efficient = serializer_averages.min_by { |serializer, values| values.sum / values.length.to_f }
|
935
|
-
avg_memory = most_efficient[1].sum / most_efficient[1].length.to_f
|
936
|
-
|
937
|
-
{
|
938
|
-
name: most_efficient[0],
|
939
|
-
memory: "#{(avg_memory / 1024.0 / 1024.0).round(2)}MB"
|
940
|
-
}
|
941
|
-
end
|
942
|
-
|
943
|
-
def setup_directories
|
944
|
-
[@output_dir, @charts_dir, @reports_dir, @assets_dir].each do |dir|
|
945
|
-
FileUtils.mkdir_p(dir)
|
946
|
-
end
|
947
|
-
FileUtils.mkdir_p(File.join(@assets_dir, 'css'))
|
948
|
-
end
|
949
|
-
|
950
|
-
def generate_css
|
951
|
-
css_content = <<~CSS
|
952
|
-
/* SerialBench Report Styles */
|
953
|
-
:root {
|
954
|
-
--primary-color: #2c3e50;
|
955
|
-
--secondary-color: #3498db;
|
956
|
-
--accent-color: #e74c3c;
|
957
|
-
--success-color: #27ae60;
|
958
|
-
--warning-color: #f39c12;
|
959
|
-
--background-color: #ffffff;
|
960
|
-
--text-color: #2c3e50;
|
961
|
-
--border-color: #bdc3c7;
|
962
|
-
--light-bg: #f8f9fa;
|
963
|
-
}
|
964
|
-
|
965
|
-
* {
|
966
|
-
margin: 0;
|
967
|
-
padding: 0;
|
968
|
-
box-sizing: border-box;
|
969
|
-
}
|
970
|
-
|
971
|
-
body {
|
972
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
973
|
-
line-height: 1.6;
|
974
|
-
color: var(--text-color);
|
975
|
-
background-color: var(--light-bg);
|
976
|
-
}
|
977
|
-
|
978
|
-
.container {
|
979
|
-
max-width: 1200px;
|
980
|
-
margin: 0 auto;
|
981
|
-
padding: 20px;
|
982
|
-
}
|
983
|
-
|
984
|
-
header {
|
985
|
-
text-align: center;
|
986
|
-
margin-bottom: 40px;
|
987
|
-
padding: 40px 20px;
|
988
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
989
|
-
color: white;
|
990
|
-
border-radius: 10px;
|
991
|
-
}
|
992
|
-
|
993
|
-
header h1 {
|
994
|
-
font-size: 2.5em;
|
995
|
-
margin-bottom: 10px;
|
996
|
-
}
|
997
|
-
|
998
|
-
.subtitle {
|
999
|
-
font-size: 1.2em;
|
1000
|
-
opacity: 0.9;
|
1001
|
-
margin-bottom: 20px;
|
1002
|
-
}
|
1003
|
-
|
1004
|
-
.metadata {
|
1005
|
-
display: flex;
|
1006
|
-
justify-content: center;
|
1007
|
-
gap: 30px;
|
1008
|
-
flex-wrap: wrap;
|
1009
|
-
font-size: 0.9em;
|
1010
|
-
}
|
1011
|
-
|
1012
|
-
.benchmark-nav {
|
1013
|
-
display: flex;
|
1014
|
-
justify-content: center;
|
1015
|
-
gap: 10px;
|
1016
|
-
margin-bottom: 40px;
|
1017
|
-
flex-wrap: wrap;
|
1018
|
-
}
|
1019
|
-
|
1020
|
-
.nav-btn {
|
1021
|
-
padding: 12px 24px;
|
1022
|
-
border: none;
|
1023
|
-
background-color: #e9ecef;
|
1024
|
-
color: #495057;
|
1025
|
-
border-radius: 25px;
|
1026
|
-
cursor: pointer;
|
1027
|
-
transition: all 0.3s ease;
|
1028
|
-
font-weight: 500;
|
1029
|
-
}
|
1030
|
-
|
1031
|
-
.nav-btn:hover {
|
1032
|
-
background-color: #dee2e6;
|
1033
|
-
transform: translateY(-2px);
|
1034
|
-
}
|
1035
|
-
|
1036
|
-
.nav-btn.active {
|
1037
|
-
background-color: var(--secondary-color);
|
1038
|
-
color: white;
|
1039
|
-
}
|
1040
|
-
|
1041
|
-
.section {
|
1042
|
-
display: none;
|
1043
|
-
background: white;
|
1044
|
-
border-radius: 10px;
|
1045
|
-
padding: 30px;
|
1046
|
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
1047
|
-
}
|
1048
|
-
|
1049
|
-
.section.active {
|
1050
|
-
display: block;
|
1051
|
-
}
|
1052
|
-
|
1053
|
-
.section h2 {
|
1054
|
-
color: var(--primary-color);
|
1055
|
-
margin-bottom: 30px;
|
1056
|
-
font-size: 2em;
|
1057
|
-
border-bottom: 3px solid var(--secondary-color);
|
1058
|
-
padding-bottom: 10px;
|
1059
|
-
}
|
1060
|
-
|
1061
|
-
.section h3 {
|
1062
|
-
color: #34495e;
|
1063
|
-
margin: 30px 0 20px 0;
|
1064
|
-
font-size: 1.5em;
|
1065
|
-
}
|
1066
|
-
|
1067
|
-
.charts-grid {
|
1068
|
-
display: grid;
|
1069
|
-
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
1070
|
-
gap: 30px;
|
1071
|
-
margin-bottom: 40px;
|
1072
|
-
}
|
1073
|
-
|
1074
|
-
.chart-container {
|
1075
|
-
background: var(--light-bg);
|
1076
|
-
padding: 20px;
|
1077
|
-
border-radius: 8px;
|
1078
|
-
border: 1px solid var(--border-color);
|
1079
|
-
}
|
1080
|
-
|
1081
|
-
.chart-container h4 {
|
1082
|
-
text-align: center;
|
1083
|
-
margin-bottom: 15px;
|
1084
|
-
color: #495057;
|
1085
|
-
font-size: 1.1em;
|
1086
|
-
}
|
1087
|
-
|
1088
|
-
.summary-grid {
|
1089
|
-
display: grid;
|
1090
|
-
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
1091
|
-
gap: 30px;
|
1092
|
-
}
|
1093
|
-
|
1094
|
-
.summary-card {
|
1095
|
-
background: var(--light-bg);
|
1096
|
-
padding: 25px;
|
1097
|
-
border-radius: 8px;
|
1098
|
-
border: 1px solid var(--border-color);
|
1099
|
-
}
|
1100
|
-
|
1101
|
-
.summary-card h3 {
|
1102
|
-
color: var(--secondary-color);
|
1103
|
-
margin-bottom: 15px;
|
1104
|
-
font-size: 1.3em;
|
1105
|
-
}
|
1106
|
-
|
1107
|
-
.summary-card ul {
|
1108
|
-
list-style: none;
|
1109
|
-
padding-left: 0;
|
1110
|
-
}
|
1111
|
-
|
1112
|
-
.summary-card li {
|
1113
|
-
padding: 8px 0;
|
1114
|
-
border-bottom: 1px solid #eee;
|
1115
|
-
}
|
1116
|
-
|
1117
|
-
.summary-card li:last-child {
|
1118
|
-
border-bottom: none;
|
1119
|
-
}
|
1120
|
-
|
1121
|
-
.environments-grid {
|
1122
|
-
display: grid;
|
1123
|
-
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
1124
|
-
gap: 20px;
|
1125
|
-
}
|
1126
|
-
|
1127
|
-
.environment-card {
|
1128
|
-
background: var(--light-bg);
|
1129
|
-
padding: 20px;
|
1130
|
-
border-radius: 8px;
|
1131
|
-
border: 1px solid var(--border-color);
|
1132
|
-
}
|
1133
|
-
|
1134
|
-
.environment-card h3 {
|
1135
|
-
color: var(--secondary-color);
|
1136
|
-
margin-bottom: 15px;
|
1137
|
-
font-size: 1.2em;
|
1138
|
-
}
|
1139
|
-
|
1140
|
-
.environment-card p {
|
1141
|
-
margin-bottom: 8px;
|
1142
|
-
color: #6c757d;
|
1143
|
-
}
|
1144
|
-
|
1145
|
-
.serializer-versions {
|
1146
|
-
margin-top: 15px;
|
1147
|
-
}
|
1148
|
-
|
1149
|
-
.serializer-versions h4 {
|
1150
|
-
color: #495057;
|
1151
|
-
margin-bottom: 10px;
|
1152
|
-
font-size: 1em;
|
1153
|
-
}
|
1154
|
-
|
1155
|
-
.serializer-versions ul {
|
1156
|
-
list-style: none;
|
1157
|
-
padding-left: 0;
|
1158
|
-
}
|
1159
|
-
|
1160
|
-
.serializer-versions li {
|
1161
|
-
padding: 4px 0;
|
1162
|
-
color: #6c757d;
|
1163
|
-
font-size: 0.9em;
|
1164
|
-
}
|
1165
|
-
|
1166
|
-
@media (max-width: 768px) {
|
1167
|
-
.container {
|
1168
|
-
padding: 10px;
|
1169
|
-
}
|
1170
|
-
|
1171
|
-
header {
|
1172
|
-
padding: 20px 10px;
|
1173
|
-
}
|
1174
|
-
|
1175
|
-
header h1 {
|
1176
|
-
font-size: 2em;
|
1177
|
-
}
|
1178
|
-
|
1179
|
-
.metadata {
|
1180
|
-
flex-direction: column;
|
1181
|
-
gap: 10px;
|
1182
|
-
}
|
1183
|
-
|
1184
|
-
.charts-grid {
|
1185
|
-
grid-template-columns: 1fr;
|
1186
|
-
}
|
1187
|
-
|
1188
|
-
.chart-container {
|
1189
|
-
padding: 15px;
|
1190
|
-
}
|
1191
|
-
|
1192
|
-
.summary-grid {
|
1193
|
-
grid-template-columns: 1fr;
|
1194
|
-
}
|
1195
|
-
}
|
1196
|
-
CSS
|
1197
|
-
|
1198
|
-
File.write(File.join(@assets_dir, 'css', 'benchmark_report.css'), css_content)
|
1199
|
-
end
|
1200
|
-
end
|
1201
|
-
end
|