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,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_parser'
4
+
5
+ module Serialbench
6
+ module Parsers
7
+ class OxParser < BaseParser
8
+ def parse_dom(xml_string)
9
+ require 'ox'
10
+ Ox.parse(xml_string)
11
+ end
12
+
13
+ def parse_sax(xml_string, handler = nil)
14
+ require 'ox'
15
+ require 'stringio'
16
+ handler ||= OxSaxHandler.new
17
+ Ox.sax_parse(handler, StringIO.new(xml_string))
18
+ handler
19
+ end
20
+
21
+ def generate_xml(document, options = {})
22
+ require 'ox'
23
+ indent = options.fetch(:indent, 0)
24
+ Ox.dump(document, indent: indent)
25
+ end
26
+
27
+ def supports_streaming?
28
+ true
29
+ end
30
+
31
+ # SAX handler for Ox
32
+ class OxSaxHandler < (defined?(::Ox) ? ::Ox::Sax : Object)
33
+ attr_reader :elements_processed, :text_nodes_processed
34
+
35
+ def initialize
36
+ @elements_processed = 0
37
+ @text_nodes_processed = 0
38
+ end
39
+
40
+ def start_element(name)
41
+ @elements_processed += 1
42
+ end
43
+
44
+ def end_element(name)
45
+ # Process end element
46
+ end
47
+
48
+ def text(value)
49
+ @text_nodes_processed += 1 unless value.strip.empty?
50
+ end
51
+
52
+ def attr(name, value)
53
+ # Process attribute
54
+ end
55
+ end
56
+
57
+ protected
58
+
59
+ def library_require_name
60
+ 'ox'
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_xml_serializer'
4
+
5
+ module Serialbench
6
+ module Serializers
7
+ module Xml
8
+ class OxSerializer < BaseXmlSerializer
9
+ def name
10
+ 'ox'
11
+ end
12
+
13
+ def parse(xml_string)
14
+ require 'ox'
15
+ Ox.parse(xml_string)
16
+ end
17
+
18
+ def generate(data)
19
+ require 'ox'
20
+ Ox.dump(data, indent: 2)
21
+ end
22
+
23
+ def parse_streaming(xml_string, &block)
24
+ require 'ox'
25
+ require 'stringio'
26
+
27
+ handler = StreamingHandler.new(&block)
28
+ Ox.sax_parse(handler, StringIO.new(xml_string))
29
+ handler.elements_processed
30
+ end
31
+
32
+ def supports_streaming?
33
+ true
34
+ end
35
+
36
+ def version
37
+ return 'unknown' unless available?
38
+
39
+ require 'ox'
40
+ Ox::VERSION
41
+ end
42
+
43
+ protected
44
+
45
+ def library_require_name
46
+ 'ox'
47
+ end
48
+
49
+ # SAX handler for streaming
50
+ class StreamingHandler < (defined?(::Ox) ? ::Ox::Sax : Object)
51
+ attr_reader :elements_processed
52
+
53
+ def initialize(&block)
54
+ @block = block
55
+ @elements_processed = 0
56
+ @current_element = nil
57
+ @element_stack = []
58
+ end
59
+
60
+ def start_element(name)
61
+ @elements_processed += 1
62
+ @current_element = { name: name, attributes: {}, children: [], text: '' }
63
+ @element_stack.push(@current_element)
64
+ end
65
+
66
+ def end_element(name)
67
+ element = @element_stack.pop
68
+ if @element_stack.empty?
69
+ @block&.call(element) if @block
70
+ else
71
+ @element_stack.last[:children] << element
72
+ end
73
+ end
74
+
75
+ def text(value)
76
+ return if value.strip.empty?
77
+
78
+ @element_stack.last[:text] += value if @element_stack.any?
79
+ end
80
+
81
+ def attr(name, value)
82
+ @element_stack.last[:attributes][name] = value if @element_stack.any?
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_parser'
4
+
5
+ module Serialbench
6
+ module Parsers
7
+ class RexmlParser < BaseParser
8
+ def parse_dom(xml_string)
9
+ require 'rexml/document'
10
+ REXML::Document.new(xml_string)
11
+ end
12
+
13
+ def parse_sax(xml_string, handler = nil)
14
+ require 'rexml/parsers/sax2parser'
15
+ handler ||= DefaultSaxHandler.new
16
+ parser = REXML::Parsers::SAX2Parser.new(xml_string)
17
+ parser.listen(handler)
18
+ parser.parse
19
+ handler
20
+ rescue LoadError
21
+ # Fallback if SAX2 is not available
22
+ require 'rexml/document'
23
+ doc = REXML::Document.new(xml_string)
24
+ handler ||= DefaultSaxHandler.new
25
+ # Simulate SAX events by walking the document
26
+ doc.root.each_recursive do |element|
27
+ if element.is_a?(REXML::Element)
28
+ handler.start_element(nil, element.name, element.name, element.attributes)
29
+ handler.end_element(nil, element.name, element.name)
30
+ elsif element.is_a?(REXML::Text)
31
+ handler.characters(element.to_s)
32
+ end
33
+ end
34
+ handler
35
+ end
36
+
37
+ def generate_xml(document, options = {})
38
+ require 'rexml/document'
39
+ indent = options.fetch(:indent, 0)
40
+ output = String.new
41
+ if indent > 0
42
+ document.write(output, indent)
43
+ else
44
+ document.write(output)
45
+ end
46
+ output
47
+ end
48
+
49
+ def supports_streaming?
50
+ true
51
+ end
52
+
53
+ protected
54
+
55
+ def library_require_name
56
+ 'rexml/document'
57
+ end
58
+
59
+ def detect_version
60
+ require 'rexml/rexml'
61
+ REXML::VERSION
62
+ rescue LoadError, NameError
63
+ 'built-in'
64
+ end
65
+
66
+ def supports_xpath?
67
+ true
68
+ end
69
+
70
+ def supports_namespaces?
71
+ true
72
+ end
73
+
74
+ def supports_validation?
75
+ false
76
+ end
77
+ end
78
+
79
+ # Default SAX handler for REXML - only define if REXML SAX is available
80
+ begin
81
+ require 'rexml/sax2listener'
82
+
83
+ class DefaultSaxHandler
84
+ include ::REXML::SAX2Listener
85
+
86
+ attr_reader :elements_processed, :text_nodes_processed
87
+
88
+ def initialize
89
+ @elements_processed = 0
90
+ @text_nodes_processed = 0
91
+ end
92
+
93
+ def start_element(uri, localname, qname, attributes)
94
+ @elements_processed += 1
95
+ end
96
+
97
+ def characters(text)
98
+ @text_nodes_processed += 1 unless text.strip.empty?
99
+ end
100
+
101
+ def end_element(uri, localname, qname)
102
+ # Element processing complete
103
+ end
104
+ end
105
+ rescue LoadError
106
+ # SAX2Listener not available, define a simple handler
107
+ class DefaultSaxHandler
108
+ attr_reader :elements_processed, :text_nodes_processed
109
+
110
+ def initialize
111
+ @elements_processed = 0
112
+ @text_nodes_processed = 0
113
+ end
114
+
115
+ def start_element(uri, localname, qname, attributes)
116
+ @elements_processed += 1
117
+ end
118
+
119
+ def characters(text)
120
+ @text_nodes_processed += 1 unless text.strip.empty?
121
+ end
122
+
123
+ def end_element(uri, localname, qname)
124
+ # Element processing complete
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_xml_serializer'
4
+
5
+ module Serialbench
6
+ module Serializers
7
+ module Xml
8
+ class RexmlSerializer < BaseXmlSerializer
9
+ def available?
10
+ require_library('rexml/document')
11
+ end
12
+
13
+ def name
14
+ 'rexml'
15
+ end
16
+
17
+ def version
18
+ require 'rexml/rexml'
19
+ REXML::VERSION
20
+ rescue LoadError, NameError
21
+ 'built-in'
22
+ end
23
+
24
+ def parse(xml_string)
25
+ require 'rexml/document'
26
+ REXML::Document.new(xml_string)
27
+ end
28
+
29
+ def generate(document, options = {})
30
+ require 'rexml/document'
31
+ indent = options.fetch(:indent, 0)
32
+ output = String.new
33
+ if indent > 0
34
+ document.write(output, indent)
35
+ else
36
+ document.write(output)
37
+ end
38
+ output
39
+ end
40
+
41
+ def stream_parse(xml_string, &block)
42
+ require 'rexml/parsers/sax2parser'
43
+ handler = SaxHandler.new(&block)
44
+ parser = REXML::Parsers::SAX2Parser.new(xml_string)
45
+ parser.listen(handler)
46
+ parser.parse
47
+ handler.result
48
+ rescue LoadError
49
+ # Fallback if SAX2 is not available
50
+ require 'rexml/document'
51
+ doc = REXML::Document.new(xml_string)
52
+ handler = SaxHandler.new(&block)
53
+ # Simulate SAX events by walking the document
54
+ doc.root.each_recursive do |element|
55
+ if element.is_a?(REXML::Element)
56
+ handler.start_element(nil, element.name, element.name, element.attributes)
57
+ handler.end_element(nil, element.name, element.name)
58
+ elsif element.is_a?(REXML::Text)
59
+ handler.characters(element.to_s)
60
+ end
61
+ end
62
+ handler.result
63
+ end
64
+
65
+ def supports_streaming?
66
+ true
67
+ end
68
+
69
+ protected
70
+
71
+ def supports_xpath?
72
+ true
73
+ end
74
+
75
+ def supports_namespaces?
76
+ true
77
+ end
78
+
79
+ def supports_validation?
80
+ false
81
+ end
82
+ end
83
+
84
+ # SAX handler for REXML streaming
85
+ class SaxHandler
86
+ attr_reader :result, :elements_processed, :text_nodes_processed
87
+
88
+ def initialize(&block)
89
+ @block = block
90
+ @elements_processed = 0
91
+ @text_nodes_processed = 0
92
+ @result = nil
93
+ end
94
+
95
+ def start_element(uri, localname, qname, attributes)
96
+ @elements_processed += 1
97
+ @block&.call(:start_element, { name: qname, attributes: attributes })
98
+ end
99
+
100
+ def characters(text)
101
+ return if text.strip.empty?
102
+
103
+ @text_nodes_processed += 1
104
+ @block&.call(:text, text)
105
+ end
106
+
107
+ def end_element(uri, localname, qname)
108
+ @block&.call(:end_element, { name: qname })
109
+ end
110
+ end
111
+
112
+ # Include SAX2Listener if available
113
+ begin
114
+ require 'rexml/sax2listener'
115
+ SaxHandler.include(::REXML::SAX2Listener)
116
+ rescue LoadError
117
+ # SAX2Listener not available, handler works without it
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'serializers/base_serializer'
4
+
5
+ # XML Serializers
6
+ require_relative 'serializers/xml/base_xml_serializer'
7
+ require_relative 'serializers/xml/rexml_serializer'
8
+ require_relative 'serializers/xml/ox_serializer'
9
+ require_relative 'serializers/xml/nokogiri_serializer'
10
+ require_relative 'serializers/xml/oga_serializer'
11
+ require_relative 'serializers/xml/libxml_serializer'
12
+
13
+ # JSON Serializers
14
+ require_relative 'serializers/json/base_json_serializer'
15
+ require_relative 'serializers/json/json_serializer'
16
+ require_relative 'serializers/json/oj_serializer'
17
+ require_relative 'serializers/json/yajl_serializer'
18
+
19
+ # TOML Serializers
20
+ require_relative 'serializers/toml/base_toml_serializer'
21
+ require_relative 'serializers/toml/toml_rb_serializer'
22
+ require_relative 'serializers/toml/tomlib_serializer'
23
+
24
+ module Serialbench
25
+ module Serializers
26
+ # Registry of all available serializers
27
+ SERIALIZERS = {
28
+ xml: [
29
+ Xml::RexmlSerializer,
30
+ Xml::OxSerializer,
31
+ Xml::NokogiriSerializer,
32
+ Xml::OgaSerializer,
33
+ Xml::LibxmlSerializer
34
+ ],
35
+ json: [
36
+ Json::JsonSerializer,
37
+ Json::OjSerializer,
38
+ Json::YajlSerializer
39
+ ],
40
+ toml: [
41
+ Toml::TomlRbSerializer,
42
+ Toml::TomlibSerializer
43
+ ]
44
+ }.freeze
45
+
46
+ def self.all
47
+ SERIALIZERS.values.flatten
48
+ end
49
+
50
+ def self.for_format(format)
51
+ SERIALIZERS[format.to_sym] || []
52
+ end
53
+
54
+ def self.available_for_format(format)
55
+ for_format(format).select { |serializer_class| serializer_class.new.available? }
56
+ end
57
+
58
+ def self.available
59
+ all.select { |serializer_class| serializer_class.new.available? }
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Serialbench
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'serialbench/version'
4
+ require_relative 'serialbench/serializers'
5
+ require_relative 'serialbench/benchmark_runner'
6
+ require_relative 'serialbench/result_merger'
7
+ require_relative 'serialbench/cli'
8
+ require_relative 'serialbench/result_formatter'
9
+ require_relative 'serialbench/chart_generator'
10
+ require_relative 'serialbench/memory_profiler'
11
+
12
+ module Serialbench
13
+ class Error < StandardError; end
14
+
15
+ # Supported serialization formats
16
+ FORMATS = %i[xml json toml].freeze
17
+
18
+ def self.run_benchmarks(formats: FORMATS, options: {})
19
+ runner = BenchmarkRunner.new(formats: formats, **options)
20
+ runner.run_all_benchmarks
21
+ end
22
+
23
+ def self.available_serializers(format = nil)
24
+ runner = BenchmarkRunner.new
25
+ if format
26
+ runner.serializers_for_format(format)
27
+ else
28
+ runner.all_serializers
29
+ end
30
+ end
31
+
32
+ def self.generate_reports(results)
33
+ result_merger = Serialbench::ResultMerger.new
34
+ result_merger.generate_all_reports(results)
35
+ end
36
+
37
+ def self.generate_reports_from_data(data_file)
38
+ require 'json'
39
+ data = JSON.parse(File.read(data_file), symbolize_names: true)
40
+ generate_reports(data)
41
+ end
42
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/serialbench/version'
4
+
5
+ all_files_in_git = Dir.chdir(File.expand_path(__dir__)) do
6
+ `git ls-files -z`.split("\x0")
7
+ end
8
+
9
+ Gem::Specification.new do |spec|
10
+ spec.name = 'serialbench'
11
+ spec.version = Serialbench::VERSION
12
+ spec.authors = ['Ribose']
13
+ spec.email = ['open.source@ribose.com']
14
+
15
+ spec.summary = 'Comprehensive serialization benchmarking tool for Ruby'
16
+ spec.description = 'A benchmarking suite for comparing performance of various serialization libraries in Ruby, including XML, JSON, and TOML parsers/generators.'
17
+ spec.homepage = 'https://github.com/metanorma/serialbench'
18
+ spec.license = 'BSD-2-Clause'
19
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
20
+
21
+ spec.metadata['homepage_uri'] = spec.homepage
22
+ spec.metadata['source_code_uri'] = spec.homepage
23
+ spec.metadata['bug_tracker_uri'] = "#{spec.homepage}/issues"
24
+
25
+ # Specify which files should be added to the gem when it is released.
26
+ spec.files = all_files_in_git
27
+ .reject { |f| f.match(%r{\A(?:spec|features|bin|\.)/}) }
28
+
29
+ spec.bindir = 'exe'
30
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ['lib']
32
+
33
+ # Runtime dependencies
34
+ spec.add_dependency 'benchmark-ips', '~> 2.10'
35
+ spec.add_dependency 'memory_profiler', '~> 1.0'
36
+ spec.add_dependency 'thor', '~> 1.2'
37
+
38
+ # XML serializers (optional)
39
+ spec.add_dependency 'libxml-ruby', '~> 4.0'
40
+ spec.add_dependency 'nokogiri', '~> 1.15'
41
+ spec.add_dependency 'oga', '~> 3.4'
42
+ spec.add_dependency 'ox', '~> 2.14'
43
+
44
+ # JSON serializers (optional)
45
+ spec.add_dependency 'oj', '~> 3.13'
46
+ spec.add_dependency 'yajl-ruby', '~> 1.4'
47
+
48
+ # TOML serializers (optional)
49
+ spec.add_dependency 'tomlib', '~> 0.6'
50
+ spec.add_dependency 'toml-rb', '~> 2.2'
51
+ end