leptris 1.9.163.5 → 1.9.163.6
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 +40 -0
- data/TODO.perf/12-document-lifetime-in-c.md +47 -0
- data/TODO.perf/13-structural-memos.md +49 -0
- data/TODO.perf/14-insert-family-c-bound.md +31 -0
- data/TODO.perf/15-compiled-expression-cache.md +35 -0
- data/ext/leptris/native/native.c +164 -2
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/document.rb +35 -3
- data/lib/leptris/xml/element.rb +51 -3
- data/lib/leptris/xml/node.rb +82 -7
- data/lib/leptris/xml/searchable.rb +28 -0
- data/lib/leptris/xml/xpath.rb +8 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed19c6383e8e83433ffcc99c8843337eb64ba48ac4f40349d5c5e0a537a31ab9
|
|
4
|
+
data.tar.gz: 3096440c8ba68d7eb54bfb2fd133098c795b025ec74389d09fd09cacd12f6311
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5bef6e81be43ea7d536252201a4a59477d6c296a14ef186f9adb28fd304a13fb533218d46ac4d2245e55b724a62d13ab6d39088f82a750bbdfdb797c26c64c93
|
|
7
|
+
data.tar.gz: 9a61e4f09d0df6fc73479e1a25b83a146e77e28bae50b1d4dd82e5ed3b5f10be0e9b879e866d70f1207ac0abeda302d32f92c7b7efb4a27da8ee1958c6c63edd
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,46 @@ 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.163.6] - 2026-09-15
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Document lifetime in C (TODO.perf/12)**: a TypedData DocHandle
|
|
13
|
+
(dfree -> leptris_document_free) replaces the ObjectSpace
|
|
14
|
+
finalizer whenever the native layer is enabled — no Ruby
|
|
15
|
+
finalizer invocation, no FFI dispatch from finalizer context,
|
|
16
|
+
for every parsed and created document. `Document.create` runs
|
|
17
|
+
as one C dispatch (engine create + ivar-seeded wrapper +
|
|
18
|
+
handle). The proc finalizer remains for `LEPTRIS_NO_NATIVE`.
|
|
19
|
+
- **Structural memos (TODO.perf/13)**: `Document#root`,
|
|
20
|
+
`Node#parent`, `#next_sibling`, `#previous_sibling` keep
|
|
21
|
+
version-stamped memos (~72-82ns hits vs ~300-500ns deriving,
|
|
22
|
+
measured under host load 17). Fixes a real FFI-mode bug: the
|
|
23
|
+
children walk seeded `@parent`, and a later move left the stale
|
|
24
|
+
parent answering. Cross-document moves clear the moved node's
|
|
25
|
+
stamps and advance the source document's version; the
|
|
26
|
+
adoption-lift ancestor walk derives unstamped (a stamped
|
|
27
|
+
derive between the version bump and the engine move would
|
|
28
|
+
record post-bump versions carrying pre-move truth); scope-owned
|
|
29
|
+
iterparse elements never memoize.
|
|
30
|
+
- **C-bound insert family (TODO.perf/14)**: `prepend_child`,
|
|
31
|
+
`add_next_sibling`, `add_previous_sibling` dispatch through
|
|
32
|
+
`Native.insert_binding_child` (gates + predicate + version bump
|
|
33
|
+
+ engine insert in one call; lift fallback preserved). The
|
|
34
|
+
cold `Element#[]` first-touch fill rides the native face.
|
|
35
|
+
- **Compiled-expression cache (TODO.perf/15)**: plain
|
|
36
|
+
`xpath`/`at_xpath` evaluate a bounded LRU (64) of compiled
|
|
37
|
+
handles — repeat expressions measured 3.7µs -> ~1.9µs (~2x);
|
|
38
|
+
CSS rides it automatically. Failed compiles never cache;
|
|
39
|
+
version-pinned and namespace-bound entries unchanged.
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- `Leptris::XML::DocHandle` (internal), `Native.doc_handle_attach/
|
|
44
|
+
release/create_binding_document/insert_binding_child`,
|
|
45
|
+
`XPath#eval_ptrs`, `Searchable.compiled_expression`, and
|
|
46
|
+
`Node#unstamped_parent` (protected ancestor-walk seam).
|
|
47
|
+
|
|
8
48
|
## [1.9.163.5] - 2026-09-15
|
|
9
49
|
|
|
10
50
|
### Changed
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# 12 — Document lifetime in C: TypedData handle replaces the ObjectSpace finalizer
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.6)
|
|
4
|
+
|
|
5
|
+
Every document (parsed AND created) registers an ObjectSpace
|
|
6
|
+
finalizer: a per-document proc allocation (~110ns), the
|
|
7
|
+
define_finalizer call (~315ns), the Freed struct, and — the real
|
|
8
|
+
cost — a Ruby finalizer invocation at GC time that must dispatch
|
|
9
|
+
back through FFI to leptris_document_free (~1-2µs per document
|
|
10
|
+
inside GC pauses). Fresh-document build loops measured 7.1µs per
|
|
11
|
+
Document.create in-loop, ~4-5µs of it GC/finalizer amortization
|
|
12
|
+
(stackprof: GC ~19% of a build loop, define_finalizer+proc in the
|
|
13
|
+
top frames).
|
|
14
|
+
|
|
15
|
+
Fix: a TypedData DocHandle (struct { void *doc; }) whose dfree
|
|
16
|
+
calls leptris_document_free directly — no Ruby finalizer, no FFI
|
|
17
|
+
from finalizer context, no proc. The handle is referenced only by
|
|
18
|
+
the Document's @doc_handle ivar, so its lifetime IS the
|
|
19
|
+
Document's. Document#free keeps its explicit path (FFI free +
|
|
20
|
+
release the handle's pointer so dfree no-ops — the same
|
|
21
|
+
double-free protocol Freed enforces today, in C). The proc
|
|
22
|
+
finalizer stays for LEPTRIS_NO_NATIVE mode.
|
|
23
|
+
|
|
24
|
+
Companion: Native.create_binding_document — the full
|
|
25
|
+
Document.create in one C dispatch (engine create + wrapper
|
|
26
|
+
allocation + @c_ptr/@c_address/@freed/@readonly/@version + handle
|
|
27
|
+
attach). Document.create today pays FFI marshaling + wrap frames
|
|
28
|
+
(~250-600ns) on top of the engine call.
|
|
29
|
+
|
|
30
|
+
Gates: lifetime_contract_spec + memory_spec green in both modes;
|
|
31
|
+
double-free impossible (free → dfire no-op, finalizer-mode
|
|
32
|
+
unchanged); Document.create and fresh-doc build rows improve.
|
|
33
|
+
|
|
34
|
+
## Outcome (1.9.163.6)
|
|
35
|
+
|
|
36
|
+
DocHandle TypedData (struct { void *doc; }, RUBY_TYPED_FREE_IMMEDIATELY,
|
|
37
|
+
dfree -> leptris_document_free) attached as @doc_handle on every
|
|
38
|
+
wrap() when native is enabled; Document#free detaches it (dfree
|
|
39
|
+
no-ops). Native.create_binding_document does engine create +
|
|
40
|
+
ivar-seeded wrapper + handle in one dispatch (Document.create
|
|
41
|
+
routes through it). Freed stays the shared free-state; the proc
|
|
42
|
+
finalizer remains for LEPTRIS_NO_NATIVE. 3,000-doc GC-reclamation
|
|
43
|
+
and explicit-free-then-GC specs green; 647/0 both modes. Measured
|
|
44
|
+
under host load 14-30: GC-amortized Document.create ~5.0us (the
|
|
45
|
+
Ruby-finalizer invocation and FFI-dispatch-at-GC are gone from
|
|
46
|
+
every document lifecycle; clean-load numbers pending the battery
|
|
47
|
+
— the box never dropped under load 13 this session).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# 13 — Version-stamped structural memos: root/parent/siblings (+ FFI-mode stale-@parent fix)
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.6)
|
|
4
|
+
|
|
5
|
+
Document#root re-derives (FFI + wrap) on every call — every
|
|
6
|
+
pipeline entry pays it. Node#parent and the sibling hops
|
|
7
|
+
(next_sibling/previous_sibling) pay FFI + Node.wrap per call, so
|
|
8
|
+
Nokogiri-style sibling-advancement loops pay ~300ns/hop.
|
|
9
|
+
|
|
10
|
+
BUG found while designing this: in FFI mode the children walk
|
|
11
|
+
seeds @parent, and a later move (add_child to another parent)
|
|
12
|
+
leaves the stale @parent — child.parent answers the OLD parent.
|
|
13
|
+
Native mode masks it (the bulk path does not seed @parent). The
|
|
14
|
+
version-stamped memo fixes both modes: derive once, stamp with
|
|
15
|
+
the owning document's mutation version, re-derive after any
|
|
16
|
+
mutation that bumps it.
|
|
17
|
+
|
|
18
|
+
Design: parent memoizes only for document-owned nodes
|
|
19
|
+
(scope-owned iterparse elements keep deriving — a scope element
|
|
20
|
+
adopted into a document moves without its scope's version
|
|
21
|
+
advancing, so a stamp would lie). Cross-document adoption bumps
|
|
22
|
+
BOTH documents' versions (the source document's other nodes hold
|
|
23
|
+
sibling/parent stamps that the move invalidates); the moved
|
|
24
|
+
node's stamps clear at the mutation site. next_sibling /
|
|
25
|
+
previous_sibling memoize the same way. Document#root memoizes
|
|
26
|
+
against the document's own version (root= bumps it; free clears).
|
|
27
|
+
|
|
28
|
+
Gates: parent correct after same-doc and cross-doc moves in BOTH
|
|
29
|
+
modes (the FFI staleness becomes a spec); sibling chains correct
|
|
30
|
+
around unlink/insert; root identity stable; traversal-loop rows
|
|
31
|
+
improve.
|
|
32
|
+
|
|
33
|
+
## Outcome (1.9.163.6)
|
|
34
|
+
|
|
35
|
+
Version-stamped memos on Document#root (pure ivar compare, ~82ns
|
|
36
|
+
under load 17), Node#parent / next_sibling / previous_sibling
|
|
37
|
+
(~72-76ns memo hits vs ~300-500ns deriving; @structure_memoizable
|
|
38
|
+
computed once at construction — including the C-constructed
|
|
39
|
+
wrappers — keeping the hit path free of method dispatch). The
|
|
40
|
+
FFI-mode stale-@parent bug is FIXED (spec-pinned: parent correct
|
|
41
|
+
after same-doc and cross-doc moves; the seed stamps only
|
|
42
|
+
constructor-KNOWN parents — nil constructor parent means unknown,
|
|
43
|
+
because the engine may attach during creation, e.g. create_child).
|
|
44
|
+
Cross-document moves clear the moved node's stamps and advance
|
|
45
|
+
the SOURCE document's version. The adoption-lift ancestor walk
|
|
46
|
+
uses Node#unstamped_parent: deriving a stamped parent between the
|
|
47
|
+
version bump and the engine move would record post-bump versions
|
|
48
|
+
carrying pre-move truth. Scope-owned iterparse elements never
|
|
49
|
+
memoize.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# 14 — C-bound insert family + cold-[] native fill
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.6)
|
|
4
|
+
|
|
5
|
+
add_child and []= are C-bound (TODO.perf/08-09); prepend_child,
|
|
6
|
+
add_next_sibling, and add_previous_sibling still run the full
|
|
7
|
+
Ruby path (ensure_writable + eager-namespace guard ordering +
|
|
8
|
+
FFI). One C face with a mode int covers all engine insertions
|
|
9
|
+
(append/prepend/insert_after/insert_before — same
|
|
10
|
+
int(void*, void*) ABI): gates + predicate + version bump + engine
|
|
11
|
+
insert in one dispatch, Qnil fallback when the child needs the
|
|
12
|
+
namespace lift, exactly like append_binding_child.
|
|
13
|
+
|
|
14
|
+
Also: the completely-cold branch of Element#[] fills through
|
|
15
|
+
leptris_element_attribute (FFI) while the partial-memo branch
|
|
16
|
+
above it uses the native face — the cold path should ride the
|
|
17
|
+
native face too (~150ns per first-touch attribute read).
|
|
18
|
+
|
|
19
|
+
Gates: insert semantics unchanged (specs exist for sibling
|
|
20
|
+
inserts); namespaced children still lift; readonly raises.
|
|
21
|
+
|
|
22
|
+
## Outcome (1.9.163.6)
|
|
23
|
+
|
|
24
|
+
Native.insert_binding_child(document, anchor, child, mode): 1
|
|
25
|
+
prepend / 2 after / 3 before — gates + predicate + version bump +
|
|
26
|
+
engine insert in one dispatch, Qnil fallback to the Ruby lift
|
|
27
|
+
path. prepend_child / add_next_sibling / add_previous_sibling
|
|
28
|
+
carry the fast branch + cross-document invalidation. The
|
|
29
|
+
completely-cold Element#[] first-touch fill rides the native face
|
|
30
|
+
(matching the partial-memo branch). Specs: ordering, lift
|
|
31
|
+
fallback, readonly raise.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# 15 — Compiled-expression cache for Searchable#xpath / #at_xpath
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.6)
|
|
4
|
+
|
|
5
|
+
Searchable#xpath passes the expression STRING to
|
|
6
|
+
leptris_xpath_eval each call. The engine caches compiled
|
|
7
|
+
expressions by string (measured: repeat-string eval 7.0µs vs
|
|
8
|
+
varied-string 34.98µs — the ~28µs is per-call parsing), but the
|
|
9
|
+
string path still pays cache lookup + hashing per call. A
|
|
10
|
+
compiled handle evaluated directly measured 4.58µs on the same
|
|
11
|
+
shape — 34% under the string path.
|
|
12
|
+
|
|
13
|
+
The binding already exposes XPath.compile (parse once, eval
|
|
14
|
+
many). Add a bounded LRU keyed on the expression string inside
|
|
15
|
+
Searchable: plain (no version pin, no ns bindings) xpath/at_xpath
|
|
16
|
+
evaluations route through cached compiled handles. Cache only
|
|
17
|
+
successful compiles; bounded (~64 entries, Hash#shift eviction);
|
|
18
|
+
GVL makes the Hash ops safe.
|
|
19
|
+
|
|
20
|
+
Gates: xpath/at_xpath results identical (nodesets, scalars,
|
|
21
|
+
empty results); invalid expressions still raise XPathError;
|
|
22
|
+
namespace-bound and version-pinned paths unchanged; repeat-call
|
|
23
|
+
rows improve ~30%.
|
|
24
|
+
|
|
25
|
+
## Outcome (1.9.163.6)
|
|
26
|
+
|
|
27
|
+
Searchable.compiled_expression: bounded LRU (64 entries, Hash#shift
|
|
28
|
+
eviction, LRU refresh on hit) keyed on the expression string;
|
|
29
|
+
plain xpath/at_xpath evaluate cached compiled handles via
|
|
30
|
+
XPath#eval_ptrs (leptris_xpath_compiled_eval). Failed compiles
|
|
31
|
+
never cache (fall back to the string entry so the error surface
|
|
32
|
+
is unchanged); version-pinned and namespace-bound paths keep
|
|
33
|
+
their dedicated entries. Measured: repeat at_xpath 3.7us ->
|
|
34
|
+
1.9us under load 17-30 (~2x); CSS rides it automatically
|
|
35
|
+
(deterministic translation -> same key).
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -74,6 +74,7 @@ static elem_create_fn f_elem_create;
|
|
|
74
74
|
static text_create_fn f_text_create;
|
|
75
75
|
static create_child_fn f_create_child;
|
|
76
76
|
static append_child_fn f_append_child;
|
|
77
|
+
static append_child_fn f_prepend_child, f_insert_after, f_insert_before;
|
|
77
78
|
static set_attr_fn f_set_attr;
|
|
78
79
|
static set_root_fn f_set_root;
|
|
79
80
|
static doc_free_fn f_doc_free;
|
|
@@ -171,13 +172,17 @@ static void resolve_symbols(const char *lib_path)
|
|
|
171
172
|
f_text_create = (text_create_fn)lib_sym(h, "leptris_text_node_create");
|
|
172
173
|
f_create_child = (create_child_fn)lib_sym(h, "leptris_element_create_child");
|
|
173
174
|
f_append_child = (append_child_fn)lib_sym(h, "leptris_element_append_child");
|
|
175
|
+
f_prepend_child = (append_child_fn)lib_sym(h, "leptris_element_prepend_child");
|
|
176
|
+
f_insert_after = (append_child_fn)lib_sym(h, "leptris_element_insert_after");
|
|
177
|
+
f_insert_before = (append_child_fn)lib_sym(h, "leptris_element_insert_before");
|
|
174
178
|
f_set_attr = (set_attr_fn)lib_sym(h, "leptris_element_set_attribute");
|
|
175
179
|
f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
|
|
176
180
|
f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
|
|
177
181
|
if (!f_elem_name || !f_text_content || !f_attr ||
|
|
178
182
|
!f_children_ex || !f_node_type || !f_next_sibling || !f_parent ||
|
|
179
183
|
!f_element_text || !f_doc_create || !f_elem_create || !f_text_create ||
|
|
180
|
-
!f_create_child || !f_append_child || !
|
|
184
|
+
!f_create_child || !f_append_child || !f_prepend_child ||
|
|
185
|
+
!f_insert_after || !f_insert_before || !f_set_attr || !f_set_root ||
|
|
181
186
|
!f_doc_free ||
|
|
182
187
|
!f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
|
|
183
188
|
!f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
|
|
@@ -602,6 +607,48 @@ static VALUE nf_set_binding_attribute(VALUE self, VALUE document,
|
|
|
602
607
|
return INT2FIX(st);
|
|
603
608
|
}
|
|
604
609
|
|
|
610
|
+
/* C-bound insert family (TODO.perf/14): prepend / insert-after /
|
|
611
|
+
* insert-before share append's gates + predicate + bump shape.
|
|
612
|
+
* mode: 1 prepend, 2 after, 3 before (anchor = receiver). */
|
|
613
|
+
static VALUE nf_insert_binding_child(VALUE self, VALUE document,
|
|
614
|
+
VALUE anchor_addr, VALUE child_addr,
|
|
615
|
+
VALUE mode)
|
|
616
|
+
{
|
|
617
|
+
void *anchor, *child;
|
|
618
|
+
const char *uri;
|
|
619
|
+
int st, m;
|
|
620
|
+
|
|
621
|
+
(void)self;
|
|
622
|
+
resolve_binding_classes();
|
|
623
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
624
|
+
rb_raise(c_use_after_free_error,
|
|
625
|
+
"owning document has been freed");
|
|
626
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
627
|
+
rb_raise(c_readonly_error,
|
|
628
|
+
"document is readonly — mutation attempted");
|
|
629
|
+
anchor = (void *)(uintptr_t)NUM2ULL(anchor_addr);
|
|
630
|
+
child = (void *)(uintptr_t)NUM2ULL(child_addr);
|
|
631
|
+
m = FIX2INT(mode);
|
|
632
|
+
if (f_node_type(child) == NT_ELEMENT) {
|
|
633
|
+
if (f_elem_ns_count(child) > 0)
|
|
634
|
+
return Qnil;
|
|
635
|
+
uri = f_elem_ns(child);
|
|
636
|
+
if (uri && *uri)
|
|
637
|
+
return Qnil;
|
|
638
|
+
}
|
|
639
|
+
rb_ivar_set(document, id_iv_version,
|
|
640
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
641
|
+
switch (m) {
|
|
642
|
+
case 1: st = f_prepend_child(anchor, child); break;
|
|
643
|
+
case 2: st = f_insert_after(anchor, child); break;
|
|
644
|
+
case 3: st = f_insert_before(anchor, child); break;
|
|
645
|
+
default:
|
|
646
|
+
rb_raise(rb_eArgError, "invalid insert mode %d", m);
|
|
647
|
+
return Qnil;
|
|
648
|
+
}
|
|
649
|
+
return INT2FIX(st);
|
|
650
|
+
}
|
|
651
|
+
|
|
605
652
|
/* Builder factories (#149): create in C, wrap as NativeNode — no
|
|
606
653
|
* FFI::Pointer, no Ruby wrap_fresh path. */
|
|
607
654
|
static VALUE nn_create_element(VALUE klass, VALUE document, VALUE name)
|
|
@@ -726,7 +773,7 @@ static VALUE nn_address(VALUE self)
|
|
|
726
773
|
* disappear. */
|
|
727
774
|
static VALUE c_b_element = Qundef, c_b_text, c_b_comment, c_b_cdata,
|
|
728
775
|
c_b_pi, c_b_node, c_b_result_text, c_b_result_attr,
|
|
729
|
-
c_b_attr, c_ffi_pointer;
|
|
776
|
+
c_b_attr, c_ffi_pointer, c_b_document, c_b_freed;
|
|
730
777
|
static ID id_ptr_new;
|
|
731
778
|
|
|
732
779
|
/* Binding classes resolve LAZILY: Init_native can run before the
|
|
@@ -746,6 +793,8 @@ static void resolve_binding_classes(void)
|
|
|
746
793
|
c_b_result_attr = rb_path2class("Leptris::XML::ResultAttr");
|
|
747
794
|
c_ffi_pointer = rb_path2class("FFI::Pointer");
|
|
748
795
|
c_b_attr = rb_path2class("Leptris::XML::Attr");
|
|
796
|
+
c_b_document = rb_path2class("Leptris::XML::Document");
|
|
797
|
+
c_b_freed = rb_path2class("Leptris::XML::Document::Freed");
|
|
749
798
|
id_ptr_new = rb_intern("new");
|
|
750
799
|
rb_gc_register_mark_object(c_b_element);
|
|
751
800
|
rb_gc_register_mark_object(c_b_text);
|
|
@@ -757,6 +806,8 @@ static void resolve_binding_classes(void)
|
|
|
757
806
|
rb_gc_register_mark_object(c_b_result_attr);
|
|
758
807
|
rb_gc_register_mark_object(c_ffi_pointer);
|
|
759
808
|
rb_gc_register_mark_object(c_b_attr);
|
|
809
|
+
rb_gc_register_mark_object(c_b_document);
|
|
810
|
+
rb_gc_register_mark_object(c_b_freed);
|
|
760
811
|
}
|
|
761
812
|
|
|
762
813
|
static VALUE binding_klass_for(int kind)
|
|
@@ -801,6 +852,7 @@ static VALUE bulk_children_impl(VALUE document, VALUE parent_addr,
|
|
|
801
852
|
rb_iv_set(node, "@c_ptr", ptr);
|
|
802
853
|
rb_iv_set(node, "@document", document);
|
|
803
854
|
rb_iv_set(node, "@parent", Qnil);
|
|
855
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
804
856
|
rb_iv_set(node, "@node_type", INT2FIX(kinds[i]));
|
|
805
857
|
rb_hash_aset(cache, key, node);
|
|
806
858
|
}
|
|
@@ -874,6 +926,7 @@ static VALUE nf_bulk_xpath(VALUE self, VALUE document, VALUE result_ptr_val)
|
|
|
874
926
|
rb_iv_set(node, "@c_ptr", ptr);
|
|
875
927
|
rb_iv_set(node, "@document", document);
|
|
876
928
|
rb_iv_set(node, "@parent", Qnil);
|
|
929
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
877
930
|
rb_iv_set(node, "@node_type", INT2FIX(0));
|
|
878
931
|
rb_hash_aset(cache, key, node);
|
|
879
932
|
}
|
|
@@ -913,6 +966,7 @@ static VALUE nf_bulk_xpath(VALUE self, VALUE document, VALUE result_ptr_val)
|
|
|
913
966
|
rb_iv_set(node, "@c_ptr", ptr);
|
|
914
967
|
rb_iv_set(node, "@document", document);
|
|
915
968
|
rb_iv_set(node, "@parent", Qnil);
|
|
969
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
916
970
|
rb_iv_set(node, "@node_type", INT2FIX(nt));
|
|
917
971
|
rb_hash_aset(cache, key, node);
|
|
918
972
|
}
|
|
@@ -972,6 +1026,7 @@ static VALUE nf_create_binding_element(VALUE self, VALUE document,
|
|
|
972
1026
|
ULL2NUM((uint64_t)(uintptr_t)ptr)));
|
|
973
1027
|
rb_iv_set(node, "@document", document);
|
|
974
1028
|
rb_iv_set(node, "@parent", Qnil);
|
|
1029
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
975
1030
|
rb_iv_set(node, "@node_type", INT2FIX(0));
|
|
976
1031
|
cache = binding_cache_of(document);
|
|
977
1032
|
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
@@ -996,6 +1051,7 @@ static VALUE nf_create_binding_text(VALUE self, VALUE document,
|
|
|
996
1051
|
ULL2NUM((uint64_t)(uintptr_t)ptr)));
|
|
997
1052
|
rb_iv_set(node, "@document", document);
|
|
998
1053
|
rb_iv_set(node, "@parent", Qnil);
|
|
1054
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
999
1055
|
rb_iv_set(node, "@node_type", INT2FIX(1));
|
|
1000
1056
|
cache = binding_cache_of(document);
|
|
1001
1057
|
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
@@ -1113,6 +1169,99 @@ static VALUE nf_fast_element_xml(VALUE self, VALUE addr,
|
|
|
1113
1169
|
* predicate answers in ONE dispatch: false when the node carries
|
|
1114
1170
|
* no namespace declarations AND its name has no prefix — the
|
|
1115
1171
|
* common programmatic-build shape (bare names, no namespaces). */
|
|
1172
|
+
/* ---- Document lifetime in C (TODO.perf/12) ----------------------
|
|
1173
|
+
* A TypedData handle holding the C document pointer, referenced
|
|
1174
|
+
* only by the binding Document's @doc_handle ivar — its lifetime
|
|
1175
|
+
* IS the document's. dfree releases the C document directly: no
|
|
1176
|
+
* Ruby finalizer, no FFI dispatch from finalizer context.
|
|
1177
|
+
* Document#free releases the pointer first, so dfree no-ops —
|
|
1178
|
+
* the same double-free protocol the Freed struct enforces on the
|
|
1179
|
+
* Ruby-finalizer path (which stays for LEPTRIS_NO_NATIVE). */
|
|
1180
|
+
struct doc_handle {
|
|
1181
|
+
void *doc;
|
|
1182
|
+
};
|
|
1183
|
+
|
|
1184
|
+
static void dh_free(void *p)
|
|
1185
|
+
{
|
|
1186
|
+
struct doc_handle *h = p;
|
|
1187
|
+
if (h->doc) {
|
|
1188
|
+
f_doc_free(h->doc);
|
|
1189
|
+
h->doc = NULL;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
static size_t dh_size(const void *p)
|
|
1194
|
+
{
|
|
1195
|
+
(void)p;
|
|
1196
|
+
return sizeof(struct doc_handle);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
static const rb_data_type_t dh_type = {
|
|
1200
|
+
"Leptris/XML/DocHandle",
|
|
1201
|
+
{ 0, dh_free, dh_size, },
|
|
1202
|
+
0, 0, RUBY_TYPED_FREE_IMMEDIATELY
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
static VALUE c_doc_handle;
|
|
1206
|
+
static ID id_iv_doc_handle, id_freed_new, id_alive;
|
|
1207
|
+
|
|
1208
|
+
static VALUE nf_doc_handle_attach(VALUE self, VALUE document)
|
|
1209
|
+
{
|
|
1210
|
+
struct doc_handle *h;
|
|
1211
|
+
VALUE handle;
|
|
1212
|
+
|
|
1213
|
+
(void)self;
|
|
1214
|
+
handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
|
|
1215
|
+
&dh_type, h);
|
|
1216
|
+
h->doc = doc_ptr_of(document);
|
|
1217
|
+
rb_ivar_set(document, id_iv_doc_handle, handle);
|
|
1218
|
+
return handle;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/* Document#free already released the C memory through the FFI
|
|
1222
|
+
* seam; detach so the GC-pass dfree no-ops. */
|
|
1223
|
+
static VALUE nf_doc_handle_release(VALUE self, VALUE document)
|
|
1224
|
+
{
|
|
1225
|
+
VALUE handle = rb_ivar_get(document, id_iv_doc_handle);
|
|
1226
|
+
(void)self;
|
|
1227
|
+
if (handle != Qnil) {
|
|
1228
|
+
struct doc_handle *h;
|
|
1229
|
+
TypedData_Get_Struct(handle, struct doc_handle, &dh_type, h);
|
|
1230
|
+
h->doc = NULL;
|
|
1231
|
+
}
|
|
1232
|
+
return Qnil;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/* The full Document.create in one dispatch: engine create,
|
|
1236
|
+
* binding wrapper (ivar-seeded, bypassing initialize), and the
|
|
1237
|
+
* lifetime handle. Returns Qnil when the engine refuses. */
|
|
1238
|
+
static VALUE nf_create_binding_document(VALUE self)
|
|
1239
|
+
{
|
|
1240
|
+
void *doc;
|
|
1241
|
+
VALUE document, addr, handle, freed;
|
|
1242
|
+
struct doc_handle *h;
|
|
1243
|
+
|
|
1244
|
+
(void)self;
|
|
1245
|
+
resolve_binding_classes();
|
|
1246
|
+
doc = f_doc_create();
|
|
1247
|
+
if (!doc)
|
|
1248
|
+
return Qnil;
|
|
1249
|
+
addr = ULL2NUM((uint64_t)(uintptr_t)doc);
|
|
1250
|
+
document = rb_obj_alloc(c_b_document);
|
|
1251
|
+
rb_iv_set(document, "@c_ptr",
|
|
1252
|
+
rb_funcall(c_ffi_pointer, id_ptr_new, 1, addr));
|
|
1253
|
+
rb_iv_set(document, "@c_address", addr);
|
|
1254
|
+
freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
|
|
1255
|
+
rb_iv_set(document, "@freed", freed);
|
|
1256
|
+
rb_iv_set(document, "@readonly", Qfalse);
|
|
1257
|
+
rb_iv_set(document, "@version", INT2FIX(0));
|
|
1258
|
+
handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
|
|
1259
|
+
&dh_type, h);
|
|
1260
|
+
h->doc = doc;
|
|
1261
|
+
rb_ivar_set(document, id_iv_doc_handle, handle);
|
|
1262
|
+
return document;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1116
1265
|
/* ---- Address-based fast readers (TODO.perf/01) -----------------
|
|
1117
1266
|
* The DEFAULT binding classes call these when the bundle is
|
|
1118
1267
|
* loaded: one C-API dispatch + rb_utf8_str_new_cstr — no FFI
|
|
@@ -1198,6 +1347,9 @@ void Init_native(void)
|
|
|
1198
1347
|
id_iv_nn_content_ver = rb_intern("@nn_content_ver");
|
|
1199
1348
|
id_iv_nn_attrs = rb_intern("@nn_attrs");
|
|
1200
1349
|
id_iv_nn_attrs_ver = rb_intern("@nn_attrs_ver");
|
|
1350
|
+
id_iv_doc_handle = rb_intern("@doc_handle");
|
|
1351
|
+
id_freed_new = rb_intern("new");
|
|
1352
|
+
id_alive = rb_intern("alive");
|
|
1201
1353
|
c_readonly_error = rb_path2class("Leptris::XML::ReadOnlyError");
|
|
1202
1354
|
c_use_after_free_error =
|
|
1203
1355
|
rb_path2class("Leptris::XML::UseAfterFreeError");
|
|
@@ -1240,6 +1392,16 @@ void Init_native(void)
|
|
|
1240
1392
|
nf_append_binding_child, 3);
|
|
1241
1393
|
rb_define_module_function(m_native, "set_binding_attribute",
|
|
1242
1394
|
nf_set_binding_attribute, 4);
|
|
1395
|
+
rb_define_module_function(m_native, "insert_binding_child",
|
|
1396
|
+
nf_insert_binding_child, 4);
|
|
1397
|
+
rb_define_module_function(m_native, "doc_handle_attach",
|
|
1398
|
+
nf_doc_handle_attach, 1);
|
|
1399
|
+
rb_define_module_function(m_native, "doc_handle_release",
|
|
1400
|
+
nf_doc_handle_release, 1);
|
|
1401
|
+
rb_define_module_function(m_native, "create_binding_document",
|
|
1402
|
+
nf_create_binding_document, 0);
|
|
1403
|
+
c_doc_handle = rb_define_class_under(m_xml, "DocHandle", rb_cObject);
|
|
1404
|
+
rb_undef_alloc_func(c_doc_handle);
|
|
1243
1405
|
rb_define_module_function(m_native, "bulk_attributes", nf_bulk_attributes, 1);
|
|
1244
1406
|
rb_define_module_function(m_native, "bulk_attr_faces",
|
|
1245
1407
|
nf_bulk_attr_faces, 2);
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -129,6 +129,15 @@ class Leptris::XML::Document
|
|
|
129
129
|
# pool. Elements for the tree are created against it via
|
|
130
130
|
# #create_element and friends, then attached with #root=.
|
|
131
131
|
def self.create
|
|
132
|
+
# One C dispatch (TODO.perf/12): engine create + wrapper
|
|
133
|
+
# (ivar-seeded) + lifetime handle. The FFI+wrap shape stays
|
|
134
|
+
# for LEPTRIS_NO_NATIVE.
|
|
135
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
136
|
+
doc = Leptris::XML::Native.create_binding_document
|
|
137
|
+
raise Leptris::XML::Error,
|
|
138
|
+
"leptris_document_create failed" if doc.nil?
|
|
139
|
+
return doc
|
|
140
|
+
end
|
|
132
141
|
raw = Leptris::XML::FFI.leptris_document_create
|
|
133
142
|
raise Leptris::XML::Error,
|
|
134
143
|
"leptris_document_create failed" if raw.null?
|
|
@@ -178,7 +187,14 @@ class Leptris::XML::Document
|
|
|
178
187
|
ptr = ::FFI::Pointer.new(addr)
|
|
179
188
|
freed = Freed.new(:alive)
|
|
180
189
|
doc = new(ptr, freed)
|
|
181
|
-
|
|
190
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
191
|
+
# TypedData dfree owns the release at GC (TODO.perf/12): no
|
|
192
|
+
# Ruby finalizer, no FFI dispatch from finalizer context.
|
|
193
|
+
# Freed stays the shared free-state for #freed?/#free.
|
|
194
|
+
Leptris::XML::Native.doc_handle_attach(doc)
|
|
195
|
+
else
|
|
196
|
+
ObjectSpace.define_finalizer(doc, finalizer(addr, freed))
|
|
197
|
+
end
|
|
182
198
|
doc
|
|
183
199
|
end
|
|
184
200
|
|
|
@@ -194,9 +210,16 @@ class Leptris::XML::Document
|
|
|
194
210
|
def root
|
|
195
211
|
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
196
212
|
return nil if @c_ptr.nil?
|
|
213
|
+
# Version-stamped memo (TODO.perf/13): root changes only
|
|
214
|
+
# through mutations that advance @version (root=, unlink) or
|
|
215
|
+
# #free (which nils @c_ptr above) — the entry point of every
|
|
216
|
+
# pipeline stops paying FFI + wrap per call.
|
|
217
|
+
return @root if @root_version == @version
|
|
197
218
|
ptr = Leptris::XML::FFI.leptris_document_root(@c_ptr)
|
|
198
|
-
|
|
199
|
-
|
|
219
|
+
result = ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, self)
|
|
220
|
+
@root = result
|
|
221
|
+
@root_version = @version
|
|
222
|
+
result
|
|
200
223
|
end
|
|
201
224
|
|
|
202
225
|
# The document node — navigation head over the whole tree chain
|
|
@@ -268,6 +291,12 @@ class Leptris::XML::Document
|
|
|
268
291
|
Leptris::XML::FFI.check_status(
|
|
269
292
|
Leptris::XML::FFI.leptris_document_set_root(@c_ptr, element.c_ptr))
|
|
270
293
|
@version += 1
|
|
294
|
+
# Seed the root memo through wrap: a cross-document element
|
|
295
|
+
# must enter THIS document's identity cache with @document
|
|
296
|
+
# pointing here, not ride its source-document wrapper.
|
|
297
|
+
@root = Leptris::XML::Node.wrap(element.c_ptr, self)
|
|
298
|
+
@root_version = @version
|
|
299
|
+
Leptris::XML::Node.invalidate_cross_document!(element, self)
|
|
271
300
|
element
|
|
272
301
|
end
|
|
273
302
|
|
|
@@ -391,6 +420,9 @@ class Leptris::XML::Document
|
|
|
391
420
|
@c_ptr = nil
|
|
392
421
|
@c_address = nil
|
|
393
422
|
@wrapper_cache&.clear
|
|
423
|
+
# Detach the lifetime handle (TODO.perf/12): the memory is
|
|
424
|
+
# already released above — the GC-pass dfree must no-op.
|
|
425
|
+
Leptris::XML::Native.doc_handle_release(self) if defined?(Leptris::XML::NATIVE_FAST)
|
|
394
426
|
end
|
|
395
427
|
|
|
396
428
|
# Enable the first-party EXSLT-style extension pack on this
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -87,12 +87,16 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
87
87
|
values[name] = v
|
|
88
88
|
return v
|
|
89
89
|
end
|
|
90
|
-
# Completely cold: one
|
|
90
|
+
# Completely cold: one read, start a partial values hash.
|
|
91
91
|
# Drop any full-face memos from a prior version — otherwise a
|
|
92
92
|
# post-mutation cold [] would stamp the new version onto a
|
|
93
93
|
# stale @attributes hash and attributes would serve it.
|
|
94
94
|
ensure_alive!
|
|
95
|
-
v =
|
|
95
|
+
v = if native_fast?
|
|
96
|
+
Leptris::XML::Native.fast_attribute(@c_ptr.address, name)
|
|
97
|
+
else
|
|
98
|
+
Leptris::XML::FFI.leptris_element_attribute(@c_ptr, name)
|
|
99
|
+
end
|
|
96
100
|
@attr_values = { name => v }
|
|
97
101
|
@attributes = nil
|
|
98
102
|
@attribute_nodes = nil
|
|
@@ -315,32 +319,71 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
315
319
|
end
|
|
316
320
|
|
|
317
321
|
def prepend_child(node)
|
|
322
|
+
# C-bound insert (TODO.perf/14): gates + predicate + version
|
|
323
|
+
# bump + engine insert in one dispatch; Qnil = the child needs
|
|
324
|
+
# the namespace lift — the full path below handles it.
|
|
325
|
+
if native_fast_children?
|
|
326
|
+
st = Leptris::XML::Native.insert_binding_child(
|
|
327
|
+
@document, @c_ptr.address, node.c_ptr.address, 1)
|
|
328
|
+
unless st.nil?
|
|
329
|
+
Leptris::XML::FFI.check_status(st)
|
|
330
|
+
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
331
|
+
return node
|
|
332
|
+
end
|
|
333
|
+
end
|
|
318
334
|
ensure_writable!
|
|
319
335
|
unless Leptris::XML::Element.skip_adoption_lift?(node)
|
|
320
336
|
Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
|
|
321
337
|
end
|
|
322
338
|
Leptris::XML::FFI.check_status(
|
|
323
339
|
Leptris::XML::FFI.leptris_element_prepend_child(@c_ptr, node.c_ptr))
|
|
340
|
+
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
324
341
|
node
|
|
325
342
|
end
|
|
326
343
|
|
|
327
344
|
def add_next_sibling(node)
|
|
345
|
+
# C-bound insert (TODO.perf/14): gates + predicate + version
|
|
346
|
+
# bump + engine insert in one dispatch; Qnil = the child needs
|
|
347
|
+
# the namespace lift — the full path below handles it.
|
|
348
|
+
if native_fast_children?
|
|
349
|
+
st = Leptris::XML::Native.insert_binding_child(
|
|
350
|
+
@document, @c_ptr.address, node.c_ptr.address, 2)
|
|
351
|
+
unless st.nil?
|
|
352
|
+
Leptris::XML::FFI.check_status(st)
|
|
353
|
+
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
354
|
+
return node
|
|
355
|
+
end
|
|
356
|
+
end
|
|
328
357
|
ensure_writable!
|
|
329
358
|
unless Leptris::XML::Element.skip_adoption_lift?(node)
|
|
330
359
|
Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
|
|
331
360
|
end
|
|
332
361
|
Leptris::XML::FFI.check_status(
|
|
333
362
|
Leptris::XML::FFI.leptris_element_insert_after(@c_ptr, node.c_ptr))
|
|
363
|
+
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
334
364
|
node
|
|
335
365
|
end
|
|
336
366
|
|
|
337
367
|
def add_previous_sibling(node)
|
|
368
|
+
# C-bound insert (TODO.perf/14): gates + predicate + version
|
|
369
|
+
# bump + engine insert in one dispatch; Qnil = the child needs
|
|
370
|
+
# the namespace lift — the full path below handles it.
|
|
371
|
+
if native_fast_children?
|
|
372
|
+
st = Leptris::XML::Native.insert_binding_child(
|
|
373
|
+
@document, @c_ptr.address, node.c_ptr.address, 3)
|
|
374
|
+
unless st.nil?
|
|
375
|
+
Leptris::XML::FFI.check_status(st)
|
|
376
|
+
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
377
|
+
return node
|
|
378
|
+
end
|
|
379
|
+
end
|
|
338
380
|
ensure_writable!
|
|
339
381
|
unless Leptris::XML::Element.skip_adoption_lift?(node)
|
|
340
382
|
Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
|
|
341
383
|
end
|
|
342
384
|
Leptris::XML::FFI.check_status(
|
|
343
385
|
Leptris::XML::FFI.leptris_element_insert_before(@c_ptr, node.c_ptr))
|
|
386
|
+
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
344
387
|
node
|
|
345
388
|
end
|
|
346
389
|
|
|
@@ -437,6 +480,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
437
480
|
@document, @c_ptr.address, node_or_markup.c_ptr.address)
|
|
438
481
|
unless st.nil?
|
|
439
482
|
Leptris::XML::FFI.check_status(st)
|
|
483
|
+
Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
|
|
440
484
|
return node_or_markup
|
|
441
485
|
end
|
|
442
486
|
end
|
|
@@ -445,6 +489,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
445
489
|
end
|
|
446
490
|
Leptris::XML::FFI.check_status(
|
|
447
491
|
Leptris::XML::FFI.leptris_element_append_child(@c_ptr, node_or_markup.c_ptr))
|
|
492
|
+
Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
|
|
448
493
|
node_or_markup
|
|
449
494
|
when String
|
|
450
495
|
frag = Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
|
|
@@ -546,7 +591,10 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
546
591
|
key = ns.prefix ? "xmlns:#{ns.prefix}" : "xmlns"
|
|
547
592
|
scopes[key] ||= ns.href
|
|
548
593
|
end
|
|
549
|
-
|
|
594
|
+
# unstamped: this walk also runs inside the adoption lift,
|
|
595
|
+
# between the version bump and the engine move (see
|
|
596
|
+
# Node#unstamped_parent).
|
|
597
|
+
node = node.unstamped_parent
|
|
550
598
|
end
|
|
551
599
|
if @document
|
|
552
600
|
@namespaces = scopes
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -14,6 +14,19 @@ class Leptris::XML::Node
|
|
|
14
14
|
@c_ptr = c_ptr
|
|
15
15
|
@document = document
|
|
16
16
|
@parent = parent
|
|
17
|
+
# Structural-memo stamp (TODO.perf/13): a constructor-seeded
|
|
18
|
+
# @parent is true AS OF the document's current version; any
|
|
19
|
+
# mutation advances the version and forces re-derivation. A
|
|
20
|
+
# NIL constructor parent means UNKNOWN, not "no parent" — the
|
|
21
|
+
# engine may attach during creation (create_child), so an
|
|
22
|
+
# unstamped memo must derive rather than trust nil.
|
|
23
|
+
@parent_version = parent ? document&.version : nil
|
|
24
|
+
# Computed once: structural stamps apply to document-owned
|
|
25
|
+
# nodes only (scope-owned iterparse elements move without
|
|
26
|
+
# their scope's version advancing — see #parent). An ivar
|
|
27
|
+
# keeps the memo-hit path free of method dispatch.
|
|
28
|
+
@structure_memoizable =
|
|
29
|
+
!document.nil? && !document.is_a?(Leptris::XML::IterationScope)
|
|
17
30
|
# wrap() already calls leptris_node_get_type for dispatch; reusing
|
|
18
31
|
# the result makes every predicate and #type call FFI-free.
|
|
19
32
|
@node_type = node_type
|
|
@@ -157,12 +170,60 @@ class Leptris::XML::Node
|
|
|
157
170
|
end
|
|
158
171
|
alias_method :pi?, :processing_instruction?
|
|
159
172
|
|
|
173
|
+
# Version-stamped structural memo (TODO.perf/13): derive once,
|
|
174
|
+
# re-derive after any mutation that advances the owning
|
|
175
|
+
# document's version. Fixes the stale seeded @parent after a
|
|
176
|
+
# move (FFI children walks seed it; the move never cleared it).
|
|
177
|
+
# Scope-owned (iterparse) elements never memoize: a scope element
|
|
178
|
+
# adopted into a document moves without its scope's version
|
|
179
|
+
# advancing, so a stamp would lie.
|
|
160
180
|
def parent
|
|
161
|
-
|
|
181
|
+
if @structure_memoizable && @parent_version == @document.version
|
|
182
|
+
return @parent
|
|
183
|
+
end
|
|
162
184
|
ensure_alive!
|
|
163
185
|
ptr = Leptris::XML::FFI.leptris_node_parent(@c_ptr)
|
|
164
|
-
|
|
165
|
-
|
|
186
|
+
result = ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, @document)
|
|
187
|
+
if @document
|
|
188
|
+
@parent = result
|
|
189
|
+
@parent_version = @document.version
|
|
190
|
+
end
|
|
191
|
+
result
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Derive the parent WITHOUT stamping the structural memo. The
|
|
195
|
+
# adoption lift reads the child's source scope BETWEEN the
|
|
196
|
+
# mutation gate's version bump and the engine move — a stamped
|
|
197
|
+
# #parent there would record post-bump versions carrying
|
|
198
|
+
# pre-move truth. Ancestor walks that run inside mutations use
|
|
199
|
+
# this; #namespaces itself is memoized, so stable trees pay the
|
|
200
|
+
# unstamped derivation at most once per version.
|
|
201
|
+
def unstamped_parent
|
|
202
|
+
ensure_alive!
|
|
203
|
+
ptr = Leptris::XML::FFI.leptris_node_parent(@c_ptr)
|
|
204
|
+
ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, @document)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Mutation sites move nodes ACROSS documents: the moved node's
|
|
208
|
+
# stamps reference the source document's version, which the move
|
|
209
|
+
# does not advance. Clear them and advance the source version
|
|
210
|
+
# (its other nodes hold parent/sibling stamps the move invalidates).
|
|
211
|
+
def invalidate_structural_stamps!
|
|
212
|
+
@parent_version = nil
|
|
213
|
+
@next_sibling_version = nil
|
|
214
|
+
@previous_sibling_version = nil
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Mutation-site helper: when +node+ moves into +target_document+
|
|
218
|
+
# from a different one, the move invalidates the node's own
|
|
219
|
+
# structural stamps (stamped against the SOURCE version) and the
|
|
220
|
+
# source document's sibling/parent stamps. Scope-owned nodes
|
|
221
|
+
# answer nil from #document and never memoize — nothing to do.
|
|
222
|
+
def self.invalidate_cross_document!(node, target_document)
|
|
223
|
+
source = node.document
|
|
224
|
+
return if source.nil? || source.equal?(target_document)
|
|
225
|
+
node.invalidate_structural_stamps!
|
|
226
|
+
source.advance_version
|
|
166
227
|
end
|
|
167
228
|
|
|
168
229
|
# Borrowed-handle lifetime: every c_ptr dereference is valid only
|
|
@@ -267,18 +328,32 @@ class Leptris::XML::Node
|
|
|
267
328
|
end
|
|
268
329
|
|
|
269
330
|
def next_sibling
|
|
331
|
+
if @structure_memoizable && @next_sibling_version == @document.version
|
|
332
|
+
return @next_sibling
|
|
333
|
+
end
|
|
270
334
|
ensure_alive!
|
|
271
335
|
ptr = Leptris::XML::FFI.leptris_node_next_sibling(@c_ptr)
|
|
272
|
-
|
|
273
|
-
|
|
336
|
+
result = ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, @document, parent: @parent)
|
|
337
|
+
if @document
|
|
338
|
+
@next_sibling = result
|
|
339
|
+
@next_sibling_version = @document.version
|
|
340
|
+
end
|
|
341
|
+
result
|
|
274
342
|
end
|
|
275
343
|
alias_method :next, :next_sibling
|
|
276
344
|
|
|
277
345
|
def previous_sibling
|
|
346
|
+
if @structure_memoizable && @previous_sibling_version == @document.version
|
|
347
|
+
return @previous_sibling
|
|
348
|
+
end
|
|
278
349
|
ensure_alive!
|
|
279
350
|
ptr = Leptris::XML::FFI.leptris_node_previous_sibling(@c_ptr)
|
|
280
|
-
|
|
281
|
-
|
|
351
|
+
result = ptr.null? ? nil : Leptris::XML::Node.wrap(ptr, @document, parent: @parent)
|
|
352
|
+
if @document
|
|
353
|
+
@previous_sibling = result
|
|
354
|
+
@previous_sibling_version = @document.version
|
|
355
|
+
end
|
|
356
|
+
result
|
|
282
357
|
end
|
|
283
358
|
alias_method :previous, :previous_sibling
|
|
284
359
|
|
|
@@ -5,6 +5,30 @@ module Leptris::XML::Searchable
|
|
|
5
5
|
# mirroring leptris-py#105); anything else is a namespace prefix.
|
|
6
6
|
VERSION_SELECTORS = ["1.0", "3.1", :xpath10, :xpath31].freeze
|
|
7
7
|
|
|
8
|
+
# Compiled-expression cache (TODO.perf/15): the engine caches
|
|
9
|
+
# compiled strings internally, but the string path still pays
|
|
10
|
+
# lookup + hashing per call — a direct handle eval measured 34%
|
|
11
|
+
# under it on repeat expressions. Bounded LRU keyed on the
|
|
12
|
+
# expression string; only successful compiles cache; GVL makes
|
|
13
|
+
# the Hash operations safe. Version-pinned and namespace-bound
|
|
14
|
+
# evaluations keep their dedicated string entries.
|
|
15
|
+
COMPILED_CACHE_LIMIT = 64
|
|
16
|
+
|
|
17
|
+
def self.compiled_expression(expr)
|
|
18
|
+
cache = (@compiled_expressions ||= {})
|
|
19
|
+
if (hit = cache[expr])
|
|
20
|
+
cache.delete(expr)
|
|
21
|
+
cache[expr] = hit # LRU refresh
|
|
22
|
+
return hit
|
|
23
|
+
end
|
|
24
|
+
compiled = Leptris::XML::XPath.compile(expr)
|
|
25
|
+
cache.shift while cache.size >= COMPILED_CACHE_LIMIT
|
|
26
|
+
cache[expr] = compiled
|
|
27
|
+
compiled
|
|
28
|
+
rescue Leptris::XML::XPathError
|
|
29
|
+
nil # fall back to the string path — its error surface is the contract
|
|
30
|
+
end
|
|
31
|
+
|
|
8
32
|
def xpath(*paths)
|
|
9
33
|
handler, ns, version = parse_search_args(paths)
|
|
10
34
|
raise ArgumentError, "custom XPath handlers not supported" if handler
|
|
@@ -25,6 +49,8 @@ module Leptris::XML::Searchable
|
|
|
25
49
|
Leptris::XML::Searchable.xpath_version_code(version), nil)
|
|
26
50
|
elsif ns && !ns.empty?
|
|
27
51
|
xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
|
|
52
|
+
elsif (compiled = Leptris::XML::Searchable.compiled_expression(expr))
|
|
53
|
+
compiled.eval_ptrs(doc_ptr, context_ptr)
|
|
28
54
|
else
|
|
29
55
|
Leptris::XML::FFI.leptris_xpath_eval(doc_ptr, context_ptr, expr)
|
|
30
56
|
end
|
|
@@ -56,6 +82,8 @@ module Leptris::XML::Searchable
|
|
|
56
82
|
Leptris::XML::Searchable.xpath_version_code(version), nil)
|
|
57
83
|
elsif ns && !ns.empty?
|
|
58
84
|
xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
|
|
85
|
+
elsif (compiled = Leptris::XML::Searchable.compiled_expression(expr))
|
|
86
|
+
compiled.eval_ptrs(doc_ptr, context_ptr)
|
|
59
87
|
else
|
|
60
88
|
Leptris::XML::FFI.leptris_xpath_eval(doc_ptr, context_ptr, expr)
|
|
61
89
|
end
|
data/lib/leptris/xml/xpath.rb
CHANGED
|
@@ -32,6 +32,14 @@ class Leptris::XML::XPath
|
|
|
32
32
|
new(expression.to_s, CompiledHandle.new(raw), version_code)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Raw-pointer evaluation for Searchable's compiled cache
|
|
36
|
+
# (TODO.perf/15): the same entry the string path uses, against
|
|
37
|
+
# the compiled handle — no per-call parse/cache lookup.
|
|
38
|
+
def eval_ptrs(doc_ptr, context_ptr)
|
|
39
|
+
Leptris::XML::FFI.leptris_xpath_compiled_eval(
|
|
40
|
+
@handle, doc_ptr, context_ptr)
|
|
41
|
+
end
|
|
42
|
+
|
|
35
43
|
def initialize(expression, handle, version_code = nil)
|
|
36
44
|
@expression = expression
|
|
37
45
|
@handle = handle
|
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.163.
|
|
4
|
+
version: 1.9.163.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -80,6 +80,10 @@ files:
|
|
|
80
80
|
- TODO.perf/09-adoption-lift-fast-path.md
|
|
81
81
|
- TODO.perf/10-bulk-attribute-materialization.md
|
|
82
82
|
- TODO.perf/11-moxml-gap-battery.md
|
|
83
|
+
- TODO.perf/12-document-lifetime-in-c.md
|
|
84
|
+
- TODO.perf/13-structural-memos.md
|
|
85
|
+
- TODO.perf/14-insert-family-c-bound.md
|
|
86
|
+
- TODO.perf/15-compiled-expression-cache.md
|
|
83
87
|
- TODO.restructure/01-constraint-compliance-audit.md
|
|
84
88
|
- TODO.restructure/02-deep-copy-seam.md
|
|
85
89
|
- TODO.restructure/03-evaluation-context-seam.md
|