serialbench 0.9.2 → 0.11.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 99cdf688cd6a4bd4662e8f0182911ec87f6b2ecf184045c6514cee6959c09e2d
4
- data.tar.gz: 3e1ba2b330008ef51fe28c7fad6b0571bad06c491ca9c6a7e46634c558b20a23
3
+ metadata.gz: b2f980d28503e644be4c2f0807637b531c91640ad50af4eeb72c5cf41b26f3eb
4
+ data.tar.gz: 2e45051b18ab56d9a4fe2d569d3abc3206f151398dca094aa0a28fecd3ceb07f
5
5
  SHA512:
6
- metadata.gz: 30fc0f72e78b350c78b918a6366793be940a35ab6b826e3a74c08547d11b0251f90d8be9ec4822a7b1c197656805e6c7e2142c483b123981c7e7e5ba64c1e030
7
- data.tar.gz: a44b14c1638c2c450b89d8f8a032041cdfb14eb1a1ccb57eb9a6b88d248f2f18d1be127095babef559f938918048b89b1c7d950cf9fb9f64eabcbfd29787de61
6
+ metadata.gz: 35d305480351a282cf7cccc5c18874d1d5f053f2ee914452082c0abed7f1b92e66a63ba582a90ed898320d55c499a43b5207c3e4f12c529111223f7841e5d89b
7
+ data.tar.gz: 7441471432ba4b498ac91d7a4bdf4b21338c36789a231014f3f0bc49bb3858952f3627eeb2209442b9c266d4664e91515e677dda472003bb20614c25648c5010
@@ -29,12 +29,34 @@ CONTENT=$(base64 < "$RESULTS_FILE" | tr -d '\r\n')
29
29
 
30
30
  echo "Pushing ${TARGET_PATH} to serialbench/data..."
31
31
 
32
- # Try create; if file exists (422), get sha and update
33
- HTTP_CODE=$(curl -s -o /tmp/data-push-resp.json -w "%{http_code}" -X PUT \
34
- -H "Authorization: Bearer ${DATA_TOKEN}" \
35
- -H "Accept: application/vnd.github+json" \
36
- "https://api.github.com/repos/serialbench/data/contents/${TARGET_PATH}" \
37
- -d "{\"message\": \"${PLATFORM} ruby-${RUBY_VERSION} ${FMT}\", \"content\": \"${CONTENT}\", \"branch\": \"main\"}")
32
+ # Try create; 409 (branch race) and 422 (exists) both retry with fresh sha
33
+ put_file() {
34
+ local sha_arg=()
35
+ [ -n "${1:-}" ] && sha_arg=(-d "{\"message\": \"${PLATFORM} ruby-${RUBY_VERSION} ${FMT} (update)\", \"content\": \"${CONTENT}\", \"sha\": \"$1\", \"branch\": \"main\"}")
36
+ [ -n "${1:-}" ] || sha_arg=(-d "{\"message\": \"${PLATFORM} ruby-${RUBY_VERSION} ${FMT}\", \"content\": \"${CONTENT}\", \"branch\": \"main\"}")
37
+ curl -s -o /tmp/data-push-resp.json -w "%{http_code}" -X PUT \
38
+ -H "Authorization: Bearer ${DATA_TOKEN}" \
39
+ -H "Accept: application/vnd.github+json" \
40
+ "https://api.github.com/repos/serialbench/data/contents/${TARGET_PATH}" \
41
+ "${sha_arg[@]}"
42
+ }
43
+
44
+ HTTP_CODE=$(put_file "")
45
+ for TRY in 1 2 3 4 5; do
46
+ if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then break; fi
47
+ if [ "$HTTP_CODE" = "409" ] || [ "$HTTP_CODE" = "422" ]; then
48
+ SHA=$(curl -sf \
49
+ -H "Authorization: Bearer ${DATA_TOKEN}" \
50
+ "https://api.github.com/repos/serialbench/data/contents/${TARGET_PATH}" \
51
+ | grep -oE '"sha":\s*"[a-f0-9]+"' | head -1 | grep -oE '[a-f0-9]{40}')
52
+ [ -z "$SHA" ] && break
53
+ echo "attempt $TRY got $HTTP_CODE — refetching sha and retrying"
54
+ sleep $((TRY * 3))
55
+ HTTP_CODE=$(put_file "$SHA")
56
+ else
57
+ break
58
+ fi
59
+ done
38
60
 
39
61
  if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then
40
62
  echo "Pushed ${TARGET_PATH}"
@@ -116,6 +116,21 @@ jobs:
116
116
  - name: Install gems
117
117
  run: bundle install
118
118
 
119
+ - name: Fetch Saxon-HE (XSLT 2.0/3.0 comparison)
120
+ run: |
121
+ mkdir -p vendor
122
+ curl -sL -o vendor/saxonhe.jar "https://repo1.maven.org/maven2/net/sf/saxon/Saxon-HE/12.10/Saxon-HE-12.10.jar"
123
+ curl -sL -o vendor/xmlresolver.jar "https://repo1.maven.org/maven2/org/xmlresolver/xmlresolver/5.2.2/xmlresolver-5.2.2.jar"
124
+ echo "SAXON_JAR=$PWD/vendor/saxonhe.jar" >> "$GITHUB_ENV"
125
+ echo "XMLRESOLVER_JAR=$PWD/vendor/xmlresolver.jar" >> "$GITHUB_ENV"
126
+ shell: bash
127
+
128
+ - name: Load canonical fixtures
129
+ run: |
130
+ git clone --depth 1 https://github.com/serialbench/fixtures.git test_data
131
+ (cd test_data && sha256sum -c SHA256SUMS) || (cd test_data && shasum -a 256 -c SHA256SUMS)
132
+ shell: bash
133
+
119
134
  - name: Point teptris at its vendored DLL (Windows)
120
135
  if: startsWith(matrix.platform, 'windows-')
121
136
  shell: bash
@@ -249,6 +264,41 @@ jobs:
249
264
  run: .github/scripts/push-to-data.sh toml ${{ matrix.ruby-version }} ${{ matrix.platform }}
250
265
  shell: bash
251
266
 
267
+ - name: Benchmark html
268
+ continue-on-error: true
269
+ shell: bash
270
+ env:
271
+ GITHUB_RUNNER_PLATFORM: ${{ matrix.platform }}
272
+ DATA_REPO_TOKEN: ${{ secrets.DATA_REPO_TOKEN }}
273
+ DATA_RUN_DATE: ${{ needs.setup.outputs.run-date }}
274
+ run: |
275
+ if [ "${{ github.run_attempt }}" -gt 1 ] && .github/scripts/format-status.sh html ${{ matrix.ruby-version }} ${{ matrix.platform }}; then
276
+ echo "html already in the data repo — skipping (gap-filling rerun)"
277
+ exit 0
278
+ fi
279
+ bundle exec serialbench environment execute \
280
+ config/environments/ci-ruby-${{ matrix.ruby-version }}.yml \
281
+ config/benchmarks/full-html.yml \
282
+ "results/runs/ci-ruby-${{ matrix.ruby-version }}-${{ matrix.platform }}/html"
283
+
284
+ - name: Upload html
285
+ if: ${{ !cancelled() }}
286
+ uses: actions/upload-artifact@v4
287
+ with:
288
+ name: benchmark-results-${{ matrix.platform }}-ruby-${{ matrix.ruby-version }}-html
289
+ path: results/runs/ci-ruby-${{ matrix.ruby-version }}-${{ matrix.platform }}/html
290
+ retention-days: 30
291
+ if-no-files-found: warn
292
+
293
+ - name: Push html to data repo
294
+ if: ${{ !cancelled() }}
295
+ continue-on-error: true
296
+ env:
297
+ DATA_REPO_TOKEN: ${{ secrets.DATA_REPO_TOKEN }}
298
+ DATA_RUN_DATE: ${{ needs.setup.outputs.run-date }}
299
+ run: .github/scripts/push-to-data.sh html ${{ matrix.ruby-version }} ${{ matrix.platform }}
300
+ shell: bash
301
+
252
302
  # Each format is an independent checkpoint with its own upload.
253
303
  # A failure in one format doesn't prevent the others from uploading.
254
304
  - name: Benchmark xml
@@ -296,7 +346,7 @@ jobs:
296
346
  DATA_RUN_DATE: ${{ needs.setup.outputs.run-date }}
297
347
  run: |
298
348
  MISSING=""
299
- for FMT in json yaml toml xml; do
349
+ for FMT in json yaml toml html xml; do
300
350
  if .github/scripts/format-status.sh "$FMT" "${{ matrix.ruby-version }}" "${{ matrix.platform }}"; then
301
351
  echo "$FMT: in data repo"
302
352
  else
data/.gitignore CHANGED
@@ -30,3 +30,4 @@ results/
30
30
 
31
31
  # Ruby version manager files
32
32
  .ruby-version
33
+ test_data/
data/README.adoc CHANGED
@@ -1,9 +1,9 @@
1
1
  = Serialbench: Ruby serialization library performance benchmarker
2
2
 
3
3
  image:https://img.shields.io/gem/v/serialbench.svg["Gem Version", link="https://rubygems.org/gems/serialbench"]
4
- image:https://github.com/metanorma/serialbench/actions/workflows/ci.yml/badge.svg["Build Status", link="https://github.com/metanorma/serialbench/actions/workflows/ci.yml"]
5
- image:https://github.com/metanorma/serialbench/actions/workflows/benchmark.yml/badge.svg["Benchmark Status", link="https://github.com/metanorma/serialbench/actions/workflows/benchmark.yml"]
6
- image:https://img.shields.io/github/issues-pr-raw/metanorma/serialbench.svg["Pull Requests", link="https://github.com/metanorma/serialbench/pulls"]
4
+ image:https://github.com/serialbench/serialbench/actions/workflows/ci.yml/badge.svg["Build Status", link="https://github.com/serialbench/serialbench/actions/workflows/ci.yml"]
5
+ image:https://github.com/serialbench/serialbench/actions/workflows/benchmark.yml/badge.svg["Benchmark Status", link="https://github.com/serialbench/serialbench/actions/workflows/benchmark.yml"]
6
+ image:https://img.shields.io/github/issues-pr-raw/serialbench/serialbench.svg["Pull Requests", link="https://github.com/serialbench/serialbench/pulls"]
7
7
 
8
8
  == Overview
9
9
 
@@ -0,0 +1,26 @@
1
+ # Per-format split of full.yml: each format runs in its own process so
2
+ # memory profiling of large documents cannot compound across formats
3
+ # (windows runners OOMed with all four formats in one process).
4
+ name: full-html
5
+
6
+ data_sizes:
7
+ - small
8
+ - medium
9
+ - large
10
+
11
+ formats:
12
+ - html
13
+
14
+ iterations:
15
+ small: 10
16
+ medium: 3
17
+ large: 1
18
+
19
+ operations:
20
+ - parsing
21
+ - generation
22
+ - streaming
23
+ - xpath
24
+ - memory
25
+
26
+ warmup: 2
@@ -21,6 +21,10 @@ operations:
21
21
  - generation
22
22
  - streaming
23
23
  - xpath
24
+ - xquery
25
+ - xslt
26
+ - xslt30
27
+ - validation
24
28
  - memory
25
29
 
26
30
  warmup: 2
@@ -26,15 +26,170 @@ module Serialbench
26
26
  end
27
27
 
28
28
  # Each operation maps to a lambda. Adding an operation = one entry.
29
+ _DEFAULT_RNG_SCHEMA = <<'RNG'.freeze
30
+ <?xml version="1.0" encoding="UTF-8"?>
31
+ <grammar xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
32
+ <start>
33
+ <choice>
34
+ <ref name="config"/>
35
+ <ref name="users"/>
36
+ <ref name="dataset"/>
37
+ </choice>
38
+ </start>
39
+
40
+ <define name="config">
41
+ <element name="config">
42
+ <element name="database">
43
+ <element name="host"><text/></element>
44
+ <element name="port"><data type="integer"/></element>
45
+ <element name="name"><text/></element>
46
+ <element name="user"><text/></element>
47
+ <element name="password"><text/></element>
48
+ </element>
49
+ <element name="cache">
50
+ <element name="enabled"><data type="boolean"/></element>
51
+ <element name="ttl"><data type="integer"/></element>
52
+ </element>
53
+ </element>
54
+ </define>
55
+
56
+ <define name="users">
57
+ <element name="users">
58
+ <oneOrMore>
59
+ <ref name="user"/>
60
+ </oneOrMore>
61
+ </element>
62
+ </define>
63
+
64
+ <define name="user">
65
+ <element name="user">
66
+ <attribute name="id"><data type="integer"/></attribute>
67
+ <element name="name"><text/></element>
68
+ <element name="email"><text/></element>
69
+ <element name="created_at"><text/></element>
70
+ <element name="profile">
71
+ <element name="age"><data type="integer"/></element>
72
+ <element name="city"><text/></element>
73
+ <element name="preferences">
74
+ <element name="theme"><text/></element>
75
+ <element name="notifications"><data type="boolean"/></element>
76
+ </element>
77
+ </element>
78
+ </element>
79
+ </define>
80
+
81
+ <define name="dataset">
82
+ <element name="dataset">
83
+ <element name="header">
84
+ <element name="created"><text/></element>
85
+ <element name="count"><data type="integer"/></element>
86
+ <element name="format"><text/></element>
87
+ </element>
88
+ <element name="records">
89
+ <oneOrMore>
90
+ <ref name="record"/>
91
+ </oneOrMore>
92
+ </element>
93
+ </element>
94
+ </define>
95
+
96
+ <define name="record">
97
+ <element name="record">
98
+ <attribute name="id"><data type="integer"/></attribute>
99
+ <element name="timestamp"><text/></element>
100
+ <element name="data">
101
+ <element name="field1"><text/></element>
102
+ <element name="field2"><data type="integer"/></element>
103
+ <element name="field3"><text/></element>
104
+ <element name="nested">
105
+ <oneOrMore><element name="item"><text/></element></oneOrMore>
106
+ </element>
107
+ </element>
108
+ <element name="metadata">
109
+ <element name="source"><text/></element>
110
+ <element name="version"><text/></element>
111
+ <element name="checksum"><text/></element>
112
+ </element>
113
+ </element>
114
+ </define>
115
+ </grammar>
116
+ RNG
117
+
118
+ _DEFAULT_XSLT_STYLESHEET = <<'XSL'.freeze
119
+ <?xml version="1.0" encoding="UTF-8"?>
120
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
121
+ <xsl:output method="xml" indent="yes"/>
122
+ <xsl:template match="/">
123
+ <report>
124
+ <xsl:apply-templates select="//user | //record | //database | //cache"/>
125
+ <total>
126
+ <xsl:value-of select="count(//user) + count(//record)"/>
127
+ </total>
128
+ </report>
129
+ </xsl:template>
130
+ <xsl:template match="user">
131
+ <row kind="user" id="{@id}" label="{name}"/>
132
+ </xsl:template>
133
+ <xsl:template match="record">
134
+ <row kind="record" id="{@id}" label="{data/field1}"/>
135
+ </xsl:template>
136
+ <xsl:template match="database">
137
+ <row kind="database" label="{name}"/>
138
+ </xsl:template>
139
+ <xsl:template match="cache">
140
+ <row kind="cache" label="{ttl}"/>
141
+ </xsl:template>
142
+ </xsl:stylesheet>
143
+ XSL
144
+
145
+ _DEFAULT_XSLT30_STYLESHEET = <<'XSL30'.freeze
146
+ <?xml version="1.0" encoding="UTF-8"?>
147
+ <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
148
+ xmlns:xs="http://www.w3.org/2001/XMLSchema"
149
+ exclude-result-prefixes="xs"
150
+ version="3.0">
151
+ <xsl:output method="xml" indent="yes"/>
152
+ <xsl:template match="/">
153
+ <report30>
154
+ <xsl:iterate select="//user | //record | //database | //cache">
155
+ <xsl:param name="labels" as="xs:string*" select="()"/>
156
+ <xsl:on-completion>
157
+ <joined><xsl:value-of select="string-join($labels, ', ')"/></joined>
158
+ <count><xsl:value-of select="count($labels)"/></count>
159
+ </xsl:on-completion>
160
+ <xsl:variable name="label" select="string((name, data/field1, ttl)[1])"/>
161
+ <xsl:if test="$label != ''">
162
+ <row kind="{local-name()}" label="{upper-case($label)}"/>
163
+ </xsl:if>
164
+ <xsl:next-iteration>
165
+ <xsl:with-param name="labels" select="if ($label != '') then ($labels, $label) else $labels"/>
166
+ </xsl:next-iteration>
167
+ </xsl:iterate>
168
+ </report30>
169
+ </xsl:template>
170
+ </xsl:stylesheet>
171
+ XSL30
172
+
173
+ XPATH_QUERIES = ['//book', "//book[@id='101']", '//book[price > 30]/title'].freeze
174
+ XQUERY_EXPRESSIONS = ['count(//user | //record)', "//record[@id='101']/data/field1", '//user[profile/age > 40]/name'].freeze
175
+ RNG_SCHEMA = File.expand_path('test_data/schema.rng', Dir.pwd).then { |p| File.exist?(p) ? File.read(p) : _DEFAULT_RNG_SCHEMA }
176
+ XSLT_STYLESHEET = File.expand_path('test_data/transform.xsl', Dir.pwd).then { |p| File.exist?(p) ? File.read(p) : _DEFAULT_XSLT_STYLESHEET }
177
+ XSLT30_STYLESHEET = File.expand_path('test_data/transform30.xsl', Dir.pwd).then { |p| File.exist?(p) ? File.read(p) : _DEFAULT_XSLT30_STYLESHEET }
178
+
29
179
  OPERATIONS = {
30
180
  'parsing' => ->(s, data) { s.parse(data) },
31
181
  'generation' => ->(s, data) { s.generate(s.parse(data)) },
32
182
  'xpath' => lambda { |s, data|
33
183
  doc = s.parse(data)
34
- s.xpath_query(doc, '//book')
35
- s.xpath_query(doc, "//book[@id='101']")
36
- s.xpath_query(doc, '//book[price > 30]/title')
184
+ XPATH_QUERIES.each { |q| s.xpath_query(doc, q) }
185
+ },
186
+ 'xquery' => lambda { |s, data|
187
+ doc = s.parse(data)
188
+ XQUERY_EXPRESSIONS.each { |x| s.xquery_eval(doc, x) }
37
189
  },
190
+ 'xslt' => ->(s, data) { s.xslt_apply(data, XSLT_STYLESHEET) },
191
+ 'xslt30' => ->(s, data) { s.xslt_apply(data, XSLT30_STYLESHEET) },
192
+ 'validation' => ->(s, data) { s.validate(s.parse(data), RNG_SCHEMA) },
38
193
  'streaming' => ->(s, data) { s.stream_parse(data) { |_event, _data| } },
39
194
  }.freeze
40
195
 
@@ -155,12 +310,22 @@ module Serialbench
155
310
  serializers = Serializers.for_format(format)
156
311
 
157
312
  case type_name
313
+ when 'parsing', 'memory'
314
+ serializers.select { |s| s.supports?(:parse) }
158
315
  when 'generation'
159
316
  serializers.select { |s| s.supports?(:generate) }
160
317
  when 'streaming'
161
318
  serializers.select { |s| s.supports?(:sax) || s.supports?(:streaming) }
162
319
  when 'xpath'
163
320
  serializers.select { |s| s.supports?(:xpath) }
321
+ when 'xquery'
322
+ serializers.select { |s| s.supports?(:xquery) }
323
+ when 'xslt'
324
+ serializers.select { |s| s.supports?(:xslt) }
325
+ when 'validation'
326
+ serializers.select { |s| s.supports?(:validation) }
327
+ when 'xslt30'
328
+ serializers.select { |s| s.supports?(:xslt30) }
164
329
  else
165
330
  serializers
166
331
  end
@@ -48,10 +48,10 @@ module Serialbench
48
48
  attribute :name, :string
49
49
  attribute :description, :string
50
50
  attribute :data_sizes, :string, collection: true, values: %w[small medium large]
51
- attribute :formats, :string, collection: true, values: %w[xml json yaml toml]
51
+ attribute :formats, :string, collection: true, values: %w[xml html json yaml toml]
52
52
  attribute :iterations, BenchmarkIteration
53
53
  attribute :warmup, :integer, default: -> { 1 }
54
- attribute :operations, :string, collection: true, values: %w[parsing generation xpath streaming memory]
54
+ attribute :operations, :string, collection: true, values: %w[parsing generation xpath xquery xslt xslt30 validation streaming memory]
55
55
 
56
56
  key_value do
57
57
  map 'name', to: :name
@@ -5,7 +5,7 @@ require 'lutaml/model'
5
5
  module Serialbench
6
6
  module Models
7
7
  class SerializerInformation < Lutaml::Model::Serializable
8
- attribute :format, :string, values: %w[xml json yaml toml]
8
+ attribute :format, :string, values: %w[xml html json yaml toml]
9
9
  attribute :name, :string
10
10
  attribute :version, :string
11
11
  attribute :features, :hash
@@ -20,7 +20,7 @@ module Serialbench
20
20
 
21
21
  class AdapterPerformance < Lutaml::Model::Serializable
22
22
  attribute :adapter, :string
23
- attribute :format, :string, values: %w[xml json yaml toml]
23
+ attribute :format, :string, values: %w[xml html json yaml toml]
24
24
  attribute :data_size, :string, values: %w[small medium large]
25
25
 
26
26
  key_value do
@@ -71,6 +71,10 @@ module Serialbench
71
71
  attribute :memory, MemoryPerformance, collection: true
72
72
  attribute :streaming, IterationPerformance, collection: true
73
73
  attribute :xpath, IterationPerformance, collection: true
74
+ attribute :xquery, IterationPerformance, collection: true
75
+ attribute :xslt, IterationPerformance, collection: true
76
+ attribute :xslt30, IterationPerformance, collection: true
77
+ attribute :validation, IterationPerformance, collection: true
74
78
 
75
79
  key_value do
76
80
  map 'serializers', to: :serializers
@@ -79,6 +83,10 @@ module Serialbench
79
83
  map 'memory', to: :memory
80
84
  map 'streaming', to: :streaming
81
85
  map 'xpath', to: :xpath
86
+ map 'xquery', to: :xquery
87
+ map 'xslt', to: :xslt
88
+ map 'xslt30', to: :xslt30
89
+ map 'validation', to: :validation
82
90
  end
83
91
  end
84
92
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_serializer'
4
+
5
+ module Serialbench
6
+ module Serializers
7
+ module Html
8
+ class BaseHtmlSerializer < BaseSerializer
9
+ def self.format
10
+ :html
11
+ end
12
+
13
+ def capabilities
14
+ super | Set.new(%i[dom parse generate])
15
+ end
16
+
17
+ def features
18
+ {
19
+ html5: supports?(:html5),
20
+ xpath: supports?(:xpath)
21
+ }
22
+ end
23
+
24
+ def generate(data)
25
+ data.is_a?(Hash) ? from_hash(data) : serialize_document(data)
26
+ end
27
+
28
+ def from_hash(hash)
29
+ rows = hash.flat_map do |key, value|
30
+ case value
31
+ when Hash then value.map { |k, v| "<tr><td>#{k}</td><td>#{v}</td></tr>" }
32
+ else ["<tr><td>#{key}</td><td>#{value}</td></tr>"]
33
+ end
34
+ end
35
+ serialize_document(parse("<html><body><table>#{rows.join}</table></body></html>"))
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_html_serializer'
4
+
5
+ module Serialbench
6
+ module Serializers
7
+ module Html
8
+ class LeptrisSerializer < BaseHtmlSerializer
9
+ def name
10
+ 'leptris'
11
+ end
12
+
13
+ def capabilities
14
+ super | Set.new(%i[xpath html5])
15
+ end
16
+
17
+ def parse(html_string)
18
+ require 'leptris'
19
+ require 'leptris/xml'
20
+ Leptris::XML.parse_html(html_string)
21
+ end
22
+
23
+ def xpath_query(document, expression)
24
+ document.xpath(expression).size
25
+ end
26
+
27
+ def serialize_document(document)
28
+ document.to_xml
29
+ end
30
+
31
+ def available?
32
+ return @available if defined?(@available)
33
+
34
+ @available = begin
35
+ require 'leptris'
36
+ require 'leptris/xml'
37
+ Leptris::XML.parse_html('<p>probe</p>')
38
+ true
39
+ rescue StandardError, LoadError => e
40
+ warn "#{name} unavailable: #{e.class}: #{e.message}"
41
+ false
42
+ end
43
+ end
44
+
45
+ def version
46
+ return 'unknown' unless available?
47
+
48
+ require 'leptris'
49
+ Leptris::VERSION
50
+ end
51
+
52
+ def library_require_name
53
+ 'leptris'
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_html_serializer'
4
+
5
+ module Serialbench
6
+ module Serializers
7
+ module Html
8
+ class NokogiriSerializer < BaseHtmlSerializer
9
+ def name
10
+ 'nokogiri'
11
+ end
12
+
13
+ def capabilities
14
+ super | Set.new(%i[xpath html5])
15
+ end
16
+
17
+ def parse(html_string)
18
+ require 'nokogiri'
19
+ Nokogiri::HTML5(html_string)
20
+ end
21
+
22
+ def xpath_query(document, expression)
23
+ document.xpath(expression).size
24
+ end
25
+
26
+ def serialize_document(document)
27
+ document.to_html
28
+ end
29
+
30
+ def available?
31
+ return @available if defined?(@available)
32
+
33
+ @available = begin
34
+ require 'nokogiri'
35
+ Nokogiri::HTML5('<p>probe</p>')
36
+ true
37
+ rescue StandardError, LoadError => e
38
+ warn "#{name} unavailable: #{e.class}: #{e.message}"
39
+ false
40
+ end
41
+ end
42
+
43
+ def version
44
+ return 'unknown' unless available?
45
+
46
+ require 'nokogiri'
47
+ Nokogiri::VERSION
48
+ end
49
+
50
+ def library_require_name
51
+ 'nokogiri'
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_html_serializer'
4
+
5
+ module Serialbench
6
+ module Serializers
7
+ module Html
8
+ class OgaSerializer < BaseHtmlSerializer
9
+ def name
10
+ 'oga'
11
+ end
12
+
13
+ def parse(html_string)
14
+ require 'oga'
15
+ Oga.parse_html(html_string)
16
+ end
17
+
18
+ def serialize_document(document)
19
+ document.to_xml
20
+ end
21
+
22
+ def available?
23
+ return @available if defined?(@available)
24
+
25
+ @available = begin
26
+ require 'oga'
27
+ Oga.parse_html('<p>probe</p>')
28
+ true
29
+ rescue StandardError, LoadError => e
30
+ warn "#{name} unavailable: #{e.class}: #{e.message}"
31
+ false
32
+ end
33
+ end
34
+
35
+ def version
36
+ return 'unknown' unless available?
37
+
38
+ require 'oga'
39
+ Oga::VERSION
40
+ end
41
+
42
+ def library_require_name
43
+ 'oga'
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -27,6 +27,10 @@ module Serialbench
27
27
  super | Set.new(%i[namespaces])
28
28
  end
29
29
 
30
+ def xslt_apply(source_xml, stylesheet)
31
+ xslt_transform(parse(source_xml), stylesheet)
32
+ end
33
+
30
34
  # XML-specific features derive from the capability set
31
35
  def features
32
36
  {
@@ -37,7 +37,26 @@ module Serialbench
37
37
  end
38
38
 
39
39
  def capabilities
40
- super | Set.new(%i[xpath sax stax])
40
+ super | Set.new(%i[xpath xquery xslt xslt30 validation sax stax])
41
+ end
42
+
43
+ def xquery_eval(document, expression)
44
+ require 'leptris'
45
+ require 'leptris/xml'
46
+ result = Leptris::XML::XQuery.parse(expression).eval(document)
47
+ result.respond_to?(:length) ? result.length : 1
48
+ end
49
+
50
+ def xslt_transform(document, stylesheet)
51
+ require 'leptris'
52
+ require 'leptris/xml'
53
+ Leptris::XML::XSLT.parse(stylesheet).apply_to(document).serialize
54
+ end
55
+
56
+ def validate(document, schema)
57
+ require 'leptris'
58
+ require 'leptris/xml'
59
+ Leptris::XML::RelaxNG.parse(schema).valid?(document)
41
60
  end
42
61
 
43
62
  def xpath_query(document, expression)
@@ -39,7 +39,17 @@ module Serialbench
39
39
  end
40
40
 
41
41
  def capabilities
42
- super | Set.new(%i[xpath sax stax])
42
+ super | Set.new(%i[xpath xslt validation sax stax])
43
+ end
44
+
45
+ def xslt_transform(document, stylesheet)
46
+ require 'nokogiri'
47
+ Nokogiri::XSLT(stylesheet).transform(document).to_s
48
+ end
49
+
50
+ def validate(document, schema)
51
+ require 'nokogiri'
52
+ Nokogiri::XML::RelaxNG(schema).valid?(document)
43
53
  end
44
54
 
45
55
  def xpath_query(document, expression)
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_xml_serializer'
4
+ require 'open3'
5
+ require 'tmpdir'
6
+
7
+ module Serialbench
8
+ module Serializers
9
+ module Xml
10
+ # Saxon-HE via its Java CLI: the reference XSLT 2.0/3.0 engine.
11
+ # Cold-process semantics: every iteration is a full JVM + compile +
12
+ # transform, so ips is comparable to leptris only end-to-end, never
13
+ # per-transform — features record cold_process: true.
14
+ class SaxonSerializer < BaseXmlSerializer
15
+ def name
16
+ 'saxon-he'
17
+ end
18
+
19
+ def capabilities
20
+ Set.new(%i[xslt xslt30])
21
+ end
22
+
23
+ def features
24
+ { 'xslt_version' => '3.0', 'cold_process' => true }
25
+ end
26
+
27
+ def classpath
28
+ jars = [ENV['SAXON_JAR'], ENV['XMLRESOLVER_JAR']].compact
29
+ jars.empty? ? nil : jars.join(File::PATH_SEPARATOR)
30
+ end
31
+
32
+ def xslt_apply(source_xml, stylesheet)
33
+ Dir.mktmpdir('saxonbench') do |dir|
34
+ src = File.join(dir, 's.xml')
35
+ xsl = File.join(dir, 's.xsl')
36
+ File.write(src, source_xml)
37
+ File.write(xsl, stylesheet)
38
+ out, _err, _st = Open3.capture3('java', '-cp', classpath, 'net.sf.saxon.Transform',
39
+ "-xsl:#{xsl}", "-s:#{src}")
40
+ raise Error, "saxon transform failed: #{out}#{_err}"[0, 200] unless _st.success?
41
+
42
+ out
43
+ end
44
+ end
45
+
46
+ def available?
47
+ return @available if defined?(@available)
48
+
49
+ @available = begin
50
+ cp = classpath
51
+ raise LoadError, 'SAXON_JAR / XMLRESOLVER_JAR not set' unless cp
52
+
53
+ out, _err, st = Open3.capture3('java', '-cp', cp, 'net.sf.saxon.Version')
54
+ raise LoadError, 'java failed' unless st.success?
55
+
56
+ true
57
+ rescue StandardError, LoadError => e
58
+ warn "#{name} unavailable: #{e.class}: #{e.message}"
59
+ false
60
+ end
61
+ end
62
+
63
+ def version
64
+ return 'unknown' unless available?
65
+
66
+ out, = Open3.capture3('java', '-cp', classpath, 'net.sf.saxon.Version')
67
+ out[/Saxon-HE (\S+)/, 1] || out.strip[0, 40]
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -11,6 +11,13 @@ require_relative 'serializers/xml/nokogiri_serializer'
11
11
  require_relative 'serializers/xml/oga_serializer'
12
12
  require_relative 'serializers/xml/libxml_serializer'
13
13
  require_relative 'serializers/xml/leptris_serializer'
14
+ require_relative 'serializers/xml/saxon_serializer'
15
+
16
+ # HTML Serializers
17
+ require_relative 'serializers/html/base_html_serializer'
18
+ require_relative 'serializers/html/nokogiri_serializer'
19
+ require_relative 'serializers/html/oga_serializer'
20
+ require_relative 'serializers/html/leptris_serializer'
14
21
 
15
22
  # JSON Serializers
16
23
  require_relative 'serializers/json/base_json_serializer'
@@ -43,7 +50,8 @@ module Serialbench
43
50
  Xml::NokogiriSerializer,
44
51
  Xml::OgaSerializer,
45
52
  Xml::LibxmlSerializer,
46
- Xml::LeptrisSerializer
53
+ Xml::LeptrisSerializer,
54
+ Xml::SaxonSerializer
47
55
  ],
48
56
  json: [
49
57
  Json::JsonSerializer,
@@ -52,6 +60,11 @@ module Serialbench
52
60
  Json::YajlSerializer,
53
61
  Json::YeptrisSerializer
54
62
  ],
63
+ html: [
64
+ Html::NokogiriSerializer,
65
+ Html::OgaSerializer,
66
+ Html::LeptrisSerializer
67
+ ],
55
68
  yaml: [
56
69
  Yaml::PsychSerializer,
57
70
  Yaml::SyckSerializer,
@@ -38,8 +38,11 @@ module Serialbench
38
38
  when 'medium.yaml' then generate_medium_yaml
39
39
  when 'large.yaml' then generate_large_yaml
40
40
  when 'small.toml' then generate_small_toml
41
+ when 'small.html' then generate_small_html
41
42
  when 'medium.toml' then generate_medium_toml
43
+ when 'medium.html' then generate_medium_html
42
44
  when 'large.toml' then generate_large_toml
45
+ when 'large.html' then generate_large_html
43
46
  else raise ArgumentError, "no test data generator for #{key}"
44
47
  end
45
48
  end
@@ -205,6 +208,50 @@ module Serialbench
205
208
  XML
206
209
  end
207
210
 
211
+ # HTML test data generators (same dataset as XML, as tables)
212
+ def generate_small_html
213
+ data = small_test_data_structure
214
+ rows = data[:config].flat_map do |_section, entries|
215
+ entries.flat_map do |_k, v|
216
+ case v
217
+ when Hash then v.map { |k2, v2| "<tr><td>#{k2}</td><td>#{v2}</td></tr>" }
218
+ else []
219
+ end
220
+ end
221
+ end
222
+ html_page('config', rows)
223
+ end
224
+
225
+ def generate_medium_html
226
+ data = medium_test_data_structure
227
+ rows = data[:users].map do |u|
228
+ "<tr><td>#{u[:id]}</td><td>#{u[:name][0, 30]}</td></tr>"
229
+ end
230
+ html_page('users', rows)
231
+ end
232
+
233
+ def generate_large_html
234
+ data = large_test_data_structure
235
+ rows = data[:dataset][:records].map do |r|
236
+ "<tr><td>#{r[:id]}</td><td>#{r[:data][:field1][0, 30]}</td></tr>"
237
+ end
238
+ html_page('dataset', rows)
239
+ end
240
+
241
+ def html_page(title, rows)
242
+ <<~HTML
243
+ <!DOCTYPE html>
244
+ <html>
245
+ <head><meta charset="utf-8"><title>serialbench #{title}</title></head>
246
+ <body>
247
+ <table>
248
+ #{rows.join("\n")}
249
+ </table>
250
+ </body>
251
+ </html>
252
+ HTML
253
+ end
254
+
208
255
  # JSON test data generators
209
256
  def generate_small_json
210
257
  JSON.generate(small_test_data_structure)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Serialbench
4
- VERSION = '0.9.2'
4
+ VERSION = '0.11.0'
5
5
  end
data/serialbench.gemspec CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
14
14
 
15
15
  spec.summary = 'Comprehensive serialization benchmarking tool for Ruby'
16
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/serialbench/serialbench'
17
+ spec.homepage = 'https://github.com/serialbench/serialbench-ruby'
18
18
  spec.license = 'BSD-2-Clause'
19
19
  spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
20
20
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: serialbench
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
@@ -270,6 +270,7 @@ files:
270
270
  - Gemfile
271
271
  - README.adoc
272
272
  - Rakefile
273
+ - config/benchmarks/full-html.yml
273
274
  - config/benchmarks/full-json.yml
274
275
  - config/benchmarks/full-toml.yml
275
276
  - config/benchmarks/full-xml.yml
@@ -323,6 +324,10 @@ files:
323
324
  - lib/serialbench/runners/local_runner.rb
324
325
  - lib/serialbench/serializers.rb
325
326
  - lib/serialbench/serializers/base_serializer.rb
327
+ - lib/serialbench/serializers/html/base_html_serializer.rb
328
+ - lib/serialbench/serializers/html/leptris_serializer.rb
329
+ - lib/serialbench/serializers/html/nokogiri_serializer.rb
330
+ - lib/serialbench/serializers/html/oga_serializer.rb
326
331
  - lib/serialbench/serializers/json/base_json_serializer.rb
327
332
  - lib/serialbench/serializers/json/json_serializer.rb
328
333
  - lib/serialbench/serializers/json/oj_serializer.rb
@@ -341,6 +346,7 @@ files:
341
346
  - lib/serialbench/serializers/xml/oga_serializer.rb
342
347
  - lib/serialbench/serializers/xml/ox_serializer.rb
343
348
  - lib/serialbench/serializers/xml/rexml_serializer.rb
349
+ - lib/serialbench/serializers/xml/saxon_serializer.rb
344
350
  - lib/serialbench/serializers/yaml/base_yaml_serializer.rb
345
351
  - lib/serialbench/serializers/yaml/psych_serializer.rb
346
352
  - lib/serialbench/serializers/yaml/syck_serializer.rb
@@ -349,13 +355,13 @@ files:
349
355
  - lib/serialbench/version.rb
350
356
  - lib/serialbench/yaml_validator.rb
351
357
  - serialbench.gemspec
352
- homepage: https://github.com/serialbench/serialbench
358
+ homepage: https://github.com/serialbench/serialbench-ruby
353
359
  licenses:
354
360
  - BSD-2-Clause
355
361
  metadata:
356
- homepage_uri: https://github.com/serialbench/serialbench
357
- source_code_uri: https://github.com/serialbench/serialbench
358
- bug_tracker_uri: https://github.com/serialbench/serialbench/issues
362
+ homepage_uri: https://github.com/serialbench/serialbench-ruby
363
+ source_code_uri: https://github.com/serialbench/serialbench-ruby
364
+ bug_tracker_uri: https://github.com/serialbench/serialbench-ruby/issues
359
365
  rdoc_options: []
360
366
  require_paths:
361
367
  - lib