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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: efe9c8c8dcce3172320cb136460cbb5be0cc2ccbecdfc4fc121c0d497dbf1c3e
4
- data.tar.gz: 76d3ddb87586fd9bbffe7b6b19d4cce8054d7573f27cec48db41f4c4e16740f5
3
+ metadata.gz: cf80f624ccd37fd9495e26c150943bc07923b82fd3e96eacb0d115d2a2651f5d
4
+ data.tar.gz: f7974a661b342dacb962a6e92bc7c9634d3a606b0ab1c2300262c469420b4bd2
5
5
  SHA512:
6
- metadata.gz: 9693e537dbb31e512481cc007cc9552ae72f0c32e5c529c8c96027fc83bbe89014c84ff60805a90203991c0dbb33e35ac214f9b4460445bb5d493d5538cd7b03
7
- data.tar.gz: c810827e0feb0e1f0f5b85f1cf776ec208356149840a0ac7df90dce95113b67c7d7fe36948d4643117dd612c239273d64cd022dc029df727f01a37c857984145
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.10"
4
+ VERSION = "1.9.12"
5
5
  end
@@ -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
@@ -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
- @document&.readonly? && instance_variable_defined?(ivar)
271
+ instance_variable_defined?(ivar)
269
272
  end
270
273
 
271
274
  def as_element_or_self
@@ -41,7 +41,8 @@ class Leptris::XML::NodeSet
41
41
  end
42
42
 
43
43
  def length
44
- @result_ptr ? Leptris::XML::FFI.leptris_xpath_result_count(@result_ptr) : @array.length
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
- if @result_ptr
66
- # Batch-fetch all node pointers through the FFI seam
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
- yield Leptris::XML::Node.wrap(ptr, @document)
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
- yield Leptris::XML::Node.wrap(ptr, @document)
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
- out = []
96
- each { |n| out << n }
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; CStringArray owns the format, we slice into 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
- Leptris::XML::CStringArray.to_ruby(attrs_ptr).each_slice(2).to_a
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
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.10
4
+ version: 1.9.12
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Ribose Inc.