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.
- checksums.yaml +7 -0
- data/.github/workflows/benchmark.yml +125 -0
- data/.github/workflows/ci.yml +74 -0
- data/.rspec +4 -0
- data/Gemfile +34 -0
- data/README.adoc +592 -0
- data/Rakefile +63 -0
- data/exe/serialbench +6 -0
- data/lib/serialbench/benchmark_runner.rb +540 -0
- data/lib/serialbench/chart_generator.rb +821 -0
- data/lib/serialbench/cli.rb +438 -0
- data/lib/serialbench/memory_profiler.rb +31 -0
- data/lib/serialbench/result_formatter.rb +182 -0
- data/lib/serialbench/result_merger.rb +1201 -0
- data/lib/serialbench/serializers/base_serializer.rb +63 -0
- data/lib/serialbench/serializers/json/base_json_serializer.rb +67 -0
- data/lib/serialbench/serializers/json/json_serializer.rb +58 -0
- data/lib/serialbench/serializers/json/oj_serializer.rb +102 -0
- data/lib/serialbench/serializers/json/yajl_serializer.rb +67 -0
- data/lib/serialbench/serializers/toml/base_toml_serializer.rb +76 -0
- data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +55 -0
- data/lib/serialbench/serializers/toml/tomlib_serializer.rb +50 -0
- data/lib/serialbench/serializers/xml/base_parser.rb +69 -0
- data/lib/serialbench/serializers/xml/base_xml_serializer.rb +71 -0
- data/lib/serialbench/serializers/xml/libxml_parser.rb +98 -0
- data/lib/serialbench/serializers/xml/libxml_serializer.rb +127 -0
- data/lib/serialbench/serializers/xml/nokogiri_parser.rb +111 -0
- data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +118 -0
- data/lib/serialbench/serializers/xml/oga_parser.rb +85 -0
- data/lib/serialbench/serializers/xml/oga_serializer.rb +125 -0
- data/lib/serialbench/serializers/xml/ox_parser.rb +64 -0
- data/lib/serialbench/serializers/xml/ox_serializer.rb +88 -0
- data/lib/serialbench/serializers/xml/rexml_parser.rb +129 -0
- data/lib/serialbench/serializers/xml/rexml_serializer.rb +121 -0
- data/lib/serialbench/serializers.rb +62 -0
- data/lib/serialbench/version.rb +5 -0
- data/lib/serialbench.rb +42 -0
- data/serialbench.gemspec +51 -0
- 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
|