leptris 1.9.4 → 1.9.6
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/CHANGELOG.md +41 -0
- data/README.adoc +11 -0
- data/leptris.gemspec +2 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/cdata.rb +1 -0
- data/lib/leptris/xml/comment.rb +1 -0
- data/lib/leptris/xml/document.rb +6 -0
- data/lib/leptris/xml/element.rb +12 -1
- data/lib/leptris/xml/node.rb +32 -2
- data/lib/leptris/xml/processing_instruction.rb +2 -0
- data/lib/leptris/xml/text.rb +1 -0
- metadata +1 -5
- data/.rspec +0 -3
- data/.rubocop.yml +0 -8
- data/benchmark/README.md +0 -12
- data/benchmark/leptris_vs_nokogiri.rb +0 -105
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 68bba6b5b68f0266571ec17a5809ea42ce7b1f37f0ba3577f9a06afd57db578f
|
|
4
|
+
data.tar.gz: d15442dd463ba2b4d09c6415e51c478c385c851eb6630ffc8c4d00e6a9ad7a57
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52a5f4a5307b9b219911a9ebb7050672be1ad27096266e1c0732eb98ab40779055dcf25e4dd31c0ef0813966539e43894c6d4cdce20bd79e778a7df043c5631a
|
|
7
|
+
data.tar.gz: cd2e0732f88060f6ff02e3496bedca3d986fde26c9400c91efe458c05e96379caeddd50a5950eb65b965be23487aac243ada697855ade71d0ed6017bcfe49433
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ All notable changes to Leptris will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.9.6] - 2026-08-26
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- The lifetime guard (`ensure_alive!`) uses the cheapest sufficient
|
|
13
|
+
check (`Document#c_ptr.nil?` — `#free` nils it, and the GC
|
|
14
|
+
finalizer cannot fire while any handle exists) instead of the
|
|
15
|
+
deeper `Document#freed?` chain. Claw back on the cheapest single-
|
|
16
|
+
dispatch read (`Element#[]` hot loops): ~20% of the guard's cost.
|
|
17
|
+
Remaining guard cost is the method dispatch itself — the price of
|
|
18
|
+
the lifetime contract on the one call shape cheap enough to
|
|
19
|
+
notice it; all other harness loops measure at parity, and
|
|
20
|
+
readonly hot loops have the memoized `attributes`/`keys`
|
|
21
|
+
alternatives.
|
|
22
|
+
|
|
23
|
+
## [1.9.5] - 2026-08-26
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **Borrowed handles validate their lender**: every c_ptr-
|
|
28
|
+
dereferencing read and mutation on Node/Element/Text/Comment/
|
|
29
|
+
CDATA/PI raises `Leptris::XML::UseAfterFreeError` once the owning
|
|
30
|
+
document has been freed (previously `doc.free; node.name` read
|
|
31
|
+
freed memory silently — verified returning `""` — and stale
|
|
32
|
+
mutations risked segfaults; only Document's own methods checked).
|
|
33
|
+
Memoized readonly results stay check-free. `Document#freed?` is
|
|
34
|
+
the single state predicate.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **`benchmark/read_paths.rb`**: the committed read-path harness
|
|
39
|
+
(readonly loops, SAX, NodeSet unions, css, parse-query-serialize)
|
|
40
|
+
with a README methodology note — review-round benchmarks are now
|
|
41
|
+
reproducible from the repo.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- The published gem no longer ships dev tooling (`benchmark/`,
|
|
46
|
+
`.rspec`, `.rubocop.yml`); it carries lib, gemspec, Rakefile, and
|
|
47
|
+
documentation only.
|
|
48
|
+
|
|
8
49
|
## [1.9.4] - 2026-08-25
|
|
9
50
|
|
|
10
51
|
### Fixed
|
data/README.adoc
CHANGED
|
@@ -411,6 +411,17 @@ Notable differences:
|
|
|
411
411
|
|
|
412
412
|
== Performance
|
|
413
413
|
|
|
414
|
+
=== Internal read-path harness
|
|
415
|
+
|
|
416
|
+
`benchmark/read_paths.rb` measures the binding's own hot paths
|
|
417
|
+
(readonly read loops, SAX, NodeSet unions, css, parse-query-
|
|
418
|
+
serialize). Machine-relative — compare before/after runs on the same
|
|
419
|
+
machine:
|
|
420
|
+
|
|
421
|
+
bundle exec ruby benchmark/read_paths.rb
|
|
422
|
+
|
|
423
|
+
=== Full-field comparison
|
|
424
|
+
|
|
414
425
|
Measured with metanorma/serialbench (Ruby 3.4.8, leptris 1.6.0,
|
|
415
426
|
libleptris 1.6.0, macOS arm64) — Leptris vs the full field:
|
|
416
427
|
|
data/leptris.gemspec
CHANGED
|
@@ -28,7 +28,8 @@ Gem::Specification.new do |spec|
|
|
|
28
28
|
spec.files = Dir.chdir(__dir__) do
|
|
29
29
|
`git ls-files -z`.split("\x0").reject do |f|
|
|
30
30
|
(File.expand_path(f) == __FILE__) ||
|
|
31
|
-
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github
|
|
31
|
+
f.start_with?(*%w[bin/ test/ spec/ features/ benchmark/ .git .github
|
|
32
|
+
appveyor Gemfile .rspec .rubocop.yml])
|
|
32
33
|
end
|
|
33
34
|
end
|
|
34
35
|
spec.bindir = "bin"
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/cdata.rb
CHANGED
data/lib/leptris/xml/comment.rb
CHANGED
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -281,6 +281,12 @@ class Leptris::XML::Document
|
|
|
281
281
|
@readonly == true
|
|
282
282
|
end
|
|
283
283
|
|
|
284
|
+
# True once #free has run (or the GC finalizer fired) — borrowed
|
|
285
|
+
# handles check this before dereferencing their c_ptr.
|
|
286
|
+
def freed?
|
|
287
|
+
@freed.state == :freed || @c_ptr.nil?
|
|
288
|
+
end
|
|
289
|
+
|
|
284
290
|
# The most recent error recorded against this document, or nil.
|
|
285
291
|
def last_error
|
|
286
292
|
msg = Leptris::XML::FFI.leptris_document_last_error(@c_ptr)
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
class Leptris::XML::Element < Leptris::XML::Node
|
|
4
4
|
def name
|
|
5
|
-
@name
|
|
5
|
+
return @name if @name
|
|
6
|
+
ensure_alive!
|
|
7
|
+
@name = Leptris::XML::FFI.leptris_element_name(@c_ptr)
|
|
6
8
|
end
|
|
7
9
|
alias_method :node_name, :name
|
|
8
10
|
|
|
@@ -16,6 +18,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
16
18
|
|
|
17
19
|
def content
|
|
18
20
|
return @content if readonly_cached?(:@content)
|
|
21
|
+
ensure_alive!
|
|
19
22
|
Leptris::XML::FFI.leptris_element_text(@c_ptr).tap do |text|
|
|
20
23
|
@content = text if @document&.readonly?
|
|
21
24
|
end
|
|
@@ -29,6 +32,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
def [](key)
|
|
35
|
+
ensure_alive!
|
|
32
36
|
Leptris::XML::FFI.leptris_element_attribute(@c_ptr, key.to_s)
|
|
33
37
|
end
|
|
34
38
|
alias_method :attr, :[]
|
|
@@ -43,6 +47,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
43
47
|
alias_method :set_attribute, :[]=
|
|
44
48
|
|
|
45
49
|
def key?(name)
|
|
50
|
+
ensure_alive!
|
|
46
51
|
Leptris::XML::FFI.leptris_element_has_attribute(@c_ptr, name.to_s) != 0
|
|
47
52
|
end
|
|
48
53
|
alias_method :has_attribute?, :key?
|
|
@@ -53,11 +58,13 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
53
58
|
# in-scope declarations. uri nil/"" matches no-namespace
|
|
54
59
|
# attributes only; xmlns declarations are invisible.
|
|
55
60
|
def attribute_ns(uri, local)
|
|
61
|
+
ensure_alive!
|
|
56
62
|
Leptris::XML::FFI.leptris_element_attribute_ns(
|
|
57
63
|
@c_ptr, uri&.to_s, local.to_s)
|
|
58
64
|
end
|
|
59
65
|
|
|
60
66
|
def has_attribute_ns?(uri, local)
|
|
67
|
+
ensure_alive!
|
|
61
68
|
Leptris::XML::FFI.leptris_element_has_attribute_ns(
|
|
62
69
|
@c_ptr, uri&.to_s, local.to_s) != 0
|
|
63
70
|
end
|
|
@@ -77,6 +84,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
77
84
|
# per-attribute namespace accessors (libleptris 1.8.0).
|
|
78
85
|
def each_attribute
|
|
79
86
|
return enum_for(:each_attribute) unless block_given?
|
|
87
|
+
ensure_alive!
|
|
80
88
|
attr = Leptris::XML::FFI.leptris_element_first_attribute(@c_ptr)
|
|
81
89
|
until attr.nil? || attr.null?
|
|
82
90
|
name = Leptris::XML::FFI.leptris_attribute_get_name(attr)
|
|
@@ -119,6 +127,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
119
127
|
# The element's own namespace prefix (e.g. "foo" for <foo:child/>),
|
|
120
128
|
# or nil when the element has none.
|
|
121
129
|
def prefix
|
|
130
|
+
ensure_alive!
|
|
122
131
|
Leptris::XML::FFI.leptris_element_prefix(@c_ptr)
|
|
123
132
|
end
|
|
124
133
|
|
|
@@ -231,6 +240,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
231
240
|
|
|
232
241
|
def namespace
|
|
233
242
|
return @namespace if readonly_cached?(:@namespace)
|
|
243
|
+
ensure_alive!
|
|
234
244
|
uri = Leptris::XML::FFI.leptris_element_namespace(@c_ptr)
|
|
235
245
|
result =
|
|
236
246
|
if uri.nil? || uri.empty?
|
|
@@ -249,6 +259,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
249
259
|
|
|
250
260
|
def namespace_definitions
|
|
251
261
|
return @namespace_definitions if readonly_cached?(:@namespace_definitions)
|
|
262
|
+
ensure_alive!
|
|
252
263
|
count = Leptris::XML::FFI.leptris_element_namespace_count(@c_ptr)
|
|
253
264
|
result = count.times.map do |i|
|
|
254
265
|
prefix = Leptris::XML::FFI.leptris_element_namespace_decl_prefix(@c_ptr, i)
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -62,7 +62,9 @@ class Leptris::XML::Node
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def type
|
|
65
|
-
@node_type
|
|
65
|
+
return @node_type if @node_type
|
|
66
|
+
ensure_alive!
|
|
67
|
+
@node_type = Leptris::XML::FFI.leptris_node_get_type(@c_ptr)
|
|
66
68
|
end
|
|
67
69
|
alias_method :node_type, :type
|
|
68
70
|
|
|
@@ -77,13 +79,31 @@ class Leptris::XML::Node
|
|
|
77
79
|
|
|
78
80
|
def parent
|
|
79
81
|
return @parent if @parent
|
|
82
|
+
ensure_alive!
|
|
80
83
|
ptr = Leptris::XML::FFI.leptris_node_parent(@c_ptr)
|
|
81
84
|
return nil if ptr.null?
|
|
82
85
|
Leptris::XML::Node.wrap(ptr, @document)
|
|
83
86
|
end
|
|
84
87
|
|
|
85
|
-
#
|
|
88
|
+
# Borrowed-handle lifetime: every c_ptr dereference is valid only
|
|
89
|
+
# while the owning document lives. Parentless nodes (iterparse
|
|
90
|
+
# yields) cannot validate and are skipped. The guard runs before
|
|
91
|
+
# every uncached FFI dispatch, so it uses the cheapest sufficient
|
|
92
|
+
# check: #free nils the document's c_ptr, and the GC-finalizer
|
|
93
|
+
# path cannot fire while any handle (which strongly references
|
|
94
|
+
# the document) exists. Document#freed? remains the accurate
|
|
95
|
+
# public predicate.
|
|
96
|
+
def ensure_alive!
|
|
97
|
+
if @document && @document.c_ptr.nil?
|
|
98
|
+
raise Leptris::XML::UseAfterFreeError,
|
|
99
|
+
"owning document has been freed — handle used on #{inspect}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Raises ReadOnlyError when the owning document was marked readonly,
|
|
104
|
+
# UseAfterFreeError when it was freed.
|
|
86
105
|
def ensure_writable!
|
|
106
|
+
ensure_alive!
|
|
87
107
|
if @document&.readonly?
|
|
88
108
|
raise Leptris::XML::ReadOnlyError,
|
|
89
109
|
"document is readonly — mutation attempted on #{inspect}"
|
|
@@ -91,16 +111,19 @@ class Leptris::XML::Node
|
|
|
91
111
|
end
|
|
92
112
|
|
|
93
113
|
def line
|
|
114
|
+
ensure_alive!
|
|
94
115
|
Leptris::XML::FFI.leptris_node_line(@c_ptr)
|
|
95
116
|
end
|
|
96
117
|
|
|
97
118
|
def <=>(other)
|
|
98
119
|
return nil unless other.is_a?(Leptris::XML::Node)
|
|
99
120
|
return nil unless @document == other.document
|
|
121
|
+
ensure_alive!
|
|
100
122
|
Leptris::XML::FFI.leptris_node_compare(@c_ptr, other.c_ptr)
|
|
101
123
|
end
|
|
102
124
|
|
|
103
125
|
def child
|
|
126
|
+
ensure_alive!
|
|
104
127
|
ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
|
|
105
128
|
return nil if ptr.null?
|
|
106
129
|
Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
|
|
@@ -110,6 +133,7 @@ class Leptris::XML::Node
|
|
|
110
133
|
# Immutable in readonly mode: the batch fetch plus wrapper
|
|
111
134
|
# construction is paid once.
|
|
112
135
|
return @children if readonly_cached?(:@children)
|
|
136
|
+
ensure_alive!
|
|
113
137
|
parent = as_element_or_self
|
|
114
138
|
nodes = Leptris::XML::FFI.fetch_children(@c_ptr).map do |ptr|
|
|
115
139
|
Leptris::XML::Node.wrap(ptr, @document, parent: parent)
|
|
@@ -120,6 +144,7 @@ class Leptris::XML::Node
|
|
|
120
144
|
end
|
|
121
145
|
|
|
122
146
|
def next_sibling
|
|
147
|
+
ensure_alive!
|
|
123
148
|
ptr = Leptris::XML::FFI.leptris_node_next_sibling(@c_ptr)
|
|
124
149
|
return nil if ptr.null?
|
|
125
150
|
Leptris::XML::Node.wrap(ptr, @document, parent: @parent)
|
|
@@ -127,6 +152,7 @@ class Leptris::XML::Node
|
|
|
127
152
|
alias_method :next, :next_sibling
|
|
128
153
|
|
|
129
154
|
def previous_sibling
|
|
155
|
+
ensure_alive!
|
|
130
156
|
ptr = Leptris::XML::FFI.leptris_node_previous_sibling(@c_ptr)
|
|
131
157
|
return nil if ptr.null?
|
|
132
158
|
Leptris::XML::Node.wrap(ptr, @document, parent: @parent)
|
|
@@ -134,6 +160,7 @@ class Leptris::XML::Node
|
|
|
134
160
|
alias_method :previous, :previous_sibling
|
|
135
161
|
|
|
136
162
|
def first_element_child
|
|
163
|
+
ensure_alive!
|
|
137
164
|
ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
|
|
138
165
|
until ptr.nil? || ptr.null?
|
|
139
166
|
node = Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
|
|
@@ -189,6 +216,7 @@ class Leptris::XML::Node
|
|
|
189
216
|
# callback (libleptris #273); the per-node FFI cost is the floor.
|
|
190
217
|
def traverse
|
|
191
218
|
return enum_for(:traverse) unless block_given?
|
|
219
|
+
ensure_alive!
|
|
192
220
|
callback = ::FFI::Function.new(:int, [:pointer, :pointer], blocking: true) do |node_ptr, _|
|
|
193
221
|
yield Leptris::XML::Node.wrap(node_ptr, @document)
|
|
194
222
|
0
|
|
@@ -199,6 +227,7 @@ class Leptris::XML::Node
|
|
|
199
227
|
|
|
200
228
|
def path
|
|
201
229
|
return @path if readonly_cached?(:@path)
|
|
230
|
+
ensure_alive!
|
|
202
231
|
str_ptr = Leptris::XML::FFI.leptris_node_get_xpath(@c_ptr)
|
|
203
232
|
result = str_ptr.null? ? nil : Leptris::XML::FFI.read_owned_string(str_ptr)
|
|
204
233
|
@path = result if @document&.readonly?
|
|
@@ -221,6 +250,7 @@ class Leptris::XML::Node
|
|
|
221
250
|
end
|
|
222
251
|
|
|
223
252
|
def dup
|
|
253
|
+
ensure_alive!
|
|
224
254
|
elem_ptr = Leptris::XML::FFI.leptris_node_as_element(@c_ptr)
|
|
225
255
|
raise Leptris::XML::Error, "dup is only supported for element nodes" if elem_ptr.null?
|
|
226
256
|
copy_ptr = Leptris::XML::FFI.leptris_element_copy(elem_ptr, @document.c_ptr)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
|
|
4
4
|
def name
|
|
5
5
|
return @name if readonly_cached?(:@name)
|
|
6
|
+
ensure_alive!
|
|
6
7
|
Leptris::XML::FFI.leptris_pi_node_get_target(@c_ptr).tap do |target|
|
|
7
8
|
@name = target if @document&.readonly?
|
|
8
9
|
end
|
|
@@ -11,6 +12,7 @@ class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
|
|
|
11
12
|
|
|
12
13
|
def content
|
|
13
14
|
return @content if readonly_cached?(:@content)
|
|
15
|
+
ensure_alive!
|
|
14
16
|
Leptris::XML::FFI.leptris_pi_node_get_data(@c_ptr).to_s.tap do |data|
|
|
15
17
|
@content = data if @document&.readonly?
|
|
16
18
|
end
|
data/lib/leptris/xml/text.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: leptris
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.9.
|
|
4
|
+
version: 1.9.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -63,15 +63,11 @@ executables: []
|
|
|
63
63
|
extensions: []
|
|
64
64
|
extra_rdoc_files: []
|
|
65
65
|
files:
|
|
66
|
-
- ".rspec"
|
|
67
|
-
- ".rubocop.yml"
|
|
68
66
|
- CHANGELOG.md
|
|
69
67
|
- CLAUDE.md
|
|
70
68
|
- LICENSE.md
|
|
71
69
|
- README.adoc
|
|
72
70
|
- Rakefile
|
|
73
|
-
- benchmark/README.md
|
|
74
|
-
- benchmark/leptris_vs_nokogiri.rb
|
|
75
71
|
- leptris.gemspec
|
|
76
72
|
- lib/leptris.rb
|
|
77
73
|
- lib/leptris/version.rb
|
data/.rspec
DELETED
data/.rubocop.yml
DELETED
data/benchmark/README.md
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# leptris vs the field
|
|
2
|
-
|
|
3
|
-
The authoritative cross-library numbers live in the README's
|
|
4
|
-
Performance section (measured with metanorma/serialbench).
|
|
5
|
-
|
|
6
|
-
This local benchmark compares against Nokogiri only:
|
|
7
|
-
|
|
8
|
-
LEPTRIS_LIB_PATH=/path/to/libleptris.dylib bundle exec ruby benchmark/leptris_vs_nokogiri.rb
|
|
9
|
-
|
|
10
|
-
For the full-field race (Ox, Nokogiri, Oga, REXML, Leptris), use
|
|
11
|
-
https://github.com/metanorma/serialbench — leptris's adapter ships
|
|
12
|
-
there under `lib/serialbench/serializers/xml/leptris_serializer.rb`.
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Ruby-level performance comparison: Leptris::XML (FFI → libleptris v0.12.0)
|
|
4
|
-
# vs Nokogiri (C extension → libxml2).
|
|
5
|
-
#
|
|
6
|
-
# Run with: bundle exec ruby -Ilib benchmark/leptris_vs_nokogiri.rb
|
|
7
|
-
|
|
8
|
-
require "benchmark"
|
|
9
|
-
require "leptris/xml"
|
|
10
|
-
require "nokogiri"
|
|
11
|
-
|
|
12
|
-
module Fixtures
|
|
13
|
-
SMALL = "<catalog>" +
|
|
14
|
-
(1..10).map { |i| "<book id='#{i}'><title>Book #{i}</title></book>" }.join +
|
|
15
|
-
"</catalog>"
|
|
16
|
-
|
|
17
|
-
MEDIUM = ("<catalog version='2.0'>" +
|
|
18
|
-
(1..100).map do |i|
|
|
19
|
-
"<book id='#{i}' lang='en'>" \
|
|
20
|
-
"<title>Book #{i}</title>" \
|
|
21
|
-
"<author id='a#{i}'>Author #{i}</author>" \
|
|
22
|
-
"<price currency='USD'>#{i}.99</price>" \
|
|
23
|
-
"</book>"
|
|
24
|
-
end.join +
|
|
25
|
-
"</catalog>").freeze
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def time_it(label, n, &block)
|
|
29
|
-
GC.start
|
|
30
|
-
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
31
|
-
n.times { yield }
|
|
32
|
-
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
33
|
-
us_per_iter = (elapsed / n) * 1_000_000
|
|
34
|
-
printf " %-40s %8.2f µs/iter (%d iters in %.3fs)\n", label, us_per_iter, n, elapsed
|
|
35
|
-
us_per_iter
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def ratio(label, leptris_us, nokogiri_us)
|
|
39
|
-
r = nokogiri_us / leptris_us
|
|
40
|
-
who = r > 1 ? "Leptris faster" : "Nokogiri faster"
|
|
41
|
-
printf " → %-30s Leptris/Nokogiri = %.2fx (%s)\n\n", label, r, who
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
N_PARSE_SMALL = 5_000
|
|
45
|
-
N_PARSE_MEDIUM = 1_000
|
|
46
|
-
N_QUERY = 10_000
|
|
47
|
-
N_TRAVERSE = 2_000
|
|
48
|
-
N_SERIALIZE = 1_000
|
|
49
|
-
|
|
50
|
-
puts "===== Parse — small (#{Fixtures::SMALL.bytesize} B) ====="
|
|
51
|
-
t = time_it("leptris parse small", N_PARSE_SMALL) { d = Leptris::XML::Document.parse(Fixtures::SMALL); d.free }
|
|
52
|
-
n = time_it("nokogiri parse small", N_PARSE_SMALL) { Nokogiri::XML(Fixtures::SMALL) }
|
|
53
|
-
ratio("parse small", t, n)
|
|
54
|
-
|
|
55
|
-
puts "===== Parse — medium (#{Fixtures::MEDIUM.bytesize} B) ====="
|
|
56
|
-
t = time_it("leptris parse medium", N_PARSE_MEDIUM) { d = Leptris::XML::Document.parse(Fixtures::MEDIUM); d.free }
|
|
57
|
-
n = time_it("nokogiri parse medium", N_PARSE_MEDIUM) { Nokogiri::XML(Fixtures::MEDIUM) }
|
|
58
|
-
ratio("parse medium", t, n)
|
|
59
|
-
|
|
60
|
-
# Pre-parse medium for query benchmarks
|
|
61
|
-
doc_t = Leptris::XML::Document.parse(Fixtures::MEDIUM)
|
|
62
|
-
doc_n = Nokogiri::XML(Fixtures::MEDIUM)
|
|
63
|
-
|
|
64
|
-
puts "===== XPath — count(//book) ====="
|
|
65
|
-
t = time_it("leptris xpath count()", N_QUERY) { doc_t.xpath("count(//book)") }
|
|
66
|
-
n = time_it("nokogiri xpath count()", N_QUERY) { doc_n.xpath("count(//book)") }
|
|
67
|
-
ratio("xpath count()", t, n)
|
|
68
|
-
|
|
69
|
-
puts "===== XPath — //book (nodeset of 100) ====="
|
|
70
|
-
t = time_it("leptris xpath //book", N_QUERY) { doc_t.xpath("//book") }
|
|
71
|
-
n = time_it("nokogiri xpath //book", N_QUERY) { doc_n.xpath("//book") }
|
|
72
|
-
ratio("xpath //book", t, n)
|
|
73
|
-
|
|
74
|
-
puts "===== XPath — predicate //book[@id='50'] ====="
|
|
75
|
-
t = time_it("leptris xpath predicate", N_QUERY) { doc_t.xpath("//book[@id='50']") }
|
|
76
|
-
n = time_it("nokogiri xpath predicate", N_QUERY) { doc_n.xpath("//book[@id='50']") }
|
|
77
|
-
ratio("xpath predicate", t, n)
|
|
78
|
-
|
|
79
|
-
puts "===== XPath — complex //book[price > 50] ====="
|
|
80
|
-
t = time_it("leptris xpath complex", N_QUERY) { doc_t.xpath("//book[price > 50]") }
|
|
81
|
-
n = time_it("nokogiri xpath complex", N_QUERY) { doc_n.xpath("//book[price > 50]") }
|
|
82
|
-
ratio("xpath complex", t, n)
|
|
83
|
-
|
|
84
|
-
puts "===== XPath — union //author | //title ====="
|
|
85
|
-
t = time_it("leptris xpath union", N_QUERY) { doc_t.xpath("//author | //title") }
|
|
86
|
-
n = time_it("nokogiri xpath union", N_QUERY) { doc_n.xpath("//author | //title") }
|
|
87
|
-
ratio("xpath union", t, n)
|
|
88
|
-
|
|
89
|
-
puts "===== Tree traversal (root.traverse) ====="
|
|
90
|
-
t = time_it("leptris traverse", N_TRAVERSE) { doc_t.root.traverse { |n| n.name } }
|
|
91
|
-
n = time_it("nokogiri traverse", N_TRAVERSE) { doc_n.root.traverse { |n| n.name } }
|
|
92
|
-
ratio("traverse", t, n)
|
|
93
|
-
|
|
94
|
-
puts "===== Serialize — Document#to_xml ====="
|
|
95
|
-
t = time_it("leptris serialize", N_SERIALIZE) { doc_t.to_xml }
|
|
96
|
-
n = time_it("nokogiri serialize", N_SERIALIZE) { doc_n.to_xml }
|
|
97
|
-
ratio("serialize", t, n)
|
|
98
|
-
|
|
99
|
-
doc_t.free
|
|
100
|
-
|
|
101
|
-
puts ""
|
|
102
|
-
puts "libleptris v0.12.0 — all upstream issues closed."
|
|
103
|
-
puts "All 176 Ruby specs passing, 0 pending."
|
|
104
|
-
|
|
105
|
-
|