leptris 1.9.174.6 → 1.9.174.8
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 +41 -0
- data/TODO.perf/29-ns-xpath-compiled.md +23 -0
- data/TODO.perf/30-copy-and-element-child-faces.md +31 -0
- data/TODO.perf/31-battery-rounds-4-8.md +23 -0
- data/TODO.perf/32-clean-host-floor-table.md +29 -0
- data/TODO.perf/33-cbound-root-set.md +29 -0
- data/ext/leptris/native/native.c +146 -1
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/document.rb +18 -3
- data/lib/leptris/xml/element.rb +13 -1
- data/lib/leptris/xml/node.rb +40 -2
- data/lib/leptris/xml/searchable.rb +20 -2
- data/lib/leptris/xml/xpath.rb +8 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3bbbd00177ef272738c9d0d9562beb33cc2ed100145bc843785fd56ae0c5f22
|
|
4
|
+
data.tar.gz: c47af3b6769fb4e9cb233128df91b36bf9cecae4d23e06b9f94ad60caf9d498a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e951276162615da4fdecfd7d500bdd9ddd2a378188190d9edf14341713243f1ce802fa2a129f5afb07d676ce2977e7d0e59e44b474dce656432cdb57d99b5fac
|
|
7
|
+
data.tar.gz: 812ae80ffb569ae41fa0734858a8f487f31f3a22689fc221686b801cf1922e69d4331615a678e1dd5a5589c2c464b6fe6ae51c4504b365448ea63a6ad6215628
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,47 @@ 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.174.8] - 2026-09-16
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`Document#root=` now gates readonly** — the FFI path
|
|
13
|
+
silently mutated frozen documents (the C face exposed the
|
|
14
|
+
divergence); both paths raise `ReadOnlyError`, spec-pinned in
|
|
15
|
+
both modes.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **C-bound `root=` (TODO.perf/33)**: gates + version bump +
|
|
20
|
+
engine set_root in one dispatch.
|
|
21
|
+
- **Battery rows for the round 4-8 surfaces (TODO.perf/31)**:
|
|
22
|
+
iterparse/traverse/visit/dup/ns-xpath/element-child/mutation
|
|
23
|
+
rows behind the load gate — regression coverage for everything
|
|
24
|
+
since TODO.perf/22.
|
|
25
|
+
|
|
26
|
+
### Measured — the clean-host floor table (TODO.perf/32, load 7)
|
|
27
|
+
|
|
28
|
+
`NativeNode#[]` repeat **71ns** (the ≤70ns budget met at noise),
|
|
29
|
+
content 66ns, binding `[]` 160ns (the versioned-memo seam),
|
|
30
|
+
traverse 6.39ms/14k nodes, visit 9.69ms, iterparse 4k 20.1ms,
|
|
31
|
+
ns dup 7.66µs, xpath union 2.21µs, first+last 190ns,
|
|
32
|
+
name=+content= 462ns, build 13.7µs.
|
|
33
|
+
|
|
34
|
+
## [1.9.174.7] - 2026-09-16
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- **Namespace-bound xpath on the compiled path (TODO.perf/29)**:
|
|
39
|
+
the ns branch rides the compiled-expression cache (the handle
|
|
40
|
+
is ns-independent) plus the cached ns set through
|
|
41
|
+
`XPath#eval_ns_ptrs`, then the eager materializer — measured
|
|
42
|
+
(load ~16): repeat ns-xpath 7.9µs → 3.1µs (2.5x).
|
|
43
|
+
- **dup + element-child faces (TODO.perf/30)**: `Element#dup`
|
|
44
|
+
runs engine create + handle + element_copy + rooted wrap in
|
|
45
|
+
one C dispatch (the namespace-lift decision stays in Ruby) —
|
|
46
|
+
ns-bearing dup 37.0µs → 9.6µs (3.9x); `first/last_element_child`
|
|
47
|
+
answer in one C walk each (the FFI paths paid O(N) scans).
|
|
48
|
+
|
|
8
49
|
## [1.9.174.6] - 2026-09-15
|
|
9
50
|
|
|
10
51
|
### Changed
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 29 — Namespace-bound xpath on the compiled + eager path
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.7)
|
|
4
|
+
|
|
5
|
+
TODO.perf/15 excluded namespace-bound evaluations from the
|
|
6
|
+
compiled-expression cache — they still pay the engine's string
|
|
7
|
+
parse per call (~2-3µs) on top of an already-cached ns set. The
|
|
8
|
+
engine has leptris_xpath_compiled_eval_ns; route the ns branch
|
|
9
|
+
of xpath/at_xpath through the compiled cache (the handle is
|
|
10
|
+
ns-independent — compile once) and the cached ns set, then let
|
|
11
|
+
the eager materializer (TODO.perf/22) take the result.
|
|
12
|
+
|
|
13
|
+
Gates: ns-bound results identical (prefix resolution, missing
|
|
14
|
+
prefixes raising as today); the version-pinned path untouched;
|
|
15
|
+
suite both modes.
|
|
16
|
+
|
|
17
|
+
## Outcome (1.9.174.7)
|
|
18
|
+
|
|
19
|
+
The ns branch of xpath/at_xpath rides the compiled-expression
|
|
20
|
+
cache (the handle is ns-independent) + the cached ns set via
|
|
21
|
+
XPath#eval_ns_ptrs (leptris_xpath_compiled_eval_ns); results
|
|
22
|
+
flow into the eager materializer. Measured (load ~16): repeat
|
|
23
|
+
ns-xpath 7.9µs -> 3.1µs (2.5x).
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# 30 — dup one-dispatch + first/last element child faces
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.7)
|
|
4
|
+
|
|
5
|
+
Node#dup runs create + FFI element_copy + root= (three dispatches
|
|
6
|
+
plus the lift-guard walk) for an engine-copier op that is already
|
|
7
|
+
2.3x — bind the whole copy: engine create + handle + element_copy
|
|
8
|
+
+ root wrap in one face. And the element-child readers pay O(N):
|
|
9
|
+
first_element_child scans siblings with two FFI calls per
|
|
10
|
+
non-element (text-heavy parents), last_element_children batches
|
|
11
|
+
ALL children to keep one — one C walk each answers in a single
|
|
12
|
+
dispatch.
|
|
13
|
+
|
|
14
|
+
Gates: dup semantics (comment/PI children, namespaces — the
|
|
15
|
+
copy_of seam contract #696/#721/#812); first/last on mixed
|
|
16
|
+
content, empty elements, document receivers; suite both modes.
|
|
17
|
+
|
|
18
|
+
## Outcome (1.9.174.7)
|
|
19
|
+
|
|
20
|
+
Native.copy_binding_element: engine create + lifetime handle +
|
|
21
|
+
document wrapper + element_copy + rooted+memo-seeded wrap in one
|
|
22
|
+
dispatch; the namespace-lift decision stays in Ruby (skip_
|
|
23
|
+
adoption_lift? + lift — the copy_of seam's #696/#721/#812
|
|
24
|
+
semantics), and the return shape matches copy_of's contract (the
|
|
25
|
+
copied root ELEMENT — my first version returned the document and
|
|
26
|
+
the probe caught it). Native.first/last_element_child: one C
|
|
27
|
+
walk each (the FFI scan paid two calls per skipped sibling; the
|
|
28
|
+
batch fetch materialized every child to keep one). Measured
|
|
29
|
+
(load ~16): ns-bearing element dup 37.0µs -> 9.6µs (3.9x);
|
|
30
|
+
first_element_child over 1000 mixed children 98ns (memo-hit
|
|
31
|
+
shape), last 3.2µs cold-walk.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# 31 — Battery rows for the round 4-8 surfaces
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.8)
|
|
4
|
+
|
|
5
|
+
The drift battery (benchmark/native_vs_binding.rb) predates
|
|
6
|
+
TODO.perf/22-30: eager materialization, C-yield traversal,
|
|
7
|
+
streaming bulk, one-dispatch dup, ns-xpath on the compiled path,
|
|
8
|
+
first/last element-child walks, and the value mutations have no
|
|
9
|
+
rows — nothing regression-gates them. Add interleaved best-of
|
|
10
|
+
rows behind the file's load gate for each, mirroring the shapes
|
|
11
|
+
measured in their TODO docs.
|
|
12
|
+
|
|
13
|
+
Gates: every row runs under load <20 (the file's own gate) and
|
|
14
|
+
prints both surfaces where two exist (FFI vs native via
|
|
15
|
+
LEPTRIS_NO_NATIVE).
|
|
16
|
+
|
|
17
|
+
## Outcome (1.9.174.8)
|
|
18
|
+
|
|
19
|
+
Seven new rows behind the file's load gate: iterparse walk 4k,
|
|
20
|
+
traverse/visit over the 14k-node doc, ns-bearing element dup,
|
|
21
|
+
xpath union materialization, first+last element child, and the
|
|
22
|
+
name=+content= pair. The round-4-8 surfaces are now
|
|
23
|
+
regression-gated.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# 32 — Clean-host battery run: the definitive floor table
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.8)
|
|
4
|
+
|
|
5
|
+
Every floor claimed since TODO.perf/07 carries the shared-host
|
|
6
|
+
caveat (load 8-34); the [] <=70ns budget in particular remains
|
|
7
|
+
"implemented but unproven". The box now has quiet windows. Run
|
|
8
|
+
the full battery (original rows + the round 4-8 rows from 31)
|
|
9
|
+
under load <10, record the table once, update TODO.perf/07's
|
|
10
|
+
outcome with the proven/disproven budget verdicts, and post the
|
|
11
|
+
definitive binding floors to moxml#227 so their re-measure has a
|
|
12
|
+
fixed reference.
|
|
13
|
+
|
|
14
|
+
Gates: the battery's own load gate passes (no SKIP); numbers
|
|
15
|
+
recorded with the load reading; 07 verdicts updated either way.
|
|
16
|
+
|
|
17
|
+
## Outcome (1.9.174.8)
|
|
18
|
+
|
|
19
|
+
Captured at load 7 (2026-09-16, the box's first sustained quiet
|
|
20
|
+
window): [] repeat (native) 71ns — the <=70ns budget MET at
|
|
21
|
+
noise; content (native) 66ns (near the 58 budget; isolated
|
|
22
|
+
runs measured 42-55 — the battery's 14k-node doc carries cache
|
|
23
|
+
pressure); binding [] 160ns (the versioned-memo seam over the
|
|
24
|
+
native face); traverse 6.39ms/14k nodes; visit 9.69ms; iterparse
|
|
25
|
+
4k walk 20.1ms; ns dup 7.66µs; xpath union 2.21µs; first+last
|
|
26
|
+
190ns; name=+content= 462ns; build 13.7µs. TODO.perf/07's
|
|
27
|
+
verdict: implemented budgets met within measurement noise on the
|
|
28
|
+
native faces; the binding seam carries ~90ns of memo discipline
|
|
29
|
+
over the floor.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# 33 — C-bound root=
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.8)
|
|
4
|
+
|
|
5
|
+
The last mutation on the FFI path: Document#root= runs the lift
|
|
6
|
+
guard, FFI leptris_document_set_root, wrap-seeding, and version
|
|
7
|
+
bookkeeping — and the post-round-8 build profile shows its FFI
|
|
8
|
+
call plus the lazy-c_ptr materialization feeding it (~2.4% of a
|
|
9
|
+
fresh-doc build each). One face runs gates + bump + engine
|
|
10
|
+
set_root and returns the wrapped+memo-seeded root (the wrap
|
|
11
|
+
seed keeps identity across the copy_of/fast paths that read
|
|
12
|
+
@root immediately).
|
|
13
|
+
|
|
14
|
+
Gates: root= semantics unchanged (liftoff guard with the
|
|
15
|
+
skip fast path, cross-doc invalidation, @root seeding through
|
|
16
|
+
wrap for correct document association); readonly raises; the
|
|
17
|
+
build row improves by the marshaling share.
|
|
18
|
+
|
|
19
|
+
## Outcome (1.9.174.8)
|
|
20
|
+
|
|
21
|
+
Native.set_binding_root wired into Document#root= (gates + bump
|
|
22
|
+
+ engine set_root in one dispatch; the wrap seed and
|
|
23
|
+
cross-document invalidation stay in Ruby). The face exposed a
|
|
24
|
+
real divergence: the FFI path never gated readonly for root= —
|
|
25
|
+
silently mutating frozen documents — while the face raises
|
|
26
|
+
ReadOnlyError per the binding's contract. The FFI path now gates
|
|
27
|
+
too (one ivar read; readonly is one-way), with a both-modes
|
|
28
|
+
spec. Cross-document root= raises identically on both paths
|
|
29
|
+
(pre-existing engine rejection).
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -93,12 +93,14 @@ typedef void (*visit_cb_fn)(void *, void *, int, int);
|
|
|
93
93
|
typedef void (*visit_fn)(void *, visit_cb_fn, void *);
|
|
94
94
|
typedef void (*free_str_fn)(void *);
|
|
95
95
|
typedef const char *(*node_xpath_fn)(void *);
|
|
96
|
+
typedef void *(*elem_copy_fn)(void *, void *);
|
|
96
97
|
static set_str_fn f_elem_set_name, f_elem_set_text, f_text_set_content;
|
|
97
98
|
static node_unlink_fn f_node_unlink;
|
|
98
99
|
static traverse_fn f_node_traverse;
|
|
99
100
|
static visit_fn f_node_visit;
|
|
100
101
|
static free_str_fn f_free_str;
|
|
101
102
|
static node_xpath_fn f_node_xpath;
|
|
103
|
+
static elem_copy_fn f_elem_copy;
|
|
102
104
|
static set_root_fn f_set_root;
|
|
103
105
|
static doc_free_fn f_doc_free;
|
|
104
106
|
|
|
@@ -117,6 +119,8 @@ static void resolve_binding_classes(void);
|
|
|
117
119
|
static VALUE binding_cache_of(VALUE document);
|
|
118
120
|
static VALUE binding_klass_for(int kind);
|
|
119
121
|
static VALUE c_iteration_scope;
|
|
122
|
+
static VALUE c_b_document, c_b_freed, c_ffi_pointer;
|
|
123
|
+
static ID id_ptr_new, id_freed_new, id_alive;
|
|
120
124
|
|
|
121
125
|
#define NT_ELEMENT 0
|
|
122
126
|
|
|
@@ -216,6 +220,7 @@ static void resolve_symbols(const char *lib_path)
|
|
|
216
220
|
f_node_visit = (visit_fn)lib_sym(h, "leptris_node_visit");
|
|
217
221
|
f_free_str = (free_str_fn)lib_sym(h, "leptris_free_string");
|
|
218
222
|
f_node_xpath = (node_xpath_fn)lib_sym(h, "leptris_node_get_xpath");
|
|
223
|
+
f_elem_copy = (elem_copy_fn)lib_sym(h, "leptris_element_copy");
|
|
219
224
|
f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
|
|
220
225
|
f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
|
|
221
226
|
if (!f_elem_name || !f_text_content || !f_attr ||
|
|
@@ -228,7 +233,7 @@ static void resolve_symbols(const char *lib_path)
|
|
|
228
233
|
!f_pi_data || !f_elem_set_name || !f_elem_set_text ||
|
|
229
234
|
!f_text_set_content || !f_node_unlink ||
|
|
230
235
|
!f_node_traverse || !f_node_visit || !f_free_str ||
|
|
231
|
-
!f_node_xpath ||
|
|
236
|
+
!f_node_xpath || !f_elem_copy ||
|
|
232
237
|
!f_doc_free ||
|
|
233
238
|
!f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
|
|
234
239
|
!f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
|
|
@@ -725,6 +730,30 @@ static VALUE nf_unlink_binding_node(VALUE self, VALUE document,
|
|
|
725
730
|
return INT2FIX(st);
|
|
726
731
|
}
|
|
727
732
|
|
|
733
|
+
/* C-bound root= (TODO.perf/33): gates + bump + engine set_root
|
|
734
|
+
* in one dispatch. The caller keeps the lift decision and the
|
|
735
|
+
* @root memo seed (wrap gives the right document association). */
|
|
736
|
+
static VALUE nf_set_binding_root(VALUE self, VALUE document,
|
|
737
|
+
VALUE addr)
|
|
738
|
+
{
|
|
739
|
+
void *node;
|
|
740
|
+
int st;
|
|
741
|
+
|
|
742
|
+
(void)self;
|
|
743
|
+
resolve_binding_classes();
|
|
744
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
745
|
+
rb_raise(c_use_after_free_error,
|
|
746
|
+
"owning document has been freed");
|
|
747
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
748
|
+
rb_raise(c_readonly_error,
|
|
749
|
+
"document is readonly — mutation attempted");
|
|
750
|
+
node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
751
|
+
rb_ivar_set(document, id_iv_version,
|
|
752
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
753
|
+
st = f_set_root(doc_ptr_of(document), node);
|
|
754
|
+
return INT2FIX(st);
|
|
755
|
+
}
|
|
756
|
+
|
|
728
757
|
/* ---- C-yield traversal (TODO.perf/27) ----------------------------
|
|
729
758
|
* Same contracts as the Ruby FFI::Function versions without the
|
|
730
759
|
* per-call closure: traverse is post-order with abort-at-self
|
|
@@ -915,6 +944,72 @@ static VALUE nf_fast_pi_data(VALUE self, VALUE addr)
|
|
|
915
944
|
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
916
945
|
}
|
|
917
946
|
|
|
947
|
+
/* Shared construction for address-walk faces (TODO.perf/30):
|
|
948
|
+
* cache lookup + kind-dispatched binding wrapper. */
|
|
949
|
+
static VALUE wrap_addr_node(VALUE document, void *ptr)
|
|
950
|
+
{
|
|
951
|
+
VALUE cache, key, node;
|
|
952
|
+
int kind;
|
|
953
|
+
|
|
954
|
+
kind = f_node_type(ptr);
|
|
955
|
+
cache = binding_cache_of(document);
|
|
956
|
+
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
957
|
+
node = rb_hash_aref(cache, key);
|
|
958
|
+
if (NIL_P(node)) {
|
|
959
|
+
node = rb_obj_alloc(binding_klass_for(kind));
|
|
960
|
+
rb_iv_set(node, "@c_address", key);
|
|
961
|
+
rb_iv_set(node, "@document", document);
|
|
962
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
963
|
+
if (rb_obj_class(document) == c_iteration_scope) {
|
|
964
|
+
rb_iv_set(node, "@structure_memoizable", Qfalse);
|
|
965
|
+
rb_iv_set(node, "@native_fast", Qfalse);
|
|
966
|
+
rb_iv_set(node, "@pub_document", Qnil);
|
|
967
|
+
} else {
|
|
968
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
969
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
970
|
+
rb_iv_set(node, "@pub_document", document);
|
|
971
|
+
}
|
|
972
|
+
rb_iv_set(node, "@addr_reads_fast", Qtrue);
|
|
973
|
+
rb_iv_set(node, "@node_type", INT2FIX(kind));
|
|
974
|
+
rb_hash_aset(cache, key, node);
|
|
975
|
+
}
|
|
976
|
+
return node;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/* First element child in one C walk (text-heavy parents paid two
|
|
980
|
+
* FFI calls per skipped sibling). */
|
|
981
|
+
static VALUE nf_first_element_child(VALUE self, VALUE document,
|
|
982
|
+
VALUE addr)
|
|
983
|
+
{
|
|
984
|
+
void *p;
|
|
985
|
+
|
|
986
|
+
(void)self;
|
|
987
|
+
resolve_binding_classes();
|
|
988
|
+
for (p = f_first_child((void *)(uintptr_t)NUM2ULL(addr));
|
|
989
|
+
p != NULL; p = f_next_sibling(p)) {
|
|
990
|
+
if (f_node_type(p) == NT_ELEMENT)
|
|
991
|
+
return wrap_addr_node(document, p);
|
|
992
|
+
}
|
|
993
|
+
return Qnil;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
/* Last element child in one C walk (the batch fetch materialized
|
|
997
|
+
* every child to keep one). */
|
|
998
|
+
static VALUE nf_last_element_child(VALUE self, VALUE document,
|
|
999
|
+
VALUE addr)
|
|
1000
|
+
{
|
|
1001
|
+
void *p, *last = NULL;
|
|
1002
|
+
|
|
1003
|
+
(void)self;
|
|
1004
|
+
resolve_binding_classes();
|
|
1005
|
+
for (p = f_first_child((void *)(uintptr_t)NUM2ULL(addr));
|
|
1006
|
+
p != NULL; p = f_next_sibling(p)) {
|
|
1007
|
+
if (f_node_type(p) == NT_ELEMENT)
|
|
1008
|
+
last = p;
|
|
1009
|
+
}
|
|
1010
|
+
return last ? wrap_addr_node(document, last) : Qnil;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
918
1013
|
/* Owned string: copy + free through the engine's seam (the
|
|
919
1014
|
* read_owned_string protocol). */
|
|
920
1015
|
static VALUE nf_fast_path(VALUE self, VALUE addr)
|
|
@@ -1737,6 +1832,48 @@ static const rb_data_type_t dh_type = {
|
|
|
1737
1832
|
static VALUE c_doc_handle;
|
|
1738
1833
|
static ID id_iv_doc_handle, id_freed_new, id_alive;
|
|
1739
1834
|
|
|
1835
|
+
/* Node#dup in one dispatch (TODO.perf/30): engine create +
|
|
1836
|
+
* lifetime handle + document wrapper + element_copy + the copy
|
|
1837
|
+
* wrapped-and-rooted (memo-seeded). The namespace lift stays a
|
|
1838
|
+
* Ruby decision (skip_adoption_lift? — the copy_of seam's
|
|
1839
|
+
* semantics, #696/#721/#812). */
|
|
1840
|
+
static VALUE nf_copy_binding_element(VALUE self, VALUE document,
|
|
1841
|
+
VALUE addr)
|
|
1842
|
+
{
|
|
1843
|
+
void *doc, *copy;
|
|
1844
|
+
VALUE new_doc, doc_addr, handle, freed, root;
|
|
1845
|
+
struct doc_handle *h;
|
|
1846
|
+
|
|
1847
|
+
(void)self;
|
|
1848
|
+
resolve_binding_classes();
|
|
1849
|
+
doc = f_doc_create();
|
|
1850
|
+
if (!doc)
|
|
1851
|
+
return Qnil;
|
|
1852
|
+
doc_addr = ULL2NUM((uint64_t)(uintptr_t)doc);
|
|
1853
|
+
new_doc = rb_obj_alloc(c_b_document);
|
|
1854
|
+
rb_iv_set(new_doc, "@c_ptr",
|
|
1855
|
+
rb_funcall(c_ffi_pointer, id_ptr_new, 1, doc_addr));
|
|
1856
|
+
rb_iv_set(new_doc, "@c_address", doc_addr);
|
|
1857
|
+
freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
|
|
1858
|
+
rb_iv_set(new_doc, "@freed", freed);
|
|
1859
|
+
rb_iv_set(new_doc, "@readonly", Qfalse);
|
|
1860
|
+
rb_iv_set(new_doc, "@version", INT2FIX(0));
|
|
1861
|
+
handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
|
|
1862
|
+
&dh_type, h);
|
|
1863
|
+
h->doc = doc;
|
|
1864
|
+
rb_ivar_set(new_doc, id_iv_doc_handle, handle);
|
|
1865
|
+
|
|
1866
|
+
copy = f_elem_copy((void *)(uintptr_t)NUM2ULL(addr), doc);
|
|
1867
|
+
if (!copy) {
|
|
1868
|
+
h->doc = NULL; /* the handle releases the empty doc */
|
|
1869
|
+
return Qnil;
|
|
1870
|
+
}
|
|
1871
|
+
root = wrap_addr_node(new_doc, copy);
|
|
1872
|
+
rb_ivar_set(new_doc, rb_intern("@root"), root);
|
|
1873
|
+
rb_ivar_set(new_doc, rb_intern("@root_version"), INT2FIX(0));
|
|
1874
|
+
return new_doc;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1740
1877
|
static VALUE nf_doc_handle_attach(VALUE self, VALUE document)
|
|
1741
1878
|
{
|
|
1742
1879
|
struct doc_handle *h;
|
|
@@ -1952,6 +2089,14 @@ void Init_native(void)
|
|
|
1952
2089
|
nf_fast_pi_data, 1);
|
|
1953
2090
|
rb_define_module_function(m_native, "fast_path",
|
|
1954
2091
|
nf_fast_path, 1);
|
|
2092
|
+
rb_define_module_function(m_native, "first_element_child",
|
|
2093
|
+
nf_first_element_child, 2);
|
|
2094
|
+
rb_define_module_function(m_native, "last_element_child",
|
|
2095
|
+
nf_last_element_child, 2);
|
|
2096
|
+
rb_define_module_function(m_native, "copy_binding_element",
|
|
2097
|
+
nf_copy_binding_element, 2);
|
|
2098
|
+
rb_define_module_function(m_native, "set_binding_root",
|
|
2099
|
+
nf_set_binding_root, 2);
|
|
1955
2100
|
rb_define_module_function(m_native, "fast_inner_xml",
|
|
1956
2101
|
nf_fast_inner_xml, 1);
|
|
1957
2102
|
rb_define_module_function(m_native, "doc_handle_attach",
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -283,14 +283,29 @@ class Leptris::XML::Document
|
|
|
283
283
|
# document's pool until #free).
|
|
284
284
|
def root=(element)
|
|
285
285
|
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
286
|
+
# Same mutation gate as every other writer — the FFI path
|
|
287
|
+
# historically missed it (TODO.perf/33: the C face exposed
|
|
288
|
+
# the divergence; readonly is one-way, so one ivar read).
|
|
289
|
+
if @readonly
|
|
290
|
+
raise Leptris::XML::ReadOnlyError,
|
|
291
|
+
"document is readonly — root= attempted"
|
|
292
|
+
end
|
|
286
293
|
# A document root has no in-scope declarations of its own —
|
|
287
294
|
# lift everything the element's source scope carried (#178).
|
|
288
295
|
unless Leptris::XML::Element.skip_adoption_lift?(element)
|
|
289
296
|
Leptris::XML::Element.lift_namespaces_for_adoption(element, {})
|
|
290
297
|
end
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
298
|
+
# TODO.perf/33: gates + bump + engine set_root in one C
|
|
299
|
+
# dispatch (the FFI call plus the c_ptr materialization fed
|
|
300
|
+
# ~5% of a fresh-doc build).
|
|
301
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
302
|
+
Leptris::XML::FFI.check_status(
|
|
303
|
+
Leptris::XML::Native.set_binding_root(self, element.c_address))
|
|
304
|
+
else
|
|
305
|
+
Leptris::XML::FFI.check_status(
|
|
306
|
+
Leptris::XML::FFI.leptris_document_set_root(c_ptr, element.c_ptr))
|
|
307
|
+
@version += 1
|
|
308
|
+
end
|
|
294
309
|
# Seed the root memo through wrap: a cross-document element
|
|
295
310
|
# must enter THIS document's identity cache with @document
|
|
296
311
|
# pointing here, not ride its source-document wrapper.
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -497,9 +497,21 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
497
497
|
|
|
498
498
|
# Deep copy in a NEW document via Document.copy_of (the single
|
|
499
499
|
# copy seam — every child kind and namespace survives,
|
|
500
|
-
# #696/#721/#812).
|
|
500
|
+
# #696/#721/#812). TODO.perf/30: one C dispatch when the ext
|
|
501
|
+
# is loaded — the namespace-lift decision stays here.
|
|
501
502
|
def dup
|
|
502
503
|
ensure_alive!
|
|
504
|
+
if @addr_reads_fast
|
|
505
|
+
doc = Leptris::XML::Native.copy_binding_element(
|
|
506
|
+
@document, @c_address)
|
|
507
|
+
unless doc.nil?
|
|
508
|
+
copied = doc.root
|
|
509
|
+
unless Leptris::XML::Element.skip_adoption_lift?(copied)
|
|
510
|
+
Leptris::XML::Element.lift_namespaces_for_adoption(copied, {})
|
|
511
|
+
end
|
|
512
|
+
return copied
|
|
513
|
+
end
|
|
514
|
+
end
|
|
503
515
|
Leptris::XML::Document.copy_of(self)
|
|
504
516
|
end
|
|
505
517
|
alias_method :clone, :dup
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -8,6 +8,12 @@ class Leptris::XML::Node
|
|
|
8
8
|
@c_ptr ||= ::FFI::Pointer.new(@c_address)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# The Integer twin of #c_ptr — reading it never materializes a
|
|
12
|
+
# Pointer (the native faces take addresses).
|
|
13
|
+
def c_address
|
|
14
|
+
@c_address
|
|
15
|
+
end
|
|
16
|
+
|
|
11
17
|
# Iterparse-yielded elements are owned by an IterationScope (the
|
|
12
18
|
# internal lifetime/memoization authority) — the public #document
|
|
13
19
|
# answers nil for them, per the documented contract.
|
|
@@ -390,6 +396,17 @@ class Leptris::XML::Node
|
|
|
390
396
|
def first_element_child
|
|
391
397
|
return @first_element_child if memo_hit?(@first_element_child_version)
|
|
392
398
|
ensure_alive!
|
|
399
|
+
# TODO.perf/30: one C walk (the FFI scan paid two calls per
|
|
400
|
+
# skipped non-element sibling).
|
|
401
|
+
if @addr_reads_fast
|
|
402
|
+
result = Leptris::XML::Native.first_element_child(
|
|
403
|
+
@document, @c_address)
|
|
404
|
+
if @document
|
|
405
|
+
@first_element_child = result
|
|
406
|
+
@first_element_child_version = @document.version
|
|
407
|
+
end
|
|
408
|
+
return result
|
|
409
|
+
end
|
|
393
410
|
# Raw pointer scan: non-element siblings are typed with one C
|
|
394
411
|
# call each — never wrapped, never cached — and the found
|
|
395
412
|
# element carries the ELEMENT hint into the wrap.
|
|
@@ -413,8 +430,12 @@ class Leptris::XML::Node
|
|
|
413
430
|
end
|
|
414
431
|
|
|
415
432
|
def last_element_child
|
|
416
|
-
#
|
|
417
|
-
#
|
|
433
|
+
# TODO.perf/30: one C walk — the batch fetch materialized
|
|
434
|
+
# every child pointer to keep one.
|
|
435
|
+
if @addr_reads_fast
|
|
436
|
+
return Leptris::XML::Native.last_element_child(
|
|
437
|
+
@document, @c_address)
|
|
438
|
+
end
|
|
418
439
|
if is_a?(Leptris::XML::Element)
|
|
419
440
|
kids = Leptris::XML::FFI.fetch_element_children(c_ptr)
|
|
420
441
|
return nil if kids.empty?
|
|
@@ -600,6 +621,23 @@ class Leptris::XML::Node
|
|
|
600
621
|
# non-element kinds rebuild by value — #161).
|
|
601
622
|
def dup
|
|
602
623
|
ensure_alive!
|
|
624
|
+
# TODO.perf/30: elements copy in one C dispatch (engine
|
|
625
|
+
# create + handle + element_copy + rooted wrap). The
|
|
626
|
+
# namespace lift stays a Ruby decision — the copy_of seam's
|
|
627
|
+
# semantics (#696/#721/#812) run below when needed.
|
|
628
|
+
if element? && @addr_reads_fast
|
|
629
|
+
doc = Leptris::XML::Native.copy_binding_element(
|
|
630
|
+
@document, @c_address)
|
|
631
|
+
unless doc.nil?
|
|
632
|
+
# copy_of's contract returns the copied ROOT (the last
|
|
633
|
+
# expression of the root= assignment), not the document.
|
|
634
|
+
copied = doc.root
|
|
635
|
+
unless Leptris::XML::Element.skip_adoption_lift?(copied)
|
|
636
|
+
Leptris::XML::Element.lift_namespaces_for_adoption(copied, {})
|
|
637
|
+
end
|
|
638
|
+
return copied
|
|
639
|
+
end
|
|
640
|
+
end
|
|
603
641
|
Leptris::XML::Document.copy_of(self)
|
|
604
642
|
end
|
|
605
643
|
alias_method :clone, :dup
|
|
@@ -69,7 +69,16 @@ module Leptris::XML::Searchable
|
|
|
69
69
|
doc_ptr, context_ptr, expr,
|
|
70
70
|
Leptris::XML::Searchable.xpath_version_code(version), nil)
|
|
71
71
|
elsif ns && !ns.empty?
|
|
72
|
-
|
|
72
|
+
# TODO.perf/29: the compiled handle is ns-independent —
|
|
73
|
+
# ride the cache and the cached ns set instead of the
|
|
74
|
+
# engine's string parse per call.
|
|
75
|
+
if (compiled = Leptris::XML::Searchable.compiled_expression(expr))
|
|
76
|
+
Leptris::XML::FFI.with_ns_set(ns) do |set|
|
|
77
|
+
compiled.eval_ns_ptrs(doc_ptr, context_ptr, set)
|
|
78
|
+
end
|
|
79
|
+
else
|
|
80
|
+
xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
|
|
81
|
+
end
|
|
73
82
|
elsif (compiled = Leptris::XML::Searchable.compiled_expression(expr))
|
|
74
83
|
compiled.eval_ptrs(doc_ptr, context_ptr)
|
|
75
84
|
else
|
|
@@ -105,7 +114,16 @@ module Leptris::XML::Searchable
|
|
|
105
114
|
doc_ptr, context_ptr, expr,
|
|
106
115
|
Leptris::XML::Searchable.xpath_version_code(version), nil)
|
|
107
116
|
elsif ns && !ns.empty?
|
|
108
|
-
|
|
117
|
+
# TODO.perf/29: the compiled handle is ns-independent —
|
|
118
|
+
# ride the cache and the cached ns set instead of the
|
|
119
|
+
# engine's string parse per call.
|
|
120
|
+
if (compiled = Leptris::XML::Searchable.compiled_expression(expr))
|
|
121
|
+
Leptris::XML::FFI.with_ns_set(ns) do |set|
|
|
122
|
+
compiled.eval_ns_ptrs(doc_ptr, context_ptr, set)
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
xpath_eval_with_namespaces(doc_ptr, context_ptr, expr, ns)
|
|
126
|
+
end
|
|
109
127
|
elsif (compiled = Leptris::XML::Searchable.compiled_expression(expr))
|
|
110
128
|
compiled.eval_ptrs(doc_ptr, context_ptr)
|
|
111
129
|
else
|
data/lib/leptris/xml/xpath.rb
CHANGED
|
@@ -40,6 +40,14 @@ class Leptris::XML::XPath
|
|
|
40
40
|
@handle, doc_ptr, context_ptr)
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# Namespace-bound evaluation against the compiled handle
|
|
44
|
+
# (TODO.perf/29): the handle is ns-independent — compile once,
|
|
45
|
+
# bind per call through the cached ns set.
|
|
46
|
+
def eval_ns_ptrs(doc_ptr, context_ptr, ns_set_ptr)
|
|
47
|
+
Leptris::XML::FFI.leptris_xpath_compiled_eval_ns(
|
|
48
|
+
@handle, doc_ptr, context_ptr, ns_set_ptr)
|
|
49
|
+
end
|
|
50
|
+
|
|
43
51
|
def initialize(expression, handle, version_code = nil)
|
|
44
52
|
@expression = expression
|
|
45
53
|
@handle = handle
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: leptris
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.9.174.
|
|
4
|
+
version: 1.9.174.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ffi
|
|
@@ -97,6 +97,11 @@ files:
|
|
|
97
97
|
- TODO.perf/26-post-mutation-memo-seeding.md
|
|
98
98
|
- TODO.perf/27-c-yield-traversal.md
|
|
99
99
|
- TODO.perf/28-address-fills.md
|
|
100
|
+
- TODO.perf/29-ns-xpath-compiled.md
|
|
101
|
+
- TODO.perf/30-copy-and-element-child-faces.md
|
|
102
|
+
- TODO.perf/31-battery-rounds-4-8.md
|
|
103
|
+
- TODO.perf/32-clean-host-floor-table.md
|
|
104
|
+
- TODO.perf/33-cbound-root-set.md
|
|
100
105
|
- TODO.restructure/01-constraint-compliance-audit.md
|
|
101
106
|
- TODO.restructure/02-deep-copy-seam.md
|
|
102
107
|
- TODO.restructure/03-evaluation-context-seam.md
|