leptris 1.9.27 → 1.9.28

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: 5269fdd7c4c775fa14fa07fcaecd4356d6d7b0a04ea0de83f10b8efee3cae4df
4
+ data.tar.gz: 2de7f895e3e4fe444375fc800e867f751fd07d5598e09bde80d41d0df2c166bf
5
5
  SHA512:
6
- metadata.gz: c9b449e54b8eecfea8ba76f07b8f7169e37c2ee1aee601e84b836052b05b1811857e7674b29fd93980754b82a53508146c26e3d7054003e82129cb536ec426aa
7
- data.tar.gz: b1dba812e5317c7f9c806183277816e06e4847ece3432f231991d3435c55ae03b43debf23d16d3199a71ce79e7ee66031a92fa33528faa5ce32473e37fa84f2e
6
+ metadata.gz: 0e8e507336b7348bba069dba172842a76463939bd472483d5bbba3785d9aa2b8a0a928f484254ea642cfc1cedfc9b1fa29b02adaaecf5675c9881527c54a51ee
7
+ data.tar.gz: 81ac2512ce3d76fe7332e7ba53ca5f60d45358271b79ee47652f4043756d7914a0f24efb4031c30401dcf41a3d33adaea6fbfa52aa8993f19b746c11a4a63b52
data/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ 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.28] - 2026-08-28
9
+
10
+ ### Changed
11
+
12
+ - **Element-child reads pay only for elements** — the
13
+ interest-proportional principle applied to tree navigation:
14
+ - `#element_children` on element receivers rides the element-only
15
+ batch (`leptris_element_children`, opportunistic single
16
+ dispatch on the shared scratch): text/comment children are
17
+ never wrapped, and every kept element carries the ELEMENT hint
18
+ so the per-child `get_type` disappears. Cold pass over
19
+ big.xml's 25,000 items: 160 -> **117 ms per doc (-27%)**.
20
+ Non-element receivers (the document node) keep the filter.
21
+ - `#first_element_child` types candidate siblings with one raw
22
+ `get_type` each — nothing wrapped or cached until the element
23
+ is found (it previously full-wrapped leading text nodes).
24
+ - `#last_element_child` on element receivers wraps only the
25
+ batch's final pointer (it previously materialized every child,
26
+ text wraps included, to scan backwards). The first/last pair
27
+ across fresh docs: **-46%**.
28
+ - The all-kind `#children` path is untouched — the cold full-tree
29
+ walk battery stays at parity.
30
+
8
31
  ## [1.9.27] - 2026-08-28
9
32
 
10
33
  ### 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.28"
5
5
  end
@@ -863,6 +863,32 @@ module Leptris
863
863
  buffer.get_array_of_pointer(0, copied)
864
864
  end
865
865
 
866
+ # Element-only child batch (the mirror of fetch_children over
867
+ # leptris_element_children): element receivers skip wrapping —
868
+ # and paying for — the text/comment children they will filter
869
+ # out anyway; every wrap carries the ELEMENT hint so the
870
+ # per-child get_type dispatch disappears too.
871
+ def self.fetch_element_children(el_ptr)
872
+ scratch = (Thread.current[:leptris_scratch] ||= {})
873
+ buffer = scratch[:pointers]
874
+ if buffer.nil?
875
+ buffer = scratch[:pointers] =
876
+ ::FFI::MemoryPointer.new(:pointer, FETCH_INITIAL)
877
+ end
878
+ cap = buffer.size / PTR_BYTES
879
+ copied = leptris_element_children(el_ptr, buffer, cap)
880
+ if copied == cap
881
+ total = leptris_element_children(el_ptr, nil, 0)
882
+ if total > cap
883
+ buffer.free
884
+ buffer = scratch[:pointers] =
885
+ ::FFI::MemoryPointer.new(:pointer, total)
886
+ copied = leptris_element_children(el_ptr, buffer, total)
887
+ end
888
+ end
889
+ buffer.get_array_of_pointer(0, copied)
890
+ end
891
+
866
892
  # XPath result-set batch (leptris_xpath_result_get_nodes_ex):
867
893
  # the call fills out_kinds in the 4-value XPATH_NODE space
868
894
  # (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.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.