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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42dec4b3313ea805ec507734e16907a059f894b85057eef2bd1f51833440ba8a
4
- data.tar.gz: 7163fb0b7ae9d1e5116f1d637bbb8905dfa94dd7a383c4c358174e82caa70cd0
3
+ metadata.gz: f91b02217a813c5c7c8eb267b6b82b3eb7689d857fbd76b7a8b108962d56d18c
4
+ data.tar.gz: c15ad7755d849144c45176d4e28f819ac090927c8a1e1d6ad58c89fe8a3006fe
5
5
  SHA512:
6
- metadata.gz: c9b449e54b8eecfea8ba76f07b8f7169e37c2ee1aee601e84b836052b05b1811857e7674b29fd93980754b82a53508146c26e3d7054003e82129cb536ec426aa
7
- data.tar.gz: b1dba812e5317c7f9c806183277816e06e4847ece3432f231991d3435c55ae03b43debf23d16d3199a71ce79e7ee66031a92fa33528faa5ce32473e37fa84f2e
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.27"
4
+ VERSION = "1.9.29"
5
5
  end
@@ -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
@@ -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
- node = Leptris::XML::Node.wrap(ptr, @document, parent: as_element_or_self)
197
- if node.element?
198
- result = node
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
- result = children.select(&:element?)
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
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.27
4
+ version: 1.9.29
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.