serialbench 0.1.0

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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/benchmark.yml +125 -0
  3. data/.github/workflows/ci.yml +74 -0
  4. data/.rspec +4 -0
  5. data/Gemfile +34 -0
  6. data/README.adoc +592 -0
  7. data/Rakefile +63 -0
  8. data/exe/serialbench +6 -0
  9. data/lib/serialbench/benchmark_runner.rb +540 -0
  10. data/lib/serialbench/chart_generator.rb +821 -0
  11. data/lib/serialbench/cli.rb +438 -0
  12. data/lib/serialbench/memory_profiler.rb +31 -0
  13. data/lib/serialbench/result_formatter.rb +182 -0
  14. data/lib/serialbench/result_merger.rb +1201 -0
  15. data/lib/serialbench/serializers/base_serializer.rb +63 -0
  16. data/lib/serialbench/serializers/json/base_json_serializer.rb +67 -0
  17. data/lib/serialbench/serializers/json/json_serializer.rb +58 -0
  18. data/lib/serialbench/serializers/json/oj_serializer.rb +102 -0
  19. data/lib/serialbench/serializers/json/yajl_serializer.rb +67 -0
  20. data/lib/serialbench/serializers/toml/base_toml_serializer.rb +76 -0
  21. data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +55 -0
  22. data/lib/serialbench/serializers/toml/tomlib_serializer.rb +50 -0
  23. data/lib/serialbench/serializers/xml/base_parser.rb +69 -0
  24. data/lib/serialbench/serializers/xml/base_xml_serializer.rb +71 -0
  25. data/lib/serialbench/serializers/xml/libxml_parser.rb +98 -0
  26. data/lib/serialbench/serializers/xml/libxml_serializer.rb +127 -0
  27. data/lib/serialbench/serializers/xml/nokogiri_parser.rb +111 -0
  28. data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +118 -0
  29. data/lib/serialbench/serializers/xml/oga_parser.rb +85 -0
  30. data/lib/serialbench/serializers/xml/oga_serializer.rb +125 -0
  31. data/lib/serialbench/serializers/xml/ox_parser.rb +64 -0
  32. data/lib/serialbench/serializers/xml/ox_serializer.rb +88 -0
  33. data/lib/serialbench/serializers/xml/rexml_parser.rb +129 -0
  34. data/lib/serialbench/serializers/xml/rexml_serializer.rb +121 -0
  35. data/lib/serialbench/serializers.rb +62 -0
  36. data/lib/serialbench/version.rb +5 -0
  37. data/lib/serialbench.rb +42 -0
  38. data/serialbench.gemspec +51 -0
  39. metadata +239 -0
@@ -0,0 +1,438 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'json'
5
+ require 'yaml'
6
+ require 'fileutils'
7
+
8
+ module Serialbench
9
+ # Thor-based command line interface for SerialBench
10
+ class Cli < Thor
11
+ include Thor::Actions
12
+
13
+ desc 'benchmark', 'Run serialization benchmarks'
14
+ long_desc <<~DESC
15
+ Run the complete benchmark suite for all available serialization libraries.
16
+
17
+ This command will test parsing, generation, streaming, and memory usage
18
+ across XML, JSON, and TOML formats using all available libraries.
19
+ DESC
20
+ option :formats, type: :array, default: %w[xml json toml],
21
+ desc: 'Formats to benchmark (xml, json, toml)'
22
+ option :output_format, type: :string, default: 'all',
23
+ desc: 'Output format: all, json, yaml, html'
24
+ option :parsing_only, type: :boolean, default: false,
25
+ desc: 'Run only parsing benchmarks'
26
+ option :generation_only, type: :boolean, default: false,
27
+ desc: 'Run only generation benchmarks'
28
+ option :streaming_only, type: :boolean, default: false,
29
+ desc: 'Run only streaming benchmarks'
30
+ option :memory_only, type: :boolean, default: false,
31
+ desc: 'Run only memory usage benchmarks'
32
+ option :iterations, type: :numeric, default: 10,
33
+ desc: 'Number of benchmark iterations'
34
+ option :warmup, type: :numeric, default: 3,
35
+ desc: 'Number of warmup iterations'
36
+ def benchmark
37
+ say 'SerialBench - Comprehensive Serialization Performance Tests', :green
38
+ say '=' * 70, :green
39
+
40
+ # Validate formats
41
+ valid_formats = %w[xml json toml]
42
+ invalid_formats = options[:formats] - valid_formats
43
+ unless invalid_formats.empty?
44
+ say "Invalid formats: #{invalid_formats.join(', ')}", :red
45
+ say "Valid formats: #{valid_formats.join(', ')}", :yellow
46
+ exit 1
47
+ end
48
+
49
+ # Convert format strings to symbols
50
+ formats = options[:formats].map(&:to_sym)
51
+
52
+ # Show available serializers
53
+ show_available_serializers(formats)
54
+
55
+ # Run benchmarks
56
+ runner_options = {
57
+ formats: formats,
58
+ iterations: options[:iterations],
59
+ warmup: options[:warmup]
60
+ }
61
+
62
+ runner = Serialbench::BenchmarkRunner.new(**runner_options)
63
+
64
+ begin
65
+ results = run_selected_benchmarks(runner)
66
+ save_results(results)
67
+ show_summary(results) unless %w[json yaml].include?(options[:output_format])
68
+ rescue StandardError => e
69
+ say "Error running benchmarks: #{e.message}", :red
70
+ say e.backtrace.first(5).join("\n"), :red if ENV['DEBUG']
71
+ exit 1
72
+ end
73
+ end
74
+
75
+ desc 'list', 'List available serializers'
76
+ long_desc <<~DESC
77
+ Display all available serialization libraries grouped by format.
78
+
79
+ Shows which libraries are installed and available for benchmarking,
80
+ along with their versions.
81
+ DESC
82
+ option :format, type: :string, desc: 'Show only serializers for specific format'
83
+ def list
84
+ say 'Available Serializers', :green
85
+ say '=' * 30, :green
86
+
87
+ if options[:format]
88
+ format_sym = options[:format].to_sym
89
+ serializers = Serialbench::Serializers.available_for_format(format_sym)
90
+
91
+ if serializers.empty?
92
+ say "No available serializers for format: #{options[:format]}", :yellow
93
+ else
94
+ show_serializers_for_format(format_sym, serializers)
95
+ end
96
+ else
97
+ %i[xml json toml].each do |format|
98
+ serializers = Serialbench::Serializers.available_for_format(format)
99
+ next if serializers.empty?
100
+
101
+ show_serializers_for_format(format, serializers)
102
+ say ''
103
+ end
104
+ end
105
+ end
106
+
107
+ desc 'version', 'Show SerialBench version'
108
+ def version
109
+ say "SerialBench version #{Serialbench::VERSION}", :green
110
+ end
111
+
112
+ desc 'merge_results INPUT_DIRS... OUTPUT_DIR', 'Merge benchmark results from multiple runs'
113
+ long_desc <<~DESC
114
+ Merge benchmark results from multiple Ruby versions or different environments.
115
+
116
+ INPUT_DIRS should contain results.json files from different benchmark runs.
117
+ OUTPUT_DIR will contain the merged results and comparative reports.
118
+
119
+ Example:
120
+ serialbench merge_results ruby-3.0/results ruby-3.1/results ruby-3.2/results merged_output/
121
+ DESC
122
+ def merge_results(*args)
123
+ if args.length < 2
124
+ say 'Error: Need at least one input directory and one output directory', :red
125
+ say 'Usage: serialbench merge_results INPUT_DIRS... OUTPUT_DIR', :yellow
126
+ exit 1
127
+ end
128
+
129
+ output_dir = args.pop
130
+ input_dirs = args
131
+
132
+ say "Merging benchmark results from #{input_dirs.length} directories to #{output_dir}", :green
133
+
134
+ begin
135
+ merger = Serialbench::ResultMerger.new
136
+ merged_file = merger.merge_directories(input_dirs, output_dir)
137
+ say "Results merged successfully to: #{merged_file}", :green
138
+ rescue StandardError => e
139
+ say "Error merging results: #{e.message}", :red
140
+ exit 1
141
+ end
142
+ end
143
+
144
+ desc 'github_pages INPUT_DIRS... OUTPUT_DIR', 'Generate GitHub Pages HTML from multiple benchmark runs'
145
+ long_desc <<~DESC
146
+ Merge benchmark results from multiple Ruby versions and generate a GitHub Pages compatible HTML report.
147
+
148
+ INPUT_DIRS should contain results.json files from different benchmark runs.
149
+ OUTPUT_DIR will contain index.html and styles.css ready for GitHub Pages deployment.
150
+
151
+ This command combines merge_results and HTML generation in one step.
152
+
153
+ Example:
154
+ serialbench github_pages ruby-3.0/results ruby-3.1/results ruby-3.2/results docs/
155
+ DESC
156
+ def github_pages(*args)
157
+ if args.length < 2
158
+ say 'Error: Need at least one input directory and one output directory', :red
159
+ say 'Usage: serialbench github_pages INPUT_DIRS... OUTPUT_DIR', :yellow
160
+ exit 1
161
+ end
162
+
163
+ output_dir = args.pop
164
+ input_dirs = args
165
+
166
+ say "Generating GitHub Pages from #{input_dirs.length} benchmark directories", :green
167
+
168
+ begin
169
+ merger = Serialbench::ResultMerger.new
170
+
171
+ # Merge results
172
+ say 'Step 1: Merging benchmark results...', :yellow
173
+ merger.merge_directories(input_dirs, output_dir)
174
+
175
+ # Generate GitHub Pages HTML
176
+ say 'Step 2: Generating GitHub Pages HTML...', :yellow
177
+ files = merger.generate_github_pages_html(output_dir)
178
+
179
+ say 'GitHub Pages generated successfully!', :green
180
+ say 'Files created:', :cyan
181
+ say " HTML: #{files[:html]}", :white
182
+ say " CSS: #{files[:css]}", :white
183
+ say '', :white
184
+ say 'To deploy to GitHub Pages:', :cyan
185
+ say '1. Commit and push the generated files to your repository', :white
186
+ say '2. Enable GitHub Pages in repository settings', :white
187
+ say '3. Set source to the branch containing these files', :white
188
+ rescue StandardError => e
189
+ say "Error generating GitHub Pages: #{e.message}", :red
190
+ exit 1
191
+ end
192
+ end
193
+
194
+ desc 'generate_reports DATA_FILE', 'Generate reports from benchmark data'
195
+ long_desc <<~DESC
196
+ Generate HTML and AsciiDoc reports from existing benchmark data.
197
+
198
+ DATA_FILE should be a JSON file containing benchmark results.
199
+ DESC
200
+ def generate_reports(data_file)
201
+ say "Generating reports from data in #{data_file}", :green
202
+
203
+ unless File.exist?(data_file)
204
+ say "Data file does not exist: #{data_file}", :red
205
+ exit 1
206
+ end
207
+
208
+ begin
209
+ Serialbench.generate_reports_from_data(data_file)
210
+ say 'Reports generated successfully!', :green
211
+ rescue StandardError => e
212
+ say "Error generating reports: #{e.message}", :red
213
+ exit 1
214
+ end
215
+ end
216
+
217
+ private
218
+
219
+ def show_available_serializers(formats)
220
+ say "\nAvailable serializers:", :cyan
221
+
222
+ formats.each do |format|
223
+ serializers = Serialbench::Serializers.available_for_format(format)
224
+ next if serializers.empty?
225
+
226
+ serializer_names = serializers.map do |serializer_class|
227
+ serializer = serializer_class.new
228
+ "#{serializer.name} v#{serializer.version}"
229
+ end
230
+
231
+ say " #{format.upcase}: #{serializer_names.join(', ')}", :white
232
+ end
233
+
234
+ say "\nTest data sizes: small, medium, large", :cyan
235
+ say ''
236
+ end
237
+
238
+ def show_serializers_for_format(format, serializers)
239
+ say "#{format.upcase}:", :cyan
240
+
241
+ serializers.each do |serializer_class|
242
+ serializer = serializer_class.new
243
+ features = []
244
+ features << 'streaming' if serializer.supports_streaming?
245
+ features << 'built-in' if %w[json rexml].include?(serializer.name)
246
+
247
+ feature_text = features.empty? ? '' : " (#{features.join(', ')})"
248
+ say " ✓ #{serializer.name} v#{serializer.version}#{feature_text}", :green
249
+ end
250
+ end
251
+
252
+ def run_selected_benchmarks(runner)
253
+ results = { environment: runner.environment_info }
254
+
255
+ if options[:parsing_only]
256
+ say 'Running parsing benchmarks...', :yellow
257
+ results[:parsing] = runner.run_parsing_benchmarks
258
+ elsif options[:generation_only]
259
+ say 'Running generation benchmarks...', :yellow
260
+ results[:generation] = runner.run_generation_benchmarks
261
+ elsif options[:streaming_only]
262
+ say 'Running streaming benchmarks...', :yellow
263
+ results[:streaming] = runner.run_streaming_benchmarks
264
+ elsif options[:memory_only]
265
+ say 'Running memory benchmarks...', :yellow
266
+ results[:memory_usage] = runner.run_memory_benchmarks
267
+ else
268
+ say 'Running all benchmarks...', :yellow
269
+ results = runner.run_all_benchmarks
270
+ end
271
+
272
+ results
273
+ end
274
+
275
+ def save_results(results)
276
+ case options[:output_format]
277
+ when 'json'
278
+ save_json_results(results)
279
+ when 'yaml'
280
+ save_yaml_results(results)
281
+ when 'html'
282
+ generate_html_reports(results)
283
+ else
284
+ # Generate all formats
285
+ save_json_results(results)
286
+ save_yaml_results(results)
287
+ generate_html_reports(results)
288
+ end
289
+
290
+ show_generated_files
291
+ end
292
+
293
+ def save_json_results(results)
294
+ FileUtils.mkdir_p('results/data')
295
+
296
+ # Add Ruby version to results
297
+ results[:ruby_version] = RUBY_VERSION
298
+ results[:ruby_platform] = RUBY_PLATFORM
299
+ results[:timestamp] = Time.now.iso8601
300
+
301
+ File.write('results/data/results.json', JSON.pretty_generate(results))
302
+ say 'JSON results saved to: results/data/results.json', :green
303
+ end
304
+
305
+ def save_yaml_results(results)
306
+ FileUtils.mkdir_p('results/data')
307
+
308
+ # Add Ruby version to results
309
+ results[:ruby_version] = RUBY_VERSION
310
+ results[:ruby_platform] = RUBY_PLATFORM
311
+ results[:timestamp] = Time.now.iso8601
312
+
313
+ File.write('results/data/results.yaml', results.to_yaml)
314
+ say 'YAML results saved to: results/data/results.yaml', :green
315
+ end
316
+
317
+ def generate_html_reports(results)
318
+ say 'Generating reports...', :yellow
319
+ report_files = Serialbench.generate_reports(results)
320
+
321
+ say 'Reports generated:', :green
322
+ say " HTML: #{report_files[:html]}", :white
323
+ say " CSS: #{report_files[:css]}", :white
324
+ end
325
+
326
+ def show_generated_files
327
+ case options[:output_format]
328
+ when 'json'
329
+ say 'Files generated:', :cyan
330
+ say ' JSON: results/data/results.json', :white
331
+ when 'yaml'
332
+ say 'Files generated:', :cyan
333
+ say ' YAML: results/data/results.yaml', :white
334
+ when 'html'
335
+ say 'Files generated:', :cyan
336
+ say ' HTML: results/reports/benchmark_report.html', :white
337
+ say ' Charts: results/charts/*.svg', :white
338
+ else
339
+ say 'Files generated:', :cyan
340
+ say ' JSON: results/data/results.json', :white
341
+ say ' YAML: results/data/results.yaml', :white
342
+ say ' HTML: results/reports/benchmark_report.html', :white
343
+ say ' Charts: results/charts/*.svg', :white
344
+ end
345
+ end
346
+
347
+ def show_summary(results)
348
+ return unless results[:parsing] || results[:generation]
349
+
350
+ say "\n" + '=' * 50, :green
351
+ say 'BENCHMARK SUMMARY', :green
352
+ say '=' * 50, :green
353
+
354
+ show_parsing_summary(results[:parsing]) if results[:parsing]
355
+
356
+ show_generation_summary(results[:generation]) if results[:generation]
357
+
358
+ return unless results[:memory_usage]
359
+
360
+ show_memory_summary(results[:memory_usage])
361
+ end
362
+
363
+ def show_parsing_summary(parsing_results)
364
+ say "\nParsing Performance (operations/second):", :cyan
365
+
366
+ %i[small medium large].each do |size|
367
+ next unless parsing_results[size]
368
+
369
+ say "\n #{size.capitalize} files:", :yellow
370
+
371
+ # Flatten the nested structure and sort by performance
372
+ flattened_results = []
373
+ parsing_results[size].each do |format, serializers|
374
+ serializers.each do |serializer_name, data|
375
+ flattened_results << ["#{format}/#{serializer_name}", data]
376
+ end
377
+ end
378
+
379
+ sorted_results = flattened_results.sort_by { |_, data| -data[:iterations_per_second] }
380
+
381
+ sorted_results.each do |serializer_name, data|
382
+ ops_per_sec = data[:iterations_per_second].round(2)
383
+ say " #{serializer_name}: #{ops_per_sec} ops/sec", :white
384
+ end
385
+ end
386
+ end
387
+
388
+ def show_generation_summary(generation_results)
389
+ say "\nGeneration Performance (operations/second):", :cyan
390
+
391
+ %i[small medium large].each do |size|
392
+ next unless generation_results[size]
393
+
394
+ say "\n #{size.capitalize} files:", :yellow
395
+
396
+ # Flatten the nested structure and sort by performance
397
+ flattened_results = []
398
+ generation_results[size].each do |format, serializers|
399
+ serializers.each do |serializer_name, data|
400
+ flattened_results << ["#{format}/#{serializer_name}", data]
401
+ end
402
+ end
403
+
404
+ sorted_results = flattened_results.sort_by { |_, data| -data[:iterations_per_second] }
405
+
406
+ sorted_results.each do |serializer_name, data|
407
+ ops_per_sec = data[:iterations_per_second].round(2)
408
+ say " #{serializer_name}: #{ops_per_sec} ops/sec", :white
409
+ end
410
+ end
411
+ end
412
+
413
+ def show_memory_summary(memory_results)
414
+ say "\nMemory Usage (MB):", :cyan
415
+
416
+ %i[small medium large].each do |size|
417
+ next unless memory_results[size]
418
+
419
+ say "\n #{size.capitalize} files:", :yellow
420
+
421
+ # Flatten the nested structure and sort by memory usage (ascending)
422
+ flattened_results = []
423
+ memory_results[size].each do |format, serializers|
424
+ serializers.each do |serializer_name, data|
425
+ flattened_results << ["#{format}/#{serializer_name}", data]
426
+ end
427
+ end
428
+
429
+ sorted_results = flattened_results.sort_by { |_, data| data[:allocated_memory] }
430
+
431
+ sorted_results.each do |serializer_name, data|
432
+ memory_mb = (data[:allocated_memory] / 1024.0 / 1024.0).round(2)
433
+ say " #{serializer_name}: #{memory_mb} MB", :white
434
+ end
435
+ end
436
+ end
437
+ end
438
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Serialbench
4
+ class MemoryProfiler
5
+ def self.profile(&block)
6
+ return yield unless defined?(::MemoryProfiler)
7
+
8
+ ::MemoryProfiler.report(&block)
9
+ end
10
+
11
+ def self.available?
12
+ require 'memory_profiler'
13
+ defined?(::MemoryProfiler) ? true : false
14
+ rescue LoadError
15
+ false
16
+ end
17
+
18
+ def self.format_report(report)
19
+ return 'Memory profiling not available' unless report
20
+
21
+ {
22
+ total_allocated: report.total_allocated,
23
+ total_retained: report.total_retained,
24
+ allocated_memory: report.total_allocated_memsize,
25
+ retained_memory: report.total_retained_memsize,
26
+ allocated_objects_by_gem: report.allocated_memory_by_gem,
27
+ retained_objects_by_gem: report.retained_memory_by_gem
28
+ }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'csv'
5
+
6
+ module Serialbench
7
+ class ResultFormatter
8
+ def initialize(results)
9
+ @results = results
10
+ end
11
+
12
+ def to_json(pretty: true)
13
+ if pretty
14
+ JSON.pretty_generate(@results)
15
+ else
16
+ JSON.generate(@results)
17
+ end
18
+ end
19
+
20
+ def to_csv
21
+ return '' unless @results && @results[:dom_parsing]
22
+
23
+ csv_data = []
24
+
25
+ # Header
26
+ csv_data << ['Category', 'File Size', 'Parser', 'Time (ms)', 'Iterations/sec', 'Memory (MB)', 'Error']
27
+
28
+ # DOM parsing results
29
+ add_category_to_csv(csv_data, 'DOM Parsing', @results[:dom_parsing])
30
+
31
+ # SAX parsing results
32
+ add_category_to_csv(csv_data, 'SAX Parsing', @results[:sax_parsing])
33
+
34
+ # XML generation results
35
+ add_category_to_csv(csv_data, 'XML Generation', @results[:xml_generation])
36
+
37
+ CSV.generate do |csv|
38
+ csv_data.each { |row| csv << row }
39
+ end
40
+ end
41
+
42
+ def save_to_files(output_dir = 'results/data')
43
+ FileUtils.mkdir_p(output_dir)
44
+
45
+ # Save JSON
46
+ json_file = File.join(output_dir, 'results.json')
47
+ File.write(json_file, to_json)
48
+
49
+ # Save CSV
50
+ csv_file = File.join(output_dir, 'results.csv')
51
+ File.write(csv_file, to_csv)
52
+
53
+ {
54
+ json: json_file,
55
+ csv: csv_file
56
+ }
57
+ end
58
+
59
+ def summary
60
+ return 'No results available' unless @results
61
+
62
+ summary_lines = []
63
+ summary_lines << 'XML Benchmarks Summary'
64
+ summary_lines << '=' * 50
65
+
66
+ if @results[:environment]
67
+ summary_lines << "Environment: Ruby #{@results[:environment][:ruby_version]} on #{@results[:environment][:ruby_platform]}"
68
+ summary_lines << "Timestamp: #{@results[:environment][:timestamp]}"
69
+ summary_lines << ''
70
+ end
71
+
72
+ # DOM parsing summary
73
+ if @results[:dom_parsing] && !@results[:dom_parsing].empty?
74
+ summary_lines << 'DOM Parsing Performance:'
75
+ add_category_summary(summary_lines, @results[:dom_parsing])
76
+ summary_lines << ''
77
+ end
78
+
79
+ # SAX parsing summary
80
+ if @results[:sax_parsing] && !@results[:sax_parsing].empty?
81
+ summary_lines << 'SAX Parsing Performance:'
82
+ add_category_summary(summary_lines, @results[:sax_parsing])
83
+ summary_lines << ''
84
+ end
85
+
86
+ # XML generation summary
87
+ if @results[:xml_generation] && !@results[:xml_generation].empty?
88
+ summary_lines << 'XML Generation Performance:'
89
+ add_category_summary(summary_lines, @results[:xml_generation])
90
+ summary_lines << ''
91
+ end
92
+
93
+ # Memory usage summary
94
+ if @results[:memory_usage] && !@results[:memory_usage].empty?
95
+ summary_lines << 'Memory Usage:'
96
+ add_memory_summary(summary_lines, @results[:memory_usage])
97
+ end
98
+
99
+ summary_lines.join("\n")
100
+ end
101
+
102
+ private
103
+
104
+ def add_category_to_csv(csv_data, category, results)
105
+ return unless results
106
+
107
+ results.each do |size, parsers|
108
+ parsers.each do |parser, data|
109
+ memory_mb = if @results[:memory_usage] && @results[:memory_usage][size] && @results[:memory_usage][size][parser]
110
+ (@results[:memory_usage][size][parser][:allocated_memory] / 1024.0 / 1024.0).round(2)
111
+ else
112
+ nil
113
+ end
114
+
115
+ csv_data << [
116
+ category,
117
+ size.to_s.capitalize,
118
+ parser.capitalize,
119
+ data[:error] ? nil : (data[:time_per_iteration] * 1000).round(2),
120
+ data[:error] ? nil : data[:iterations_per_second].round(2),
121
+ memory_mb,
122
+ data[:error] || nil
123
+ ]
124
+ end
125
+ end
126
+ end
127
+
128
+ def add_category_summary(summary_lines, results)
129
+ results.each do |size, parsers|
130
+ summary_lines << " #{size.to_s.capitalize} files:"
131
+
132
+ # Sort parsers by performance (fastest first)
133
+ sorted_parsers = parsers.reject { |_, data| data[:error] }
134
+ .sort_by { |_, data| data[:time_per_iteration] }
135
+
136
+ sorted_parsers.each_with_index do |(parser, data), index|
137
+ time_ms = (data[:time_per_iteration] * 1000).round(2)
138
+ rank = case index
139
+ when 0 then '🥇'
140
+ when 1 then '🥈'
141
+ when 2 then '🥉'
142
+ else ' '
143
+ end
144
+ summary_lines << " #{rank} #{parser.capitalize}: #{time_ms}ms"
145
+ end
146
+
147
+ # Show errors if any
148
+ errors = parsers.select { |_, data| data[:error] }
149
+ errors.each do |parser, data|
150
+ summary_lines << " ❌ #{parser.capitalize}: #{data[:error]}"
151
+ end
152
+ end
153
+ end
154
+
155
+ def add_memory_summary(summary_lines, results)
156
+ results.each do |size, parsers|
157
+ summary_lines << " #{size.to_s.capitalize} files:"
158
+
159
+ # Sort parsers by memory usage (lowest first)
160
+ sorted_parsers = parsers.reject { |_, data| data[:error] }
161
+ .sort_by { |_, data| data[:allocated_memory] }
162
+
163
+ sorted_parsers.each_with_index do |(parser, data), index|
164
+ memory_mb = (data[:allocated_memory] / 1024.0 / 1024.0).round(2)
165
+ rank = case index
166
+ when 0 then '🥇'
167
+ when 1 then '🥈'
168
+ when 2 then '🥉'
169
+ else ' '
170
+ end
171
+ summary_lines << " #{rank} #{parser.capitalize}: #{memory_mb}MB"
172
+ end
173
+
174
+ # Show errors if any
175
+ errors = parsers.select { |_, data| data[:error] }
176
+ errors.each do |parser, data|
177
+ summary_lines << " ❌ #{parser.capitalize}: #{data[:error]}"
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end