leptris 1.9.27 → 1.9.29
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 +55 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/ffi.rb +29 -1
- data/lib/leptris/xml/node.rb +31 -4
- 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: f91b02217a813c5c7c8eb267b6b82b3eb7689d857fbd76b7a8b108962d56d18c
|
|
4
|
+
data.tar.gz: c15ad7755d849144c45176d4e28f819ac090927c8a1e1d6ad58c89fe8a3006fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db0800af7aafe8cf8efb263a0fde99d8876b3d8f487a9cbe738bf2d086a9be8a9e6805e9033b3b695b9064e58d4593f0aee4361dcdc55fa2f920e5a71484acb7
|
|
7
|
+
data.tar.gz: ae1282d974199e33a98c9361aee2afb36b7dacfb782259380688cd13e480065d3ff59108be44f868b3283f0f11fb958c37d16f28c9eb5fcfc1a6ac6331020c26
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,61 @@ 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.29] - 2026-08-28
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Element-batch truncation (1.9.28 regression)**:
|
|
13
|
+
`fetch_element_children`'s opportunistic growth read its sizing
|
|
14
|
+
from `leptris_element_children(ptr, NULL, 0)` — but that entry
|
|
15
|
+
point has NO count-only mode (NULL returns 0, unlike
|
|
16
|
+
`leptris_node_children`), so the truncation check always judged
|
|
17
|
+
"fits" and wide element families were cut to the scratch
|
|
18
|
+
capacity (32 on a fresh thread/process). `#element_children`
|
|
19
|
+
and `#last_element_child` both affected; elements with 32 or
|
|
20
|
+
fewer element children — and any process whose scratch had
|
|
21
|
+
already grown — were unaffected, which is why the round-XXIII
|
|
22
|
+
suite passed. Sizing now comes from the dedicated
|
|
23
|
+
`leptris_element_child_count`. Two fresh-Thread regression
|
|
24
|
+
specs pin the fresh-scratch state (the scratch is thread-local:
|
|
25
|
+
same-process test ordering had masked it); they fail on 1.9.28.
|
|
26
|
+
The count query fires only when the buffer fills exactly —
|
|
27
|
+
steady-state cost unchanged, the batch's -27% element_children
|
|
28
|
+
win holds.
|
|
29
|
+
|
|
30
|
+
### Meta
|
|
31
|
+
|
|
32
|
+
- Compiled-expression cache for `Searchable#xpath` prototyped and
|
|
33
|
+
retired by measurement: mixed deltas across expression shapes,
|
|
34
|
+
one stable regression (parent-axis `..` +58% through
|
|
35
|
+
`leptris_xpath_compiled_eval` vs string eval, three interleaved
|
|
36
|
+
passes), suggesting the engine's string path already amortizes
|
|
37
|
+
compilation. Reverted in full; the parent-axis asymmetry is
|
|
38
|
+
worth an upstream look.
|
|
39
|
+
|
|
40
|
+
## [1.9.28] - 2026-08-28
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **Element-child reads pay only for elements** — the
|
|
45
|
+
interest-proportional principle applied to tree navigation:
|
|
46
|
+
- `#element_children` on element receivers rides the element-only
|
|
47
|
+
batch (`leptris_element_children`, opportunistic single
|
|
48
|
+
dispatch on the shared scratch): text/comment children are
|
|
49
|
+
never wrapped, and every kept element carries the ELEMENT hint
|
|
50
|
+
so the per-child `get_type` disappears. Cold pass over
|
|
51
|
+
big.xml's 25,000 items: 160 -> **117 ms per doc (-27%)**.
|
|
52
|
+
Non-element receivers (the document node) keep the filter.
|
|
53
|
+
- `#first_element_child` types candidate siblings with one raw
|
|
54
|
+
`get_type` each — nothing wrapped or cached until the element
|
|
55
|
+
is found (it previously full-wrapped leading text nodes).
|
|
56
|
+
- `#last_element_child` on element receivers wraps only the
|
|
57
|
+
batch's final pointer (it previously materialized every child,
|
|
58
|
+
text wraps included, to scan backwards). The first/last pair
|
|
59
|
+
across fresh docs: **-46%**.
|
|
60
|
+
- The all-kind `#children` path is untouched — the cold full-tree
|
|
61
|
+
walk battery stays at parity.
|
|
62
|
+
|
|
8
63
|
## [1.9.27] - 2026-08-28
|
|
9
64
|
|
|
10
65
|
### Fixed
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -533,7 +533,6 @@ module Leptris
|
|
|
533
533
|
attach_function :leptris_xpath_compiled_free,
|
|
534
534
|
|
|
535
535
|
[:leptris_xpath_compiled], :void
|
|
536
|
-
|
|
537
536
|
attach_function :leptris_sax_parse,
|
|
538
537
|
[:string, :size_t, :pointer, :pointer], :int
|
|
539
538
|
attach_function :leptris_sax_parser_create,
|
|
@@ -863,6 +862,35 @@ module Leptris
|
|
|
863
862
|
buffer.get_array_of_pointer(0, copied)
|
|
864
863
|
end
|
|
865
864
|
|
|
865
|
+
# Element-only child batch (the mirror of fetch_children over
|
|
866
|
+
# leptris_element_children): element receivers skip wrapping —
|
|
867
|
+
# and paying for — the text/comment children they will filter
|
|
868
|
+
# out anyway; every wrap carries the ELEMENT hint so the
|
|
869
|
+
# per-child get_type dispatch disappears too.
|
|
870
|
+
def self.fetch_element_children(el_ptr)
|
|
871
|
+
scratch = (Thread.current[:leptris_scratch] ||= {})
|
|
872
|
+
buffer = scratch[:pointers]
|
|
873
|
+
if buffer.nil?
|
|
874
|
+
buffer = scratch[:pointers] =
|
|
875
|
+
::FFI::MemoryPointer.new(:pointer, FETCH_INITIAL)
|
|
876
|
+
end
|
|
877
|
+
cap = buffer.size / PTR_BYTES
|
|
878
|
+
copied = leptris_element_children(el_ptr, buffer, cap)
|
|
879
|
+
if copied == cap
|
|
880
|
+
# leptris_element_children has NO count-only mode (NULL
|
|
881
|
+
# returns 0, unlike leptris_node_children) — size the
|
|
882
|
+
# regrow from the dedicated counter.
|
|
883
|
+
total = leptris_element_child_count(el_ptr)
|
|
884
|
+
if total > cap
|
|
885
|
+
buffer.free
|
|
886
|
+
buffer = scratch[:pointers] =
|
|
887
|
+
::FFI::MemoryPointer.new(:pointer, total)
|
|
888
|
+
copied = leptris_element_children(el_ptr, buffer, total)
|
|
889
|
+
end
|
|
890
|
+
end
|
|
891
|
+
buffer.get_array_of_pointer(0, copied)
|
|
892
|
+
end
|
|
893
|
+
|
|
866
894
|
# XPath result-set batch (leptris_xpath_result_get_nodes_ex):
|
|
867
895
|
# the call fills out_kinds in the 4-value XPATH_NODE space
|
|
868
896
|
# (ELEMENT/ATTRIBUTE/TEXT/OTHER). ELEMENT is the only value
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -190,12 +190,17 @@ class Leptris::XML::Node
|
|
|
190
190
|
def first_element_child
|
|
191
191
|
return @first_element_child if memo_hit?(@first_element_child_version)
|
|
192
192
|
ensure_alive!
|
|
193
|
+
# Raw pointer scan: non-element siblings are typed with one C
|
|
194
|
+
# call each — never wrapped, never cached — and the found
|
|
195
|
+
# element carries the ELEMENT hint into the wrap.
|
|
193
196
|
ptr = Leptris::XML::FFI.leptris_node_first_child(@c_ptr)
|
|
194
197
|
result = nil
|
|
195
198
|
until ptr.nil? || ptr.null?
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
result =
|
|
199
|
+
if Leptris::XML::FFI.leptris_node_get_type(ptr) ==
|
|
200
|
+
Leptris::XML::FFI::NODE_ELEMENT
|
|
201
|
+
result = Leptris::XML::Node.wrap(
|
|
202
|
+
ptr, @document, parent: as_element_or_self,
|
|
203
|
+
node_type: Leptris::XML::FFI::NODE_ELEMENT)
|
|
199
204
|
break
|
|
200
205
|
end
|
|
201
206
|
ptr = Leptris::XML::FFI.leptris_node_next_sibling(ptr)
|
|
@@ -208,12 +213,34 @@ class Leptris::XML::Node
|
|
|
208
213
|
end
|
|
209
214
|
|
|
210
215
|
def last_element_child
|
|
216
|
+
# Element receivers: the element-only batch fetches pointers
|
|
217
|
+
# without wrapping any text child; only the last is wrapped.
|
|
218
|
+
if is_a?(Leptris::XML::Element)
|
|
219
|
+
kids = Leptris::XML::FFI.fetch_element_children(@c_ptr)
|
|
220
|
+
return nil if kids.empty?
|
|
221
|
+
return Leptris::XML::Node.wrap(
|
|
222
|
+
kids.last, @document, parent: self,
|
|
223
|
+
node_type: Leptris::XML::FFI::NODE_ELEMENT)
|
|
224
|
+
end
|
|
211
225
|
children.reverse_each.find(&:element?)
|
|
212
226
|
end
|
|
213
227
|
|
|
214
228
|
def element_children
|
|
215
229
|
return @element_children if memo_hit?(@element_children_version)
|
|
216
|
-
|
|
230
|
+
ensure_alive!
|
|
231
|
+
# Element receivers ride the element-only batch: text/comment
|
|
232
|
+
# children are never wrapped (nor their get_type paid — the
|
|
233
|
+
# ELEMENT hint rides along). Other nodes keep the filter.
|
|
234
|
+
result =
|
|
235
|
+
if is_a?(Leptris::XML::Element)
|
|
236
|
+
parent = as_element_or_self
|
|
237
|
+
Leptris::XML::FFI.fetch_element_children(@c_ptr).map do |ptr|
|
|
238
|
+
Leptris::XML::Node.wrap(ptr, @document, parent: parent,
|
|
239
|
+
node_type: Leptris::XML::FFI::NODE_ELEMENT)
|
|
240
|
+
end
|
|
241
|
+
else
|
|
242
|
+
children.select(&:element?)
|
|
243
|
+
end
|
|
217
244
|
if @document
|
|
218
245
|
@element_children = result
|
|
219
246
|
@element_children_version = @document.version
|