serialbench 0.9.2 → 0.10.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 +4 -4
- data/.github/scripts/push-to-data.sh +28 -6
- data/.github/workflows/benchmark.yml +42 -1
- data/.gitignore +1 -0
- data/README.adoc +3 -3
- data/config/benchmarks/full-html.yml +26 -0
- data/config/benchmarks/full-xml.yml +3 -0
- data/lib/serialbench/benchmark_runner.rb +134 -3
- data/lib/serialbench/models/benchmark_config.rb +2 -2
- data/lib/serialbench/models/benchmark_result.rb +8 -2
- data/lib/serialbench/serializers/html/base_html_serializer.rb +40 -0
- data/lib/serialbench/serializers/html/leptris_serializer.rb +58 -0
- data/lib/serialbench/serializers/html/nokogiri_serializer.rb +56 -0
- data/lib/serialbench/serializers/html/oga_serializer.rb +48 -0
- data/lib/serialbench/serializers/xml/leptris_serializer.rb +20 -1
- data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +11 -1
- data/lib/serialbench/serializers.rb +11 -0
- data/lib/serialbench/test_data.rb +47 -0
- data/lib/serialbench/version.rb +1 -1
- data/serialbench.gemspec +1 -1
- metadata +10 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fb37afdfb65ed1d7226788c226fad30d1779b39578ad762bf097783fea638b08
|
|
4
|
+
data.tar.gz: 8d7a5f9ae73d6b2cd74b84d9861e4fbc9934c371960cc578d3bd0cef2798bc44
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a426f90212feb0b50c4a5b488f82bac91bbdfe30208cefc66390baa864ff217b92d551c38504d666dee1aa6f96b389cbdda65a35b91fac0cb34909ae5a280b9
|
|
7
|
+
data.tar.gz: 1202d30b4bfc472ea3a30f7b66f5eb0ac2874933411031b0add26ceb3f69dd1fd0d469df71a39665cfc8a53fb84dabf29c7f51811a6e121247d486ea2e235be8
|
|
@@ -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;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
"
|
|
37
|
-
-
|
|
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,12 @@ jobs:
|
|
|
116
116
|
- name: Install gems
|
|
117
117
|
run: bundle install
|
|
118
118
|
|
|
119
|
+
- name: Load canonical fixtures
|
|
120
|
+
run: |
|
|
121
|
+
git clone --depth 1 https://github.com/serialbench/fixtures.git test_data
|
|
122
|
+
(cd test_data && sha256sum -c SHA256SUMS) || (cd test_data && shasum -a 256 -c SHA256SUMS)
|
|
123
|
+
shell: bash
|
|
124
|
+
|
|
119
125
|
- name: Point teptris at its vendored DLL (Windows)
|
|
120
126
|
if: startsWith(matrix.platform, 'windows-')
|
|
121
127
|
shell: bash
|
|
@@ -249,6 +255,41 @@ jobs:
|
|
|
249
255
|
run: .github/scripts/push-to-data.sh toml ${{ matrix.ruby-version }} ${{ matrix.platform }}
|
|
250
256
|
shell: bash
|
|
251
257
|
|
|
258
|
+
- name: Benchmark html
|
|
259
|
+
continue-on-error: true
|
|
260
|
+
shell: bash
|
|
261
|
+
env:
|
|
262
|
+
GITHUB_RUNNER_PLATFORM: ${{ matrix.platform }}
|
|
263
|
+
DATA_REPO_TOKEN: ${{ secrets.DATA_REPO_TOKEN }}
|
|
264
|
+
DATA_RUN_DATE: ${{ needs.setup.outputs.run-date }}
|
|
265
|
+
run: |
|
|
266
|
+
if [ "${{ github.run_attempt }}" -gt 1 ] && .github/scripts/format-status.sh html ${{ matrix.ruby-version }} ${{ matrix.platform }}; then
|
|
267
|
+
echo "html already in the data repo — skipping (gap-filling rerun)"
|
|
268
|
+
exit 0
|
|
269
|
+
fi
|
|
270
|
+
bundle exec serialbench environment execute \
|
|
271
|
+
config/environments/ci-ruby-${{ matrix.ruby-version }}.yml \
|
|
272
|
+
config/benchmarks/full-html.yml \
|
|
273
|
+
"results/runs/ci-ruby-${{ matrix.ruby-version }}-${{ matrix.platform }}/html"
|
|
274
|
+
|
|
275
|
+
- name: Upload html
|
|
276
|
+
if: ${{ !cancelled() }}
|
|
277
|
+
uses: actions/upload-artifact@v4
|
|
278
|
+
with:
|
|
279
|
+
name: benchmark-results-${{ matrix.platform }}-ruby-${{ matrix.ruby-version }}-html
|
|
280
|
+
path: results/runs/ci-ruby-${{ matrix.ruby-version }}-${{ matrix.platform }}/html
|
|
281
|
+
retention-days: 30
|
|
282
|
+
if-no-files-found: warn
|
|
283
|
+
|
|
284
|
+
- name: Push html to data repo
|
|
285
|
+
if: ${{ !cancelled() }}
|
|
286
|
+
continue-on-error: true
|
|
287
|
+
env:
|
|
288
|
+
DATA_REPO_TOKEN: ${{ secrets.DATA_REPO_TOKEN }}
|
|
289
|
+
DATA_RUN_DATE: ${{ needs.setup.outputs.run-date }}
|
|
290
|
+
run: .github/scripts/push-to-data.sh html ${{ matrix.ruby-version }} ${{ matrix.platform }}
|
|
291
|
+
shell: bash
|
|
292
|
+
|
|
252
293
|
# Each format is an independent checkpoint with its own upload.
|
|
253
294
|
# A failure in one format doesn't prevent the others from uploading.
|
|
254
295
|
- name: Benchmark xml
|
|
@@ -296,7 +337,7 @@ jobs:
|
|
|
296
337
|
DATA_RUN_DATE: ${{ needs.setup.outputs.run-date }}
|
|
297
338
|
run: |
|
|
298
339
|
MISSING=""
|
|
299
|
-
for FMT in json yaml toml xml; do
|
|
340
|
+
for FMT in json yaml toml html xml; do
|
|
300
341
|
if .github/scripts/format-status.sh "$FMT" "${{ matrix.ruby-version }}" "${{ matrix.platform }}"; then
|
|
301
342
|
echo "$FMT: in data repo"
|
|
302
343
|
else
|
data/.gitignore
CHANGED
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/
|
|
5
|
-
image:https://github.com/
|
|
6
|
-
image:https://img.shields.io/github/issues-pr-raw/
|
|
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
|
|
@@ -26,15 +26,140 @@ 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
|
+
XPATH_QUERIES = ['//book', "//book[@id='101']", '//book[price > 30]/title'].freeze
|
|
146
|
+
XQUERY_EXPRESSIONS = ['count(//user | //record)', "//record[@id='101']/data/field1", '//user[profile/age > 40]/name'].freeze
|
|
147
|
+
RNG_SCHEMA = File.expand_path('test_data/schema.rng', Dir.pwd).then { |p| File.exist?(p) ? File.read(p) : _DEFAULT_RNG_SCHEMA }
|
|
148
|
+
XSLT_STYLESHEET = File.expand_path('test_data/transform.xsl', Dir.pwd).then { |p| File.exist?(p) ? File.read(p) : _DEFAULT_XSLT_STYLESHEET }
|
|
149
|
+
|
|
29
150
|
OPERATIONS = {
|
|
30
151
|
'parsing' => ->(s, data) { s.parse(data) },
|
|
31
152
|
'generation' => ->(s, data) { s.generate(s.parse(data)) },
|
|
32
153
|
'xpath' => lambda { |s, data|
|
|
33
154
|
doc = s.parse(data)
|
|
34
|
-
s.xpath_query(doc,
|
|
35
|
-
|
|
36
|
-
|
|
155
|
+
XPATH_QUERIES.each { |q| s.xpath_query(doc, q) }
|
|
156
|
+
},
|
|
157
|
+
'xquery' => lambda { |s, data|
|
|
158
|
+
doc = s.parse(data)
|
|
159
|
+
XQUERY_EXPRESSIONS.each { |x| s.xquery_eval(doc, x) }
|
|
37
160
|
},
|
|
161
|
+
'xslt' => ->(s, data) { s.xslt_transform(s.parse(data), XSLT_STYLESHEET) },
|
|
162
|
+
'validation' => ->(s, data) { s.validate(s.parse(data), RNG_SCHEMA) },
|
|
38
163
|
'streaming' => ->(s, data) { s.stream_parse(data) { |_event, _data| } },
|
|
39
164
|
}.freeze
|
|
40
165
|
|
|
@@ -161,6 +286,12 @@ module Serialbench
|
|
|
161
286
|
serializers.select { |s| s.supports?(:sax) || s.supports?(:streaming) }
|
|
162
287
|
when 'xpath'
|
|
163
288
|
serializers.select { |s| s.supports?(:xpath) }
|
|
289
|
+
when 'xquery'
|
|
290
|
+
serializers.select { |s| s.supports?(:xquery) }
|
|
291
|
+
when 'xslt'
|
|
292
|
+
serializers.select { |s| s.supports?(:xslt) }
|
|
293
|
+
when 'validation'
|
|
294
|
+
serializers.select { |s| s.supports?(:validation) }
|
|
164
295
|
else
|
|
165
296
|
serializers
|
|
166
297
|
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 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,9 @@ 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 :validation, IterationPerformance, collection: true
|
|
74
77
|
|
|
75
78
|
key_value do
|
|
76
79
|
map 'serializers', to: :serializers
|
|
@@ -79,6 +82,9 @@ module Serialbench
|
|
|
79
82
|
map 'memory', to: :memory
|
|
80
83
|
map 'streaming', to: :streaming
|
|
81
84
|
map 'xpath', to: :xpath
|
|
85
|
+
map 'xquery', to: :xquery
|
|
86
|
+
map 'xslt', to: :xslt
|
|
87
|
+
map 'validation', to: :validation
|
|
82
88
|
end
|
|
83
89
|
end
|
|
84
90
|
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
|
|
@@ -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 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)
|
|
@@ -12,6 +12,12 @@ require_relative 'serializers/xml/oga_serializer'
|
|
|
12
12
|
require_relative 'serializers/xml/libxml_serializer'
|
|
13
13
|
require_relative 'serializers/xml/leptris_serializer'
|
|
14
14
|
|
|
15
|
+
# HTML Serializers
|
|
16
|
+
require_relative 'serializers/html/base_html_serializer'
|
|
17
|
+
require_relative 'serializers/html/nokogiri_serializer'
|
|
18
|
+
require_relative 'serializers/html/oga_serializer'
|
|
19
|
+
require_relative 'serializers/html/leptris_serializer'
|
|
20
|
+
|
|
15
21
|
# JSON Serializers
|
|
16
22
|
require_relative 'serializers/json/base_json_serializer'
|
|
17
23
|
require_relative 'serializers/json/json_serializer'
|
|
@@ -52,6 +58,11 @@ module Serialbench
|
|
|
52
58
|
Json::YajlSerializer,
|
|
53
59
|
Json::YeptrisSerializer
|
|
54
60
|
],
|
|
61
|
+
html: [
|
|
62
|
+
Html::NokogiriSerializer,
|
|
63
|
+
Html::OgaSerializer,
|
|
64
|
+
Html::LeptrisSerializer
|
|
65
|
+
],
|
|
55
66
|
yaml: [
|
|
56
67
|
Yaml::PsychSerializer,
|
|
57
68
|
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)
|
data/lib/serialbench/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.10.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
|
|
@@ -349,13 +354,13 @@ files:
|
|
|
349
354
|
- lib/serialbench/version.rb
|
|
350
355
|
- lib/serialbench/yaml_validator.rb
|
|
351
356
|
- serialbench.gemspec
|
|
352
|
-
homepage: https://github.com/serialbench/serialbench
|
|
357
|
+
homepage: https://github.com/serialbench/serialbench-ruby
|
|
353
358
|
licenses:
|
|
354
359
|
- BSD-2-Clause
|
|
355
360
|
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
|
|
361
|
+
homepage_uri: https://github.com/serialbench/serialbench-ruby
|
|
362
|
+
source_code_uri: https://github.com/serialbench/serialbench-ruby
|
|
363
|
+
bug_tracker_uri: https://github.com/serialbench/serialbench-ruby/issues
|
|
359
364
|
rdoc_options: []
|
|
360
365
|
require_paths:
|
|
361
366
|
- lib
|