leptris 1.9.174.5 → 1.9.174.7
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 +31 -0
- data/TODO.perf/27-c-yield-traversal.md +38 -0
- data/TODO.perf/28-address-fills.md +25 -0
- data/TODO.perf/29-ns-xpath-compiled.md +23 -0
- data/TODO.perf/30-copy-and-element-child-faces.md +31 -0
- data/ext/leptris/native/native.c +360 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/cdata.rb +5 -1
- data/lib/leptris/xml/comment.rb +5 -1
- data/lib/leptris/xml/element.rb +16 -2
- data/lib/leptris/xml/node.rb +58 -6
- data/lib/leptris/xml/processing_instruction.rb +11 -3
- data/lib/leptris/xml/searchable.rb +20 -2
- data/lib/leptris/xml/text.rb +5 -1
- 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: 2b55134beeb4963b45dab45f2702b840c8db5e4f1c5e282622f5e4c4b5f55b1e
|
|
4
|
+
data.tar.gz: 92278400c3c9a6ff0450a151583a9edd383677b621d6c95bdbd5286d0ba0b8a1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e476a9c2aaecd83069e118a4a7c1aa4ed141df2c853b238f4eb745b916be151ce46b0a5c29686f3d481d2bb816fe4c6f4744df7e76835bfe53ccc82c998134d
|
|
7
|
+
data.tar.gz: af8d33a0aaa87d762405979bcbc08be5af416e8dac2de924027eb900184df6740fcc4a8ef30e533becf131db08c2502a9dfc4a634b9678d9a87b5fa0513af212
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ 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.7] - 2026-09-16
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Namespace-bound xpath on the compiled path (TODO.perf/29)**:
|
|
13
|
+
the ns branch rides the compiled-expression cache (the handle
|
|
14
|
+
is ns-independent) plus the cached ns set through
|
|
15
|
+
`XPath#eval_ns_ptrs`, then the eager materializer — measured
|
|
16
|
+
(load ~16): repeat ns-xpath 7.9µs → 3.1µs (2.5x).
|
|
17
|
+
- **dup + element-child faces (TODO.perf/30)**: `Element#dup`
|
|
18
|
+
runs engine create + handle + element_copy + rooted wrap in
|
|
19
|
+
one C dispatch (the namespace-lift decision stays in Ruby) —
|
|
20
|
+
ns-bearing dup 37.0µs → 9.6µs (3.9x); `first/last_element_child`
|
|
21
|
+
answer in one C walk each (the FFI paths paid O(N) scans).
|
|
22
|
+
|
|
23
|
+
## [1.9.174.6] - 2026-09-15
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **C-yield traversal (TODO.perf/27)**: `traverse` and `visit`
|
|
28
|
+
no longer allocate an FFI::Function closure per call — the
|
|
29
|
+
ext's callbacks rb_yield directly, preserving post-order +
|
|
30
|
+
abort-at-self + stash-abort-raise (traverse) and the
|
|
31
|
+
(node, entering, depth) visit contract. Measured (load 34):
|
|
32
|
+
**traverse over 8k nodes 1663.5µs → 379.9µs (4.4x; 6.81x
|
|
33
|
+
Nokogiri); visit 2288.6µs → 545.4µs (4.2x)**.
|
|
34
|
+
- **Address-based first-touch fills (TODO.perf/28)**:
|
|
35
|
+
text/comment/CDATA/PI content and `path` fill through address
|
|
36
|
+
faces (scope-eligible); the bulk attribute faces accept
|
|
37
|
+
scope-owned elements.
|
|
38
|
+
|
|
8
39
|
## [1.9.174.5] - 2026-09-15
|
|
9
40
|
|
|
10
41
|
### Changed
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# 27 — C-yield traversal (traverse/visit without the FFI::Function closure)
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.6)
|
|
4
|
+
|
|
5
|
+
Node#traverse and #visit allocate an FFI::Function closure per
|
|
6
|
+
call (~1µs) and dispatch every visited node back through the
|
|
7
|
+
Ruby closure — the dominant per-node cost of whole-subtree
|
|
8
|
+
iteration, paid even on small subtrees where the walk itself is
|
|
9
|
+
microseconds.
|
|
10
|
+
|
|
11
|
+
Bind both to C callbacks that rb_yield directly: the ext's
|
|
12
|
+
traverse face keeps the abort-at-self post-order contract and
|
|
13
|
+
the exception discipline (rb_protect in the callback stashes,
|
|
14
|
+
aborts the walk, re-raises after — exactly the Ruby version's
|
|
15
|
+
contract); visit keeps its (node, entering, depth) callback
|
|
16
|
+
shape with the same stash-and-raise treatment. Node wrapping per
|
|
17
|
+
visit rides the shared binding construction (identity cache +
|
|
18
|
+
kind dispatch), and the walk state lives on the C stack — no
|
|
19
|
+
globals, nested traversals safe.
|
|
20
|
+
|
|
21
|
+
Gates: post-order sequence identical (receiver last); the
|
|
22
|
+
subtree boundary holds (abort-at-self); block exceptions
|
|
23
|
+
re-raise after the walk stops; enum_for forms unchanged; suite
|
|
24
|
+
both modes.
|
|
25
|
+
|
|
26
|
+
## Outcome (1.9.174.6)
|
|
27
|
+
|
|
28
|
+
Native.traverse_binding / visit_binding: C callbacks that
|
|
29
|
+
rb_yield directly (block forwarded explicitly — rb_yield needs
|
|
30
|
+
it on the C entry's frame). traverse keeps post-order +
|
|
31
|
+
abort-at-self + stash-abort-raise (rb_protect in the callback,
|
|
32
|
+
walk state on the C stack — nested traversals independent);
|
|
33
|
+
visit keeps (node, entering, depth) with the engine's
|
|
34
|
+
(user, node, ...) callback order. Two bugs found by the suite:
|
|
35
|
+
the visit callback's argument order (user first — the segfault
|
|
36
|
+
spec caught it) and the block forwarding. Measured (load 34):
|
|
37
|
+
traverse over 8k nodes 1663.5µs (FFI closure) -> 379.9µs
|
|
38
|
+
(4.4x; 6.81x Nokogiri); visit 2288.6µs -> 545.4µs (4.2x).
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# 28 — Address-based first-touch fills for the remaining reads
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.6)
|
|
4
|
+
|
|
5
|
+
The memoized content readers still fill through FFI marshaling on
|
|
6
|
+
first touch — Text/Comment/CDATA/PI content, and Node#path
|
|
7
|
+
(owned-string read + free) — and the bulk attribute faces exclude
|
|
8
|
+
scope-owned elements though they touch no document state. Add the
|
|
9
|
+
trivial address faces (fast_text/comment/cdata content, pi
|
|
10
|
+
target/data, node path with the free protocol) and switch the
|
|
11
|
+
fills to @addr_reads_fast (scope-eligible); the attributes bulk
|
|
12
|
+
path follows.
|
|
13
|
+
|
|
14
|
+
Gates: identical values for every kind (UTF-8, escapes, nils);
|
|
15
|
+
scope-owned fills answer the same; path memo unchanged; suite
|
|
16
|
+
both modes.
|
|
17
|
+
|
|
18
|
+
## Outcome (1.9.174.6)
|
|
19
|
+
|
|
20
|
+
fast_text/comment/cdata content, fast_pi_target/data (the
|
|
21
|
+
read_pi_data strip preserved), and fast_path (owned-string copy
|
|
22
|
+
+ engine free) — every first-touch content fill now rides the
|
|
23
|
+
address faces via @addr_reads_fast (scope-eligible), and the
|
|
24
|
+
bulk attribute faces accept scope-owned elements (they touch no
|
|
25
|
+
document state; the scope's version stamps the memo).
|
|
@@ -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.
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -87,8 +87,20 @@ static node_str_fn f_comment_content, f_cdata_content, f_pi_target,
|
|
|
87
87
|
f_pi_data;
|
|
88
88
|
typedef int (*set_str_fn)(void *, const char *);
|
|
89
89
|
typedef int (*node_unlink_fn)(void *);
|
|
90
|
+
typedef int (*traverse_cb_fn)(void *, void *);
|
|
91
|
+
typedef int (*traverse_fn)(void *, int, traverse_cb_fn, void *);
|
|
92
|
+
typedef void (*visit_cb_fn)(void *, void *, int, int);
|
|
93
|
+
typedef void (*visit_fn)(void *, visit_cb_fn, void *);
|
|
94
|
+
typedef void (*free_str_fn)(void *);
|
|
95
|
+
typedef const char *(*node_xpath_fn)(void *);
|
|
96
|
+
typedef void *(*elem_copy_fn)(void *, void *);
|
|
90
97
|
static set_str_fn f_elem_set_name, f_elem_set_text, f_text_set_content;
|
|
91
98
|
static node_unlink_fn f_node_unlink;
|
|
99
|
+
static traverse_fn f_node_traverse;
|
|
100
|
+
static visit_fn f_node_visit;
|
|
101
|
+
static free_str_fn f_free_str;
|
|
102
|
+
static node_xpath_fn f_node_xpath;
|
|
103
|
+
static elem_copy_fn f_elem_copy;
|
|
92
104
|
static set_root_fn f_set_root;
|
|
93
105
|
static doc_free_fn f_doc_free;
|
|
94
106
|
|
|
@@ -105,6 +117,10 @@ static VALUE c_use_after_free_error;
|
|
|
105
117
|
static VALUE native_cache_of(VALUE document);
|
|
106
118
|
static void resolve_binding_classes(void);
|
|
107
119
|
static VALUE binding_cache_of(VALUE document);
|
|
120
|
+
static VALUE binding_klass_for(int kind);
|
|
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;
|
|
108
124
|
|
|
109
125
|
#define NT_ELEMENT 0
|
|
110
126
|
|
|
@@ -200,6 +216,11 @@ static void resolve_symbols(const char *lib_path)
|
|
|
200
216
|
f_elem_set_text = (set_str_fn)lib_sym(h, "leptris_element_set_text");
|
|
201
217
|
f_text_set_content = (set_str_fn)lib_sym(h, "leptris_text_node_set_content");
|
|
202
218
|
f_node_unlink = (node_unlink_fn)lib_sym(h, "leptris_node_unlink");
|
|
219
|
+
f_node_traverse = (traverse_fn)lib_sym(h, "leptris_node_traverse");
|
|
220
|
+
f_node_visit = (visit_fn)lib_sym(h, "leptris_node_visit");
|
|
221
|
+
f_free_str = (free_str_fn)lib_sym(h, "leptris_free_string");
|
|
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");
|
|
203
224
|
f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
|
|
204
225
|
f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
|
|
205
226
|
if (!f_elem_name || !f_text_content || !f_attr ||
|
|
@@ -211,6 +232,8 @@ static void resolve_symbols(const char *lib_path)
|
|
|
211
232
|
!f_comment_content || !f_cdata_content || !f_pi_target ||
|
|
212
233
|
!f_pi_data || !f_elem_set_name || !f_elem_set_text ||
|
|
213
234
|
!f_text_set_content || !f_node_unlink ||
|
|
235
|
+
!f_node_traverse || !f_node_visit || !f_free_str ||
|
|
236
|
+
!f_node_xpath || !f_elem_copy ||
|
|
214
237
|
!f_doc_free ||
|
|
215
238
|
!f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
|
|
216
239
|
!f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
|
|
@@ -707,6 +730,277 @@ static VALUE nf_unlink_binding_node(VALUE self, VALUE document,
|
|
|
707
730
|
return INT2FIX(st);
|
|
708
731
|
}
|
|
709
732
|
|
|
733
|
+
/* ---- C-yield traversal (TODO.perf/27) ----------------------------
|
|
734
|
+
* Same contracts as the Ruby FFI::Function versions without the
|
|
735
|
+
* per-call closure: traverse is post-order with abort-at-self
|
|
736
|
+
* (the receiver is the LAST node of its subtree in post-order,
|
|
737
|
+
* so stopping at self bounds the walk exactly) and
|
|
738
|
+
* stash-abort-raise exception discipline; visit passes (node,
|
|
739
|
+
* entering, depth). Walk state lives on the caller's C stack —
|
|
740
|
+
* nested traversals are independent. */
|
|
741
|
+
struct trav_state {
|
|
742
|
+
VALUE document;
|
|
743
|
+
VALUE err;
|
|
744
|
+
void *self_ptr;
|
|
745
|
+
int for_visit;
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
static VALUE trav_yield_one(VALUE arg)
|
|
749
|
+
{
|
|
750
|
+
return rb_yield(arg);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
static int trav_cb(void *node_ptr, void *user)
|
|
754
|
+
{
|
|
755
|
+
struct trav_state *st = user;
|
|
756
|
+
VALUE cache, key, node;
|
|
757
|
+
int kind, state;
|
|
758
|
+
|
|
759
|
+
if (st->err != Qnil)
|
|
760
|
+
return 1;
|
|
761
|
+
kind = f_node_type(node_ptr);
|
|
762
|
+
cache = binding_cache_of(st->document);
|
|
763
|
+
key = ULL2NUM((uint64_t)(uintptr_t)node_ptr);
|
|
764
|
+
node = rb_hash_aref(cache, key);
|
|
765
|
+
if (NIL_P(node)) {
|
|
766
|
+
node = rb_obj_alloc(binding_klass_for(kind));
|
|
767
|
+
rb_iv_set(node, "@c_address", key);
|
|
768
|
+
rb_iv_set(node, "@document", st->document);
|
|
769
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
770
|
+
if (rb_obj_class(st->document) == c_iteration_scope) {
|
|
771
|
+
rb_iv_set(node, "@structure_memoizable", Qfalse);
|
|
772
|
+
rb_iv_set(node, "@native_fast", Qfalse);
|
|
773
|
+
rb_iv_set(node, "@pub_document", Qnil);
|
|
774
|
+
} else {
|
|
775
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
776
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
777
|
+
rb_iv_set(node, "@pub_document", st->document);
|
|
778
|
+
}
|
|
779
|
+
rb_iv_set(node, "@addr_reads_fast", Qtrue);
|
|
780
|
+
rb_iv_set(node, "@node_type", INT2FIX(kind));
|
|
781
|
+
rb_hash_aset(cache, key, node);
|
|
782
|
+
}
|
|
783
|
+
rb_protect(trav_yield_one, node, &state);
|
|
784
|
+
if (state) {
|
|
785
|
+
st->err = rb_errinfo();
|
|
786
|
+
return 1; /* abort the walk */
|
|
787
|
+
}
|
|
788
|
+
return node_ptr == st->self_ptr ? 1 : 0; /* abort-at-self */
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
static VALUE nf_traverse_binding(VALUE self, VALUE document,
|
|
792
|
+
VALUE addr)
|
|
793
|
+
{
|
|
794
|
+
struct trav_state st;
|
|
795
|
+
|
|
796
|
+
(void)self;
|
|
797
|
+
resolve_binding_classes();
|
|
798
|
+
st.document = document;
|
|
799
|
+
st.err = Qnil;
|
|
800
|
+
st.self_ptr = (void *)(uintptr_t)NUM2ULL(addr);
|
|
801
|
+
st.for_visit = 0;
|
|
802
|
+
f_node_traverse(st.self_ptr, 1 /* TRAVERSE_POST_ORDER */,
|
|
803
|
+
trav_cb, &st);
|
|
804
|
+
if (st.err != Qnil)
|
|
805
|
+
rb_exc_raise(st.err);
|
|
806
|
+
return Qnil;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
struct visit_yield_args {
|
|
810
|
+
VALUE node, entering, depth;
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
static VALUE trav_yield_args(VALUE arg)
|
|
814
|
+
{
|
|
815
|
+
struct visit_yield_args *a = (struct visit_yield_args *)arg;
|
|
816
|
+
return rb_yield_values(3, a->node, a->entering, a->depth);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/* Engine visit callback order is (user, node, entering, depth) —
|
|
820
|
+
* the Ruby closure's |_, node_ptr, entering, depth| mirrors it. */
|
|
821
|
+
static void visit_cb(void *user, void *node_ptr, int entering,
|
|
822
|
+
int depth)
|
|
823
|
+
{
|
|
824
|
+
/* visit's engine signature cannot abort; exceptions are
|
|
825
|
+
* stashed and every later node is skipped — the raise
|
|
826
|
+
* happens after the walk, same observable outcome. */
|
|
827
|
+
struct trav_state *st = user;
|
|
828
|
+
struct visit_yield_args va;
|
|
829
|
+
VALUE cache, key, node;
|
|
830
|
+
int kind, state;
|
|
831
|
+
|
|
832
|
+
if (st->err != Qnil)
|
|
833
|
+
return;
|
|
834
|
+
kind = f_node_type(node_ptr);
|
|
835
|
+
cache = binding_cache_of(st->document);
|
|
836
|
+
key = ULL2NUM((uint64_t)(uintptr_t)node_ptr);
|
|
837
|
+
node = rb_hash_aref(cache, key);
|
|
838
|
+
if (NIL_P(node)) {
|
|
839
|
+
node = rb_obj_alloc(binding_klass_for(kind));
|
|
840
|
+
rb_iv_set(node, "@c_address", key);
|
|
841
|
+
rb_iv_set(node, "@document", st->document);
|
|
842
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
843
|
+
if (rb_obj_class(st->document) == c_iteration_scope) {
|
|
844
|
+
rb_iv_set(node, "@structure_memoizable", Qfalse);
|
|
845
|
+
rb_iv_set(node, "@native_fast", Qfalse);
|
|
846
|
+
rb_iv_set(node, "@pub_document", Qnil);
|
|
847
|
+
} else {
|
|
848
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
849
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
850
|
+
rb_iv_set(node, "@pub_document", st->document);
|
|
851
|
+
}
|
|
852
|
+
rb_iv_set(node, "@addr_reads_fast", Qtrue);
|
|
853
|
+
rb_iv_set(node, "@node_type", INT2FIX(kind));
|
|
854
|
+
rb_hash_aset(cache, key, node);
|
|
855
|
+
}
|
|
856
|
+
va.node = node;
|
|
857
|
+
va.entering = entering ? Qtrue : Qfalse;
|
|
858
|
+
va.depth = INT2NUM(depth);
|
|
859
|
+
rb_protect(trav_yield_args, (VALUE)&va, &state);
|
|
860
|
+
if (state)
|
|
861
|
+
st->err = rb_errinfo();
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
static VALUE nf_visit_binding(VALUE self, VALUE document, VALUE addr)
|
|
865
|
+
{
|
|
866
|
+
struct trav_state st;
|
|
867
|
+
|
|
868
|
+
(void)self;
|
|
869
|
+
resolve_binding_classes();
|
|
870
|
+
st.document = document;
|
|
871
|
+
st.err = Qnil;
|
|
872
|
+
st.self_ptr = (void *)(uintptr_t)NUM2ULL(addr);
|
|
873
|
+
st.for_visit = 1;
|
|
874
|
+
f_node_visit(st.self_ptr, visit_cb, &st);
|
|
875
|
+
if (st.err != Qnil)
|
|
876
|
+
rb_exc_raise(st.err);
|
|
877
|
+
return Qnil;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/* ---- Address-based fills (TODO.perf/28) -------------------------
|
|
881
|
+
* Pure reads of node-local data — no cache, no version — the
|
|
882
|
+
* same shapes the inner_html pass uses. */
|
|
883
|
+
static VALUE nf_fast_text_content(VALUE self, VALUE addr)
|
|
884
|
+
{
|
|
885
|
+
const char *s;
|
|
886
|
+
(void)self;
|
|
887
|
+
s = f_text_content((void *)(uintptr_t)NUM2ULL(addr));
|
|
888
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
static VALUE nf_fast_comment_content(VALUE self, VALUE addr)
|
|
892
|
+
{
|
|
893
|
+
const char *s;
|
|
894
|
+
(void)self;
|
|
895
|
+
s = f_comment_content((void *)(uintptr_t)NUM2ULL(addr));
|
|
896
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
static VALUE nf_fast_cdata_content(VALUE self, VALUE addr)
|
|
900
|
+
{
|
|
901
|
+
const char *s;
|
|
902
|
+
(void)self;
|
|
903
|
+
s = f_cdata_content((void *)(uintptr_t)NUM2ULL(addr));
|
|
904
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
static VALUE nf_fast_pi_target(VALUE self, VALUE addr)
|
|
908
|
+
{
|
|
909
|
+
const char *s;
|
|
910
|
+
(void)self;
|
|
911
|
+
s = f_pi_target((void *)(uintptr_t)NUM2ULL(addr));
|
|
912
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
static VALUE nf_fast_pi_data(VALUE self, VALUE addr)
|
|
916
|
+
{
|
|
917
|
+
const char *s;
|
|
918
|
+
(void)self;
|
|
919
|
+
s = f_pi_data((void *)(uintptr_t)NUM2ULL(addr));
|
|
920
|
+
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/* Shared construction for address-walk faces (TODO.perf/30):
|
|
924
|
+
* cache lookup + kind-dispatched binding wrapper. */
|
|
925
|
+
static VALUE wrap_addr_node(VALUE document, void *ptr)
|
|
926
|
+
{
|
|
927
|
+
VALUE cache, key, node;
|
|
928
|
+
int kind;
|
|
929
|
+
|
|
930
|
+
kind = f_node_type(ptr);
|
|
931
|
+
cache = binding_cache_of(document);
|
|
932
|
+
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
933
|
+
node = rb_hash_aref(cache, key);
|
|
934
|
+
if (NIL_P(node)) {
|
|
935
|
+
node = rb_obj_alloc(binding_klass_for(kind));
|
|
936
|
+
rb_iv_set(node, "@c_address", key);
|
|
937
|
+
rb_iv_set(node, "@document", document);
|
|
938
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
939
|
+
if (rb_obj_class(document) == c_iteration_scope) {
|
|
940
|
+
rb_iv_set(node, "@structure_memoizable", Qfalse);
|
|
941
|
+
rb_iv_set(node, "@native_fast", Qfalse);
|
|
942
|
+
rb_iv_set(node, "@pub_document", Qnil);
|
|
943
|
+
} else {
|
|
944
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
945
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
946
|
+
rb_iv_set(node, "@pub_document", document);
|
|
947
|
+
}
|
|
948
|
+
rb_iv_set(node, "@addr_reads_fast", Qtrue);
|
|
949
|
+
rb_iv_set(node, "@node_type", INT2FIX(kind));
|
|
950
|
+
rb_hash_aset(cache, key, node);
|
|
951
|
+
}
|
|
952
|
+
return node;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/* First element child in one C walk (text-heavy parents paid two
|
|
956
|
+
* FFI calls per skipped sibling). */
|
|
957
|
+
static VALUE nf_first_element_child(VALUE self, VALUE document,
|
|
958
|
+
VALUE addr)
|
|
959
|
+
{
|
|
960
|
+
void *p;
|
|
961
|
+
|
|
962
|
+
(void)self;
|
|
963
|
+
resolve_binding_classes();
|
|
964
|
+
for (p = f_first_child((void *)(uintptr_t)NUM2ULL(addr));
|
|
965
|
+
p != NULL; p = f_next_sibling(p)) {
|
|
966
|
+
if (f_node_type(p) == NT_ELEMENT)
|
|
967
|
+
return wrap_addr_node(document, p);
|
|
968
|
+
}
|
|
969
|
+
return Qnil;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/* Last element child in one C walk (the batch fetch materialized
|
|
973
|
+
* every child to keep one). */
|
|
974
|
+
static VALUE nf_last_element_child(VALUE self, VALUE document,
|
|
975
|
+
VALUE addr)
|
|
976
|
+
{
|
|
977
|
+
void *p, *last = NULL;
|
|
978
|
+
|
|
979
|
+
(void)self;
|
|
980
|
+
resolve_binding_classes();
|
|
981
|
+
for (p = f_first_child((void *)(uintptr_t)NUM2ULL(addr));
|
|
982
|
+
p != NULL; p = f_next_sibling(p)) {
|
|
983
|
+
if (f_node_type(p) == NT_ELEMENT)
|
|
984
|
+
last = p;
|
|
985
|
+
}
|
|
986
|
+
return last ? wrap_addr_node(document, last) : Qnil;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/* Owned string: copy + free through the engine's seam (the
|
|
990
|
+
* read_owned_string protocol). */
|
|
991
|
+
static VALUE nf_fast_path(VALUE self, VALUE addr)
|
|
992
|
+
{
|
|
993
|
+
const char *s;
|
|
994
|
+
VALUE out;
|
|
995
|
+
(void)self;
|
|
996
|
+
s = f_node_xpath((void *)(uintptr_t)NUM2ULL(addr));
|
|
997
|
+
if (!s)
|
|
998
|
+
return Qnil;
|
|
999
|
+
out = rb_utf8_str_new_cstr(s);
|
|
1000
|
+
f_free_str((void *)s);
|
|
1001
|
+
return out;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
710
1004
|
/* C-bound insert family (TODO.perf/14): prepend / insert-after /
|
|
711
1005
|
* insert-before share append's gates + predicate + bump shape.
|
|
712
1006
|
* mode: 1 prepend, 2 after, 3 before (anchor = receiver). */
|
|
@@ -1003,6 +1297,8 @@ static VALUE m_native_sentinel; /* module object = fallback marker */
|
|
|
1003
1297
|
static VALUE materialize_xp_entry(VALUE document, VALUE cache,
|
|
1004
1298
|
void *result, void *ptr, int kind,
|
|
1005
1299
|
int idx);
|
|
1300
|
+
static VALUE binding_klass_for(int kind);
|
|
1301
|
+
static VALUE c_iteration_scope;
|
|
1006
1302
|
|
|
1007
1303
|
static VALUE bulk_xpath_array(VALUE document, void *result);
|
|
1008
1304
|
|
|
@@ -1512,6 +1808,48 @@ static const rb_data_type_t dh_type = {
|
|
|
1512
1808
|
static VALUE c_doc_handle;
|
|
1513
1809
|
static ID id_iv_doc_handle, id_freed_new, id_alive;
|
|
1514
1810
|
|
|
1811
|
+
/* Node#dup in one dispatch (TODO.perf/30): engine create +
|
|
1812
|
+
* lifetime handle + document wrapper + element_copy + the copy
|
|
1813
|
+
* wrapped-and-rooted (memo-seeded). The namespace lift stays a
|
|
1814
|
+
* Ruby decision (skip_adoption_lift? — the copy_of seam's
|
|
1815
|
+
* semantics, #696/#721/#812). */
|
|
1816
|
+
static VALUE nf_copy_binding_element(VALUE self, VALUE document,
|
|
1817
|
+
VALUE addr)
|
|
1818
|
+
{
|
|
1819
|
+
void *doc, *copy;
|
|
1820
|
+
VALUE new_doc, doc_addr, handle, freed, root;
|
|
1821
|
+
struct doc_handle *h;
|
|
1822
|
+
|
|
1823
|
+
(void)self;
|
|
1824
|
+
resolve_binding_classes();
|
|
1825
|
+
doc = f_doc_create();
|
|
1826
|
+
if (!doc)
|
|
1827
|
+
return Qnil;
|
|
1828
|
+
doc_addr = ULL2NUM((uint64_t)(uintptr_t)doc);
|
|
1829
|
+
new_doc = rb_obj_alloc(c_b_document);
|
|
1830
|
+
rb_iv_set(new_doc, "@c_ptr",
|
|
1831
|
+
rb_funcall(c_ffi_pointer, id_ptr_new, 1, doc_addr));
|
|
1832
|
+
rb_iv_set(new_doc, "@c_address", doc_addr);
|
|
1833
|
+
freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
|
|
1834
|
+
rb_iv_set(new_doc, "@freed", freed);
|
|
1835
|
+
rb_iv_set(new_doc, "@readonly", Qfalse);
|
|
1836
|
+
rb_iv_set(new_doc, "@version", INT2FIX(0));
|
|
1837
|
+
handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
|
|
1838
|
+
&dh_type, h);
|
|
1839
|
+
h->doc = doc;
|
|
1840
|
+
rb_ivar_set(new_doc, id_iv_doc_handle, handle);
|
|
1841
|
+
|
|
1842
|
+
copy = f_elem_copy((void *)(uintptr_t)NUM2ULL(addr), doc);
|
|
1843
|
+
if (!copy) {
|
|
1844
|
+
h->doc = NULL; /* the handle releases the empty doc */
|
|
1845
|
+
return Qnil;
|
|
1846
|
+
}
|
|
1847
|
+
root = wrap_addr_node(new_doc, copy);
|
|
1848
|
+
rb_ivar_set(new_doc, rb_intern("@root"), root);
|
|
1849
|
+
rb_ivar_set(new_doc, rb_intern("@root_version"), INT2FIX(0));
|
|
1850
|
+
return new_doc;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1515
1853
|
static VALUE nf_doc_handle_attach(VALUE self, VALUE document)
|
|
1516
1854
|
{
|
|
1517
1855
|
struct doc_handle *h;
|
|
@@ -1711,6 +2049,28 @@ void Init_native(void)
|
|
|
1711
2049
|
nf_set_binding_text, 3);
|
|
1712
2050
|
rb_define_module_function(m_native, "unlink_binding_node",
|
|
1713
2051
|
nf_unlink_binding_node, 2);
|
|
2052
|
+
rb_define_module_function(m_native, "traverse_binding",
|
|
2053
|
+
nf_traverse_binding, 2);
|
|
2054
|
+
rb_define_module_function(m_native, "visit_binding",
|
|
2055
|
+
nf_visit_binding, 2);
|
|
2056
|
+
rb_define_module_function(m_native, "fast_text_content",
|
|
2057
|
+
nf_fast_text_content, 1);
|
|
2058
|
+
rb_define_module_function(m_native, "fast_comment_content",
|
|
2059
|
+
nf_fast_comment_content, 1);
|
|
2060
|
+
rb_define_module_function(m_native, "fast_cdata_content",
|
|
2061
|
+
nf_fast_cdata_content, 1);
|
|
2062
|
+
rb_define_module_function(m_native, "fast_pi_target",
|
|
2063
|
+
nf_fast_pi_target, 1);
|
|
2064
|
+
rb_define_module_function(m_native, "fast_pi_data",
|
|
2065
|
+
nf_fast_pi_data, 1);
|
|
2066
|
+
rb_define_module_function(m_native, "fast_path",
|
|
2067
|
+
nf_fast_path, 1);
|
|
2068
|
+
rb_define_module_function(m_native, "first_element_child",
|
|
2069
|
+
nf_first_element_child, 2);
|
|
2070
|
+
rb_define_module_function(m_native, "last_element_child",
|
|
2071
|
+
nf_last_element_child, 2);
|
|
2072
|
+
rb_define_module_function(m_native, "copy_binding_element",
|
|
2073
|
+
nf_copy_binding_element, 2);
|
|
1714
2074
|
rb_define_module_function(m_native, "fast_inner_xml",
|
|
1715
2075
|
nf_fast_inner_xml, 1);
|
|
1716
2076
|
rb_define_module_function(m_native, "doc_handle_attach",
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/cdata.rb
CHANGED
|
@@ -6,7 +6,11 @@ class Leptris::XML::CDATA < Leptris::XML::Text
|
|
|
6
6
|
def content
|
|
7
7
|
return @content if memo_hit?(@content_version)
|
|
8
8
|
ensure_alive!
|
|
9
|
-
result =
|
|
9
|
+
result = if @addr_reads_fast
|
|
10
|
+
Leptris::XML::Native.fast_cdata_content(@c_address)
|
|
11
|
+
else
|
|
12
|
+
Leptris::XML::FFI.leptris_cdata_node_get_content(c_ptr)
|
|
13
|
+
end
|
|
10
14
|
if @document
|
|
11
15
|
@content = result
|
|
12
16
|
@content_version = @document.version
|
data/lib/leptris/xml/comment.rb
CHANGED
|
@@ -6,7 +6,11 @@ class Leptris::XML::Comment < Leptris::XML::Node
|
|
|
6
6
|
def content
|
|
7
7
|
return @content if memo_hit?(@content_version)
|
|
8
8
|
ensure_alive!
|
|
9
|
-
result =
|
|
9
|
+
result = if @addr_reads_fast
|
|
10
|
+
Leptris::XML::Native.fast_comment_content(@c_address)
|
|
11
|
+
else
|
|
12
|
+
Leptris::XML::FFI.leptris_comment_node_get_content(c_ptr)
|
|
13
|
+
end
|
|
10
14
|
if @document
|
|
11
15
|
@content = result
|
|
12
16
|
@content_version = @document.version
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -260,7 +260,9 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
260
260
|
# Require the full Attr-hash face — a cold [] may have set the
|
|
261
261
|
# version with only a partial @attr_values (ruby#150).
|
|
262
262
|
return @attributes if @attributes && memo_hit?(@attributes_version)
|
|
263
|
-
|
|
263
|
+
# TODO.perf/28: the bulk faces touch no document state —
|
|
264
|
+
# scope-eligible via @addr_reads_fast.
|
|
265
|
+
if @addr_reads_fast
|
|
264
266
|
# TODO.perf/10: both memo faces in one C walk.
|
|
265
267
|
result, values = Leptris::XML::Native.bulk_attr_faces(
|
|
266
268
|
@c_address, self)
|
|
@@ -495,9 +497,21 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
495
497
|
|
|
496
498
|
# Deep copy in a NEW document via Document.copy_of (the single
|
|
497
499
|
# copy seam — every child kind and namespace survives,
|
|
498
|
-
# #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.
|
|
499
502
|
def dup
|
|
500
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
|
|
501
515
|
Leptris::XML::Document.copy_of(self)
|
|
502
516
|
end
|
|
503
517
|
alias_method :clone, :dup
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -390,6 +390,17 @@ class Leptris::XML::Node
|
|
|
390
390
|
def first_element_child
|
|
391
391
|
return @first_element_child if memo_hit?(@first_element_child_version)
|
|
392
392
|
ensure_alive!
|
|
393
|
+
# TODO.perf/30: one C walk (the FFI scan paid two calls per
|
|
394
|
+
# skipped non-element sibling).
|
|
395
|
+
if @addr_reads_fast
|
|
396
|
+
result = Leptris::XML::Native.first_element_child(
|
|
397
|
+
@document, @c_address)
|
|
398
|
+
if @document
|
|
399
|
+
@first_element_child = result
|
|
400
|
+
@first_element_child_version = @document.version
|
|
401
|
+
end
|
|
402
|
+
return result
|
|
403
|
+
end
|
|
393
404
|
# Raw pointer scan: non-element siblings are typed with one C
|
|
394
405
|
# call each — never wrapped, never cached — and the found
|
|
395
406
|
# element carries the ELEMENT hint into the wrap.
|
|
@@ -413,8 +424,12 @@ class Leptris::XML::Node
|
|
|
413
424
|
end
|
|
414
425
|
|
|
415
426
|
def last_element_child
|
|
416
|
-
#
|
|
417
|
-
#
|
|
427
|
+
# TODO.perf/30: one C walk — the batch fetch materialized
|
|
428
|
+
# every child pointer to keep one.
|
|
429
|
+
if @addr_reads_fast
|
|
430
|
+
return Leptris::XML::Native.last_element_child(
|
|
431
|
+
@document, @c_address)
|
|
432
|
+
end
|
|
418
433
|
if is_a?(Leptris::XML::Element)
|
|
419
434
|
kids = Leptris::XML::FFI.fetch_element_children(c_ptr)
|
|
420
435
|
return nil if kids.empty?
|
|
@@ -492,6 +507,13 @@ class Leptris::XML::Node
|
|
|
492
507
|
def visit(&block)
|
|
493
508
|
return enum_for(:visit) unless block
|
|
494
509
|
ensure_alive!
|
|
510
|
+
# TODO.perf/27: the ext's callback rb_yields directly — no
|
|
511
|
+
# FFI::Function closure per call.
|
|
512
|
+
if @addr_reads_fast
|
|
513
|
+
Leptris::XML::Native.visit_binding(@document, @c_address,
|
|
514
|
+
&block)
|
|
515
|
+
return self
|
|
516
|
+
end
|
|
495
517
|
document = @document
|
|
496
518
|
visitor = ::FFI::Function.new(
|
|
497
519
|
:void, [:pointer, :pointer, :int, :int], blocking: true) do |_, node_ptr, entering, depth|
|
|
@@ -524,9 +546,18 @@ class Leptris::XML::Node
|
|
|
524
546
|
# exception and returns non-zero, aborting the C walk — without
|
|
525
547
|
# it the FFI dispatch silently swallowed the exception and the
|
|
526
548
|
# walk continued with partially processed data.
|
|
527
|
-
def traverse
|
|
528
|
-
return enum_for(:traverse) unless
|
|
549
|
+
def traverse(&block)
|
|
550
|
+
return enum_for(:traverse) unless block
|
|
529
551
|
ensure_alive!
|
|
552
|
+
# TODO.perf/27: post-order + abort-at-self + stash-abort-raise
|
|
553
|
+
# all preserved in the C callback (walk state on its stack).
|
|
554
|
+
# The block forwards explicitly — rb_yield needs it on the C
|
|
555
|
+
# entry's own frame.
|
|
556
|
+
if @addr_reads_fast
|
|
557
|
+
Leptris::XML::Native.traverse_binding(@document, @c_address,
|
|
558
|
+
&block)
|
|
559
|
+
return self
|
|
560
|
+
end
|
|
530
561
|
error = nil
|
|
531
562
|
self_address = @c_address
|
|
532
563
|
callback = ::FFI::Function.new(:int, [:pointer, :pointer], blocking: true) do |node_ptr, _|
|
|
@@ -547,8 +578,12 @@ class Leptris::XML::Node
|
|
|
547
578
|
def path
|
|
548
579
|
return @path if memo_hit?(@path_version)
|
|
549
580
|
ensure_alive!
|
|
550
|
-
|
|
551
|
-
|
|
581
|
+
result = if @addr_reads_fast
|
|
582
|
+
Leptris::XML::Native.fast_path(@c_address)
|
|
583
|
+
else
|
|
584
|
+
str_ptr = Leptris::XML::FFI.leptris_node_get_xpath(c_ptr)
|
|
585
|
+
str_ptr.null? ? nil : Leptris::XML::FFI.read_owned_string(str_ptr)
|
|
586
|
+
end
|
|
552
587
|
if @document
|
|
553
588
|
@path = result
|
|
554
589
|
@path_version = @document.version
|
|
@@ -580,6 +615,23 @@ class Leptris::XML::Node
|
|
|
580
615
|
# non-element kinds rebuild by value — #161).
|
|
581
616
|
def dup
|
|
582
617
|
ensure_alive!
|
|
618
|
+
# TODO.perf/30: elements copy in one C dispatch (engine
|
|
619
|
+
# create + handle + element_copy + rooted wrap). The
|
|
620
|
+
# namespace lift stays a Ruby decision — the copy_of seam's
|
|
621
|
+
# semantics (#696/#721/#812) run below when needed.
|
|
622
|
+
if element? && @addr_reads_fast
|
|
623
|
+
doc = Leptris::XML::Native.copy_binding_element(
|
|
624
|
+
@document, @c_address)
|
|
625
|
+
unless doc.nil?
|
|
626
|
+
# copy_of's contract returns the copied ROOT (the last
|
|
627
|
+
# expression of the root= assignment), not the document.
|
|
628
|
+
copied = doc.root
|
|
629
|
+
unless Leptris::XML::Element.skip_adoption_lift?(copied)
|
|
630
|
+
Leptris::XML::Element.lift_namespaces_for_adoption(copied, {})
|
|
631
|
+
end
|
|
632
|
+
return copied
|
|
633
|
+
end
|
|
634
|
+
end
|
|
583
635
|
Leptris::XML::Document.copy_of(self)
|
|
584
636
|
end
|
|
585
637
|
alias_method :clone, :dup
|
|
@@ -4,7 +4,11 @@ class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
|
|
|
4
4
|
def name
|
|
5
5
|
return @name if memo_hit?(@name_version)
|
|
6
6
|
ensure_alive!
|
|
7
|
-
result =
|
|
7
|
+
result = if @addr_reads_fast
|
|
8
|
+
Leptris::XML::Native.fast_pi_target(@c_address)
|
|
9
|
+
else
|
|
10
|
+
Leptris::XML::FFI.leptris_pi_node_get_target(c_ptr)
|
|
11
|
+
end
|
|
8
12
|
if @document
|
|
9
13
|
@name = result
|
|
10
14
|
@name_version = @document.version
|
|
@@ -16,8 +20,12 @@ class Leptris::XML::ProcessingInstruction < Leptris::XML::Node
|
|
|
16
20
|
def content
|
|
17
21
|
return @content if memo_hit?(@content_version)
|
|
18
22
|
ensure_alive!
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
raw = if @addr_reads_fast
|
|
24
|
+
Leptris::XML::Native.fast_pi_data(@c_address)
|
|
25
|
+
else
|
|
26
|
+
Leptris::XML::FFI.leptris_pi_node_get_data(c_ptr)
|
|
27
|
+
end
|
|
28
|
+
result = Leptris::XML::FFI.read_pi_data(raw).to_s
|
|
21
29
|
if @document
|
|
22
30
|
@content = result
|
|
23
31
|
@content_version = @document.version
|
|
@@ -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/text.rb
CHANGED
|
@@ -6,7 +6,11 @@ class Leptris::XML::Text < Leptris::XML::Node
|
|
|
6
6
|
def content
|
|
7
7
|
return @content if memo_hit?(@content_version)
|
|
8
8
|
ensure_alive!
|
|
9
|
-
result =
|
|
9
|
+
result = if @addr_reads_fast
|
|
10
|
+
Leptris::XML::Native.fast_text_content(@c_address)
|
|
11
|
+
else
|
|
12
|
+
Leptris::XML::FFI.leptris_text_node_get_content(c_ptr)
|
|
13
|
+
end
|
|
10
14
|
if @document
|
|
11
15
|
@content = result
|
|
12
16
|
@content_version = @document.version
|
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,7 +1,7 @@
|
|
|
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.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -95,6 +95,10 @@ files:
|
|
|
95
95
|
- TODO.perf/24-immutable-read-lanes.md
|
|
96
96
|
- TODO.perf/25-scope-owned-bulk-path.md
|
|
97
97
|
- TODO.perf/26-post-mutation-memo-seeding.md
|
|
98
|
+
- TODO.perf/27-c-yield-traversal.md
|
|
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
|
|
98
102
|
- TODO.restructure/01-constraint-compliance-audit.md
|
|
99
103
|
- TODO.restructure/02-deep-copy-seam.md
|
|
100
104
|
- TODO.restructure/03-evaluation-context-seam.md
|