leptris 1.9.24 → 1.9.25
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 +28 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/ffi.rb +63 -29
- data/lib/leptris/xml/node.rb +10 -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: afa52b007ec23d222a3562207fcef3f9975f62c11378da91de2d238eff7a74be
|
|
4
|
+
data.tar.gz: 878468ec897ba3f9b60aca28e1379ac712e261c4500c4cc51c2a7379545730ee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a8caa0b2d6052ff1e72a1d23c59d1f283b887c852e7cb669fa8449d85438f8f89a2005726f96644f6bc46c880d717e54c922210fb9b638c2d890d6d0cdc57fbe
|
|
7
|
+
data.tar.gz: 19fab1e638971c2402b69e70447657c9dfe4d140ba55e588f09b4a394690b34c2e43ba46433615209f66be1660ee6755b83827e7b51e84516c5793b67f44d0d2
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ 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.25] - 2026-08-28
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Cold full-tree walk regression (1.9.1 → 1.9.24, ~1.4-1.5x)
|
|
13
|
+
recovered to parity**: the batch-fetch era (1.9.11-1.9.17) bought
|
|
14
|
+
its warm-path wins with per-call machinery the cold branch paid
|
|
15
|
+
on every children() — reproduced, bisected across every published
|
|
16
|
+
release, and profiled end to end this round. Raw cold walk of a
|
|
17
|
+
~4,200-node catalog (interleaved best-of-5): 1.9.24 5,441 µs ->
|
|
18
|
+
**4,519 µs (-17%)**, vs 1.9.1's 4,369 µs = parity. Through moxml
|
|
19
|
+
the same shape improves 20.1 ms -> 16.5 ms (-18%).
|
|
20
|
+
- `FFI.fetch_children` (and the XPath result batch) now use
|
|
21
|
+
thread-local scratch buffers that grow to the largest family
|
|
22
|
+
seen and are reused — no MemoryPointer allocation/free per
|
|
23
|
+
call, and materially less GC. The count query is paid only when
|
|
24
|
+
the buffer fills exactly (possible truncation): a family that
|
|
25
|
+
fits costs ONE dispatch, not two.
|
|
26
|
+
- `Node.wrap`'s miss path resolves the wrapper cache and pointer
|
|
27
|
+
address once instead of twice — a cold walk wraps every node
|
|
28
|
+
exactly once, so the cache is 100% misses and every redundant
|
|
29
|
+
resolution was pure cost. Identity semantics unchanged.
|
|
30
|
+
- Kept: the per-field version-stamped memo stores on writable
|
|
31
|
+
documents (ADR-0003's writable extension — the residual ~3% vs
|
|
32
|
+
1.9.1 is that feature working) and the batch architecture
|
|
33
|
+
itself (never more FFI dispatches than the linked-list walk,
|
|
34
|
+
and 2 vs 701 for the 700-child root).
|
|
35
|
+
|
|
8
36
|
## [1.9.24] - 2026-08-27
|
|
9
37
|
|
|
10
38
|
### Changed
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/ffi.rb
CHANGED
|
@@ -817,16 +817,33 @@ module Leptris
|
|
|
817
817
|
end
|
|
818
818
|
end
|
|
819
819
|
|
|
820
|
-
# All-kind child handle batch (libleptris 1.7.0)
|
|
821
|
-
#
|
|
822
|
-
#
|
|
820
|
+
# All-kind child handle batch (libleptris 1.7.0). Opportunistic
|
|
821
|
+
# single dispatch: the C call copies up to the capacity handed
|
|
822
|
+
# it and returns what fit, so the count query is paid only when
|
|
823
|
+
# the buffer fills exactly (a possible truncation) — families
|
|
824
|
+
# that fit the scratch buffer cost ONE dispatch, not two.
|
|
825
|
+
FETCH_INITIAL = 32
|
|
826
|
+
private_constant :FETCH_INITIAL
|
|
827
|
+
|
|
823
828
|
def self.fetch_children(c_ptr)
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
+
scratch = (Thread.current[:leptris_scratch] ||= {})
|
|
830
|
+
buffer = scratch[:pointers]
|
|
831
|
+
if buffer.nil?
|
|
832
|
+
buffer = scratch[:pointers] =
|
|
833
|
+
::FFI::MemoryPointer.new(:pointer, FETCH_INITIAL)
|
|
834
|
+
end
|
|
835
|
+
cap = buffer.size / PTR_BYTES
|
|
836
|
+
copied = leptris_node_children(c_ptr, buffer, cap)
|
|
837
|
+
if copied == cap
|
|
838
|
+
total = leptris_node_children(c_ptr, nil, 0)
|
|
839
|
+
if total > cap
|
|
840
|
+
buffer.free
|
|
841
|
+
buffer = scratch[:pointers] =
|
|
842
|
+
::FFI::MemoryPointer.new(:pointer, total)
|
|
843
|
+
copied = leptris_node_children(c_ptr, buffer, total)
|
|
844
|
+
end
|
|
829
845
|
end
|
|
846
|
+
buffer.get_array_of_pointer(0, copied)
|
|
830
847
|
end
|
|
831
848
|
|
|
832
849
|
# XPath result-set batch (leptris_xpath_result_get_nodes_ex):
|
|
@@ -840,18 +857,13 @@ module Leptris
|
|
|
840
857
|
|
|
841
858
|
def self.fetch_result_nodes(result_ptr, count)
|
|
842
859
|
return [[], nil] if count.zero?
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
[buffer.get_array_of_pointer(0, copied), hints]
|
|
851
|
-
ensure
|
|
852
|
-
kinds.free
|
|
853
|
-
end
|
|
854
|
-
end
|
|
860
|
+
buffer = scratch_pointers(count)
|
|
861
|
+
kinds = scratch_ints(count)
|
|
862
|
+
copied = leptris_xpath_result_get_nodes_ex(
|
|
863
|
+
result_ptr, buffer, kinds, count)
|
|
864
|
+
hints = kinds.get_array_of_int(0, copied)
|
|
865
|
+
.map { |k| XPATH_KIND_HINT[k] }
|
|
866
|
+
[buffer.get_array_of_pointer(0, copied), hints]
|
|
855
867
|
end
|
|
856
868
|
|
|
857
869
|
# Fragment parse with the status out-param read: returns
|
|
@@ -863,16 +875,38 @@ module Leptris
|
|
|
863
875
|
[raw, status.read_int]
|
|
864
876
|
end
|
|
865
877
|
|
|
866
|
-
#
|
|
867
|
-
#
|
|
868
|
-
#
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
878
|
+
# Thread-local scratch buffers for batch handle fetches (round
|
|
879
|
+
# XX): the per-call MemoryPointer new/free was pure cold-path
|
|
880
|
+
# overhead — two allocations and a GC visit per children() /
|
|
881
|
+
# result-set batch, ~15% of a cold full-tree walk between
|
|
882
|
+
# 1.9.1 and 1.9.24. The buffers grow to the largest request
|
|
883
|
+
# seen (bounded by the widest element or result set) and are
|
|
884
|
+
# reused. Safe: no Ruby reentrancy during the C call, and
|
|
885
|
+
# per-thread storage keeps concurrent fetches independent.
|
|
886
|
+
# Never freed — a few KB pinned per thread.
|
|
887
|
+
PTR_BYTES = ::FFI.type_size(:pointer)
|
|
888
|
+
private_constant :PTR_BYTES
|
|
889
|
+
INT_BYTES = ::FFI.type_size(:int)
|
|
890
|
+
private_constant :INT_BYTES
|
|
891
|
+
|
|
892
|
+
def self.scratch_pointers(count)
|
|
893
|
+
scratch = (Thread.current[:leptris_scratch] ||= {})
|
|
894
|
+
ptr = scratch[:pointers]
|
|
895
|
+
if ptr.nil? || ptr.size / PTR_BYTES < count
|
|
896
|
+
ptr&.free
|
|
897
|
+
ptr = scratch[:pointers] = ::FFI::MemoryPointer.new(:pointer, count)
|
|
898
|
+
end
|
|
899
|
+
ptr
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
def self.scratch_ints(count)
|
|
903
|
+
scratch = (Thread.current[:leptris_scratch] ||= {})
|
|
904
|
+
ptr = scratch[:ints]
|
|
905
|
+
if ptr.nil? || ptr.size / INT_BYTES < count
|
|
906
|
+
ptr&.free
|
|
907
|
+
ptr = scratch[:ints] = ::FFI::MemoryPointer.new(:int, count)
|
|
875
908
|
end
|
|
909
|
+
ptr
|
|
876
910
|
end
|
|
877
911
|
end
|
|
878
912
|
end
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -19,9 +19,15 @@ class Leptris::XML::Node
|
|
|
19
19
|
# Per-document weak-ref cache. Returns the existing wrapper when the
|
|
20
20
|
# same c_ptr is wrapped twice (common in children/sibling walks,
|
|
21
21
|
# repeated xpath queries, traverse-then-access patterns). The cache
|
|
22
|
-
# dies with the document so no stale entries.
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
# dies with the document so no stale entries. The miss path
|
|
23
|
+
# resolves the cache and address once — a cold walk wraps every
|
|
24
|
+
# node exactly once and pays both only on the store.
|
|
25
|
+
if document
|
|
26
|
+
cache = document.wrapper_cache
|
|
27
|
+
address = c_ptr.address
|
|
28
|
+
if (cached = cache[address])
|
|
29
|
+
return cached
|
|
30
|
+
end
|
|
25
31
|
end
|
|
26
32
|
|
|
27
33
|
node_type ||= Leptris::XML::FFI.leptris_node_get_type(c_ptr)
|
|
@@ -41,7 +47,7 @@ class Leptris::XML::Node
|
|
|
41
47
|
new(c_ptr, document, parent: parent, node_type: node_type)
|
|
42
48
|
end
|
|
43
49
|
|
|
44
|
-
|
|
50
|
+
cache[address] = node if document
|
|
45
51
|
node
|
|
46
52
|
end
|
|
47
53
|
|