leptris 1.9.10-aarch64-linux → 1.9.12-aarch64-linux
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 +53 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/element.rb +8 -0
- data/lib/leptris/xml/node.rb +4 -1
- data/lib/leptris/xml/node_set.rb +30 -19
- data/lib/leptris/xml/sax/parser.rb +13 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf80f624ccd37fd9495e26c150943bc07923b82fd3e96eacb0d115d2a2651f5d
|
|
4
|
+
data.tar.gz: f7974a661b342dacb962a6e92bc7c9634d3a606b0ab1c2300262c469420b4bd2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b4524b049a05c7dffe328ccb16742dede857b442b8dc5443ceddb9aad27c9b71ea7ab02bcf7d0d6b6fc724828587b3557d11042b939c15c97727d7ed048384e2
|
|
7
|
+
data.tar.gz: 74811080bd538e8b9023de303cbca5ade92c6005cb76aebd72e548c44b9504fae44dfa44055b7d34d2e157c5b779a330cc5da2c875fc485574728fcaaf237e26
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,59 @@ 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.12] - 2026-08-27
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **NodeSet#each materializes on the first pass**: the lazy batch
|
|
13
|
+
fetch builds the array while yielding; every later each/[]/length
|
|
14
|
+
serves from it. Iterating twice without an explicit to_a paid the
|
|
15
|
+
batch twice — measured 0.220 s -> 0.127 s (42%) on the repeated-
|
|
16
|
+
iteration loop, now matching the materialized shape.
|
|
17
|
+
- **Leaner memo guard**: `readonly_cached?` checks ivar presence
|
|
18
|
+
alone — memo presence proves readonly, because every memo site
|
|
19
|
+
assigns only under readonly and readonly is one-way. Saves a
|
|
20
|
+
document round-trip on every memoized read; the harness's
|
|
21
|
+
readonly loops drop another 22-45% (attrs 0.078 -> 0.061 s,
|
|
22
|
+
namespaces 0.033 -> 0.018 s, content 0.066 -> 0.037 s).
|
|
23
|
+
|
|
24
|
+
### Measured and rejected
|
|
25
|
+
|
|
26
|
+
- SAX bulk pointer read for attribute pairs: a bulk read needs a
|
|
27
|
+
- counting pass first, which adds calls to a one-pass walk — no
|
|
28
|
+
win, withdrawn on inspection.
|
|
29
|
+
- Unconditional writable-content memoization and version-stamp
|
|
30
|
+
memoization (see the round-X report): invalidation completeness
|
|
31
|
+
and a compare costing the read, respectively.
|
|
32
|
+
|
|
33
|
+
## [1.9.11] - 2026-08-26
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **Readonly `Element#[]` serves from the memoized attributes
|
|
38
|
+
hash** (materializing on demand): repeated reads become hash
|
|
39
|
+
lookups — no FFI dispatch, no lifetime guard. Completes the
|
|
40
|
+
readonly contract on its hottest member. Writable documents keep
|
|
41
|
+
the direct path (values can change).
|
|
42
|
+
- **Materialized NodeSets stop re-batching**: `each`, `[]`, and
|
|
43
|
+
`length` consult the materialized array first; iterating or
|
|
44
|
+
indexing after `to_a` no longer re-pays the count + fetch + wrap
|
|
45
|
+
pass. Repeated iteration measures 2.4x faster.
|
|
46
|
+
- **NodeSet negative indexes are consistent**: `ns[-1]` answers the
|
|
47
|
+
last element for lazy and eager sets alike (Ruby-Array slice
|
|
48
|
+
semantics, Nokogiri parity); previously only eager sets
|
|
49
|
+
supported them.
|
|
50
|
+
- **SAX start_element attributes build pairs in one pass** — no
|
|
51
|
+
intermediate flat string array or each_slice enumerator per
|
|
52
|
+
event (~13% on the SAX parse loop).
|
|
53
|
+
|
|
54
|
+
### Measured and rejected
|
|
55
|
+
|
|
56
|
+
- A compiled-expression cache for ad-hoc xpath (the round's main
|
|
57
|
+
hypothesis): compiled vs ad-hoc eval measured within 2.5% on
|
|
58
|
+
500-member loops — the engine's expression parse is effectively
|
|
59
|
+
free. Killed before implementation.
|
|
60
|
+
|
|
8
61
|
## [1.9.10] - 2026-08-26
|
|
9
62
|
|
|
10
63
|
### Changed
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -32,6 +32,14 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def [](key)
|
|
35
|
+
# Readonly: the attributes hash cannot go stale (ADR 0003) and
|
|
36
|
+
# materializes on demand — repeated reads become hash lookups,
|
|
37
|
+
# with no dispatch and no lifetime guard (a hash read cannot
|
|
38
|
+
# use-after-free).
|
|
39
|
+
if @document&.readonly?
|
|
40
|
+
cached = attributes[key.to_s]
|
|
41
|
+
return cached.nil? ? nil : cached.value
|
|
42
|
+
end
|
|
35
43
|
ensure_alive!
|
|
36
44
|
Leptris::XML::FFI.leptris_element_attribute(@c_ptr, key.to_s)
|
|
37
45
|
end
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -264,8 +264,11 @@ class Leptris::XML::Node
|
|
|
264
264
|
|
|
265
265
|
protected
|
|
266
266
|
|
|
267
|
+
# Memo presence alone proves readonly: every memo site assigns
|
|
268
|
+
# only under readonly, and readonly is one-way — so the guard
|
|
269
|
+
# saves the document round-trip on every memoized read.
|
|
267
270
|
def readonly_cached?(ivar)
|
|
268
|
-
|
|
271
|
+
instance_variable_defined?(ivar)
|
|
269
272
|
end
|
|
270
273
|
|
|
271
274
|
def as_element_or_self
|
data/lib/leptris/xml/node_set.rb
CHANGED
|
@@ -41,7 +41,8 @@ class Leptris::XML::NodeSet
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def length
|
|
44
|
-
@
|
|
44
|
+
return @array.length if @array
|
|
45
|
+
@result_ptr ? Leptris::XML::FFI.leptris_xpath_result_count(@result_ptr) : 0
|
|
45
46
|
end
|
|
46
47
|
alias_method :size, :length
|
|
47
48
|
|
|
@@ -49,53 +50,63 @@ class Leptris::XML::NodeSet
|
|
|
49
50
|
length == 0
|
|
50
51
|
end
|
|
51
52
|
|
|
53
|
+
# The materialized array (after to_a) is authoritative: every
|
|
54
|
+
# reader consults it first so a materialized set stops paying the
|
|
55
|
+
# batch fetch on each iteration or index.
|
|
52
56
|
def [](idx)
|
|
57
|
+
return @array[idx] if @array
|
|
58
|
+
# Negative indexes are Ruby-Array semantics (Nokogiri's NodeSet
|
|
59
|
+
# is slice-like); materialize rather than answer nil
|
|
60
|
+
# inconsistently with an eager set.
|
|
61
|
+
return to_a[idx] if idx.negative?
|
|
53
62
|
if @result_ptr
|
|
54
63
|
return nil if idx < 0 || idx >= length
|
|
55
64
|
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, idx)
|
|
56
65
|
return nil if ptr.null?
|
|
57
66
|
Leptris::XML::Node.wrap(ptr, @document)
|
|
58
|
-
else
|
|
59
|
-
@array[idx]
|
|
60
67
|
end
|
|
61
68
|
end
|
|
62
69
|
|
|
70
|
+
# Iteration materializes: the first pass batch-fetches into @array
|
|
71
|
+
# (yielding as it wraps), and every subsequent reader — each, [],
|
|
72
|
+
# length — serves from the array. Repeated iteration without an
|
|
73
|
+
# explicit to_a pays the batch exactly once.
|
|
63
74
|
def each
|
|
64
75
|
return enum_for(:each) unless block_given?
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# (leptris_xpath_result_get_nodes_ex copies every node kind,
|
|
68
|
-
# not just elements) and wrap each. Wrappers are cached
|
|
69
|
-
# per-Document via Node.wrap, so re-iteration of the same
|
|
70
|
-
# NodeSet hits it.
|
|
76
|
+
unless @array
|
|
77
|
+
return self unless @result_ptr
|
|
71
78
|
n = length
|
|
72
79
|
if n > 0
|
|
80
|
+
# Batch-fetch all node pointers through the FFI seam
|
|
81
|
+
# (leptris_xpath_result_get_nodes_ex copies every node kind,
|
|
82
|
+
# not just elements); the batch accessor under-copies
|
|
83
|
+
# mixed-kind results (upstream leptris#477), so per-index
|
|
84
|
+
# fetch covers the remainder.
|
|
73
85
|
pointers = Leptris::XML::FFI.fetch_result_nodes(@result_ptr, n)
|
|
86
|
+
nodes = []
|
|
74
87
|
pointers.each do |ptr|
|
|
75
88
|
next if ptr.null?
|
|
76
|
-
|
|
89
|
+
nodes << Leptris::XML::Node.wrap(ptr, @document)
|
|
77
90
|
end
|
|
78
|
-
# The batch accessor under-copies mixed-kind results (upstream
|
|
79
|
-
# leptris#477); fall back to per-index fetch for the remainder.
|
|
80
91
|
(pointers.length...n).each do |i|
|
|
81
92
|
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, i)
|
|
82
93
|
next if ptr.null?
|
|
83
|
-
|
|
94
|
+
nodes << Leptris::XML::Node.wrap(ptr, @document)
|
|
84
95
|
end
|
|
96
|
+
@array = nodes
|
|
97
|
+
else
|
|
98
|
+
@array = []
|
|
85
99
|
end
|
|
86
|
-
else
|
|
87
|
-
@array.each { |n| yield n }
|
|
88
100
|
end
|
|
101
|
+
@array.each { |n| yield n }
|
|
89
102
|
self
|
|
90
103
|
end
|
|
91
104
|
|
|
92
105
|
def to_a
|
|
93
106
|
return @array if @array
|
|
94
107
|
return [] if @result_ptr.nil?
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
@array = out # memoize so subsequent to_a / each avoids re-fetch
|
|
98
|
-
out
|
|
108
|
+
each { |n| } # materializes @array as a side effect
|
|
109
|
+
@array
|
|
99
110
|
end
|
|
100
111
|
alias_method :to_ary, :to_a
|
|
101
112
|
|
|
@@ -148,8 +148,19 @@ class Leptris::XML::SAX::Parser
|
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
# The C `const char** attrs` is a NULL-terminated flat array of
|
|
151
|
-
# name/value pairs
|
|
151
|
+
# name/value pairs. Build the [name, value] pairs in one pass —
|
|
152
|
+
# no intermediate flat string array, no each_slice enumerator.
|
|
152
153
|
def walk_attr_array(attrs_ptr)
|
|
153
|
-
|
|
154
|
+
ptr_size = ::FFI.type_size(:pointer)
|
|
155
|
+
pairs = []
|
|
156
|
+
i = 0
|
|
157
|
+
loop do
|
|
158
|
+
name_ptr = attrs_ptr.get_pointer(i * ptr_size)
|
|
159
|
+
break if name_ptr.null?
|
|
160
|
+
value_ptr = attrs_ptr.get_pointer((i + 1) * ptr_size)
|
|
161
|
+
pairs << [utf8(name_ptr.read_string), utf8(value_ptr.read_string)]
|
|
162
|
+
i += 2
|
|
163
|
+
end
|
|
164
|
+
pairs
|
|
154
165
|
end
|
|
155
166
|
end
|