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,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_parser'
4
+
5
+ module Serialbench
6
+ module Parsers
7
+ class LibxmlParser < BaseParser
8
+ def parse_dom(xml_string)
9
+ require 'libxml'
10
+ LibXML::XML::Document.string(xml_string)
11
+ end
12
+
13
+ def parse_sax(xml_string, handler = nil)
14
+ require 'libxml'
15
+ handler ||= LibxmlSaxHandler.new
16
+ parser = LibXML::XML::SaxParser.string(xml_string)
17
+ parser.callbacks = handler
18
+ parser.parse
19
+ handler
20
+ end
21
+
22
+ def generate_xml(document, options = {})
23
+ require 'libxml'
24
+ document.to_s
25
+ end
26
+
27
+ def supports_streaming?
28
+ true
29
+ end
30
+
31
+ # SAX handler for LibXML
32
+ class LibxmlSaxHandler
33
+ attr_reader :elements_processed, :text_nodes_processed
34
+
35
+ def initialize
36
+ @elements_processed = 0
37
+ @text_nodes_processed = 0
38
+ # Include LibXML callbacks if available
39
+ return unless defined?(::LibXML)
40
+
41
+ return if self.class.ancestors.include?(::LibXML::XML::SaxParser::Callbacks)
42
+
43
+ self.class.send(:include,
44
+ ::LibXML::XML::SaxParser::Callbacks)
45
+ end
46
+
47
+ def on_start_document
48
+ # Document processing started
49
+ end
50
+
51
+ def on_end_document
52
+ # Document processing complete
53
+ end
54
+
55
+ def on_start_element(element, attributes)
56
+ @elements_processed += 1
57
+ end
58
+
59
+ def on_characters(chars)
60
+ @text_nodes_processed += 1 unless chars.strip.empty?
61
+ end
62
+
63
+ def on_end_element(element)
64
+ # Element processing complete
65
+ end
66
+
67
+ def on_error(msg)
68
+ # Handle parsing errors
69
+ end
70
+ end
71
+
72
+ protected
73
+
74
+ def library_require_name
75
+ 'libxml'
76
+ end
77
+
78
+ def detect_version
79
+ require 'libxml'
80
+ LibXML::XML::VERSION
81
+ rescue LoadError
82
+ 'not available'
83
+ end
84
+
85
+ def supports_xpath?
86
+ true
87
+ end
88
+
89
+ def supports_namespaces?
90
+ true
91
+ end
92
+
93
+ def supports_validation?
94
+ true
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_xml_serializer'
4
+
5
+ module Serialbench
6
+ module Serializers
7
+ module Xml
8
+ class LibxmlSerializer < BaseXmlSerializer
9
+ def name
10
+ 'libxml'
11
+ end
12
+
13
+ def parse(xml_string)
14
+ require 'libxml'
15
+ LibXML::XML::Parser.string(xml_string).parse
16
+ end
17
+
18
+ def generate(data)
19
+ require 'libxml'
20
+ # If data is already a LibXML document, convert to string
21
+ if data.respond_to?(:to_s) && data.class.name.include?('LibXML')
22
+ data.to_s(indent: true)
23
+ else
24
+ # Create a simple XML structure from hash-like data
25
+ doc = LibXML::XML::Document.new
26
+ doc.root = build_xml_from_data(data)
27
+ doc.to_s(indent: true)
28
+ end
29
+ end
30
+
31
+ def parse_streaming(xml_string, &block)
32
+ require 'libxml'
33
+
34
+ handler = StreamingHandler.new(&block)
35
+ parser = LibXML::XML::SaxParser.string(xml_string)
36
+ parser.callbacks = handler
37
+ parser.parse
38
+ handler.elements_processed
39
+ end
40
+
41
+ def supports_streaming?
42
+ true
43
+ end
44
+
45
+ def version
46
+ return 'unknown' unless available?
47
+
48
+ require 'libxml'
49
+ LibXML::XML::VERSION
50
+ end
51
+
52
+ protected
53
+
54
+ def library_require_name
55
+ 'libxml'
56
+ end
57
+
58
+ private
59
+
60
+ def build_xml_from_data(data, name = 'root')
61
+ require 'libxml'
62
+
63
+ case data
64
+ when Hash
65
+ element = LibXML::XML::Node.new(name.to_s)
66
+ data.each do |key, value|
67
+ child = build_xml_from_data(value, key.to_s)
68
+ element << child
69
+ end
70
+ element
71
+ when Array
72
+ element = LibXML::XML::Node.new(name.to_s)
73
+ data.each_with_index do |item, index|
74
+ child = build_xml_from_data(item, "item_#{index}")
75
+ element << child
76
+ end
77
+ element
78
+ else
79
+ element = LibXML::XML::Node.new(name.to_s)
80
+ element.content = data.to_s
81
+ element
82
+ end
83
+ end
84
+
85
+ # SAX handler for streaming
86
+ class StreamingHandler
87
+ include LibXML::XML::SaxParser::Callbacks if defined?(LibXML::XML::SaxParser::Callbacks)
88
+
89
+ attr_reader :elements_processed
90
+
91
+ def initialize(&block)
92
+ @block = block
93
+ @elements_processed = 0
94
+ @current_element = nil
95
+ @element_stack = []
96
+ end
97
+
98
+ def on_start_element(element, attributes)
99
+ @elements_processed += 1
100
+ attrs = Hash[attributes || []]
101
+ @current_element = { name: element, attributes: attrs, children: [], text: '' }
102
+ @element_stack.push(@current_element)
103
+ end
104
+
105
+ def on_end_element(element)
106
+ element_data = @element_stack.pop
107
+ if @element_stack.empty?
108
+ @block&.call(element_data) if @block
109
+ else
110
+ @element_stack.last[:children] << element_data
111
+ end
112
+ end
113
+
114
+ def on_characters(chars)
115
+ return if chars.strip.empty?
116
+
117
+ @element_stack.last[:text] += chars if @element_stack.any?
118
+ end
119
+
120
+ def on_cdata_block(cdata)
121
+ @element_stack.last[:text] += cdata if @element_stack.any?
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_parser'
4
+
5
+ module Serialbench
6
+ module Parsers
7
+ class NokogiriParser < BaseParser
8
+ def parse_dom(xml_string)
9
+ require 'nokogiri'
10
+ Nokogiri::XML(xml_string)
11
+ end
12
+
13
+ def parse_sax(xml_string, handler = nil)
14
+ require 'nokogiri'
15
+ handler ||= NokogiriSaxHandler.new
16
+ parser = Nokogiri::XML::SAX::Parser.new(handler)
17
+ parser.parse(xml_string)
18
+ handler
19
+ end
20
+
21
+ def generate_xml(document, options = {})
22
+ require 'nokogiri'
23
+ indent = options.fetch(:indent, 0)
24
+ if indent > 0
25
+ document.to_xml(indent: indent)
26
+ else
27
+ document.to_xml
28
+ end
29
+ end
30
+
31
+ def supports_streaming?
32
+ true
33
+ end
34
+
35
+ # SAX handler for Nokogiri
36
+ class NokogiriSaxHandler < (defined?(::Nokogiri) ? ::Nokogiri::XML::SAX::Document : Object)
37
+ attr_reader :elements_processed, :text_nodes_processed
38
+
39
+ def initialize
40
+ @elements_processed = 0
41
+ @text_nodes_processed = 0
42
+ end
43
+
44
+ def start_document
45
+ # Document processing started
46
+ end
47
+
48
+ def end_document
49
+ # Document processing complete
50
+ end
51
+
52
+ def start_element(name, attrs = [])
53
+ @elements_processed += 1
54
+ end
55
+
56
+ def start_element_namespace(name, attrs = [], prefix = nil, uri = nil, ns = [])
57
+ @elements_processed += 1
58
+ end
59
+
60
+ def characters(string)
61
+ @text_nodes_processed += 1 unless string.strip.empty?
62
+ end
63
+
64
+ def end_element(name)
65
+ # Element processing complete
66
+ end
67
+
68
+ def end_element_namespace(name, prefix = nil, uri = nil)
69
+ # Element processing complete
70
+ end
71
+
72
+ def error(message)
73
+ # Handle parsing errors
74
+ end
75
+
76
+ def warning(message)
77
+ # Handle parsing warnings
78
+ end
79
+
80
+ def xmldecl(version, encoding, standalone)
81
+ # Handle XML declaration
82
+ end
83
+ end
84
+
85
+ protected
86
+
87
+ def library_require_name
88
+ 'nokogiri'
89
+ end
90
+
91
+ def detect_version
92
+ require 'nokogiri'
93
+ Nokogiri::VERSION
94
+ rescue LoadError
95
+ 'not available'
96
+ end
97
+
98
+ def supports_xpath?
99
+ true
100
+ end
101
+
102
+ def supports_namespaces?
103
+ true
104
+ end
105
+
106
+ def supports_validation?
107
+ true
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_xml_serializer'
4
+
5
+ module Serialbench
6
+ module Serializers
7
+ module Xml
8
+ class NokogiriSerializer < BaseXmlSerializer
9
+ def name
10
+ 'nokogiri'
11
+ end
12
+
13
+ def parse(xml_string)
14
+ require 'nokogiri'
15
+ Nokogiri::XML(xml_string)
16
+ end
17
+
18
+ def generate(data)
19
+ require 'nokogiri'
20
+ # If data is already a Nokogiri document, convert to string
21
+ if data.respond_to?(:to_xml)
22
+ data.to_xml(indent: 2)
23
+ else
24
+ # Create a simple XML structure from hash-like data
25
+ builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
26
+ build_xml_from_data(xml, data)
27
+ end
28
+ builder.to_xml
29
+ end
30
+ end
31
+
32
+ def parse_streaming(xml_string, &block)
33
+ require 'nokogiri'
34
+
35
+ handler = StreamingHandler.new(&block)
36
+ parser = Nokogiri::XML::SAX::Parser.new(handler)
37
+ parser.parse(xml_string)
38
+ handler.elements_processed
39
+ end
40
+
41
+ def supports_streaming?
42
+ true
43
+ end
44
+
45
+ def version
46
+ return 'unknown' unless available?
47
+
48
+ require 'nokogiri'
49
+ Nokogiri::VERSION
50
+ end
51
+
52
+ protected
53
+
54
+ def library_require_name
55
+ 'nokogiri'
56
+ end
57
+
58
+ private
59
+
60
+ def build_xml_from_data(xml, data, root_name = 'root')
61
+ case data
62
+ when Hash
63
+ xml.send(root_name) do
64
+ data.each do |key, value|
65
+ build_xml_from_data(xml, value, key)
66
+ end
67
+ end
68
+ when Array
69
+ data.each_with_index do |item, index|
70
+ build_xml_from_data(xml, item, "item_#{index}")
71
+ end
72
+ else
73
+ xml.send(root_name, data.to_s)
74
+ end
75
+ end
76
+
77
+ # SAX handler for streaming
78
+ class StreamingHandler
79
+ attr_reader :elements_processed
80
+
81
+ def initialize(&block)
82
+ require 'nokogiri'
83
+ @block = block
84
+ @elements_processed = 0
85
+ @current_element = nil
86
+ @element_stack = []
87
+ end
88
+
89
+ def start_element(name, attributes = [])
90
+ @elements_processed += 1
91
+ attrs = Hash[attributes]
92
+ @current_element = { name: name, attributes: attrs, children: [], text: '' }
93
+ @element_stack.push(@current_element)
94
+ end
95
+
96
+ def end_element(name)
97
+ element = @element_stack.pop
98
+ if @element_stack.empty?
99
+ @block&.call(element) if @block
100
+ else
101
+ @element_stack.last[:children] << element
102
+ end
103
+ end
104
+
105
+ def characters(string)
106
+ return if string.strip.empty?
107
+
108
+ @element_stack.last[:text] += string if @element_stack.any?
109
+ end
110
+
111
+ def cdata_block(string)
112
+ @element_stack.last[:text] += string if @element_stack.any?
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_parser'
4
+
5
+ module Serialbench
6
+ module Parsers
7
+ class OgaParser < BaseParser
8
+ def parse_dom(xml_string)
9
+ require 'oga'
10
+ Oga.parse_xml(xml_string)
11
+ end
12
+
13
+ def parse_sax(xml_string, handler = nil)
14
+ require 'oga'
15
+ handler ||= DefaultSaxHandler.new
16
+ Oga.sax_parse_xml(xml_string, handler)
17
+ handler
18
+ end
19
+
20
+ def generate_xml(document, options = {})
21
+ require 'oga'
22
+ document.to_xml
23
+ end
24
+
25
+ def supports_streaming?
26
+ true
27
+ end
28
+
29
+ protected
30
+
31
+ def library_require_name
32
+ 'oga'
33
+ end
34
+
35
+ def detect_version
36
+ require 'oga'
37
+ Oga::VERSION
38
+ rescue LoadError
39
+ 'not available'
40
+ end
41
+
42
+ def supports_xpath?
43
+ true
44
+ end
45
+
46
+ def supports_namespaces?
47
+ true
48
+ end
49
+
50
+ def supports_validation?
51
+ false
52
+ end
53
+
54
+ # Default SAX handler for Oga
55
+ class DefaultSaxHandler
56
+ attr_reader :elements_processed, :text_nodes_processed
57
+
58
+ def initialize
59
+ @elements_processed = 0
60
+ @text_nodes_processed = 0
61
+ end
62
+
63
+ def on_element(namespace, name, attributes = {})
64
+ @elements_processed += 1
65
+ end
66
+
67
+ def on_text(text)
68
+ @text_nodes_processed += 1 unless text.strip.empty?
69
+ end
70
+
71
+ def on_cdata(text)
72
+ @text_nodes_processed += 1 unless text.strip.empty?
73
+ end
74
+
75
+ def on_comment(text)
76
+ # Handle comments
77
+ end
78
+
79
+ def on_processing_instruction(name, text)
80
+ # Handle processing instructions
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_xml_serializer'
4
+
5
+ module Serialbench
6
+ module Serializers
7
+ module Xml
8
+ class OgaSerializer < BaseXmlSerializer
9
+ def name
10
+ 'oga'
11
+ end
12
+
13
+ def parse(xml_string)
14
+ require 'oga'
15
+ Oga.parse_xml(xml_string)
16
+ end
17
+
18
+ def generate(data)
19
+ require 'oga'
20
+ # If data is already an Oga document, convert to string
21
+ if data.respond_to?(:to_xml)
22
+ data.to_xml
23
+ else
24
+ # Create a simple XML structure from hash-like data
25
+ document = Oga::XML::Document.new
26
+ root = build_xml_from_data(data)
27
+ document.children << root
28
+ document.to_xml
29
+ end
30
+ end
31
+
32
+ def parse_streaming(xml_string, &block)
33
+ require 'oga'
34
+
35
+ handler = StreamingHandler.new(&block)
36
+ parser = Oga::XML::SaxParser.new(handler, xml_string)
37
+ parser.parse
38
+ handler.elements_processed
39
+ end
40
+
41
+ def supports_streaming?
42
+ true
43
+ end
44
+
45
+ def version
46
+ return 'unknown' unless available?
47
+
48
+ require 'oga'
49
+ Oga::VERSION
50
+ end
51
+
52
+ protected
53
+
54
+ def library_require_name
55
+ 'oga'
56
+ end
57
+
58
+ private
59
+
60
+ def build_xml_from_data(data, name = 'root')
61
+ require 'oga'
62
+
63
+ case data
64
+ when Hash
65
+ element = Oga::XML::Element.new(name: name)
66
+ data.each do |key, value|
67
+ child = build_xml_from_data(value, key.to_s)
68
+ element.children << child
69
+ end
70
+ element
71
+ when Array
72
+ element = Oga::XML::Element.new(name: name)
73
+ data.each_with_index do |item, index|
74
+ child = build_xml_from_data(item, "item_#{index}")
75
+ element.children << child
76
+ end
77
+ element
78
+ else
79
+ element = Oga::XML::Element.new(name: name)
80
+ text_node = Oga::XML::Text.new(text: data.to_s)
81
+ element.children << text_node
82
+ element
83
+ end
84
+ end
85
+
86
+ # SAX handler for streaming
87
+ class StreamingHandler
88
+ attr_reader :elements_processed
89
+
90
+ def initialize(&block)
91
+ @block = block
92
+ @elements_processed = 0
93
+ @current_element = nil
94
+ @element_stack = []
95
+ end
96
+
97
+ def on_element(namespace, name, attributes = {})
98
+ @elements_processed += 1
99
+ @current_element = { name: name, namespace: namespace, attributes: attributes, children: [], text: '' }
100
+ @element_stack.push(@current_element)
101
+ end
102
+
103
+ def on_text(text)
104
+ return if text.strip.empty?
105
+
106
+ @element_stack.last[:text] += text if @element_stack.any?
107
+ end
108
+
109
+ def on_cdata(text)
110
+ @element_stack.last[:text] += text if @element_stack.any?
111
+ end
112
+
113
+ def after_element(namespace, name)
114
+ element = @element_stack.pop
115
+ if @element_stack.empty?
116
+ @block&.call(element) if @block
117
+ else
118
+ @element_stack.last[:children] << element
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end