serialbench 0.1.1 → 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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/benchmark.yml +13 -5
  3. data/.github/workflows/docker.yml +35 -9
  4. data/.github/workflows/rake.yml +15 -0
  5. data/Gemfile +2 -1
  6. data/README.adoc +267 -1129
  7. data/Rakefile +0 -55
  8. data/config/benchmarks/full.yml +29 -0
  9. data/config/benchmarks/short.yml +26 -0
  10. data/config/environments/asdf-ruby-3.2.yml +8 -0
  11. data/config/environments/asdf-ruby-3.3.yml +8 -0
  12. data/config/environments/docker-ruby-3.0.yml +9 -0
  13. data/config/environments/docker-ruby-3.1.yml +9 -0
  14. data/config/environments/docker-ruby-3.2.yml +9 -0
  15. data/config/environments/docker-ruby-3.3.yml +9 -0
  16. data/config/environments/docker-ruby-3.4.yml +9 -0
  17. data/docker/Dockerfile.alpine +33 -0
  18. data/docker/{Dockerfile.benchmark → Dockerfile.ubuntu} +4 -3
  19. data/docker/README.md +2 -2
  20. data/exe/serialbench +1 -1
  21. data/lib/serialbench/benchmark_runner.rb +261 -423
  22. data/lib/serialbench/cli/base_cli.rb +51 -0
  23. data/lib/serialbench/cli/benchmark_cli.rb +380 -0
  24. data/lib/serialbench/cli/environment_cli.rb +181 -0
  25. data/lib/serialbench/cli/resultset_cli.rb +215 -0
  26. data/lib/serialbench/cli/ruby_build_cli.rb +238 -0
  27. data/lib/serialbench/cli.rb +58 -601
  28. data/lib/serialbench/config_manager.rb +140 -0
  29. data/lib/serialbench/models/benchmark_config.rb +63 -0
  30. data/lib/serialbench/models/benchmark_result.rb +45 -0
  31. data/lib/serialbench/models/environment_config.rb +71 -0
  32. data/lib/serialbench/models/platform.rb +59 -0
  33. data/lib/serialbench/models/result.rb +53 -0
  34. data/lib/serialbench/models/result_set.rb +71 -0
  35. data/lib/serialbench/models/result_store.rb +108 -0
  36. data/lib/serialbench/models.rb +54 -0
  37. data/lib/serialbench/ruby_build_manager.rb +153 -0
  38. data/lib/serialbench/runners/asdf_runner.rb +296 -0
  39. data/lib/serialbench/runners/base.rb +32 -0
  40. data/lib/serialbench/runners/docker_runner.rb +142 -0
  41. data/lib/serialbench/serializers/base_serializer.rb +8 -16
  42. data/lib/serialbench/serializers/json/base_json_serializer.rb +4 -4
  43. data/lib/serialbench/serializers/json/json_serializer.rb +0 -2
  44. data/lib/serialbench/serializers/json/oj_serializer.rb +0 -2
  45. data/lib/serialbench/serializers/json/yajl_serializer.rb +0 -2
  46. data/lib/serialbench/serializers/toml/base_toml_serializer.rb +5 -3
  47. data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +0 -2
  48. data/lib/serialbench/serializers/toml/tomlib_serializer.rb +0 -2
  49. data/lib/serialbench/serializers/toml/tomlrb_serializer.rb +56 -0
  50. data/lib/serialbench/serializers/xml/base_xml_serializer.rb +4 -9
  51. data/lib/serialbench/serializers/xml/libxml_serializer.rb +0 -2
  52. data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +0 -2
  53. data/lib/serialbench/serializers/xml/oga_serializer.rb +0 -2
  54. data/lib/serialbench/serializers/xml/ox_serializer.rb +0 -2
  55. data/lib/serialbench/serializers/xml/rexml_serializer.rb +0 -2
  56. data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +5 -1
  57. data/lib/serialbench/serializers/yaml/syck_serializer.rb +59 -22
  58. data/lib/serialbench/serializers.rb +23 -6
  59. data/lib/serialbench/site_generator.rb +105 -0
  60. data/lib/serialbench/templates/assets/css/benchmark_report.css +535 -0
  61. data/lib/serialbench/templates/assets/css/format_based.css +526 -0
  62. data/lib/serialbench/templates/assets/css/themes.css +588 -0
  63. data/lib/serialbench/templates/assets/js/chart_helpers.js +381 -0
  64. data/lib/serialbench/templates/assets/js/dashboard.js +796 -0
  65. data/lib/serialbench/templates/assets/js/navigation.js +142 -0
  66. data/lib/serialbench/templates/base.liquid +49 -0
  67. data/lib/serialbench/templates/format_based.liquid +279 -0
  68. data/lib/serialbench/templates/partials/chart_section.liquid +4 -0
  69. data/lib/serialbench/version.rb +1 -1
  70. data/lib/serialbench.rb +2 -31
  71. data/serialbench.gemspec +4 -1
  72. metadata +86 -16
  73. data/config/ci.yml +0 -22
  74. data/config/full.yml +0 -30
  75. data/docker/run-benchmarks.sh +0 -356
  76. data/lib/serialbench/chart_generator.rb +0 -821
  77. data/lib/serialbench/result_formatter.rb +0 -182
  78. data/lib/serialbench/result_merger.rb +0 -1201
  79. data/lib/serialbench/serializers/xml/base_parser.rb +0 -69
  80. data/lib/serialbench/serializers/xml/libxml_parser.rb +0 -98
  81. data/lib/serialbench/serializers/xml/nokogiri_parser.rb +0 -111
  82. data/lib/serialbench/serializers/xml/oga_parser.rb +0 -85
  83. data/lib/serialbench/serializers/xml/ox_parser.rb +0 -64
  84. 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