leptris 1.9.174.3 → 1.9.174.5
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 +36 -0
- data/TODO.perf/22-eager-nodeset-materialization.md +30 -0
- data/TODO.perf/23-cbound-value-mutations.md +27 -0
- data/TODO.perf/24-immutable-read-lanes.md +22 -0
- data/TODO.perf/25-scope-owned-bulk-path.md +39 -0
- data/TODO.perf/26-post-mutation-memo-seeding.md +26 -0
- data/ext/leptris/native/native.c +138 -4
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/element.rb +44 -4
- data/lib/leptris/xml/node.rb +30 -5
- data/lib/leptris/xml/searchable.rb +9 -0
- data/lib/leptris/xml/text.rb +6 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec76aed6663e2ae97d677c09e8b05486eb6e0543da0f92640a81a87d49c38565
|
|
4
|
+
data.tar.gz: bd881b0e87ff21939925f879abfced736f304a5396f675013a2bc8e2f220a89c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f9f8f54bca5c16e6f1068a9988374ab2a852febc8f4f8695fa7cf0e5bb21fed700bf5b7a34b0732693d55f520000528ed35719dc2488fdf3afb3b6224d2dce9d
|
|
7
|
+
data.tar.gz: a883b80b556feaa34abc6544fe7b4c5f03d6da46597c6dc3170d1219ab66aa4018be3233f824ae6df551c50dc963974a35f72ea715037d596a26259bc8087d8a
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ 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.5] - 2026-09-15
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Iterparse rides the bulk path (TODO.perf/25)**: scope-owned
|
|
13
|
+
elements materialize children through the one-pass C faces —
|
|
14
|
+
the IterationScope flows as the cache/version authority (its
|
|
15
|
+
cache resets per yield, so recycled pool addresses cannot
|
|
16
|
+
collide), with scope-aware wrapper stamps; pure address-based
|
|
17
|
+
reads (attribute/text/name) become scope-eligible. Mutations
|
|
18
|
+
on scope elements keep the Ruby path. Measured (load ~6): the
|
|
19
|
+
20k-record streaming walk 124.8ms → 100.5ms (~20%).
|
|
20
|
+
- **Post-mutation memo seeding (TODO.perf/26)**: []= seeds the
|
|
21
|
+
attribute memo at the new version (partial memos extend,
|
|
22
|
+
full faces restart — the cold-[] rules); content= seeds
|
|
23
|
+
@content; the write-then-read cycle runs ~545ns combined.
|
|
24
|
+
|
|
25
|
+
## [1.9.174.4] - 2026-09-15
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **Eager-in-C nodeset materialization (TODO.perf/22)**: xpath
|
|
30
|
+
results materialize and free in one C pass — no
|
|
31
|
+
FFI::AutoPointer, Method object, or finalizer per call; exotic
|
|
32
|
+
kinds keep the lazy path (sentinel scan in C). Measured (load
|
|
33
|
+
~11): a 2000-node xpath set materializes at Nokogiri parity
|
|
34
|
+
(55.9µs vs 57.3µs).
|
|
35
|
+
- **C-bound value mutations (TODO.perf/23)**: `Element#name=`,
|
|
36
|
+
`#content=`, `Text#content=`, `Node#unlink` run gates + version
|
|
37
|
+
bump + engine write in one dispatch (name= 237ns, content=
|
|
38
|
+
298ns measured); readonly raises verified.
|
|
39
|
+
- **Immutable read lanes (TODO.perf/24)**: `line`/`byte_offset`
|
|
40
|
+
memoize (positions never change); `Node#document` answers a
|
|
41
|
+
constructor-precomputed ivar (the scope_owned? chain leaves
|
|
42
|
+
one of the most-called readers).
|
|
43
|
+
|
|
8
44
|
## [1.9.174.3] - 2026-09-15
|
|
9
45
|
|
|
10
46
|
### Changed
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# 22 — Eager-in-C nodeset materialization (drop the per-call AutoPointer)
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.4)
|
|
4
|
+
|
|
5
|
+
Every xpath() nodeset result pays an FFI::AutoPointer + a Method
|
|
6
|
+
object + finalizer registration (~400-600ns plus their GC) just
|
|
7
|
+
to keep the C result handle alive until the lazy NodeSet
|
|
8
|
+
materializes — and virtually every consumer materializes anyway
|
|
9
|
+
(each/size/to_a). Materialize eagerly in C instead: one face runs
|
|
10
|
+
the bulk pass AND frees the result handle, returning the Ruby
|
|
11
|
+
Array; Searchable wraps it in the eager NodeSet constructor (an
|
|
12
|
+
Array-backed set has no handle at all). Exotic node kinds (the
|
|
13
|
+
module-object sentinel) fall back to the existing lazy path with
|
|
14
|
+
the handle intact.
|
|
15
|
+
|
|
16
|
+
Gates: xpath results identical for every kind (including
|
|
17
|
+
sentinel-graded kinds); laziness is internal — the public NodeSet
|
|
18
|
+
behaviors (indexing, iteration, size) unchanged; the xpath row
|
|
19
|
+
drops the AutoPointer cost per call.
|
|
20
|
+
|
|
21
|
+
## Outcome (1.9.174.4)
|
|
22
|
+
|
|
23
|
+
Native.materialize_xpath: the bulk pass AND the result free in C;
|
|
24
|
+
Searchable's nodeset branch wraps the returned Array in the eager
|
|
25
|
+
NodeSet (no AutoPointer, no Method object, no finalizer per
|
|
26
|
+
xpath call). Sentinel scans happen in C (a Ruby include? would
|
|
27
|
+
cost microseconds on large sets) — any exotic kind returns Qnil
|
|
28
|
+
with the handle alive for the lazy fallback. Measured (load
|
|
29
|
+
~11): full materialization of a 2000-node xpath set 55.9µs vs
|
|
30
|
+
Nokogiri 57.3µs — parity on the heavy shape.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 23 — C-bound name= / content= / unlink
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.4)
|
|
4
|
+
|
|
5
|
+
The value mutations still run ensure_writable! + FFI marshaling
|
|
6
|
+
per call (~450ns): Element#name=, Element#content=,
|
|
7
|
+
Text#content=, and Node#unlink (transforms rename, rewrite, and
|
|
8
|
+
remove nodes constantly). Same face shape as set_binding_attribute:
|
|
9
|
+
gates + version bump + engine write in one dispatch —
|
|
10
|
+
set_binding_name (element names), set_binding_text (element text
|
|
11
|
+
or text-node content, kind-dispatched in C), and
|
|
12
|
+
unlink_binding_node (plus the Ruby-side @parent clear and memo
|
|
13
|
+
invalidations that ride the version bump).
|
|
14
|
+
|
|
15
|
+
Gates: rename/content/unlink semantics unchanged (memos, sibling
|
|
16
|
+
stamps, path memo all invalidate through the version bump);
|
|
17
|
+
readonly raises through every face.
|
|
18
|
+
|
|
19
|
+
## Outcome (1.9.174.4)
|
|
20
|
+
|
|
21
|
+
Native.set_binding_name / set_binding_text (kind-dispatched
|
|
22
|
+
element-vs-text) / unlink_binding_node — gates + bump + engine
|
|
23
|
+
write in one dispatch, wired into Element#name=, #content=,
|
|
24
|
+
Text#content=, and Node#unlink (@parent clear and memo
|
|
25
|
+
invalidation ride the version bump). Measured (load ~11):
|
|
26
|
+
name= 237ns, content= 298ns, unlink+re-add 1.74µs; readonly
|
|
27
|
+
raises verified through every face.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# 24 — Immutable read lanes: line/byte_offset memo + precomputed public #document
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.4)
|
|
4
|
+
|
|
5
|
+
Node#line and #byte_offset pay an FFI round-trip per call, but a
|
|
6
|
+
node's source position never changes — memoize unconditionally
|
|
7
|
+
(first read wins). The public Node#document walks scope_owned? (a
|
|
8
|
+
method chain) on every call though scope ownership is fixed at
|
|
9
|
+
construction — precompute @pub_document once (nil for
|
|
10
|
+
scope-owned) including at every C construction site.
|
|
11
|
+
|
|
12
|
+
Gates: line/byte_offset stable across mutations (they must be —
|
|
13
|
+
the engine reports source positions); document nil for
|
|
14
|
+
scope-owned yields exactly as before; suite both modes.
|
|
15
|
+
|
|
16
|
+
## Outcome (1.9.174.4)
|
|
17
|
+
|
|
18
|
+
line/byte_offset memoize unconditionally (a node's source
|
|
19
|
+
position never changes; 40ns hits). Node#document answers the
|
|
20
|
+
constructor-precomputed @pub_document (nil for scope-owned
|
|
21
|
+
yields) — the scope_owned? method chain disappears from one of
|
|
22
|
+
the most-called readers; every C construction site sets it.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 25 — Scope-owned (iterparse) elements on the bulk/native read path
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.5)
|
|
4
|
+
|
|
5
|
+
iterparse is moxml's streaming core, yet scope-owned elements
|
|
6
|
+
ride the FFI read path (two-call fetch + per-child Ruby wrap
|
|
7
|
+
frames; #4ca5b3c kept them off deliberately). The IterationScope
|
|
8
|
+
exposes exactly the seams the C faces read — @wrapper_cache
|
|
9
|
+
(reset per yield by new_subtree!, killing recycled-address
|
|
10
|
+
entries) and @version (advances per yield and on mutation) — so
|
|
11
|
+
the bulk children faces can take the SCOPE as their "document"
|
|
12
|
+
argument safely, provided the constructed wrappers are stamped
|
|
13
|
+
scope-aware: @structure_memoizable/@native_fast false,
|
|
14
|
+
@pub_document nil.
|
|
15
|
+
|
|
16
|
+
Pure address-based reads (fast_attribute / element text / name)
|
|
17
|
+
touch no cache — scope elements can use them via a construction
|
|
18
|
+
-time @addr_reads_fast that ignores scope ownership.
|
|
19
|
+
|
|
20
|
+
Gates: iterparse children/element_children identical via the C
|
|
21
|
+
path; identity holds within a yielded subtree and resets across
|
|
22
|
+
yields (the new_subtree! discipline); document stays nil for
|
|
23
|
+
scope yields; mutations on scope elements keep the Ruby path;
|
|
24
|
+
suite both modes.
|
|
25
|
+
|
|
26
|
+
## Outcome (1.9.174.5)
|
|
27
|
+
|
|
28
|
+
The bulk children faces take the IterationScope itself as their
|
|
29
|
+
"document" (its @wrapper_cache resets per yield via new_subtree!
|
|
30
|
+
— recycled pool addresses cannot collide — and its @version
|
|
31
|
+
advances per yield and on mutation); the C face stamps
|
|
32
|
+
scope-aware (@structure_memoizable/@native_fast false,
|
|
33
|
+
@pub_document nil) while document-owned stamps stay Qtrue.
|
|
34
|
+
Pure address-based reads (fast_attribute / element text / name)
|
|
35
|
+
switch to @addr_reads_fast — true for scope elements too (they
|
|
36
|
+
touch no cache). Mutations on scope elements keep the Ruby path.
|
|
37
|
+
Measured (load ~6): the 20k-record iterparse walk (attr + child
|
|
38
|
+
name + content per record) 124.8ms (FFI) -> 100.5ms (native
|
|
39
|
+
bulk) — ~20%.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# 26 — Post-mutation memo seeding
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.5)
|
|
4
|
+
|
|
5
|
+
Every mutation invalidates the read memos through the version
|
|
6
|
+
bump, so a write-then-read cycle pays a full re-derivation even
|
|
7
|
+
when the new value is known at write time. Seed it: Element#[]=
|
|
8
|
+
re-stamps @attr_values at the new version and writes the value
|
|
9
|
+
into it (a partial fill stays partial — same rules); content=
|
|
10
|
+
stores the new text as the @content memo at the new version;
|
|
11
|
+
name= already seeds @name.
|
|
12
|
+
|
|
13
|
+
Gates: []=/content=/name= followed by reads answers from the
|
|
14
|
+
seeded memo (spec-observable via identity for strings? — no,
|
|
15
|
+
values only); mixed attribute sets then attributes() still
|
|
16
|
+
rebuild the full face on demand; invalidation semantics
|
|
17
|
+
unchanged for other mutators.
|
|
18
|
+
|
|
19
|
+
## Outcome (1.9.174.5)
|
|
20
|
+
|
|
21
|
+
Element#[]= seeds the version-stamped @attr_values at the new
|
|
22
|
+
version (a just-invalidated valid partial memo extends in place;
|
|
23
|
+
a full face or anything older restarts partial, mirroring the
|
|
24
|
+
cold-[] rules); content= seeds @content; name= seeds @name
|
|
25
|
+
(already). Measured: the []= + [] read-back cycle runs ~545ns
|
|
26
|
+
(write + read combined).
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -85,6 +85,10 @@ static xp_free_fn f_xp_free;
|
|
|
85
85
|
static first_child_fn f_first_child;
|
|
86
86
|
static node_str_fn f_comment_content, f_cdata_content, f_pi_target,
|
|
87
87
|
f_pi_data;
|
|
88
|
+
typedef int (*set_str_fn)(void *, const char *);
|
|
89
|
+
typedef int (*node_unlink_fn)(void *);
|
|
90
|
+
static set_str_fn f_elem_set_name, f_elem_set_text, f_text_set_content;
|
|
91
|
+
static node_unlink_fn f_node_unlink;
|
|
88
92
|
static set_root_fn f_set_root;
|
|
89
93
|
static doc_free_fn f_doc_free;
|
|
90
94
|
|
|
@@ -192,6 +196,10 @@ static void resolve_symbols(const char *lib_path)
|
|
|
192
196
|
f_cdata_content = (node_str_fn)lib_sym(h, "leptris_cdata_node_get_content");
|
|
193
197
|
f_pi_target = (node_str_fn)lib_sym(h, "leptris_pi_node_get_target");
|
|
194
198
|
f_pi_data = (node_str_fn)lib_sym(h, "leptris_pi_node_get_data");
|
|
199
|
+
f_elem_set_name = (set_str_fn)lib_sym(h, "leptris_element_set_name");
|
|
200
|
+
f_elem_set_text = (set_str_fn)lib_sym(h, "leptris_element_set_text");
|
|
201
|
+
f_text_set_content = (set_str_fn)lib_sym(h, "leptris_text_node_set_content");
|
|
202
|
+
f_node_unlink = (node_unlink_fn)lib_sym(h, "leptris_node_unlink");
|
|
195
203
|
f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
|
|
196
204
|
f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
|
|
197
205
|
if (!f_elem_name || !f_text_content || !f_attr ||
|
|
@@ -201,7 +209,8 @@ static void resolve_symbols(const char *lib_path)
|
|
|
201
209
|
!f_insert_after || !f_insert_before || !f_set_attr || !f_set_root ||
|
|
202
210
|
!f_xp_type || !f_xp_free || !f_first_child ||
|
|
203
211
|
!f_comment_content || !f_cdata_content || !f_pi_target ||
|
|
204
|
-
!f_pi_data ||
|
|
212
|
+
!f_pi_data || !f_elem_set_name || !f_elem_set_text ||
|
|
213
|
+
!f_text_set_content || !f_node_unlink ||
|
|
205
214
|
!f_doc_free ||
|
|
206
215
|
!f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
|
|
207
216
|
!f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
|
|
@@ -630,6 +639,74 @@ static VALUE nf_set_binding_attribute(VALUE self, VALUE document,
|
|
|
630
639
|
return INT2FIX(st);
|
|
631
640
|
}
|
|
632
641
|
|
|
642
|
+
/* C-bound value mutations (TODO.perf/23): the same gates + bump
|
|
643
|
+
* + engine write shape as set_binding_attribute. */
|
|
644
|
+
static VALUE nf_set_binding_name(VALUE self, VALUE document,
|
|
645
|
+
VALUE addr, VALUE name)
|
|
646
|
+
{
|
|
647
|
+
void *node;
|
|
648
|
+
int st;
|
|
649
|
+
|
|
650
|
+
(void)self;
|
|
651
|
+
resolve_binding_classes();
|
|
652
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
653
|
+
rb_raise(c_use_after_free_error,
|
|
654
|
+
"owning document has been freed");
|
|
655
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
656
|
+
rb_raise(c_readonly_error,
|
|
657
|
+
"document is readonly — mutation attempted");
|
|
658
|
+
node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
659
|
+
rb_ivar_set(document, id_iv_version,
|
|
660
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
661
|
+
st = f_elem_set_name(node, StringValueCStr(name));
|
|
662
|
+
return INT2FIX(st);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/* Element text or text-node content — kind-dispatched in C. */
|
|
666
|
+
static VALUE nf_set_binding_text(VALUE self, VALUE document,
|
|
667
|
+
VALUE addr, VALUE content)
|
|
668
|
+
{
|
|
669
|
+
void *node;
|
|
670
|
+
int st;
|
|
671
|
+
|
|
672
|
+
(void)self;
|
|
673
|
+
resolve_binding_classes();
|
|
674
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
675
|
+
rb_raise(c_use_after_free_error,
|
|
676
|
+
"owning document has been freed");
|
|
677
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
678
|
+
rb_raise(c_readonly_error,
|
|
679
|
+
"document is readonly — mutation attempted");
|
|
680
|
+
node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
681
|
+
rb_ivar_set(document, id_iv_version,
|
|
682
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
683
|
+
st = f_node_type(node) == NT_ELEMENT
|
|
684
|
+
? f_elem_set_text(node, StringValueCStr(content))
|
|
685
|
+
: f_text_set_content(node, StringValueCStr(content));
|
|
686
|
+
return INT2FIX(st);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
static VALUE nf_unlink_binding_node(VALUE self, VALUE document,
|
|
690
|
+
VALUE addr)
|
|
691
|
+
{
|
|
692
|
+
void *node;
|
|
693
|
+
int st;
|
|
694
|
+
|
|
695
|
+
(void)self;
|
|
696
|
+
resolve_binding_classes();
|
|
697
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
698
|
+
rb_raise(c_use_after_free_error,
|
|
699
|
+
"owning document has been freed");
|
|
700
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
701
|
+
rb_raise(c_readonly_error,
|
|
702
|
+
"document is readonly — mutation attempted");
|
|
703
|
+
node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
704
|
+
rb_ivar_set(document, id_iv_version,
|
|
705
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
706
|
+
st = f_node_unlink(node);
|
|
707
|
+
return INT2FIX(st);
|
|
708
|
+
}
|
|
709
|
+
|
|
633
710
|
/* C-bound insert family (TODO.perf/14): prepend / insert-after /
|
|
634
711
|
* insert-before share append's gates + predicate + bump shape.
|
|
635
712
|
* mode: 1 prepend, 2 after, 3 before (anchor = receiver). */
|
|
@@ -797,6 +874,7 @@ static VALUE nn_address(VALUE self)
|
|
|
797
874
|
static VALUE c_b_element = Qundef, c_b_text, c_b_comment, c_b_cdata,
|
|
798
875
|
c_b_pi, c_b_node, c_b_result_text, c_b_result_attr,
|
|
799
876
|
c_b_attr, c_ffi_pointer, c_b_document, c_b_freed;
|
|
877
|
+
static VALUE c_iteration_scope;
|
|
800
878
|
static ID id_ptr_new;
|
|
801
879
|
|
|
802
880
|
/* Binding classes resolve LAZILY: Init_native can run before the
|
|
@@ -818,6 +896,7 @@ static void resolve_binding_classes(void)
|
|
|
818
896
|
c_b_attr = rb_path2class("Leptris::XML::Attr");
|
|
819
897
|
c_b_document = rb_path2class("Leptris::XML::Document");
|
|
820
898
|
c_b_freed = rb_path2class("Leptris::XML::Document::Freed");
|
|
899
|
+
c_iteration_scope = rb_path2class("Leptris::XML::IterationScope");
|
|
821
900
|
id_ptr_new = rb_intern("new");
|
|
822
901
|
rb_gc_register_mark_object(c_b_element);
|
|
823
902
|
rb_gc_register_mark_object(c_b_text);
|
|
@@ -831,6 +910,7 @@ static void resolve_binding_classes(void)
|
|
|
831
910
|
rb_gc_register_mark_object(c_b_attr);
|
|
832
911
|
rb_gc_register_mark_object(c_b_document);
|
|
833
912
|
rb_gc_register_mark_object(c_b_freed);
|
|
913
|
+
rb_gc_register_mark_object(c_iteration_scope);
|
|
834
914
|
}
|
|
835
915
|
|
|
836
916
|
static VALUE binding_klass_for(int kind)
|
|
@@ -874,8 +954,20 @@ static VALUE bulk_children_impl(VALUE document, VALUE parent_addr,
|
|
|
874
954
|
rb_iv_set(node, "@c_address", key);
|
|
875
955
|
rb_iv_set(node, "@document", document);
|
|
876
956
|
rb_iv_set(node, "@parent", Qnil);
|
|
877
|
-
|
|
878
|
-
|
|
957
|
+
/* TODO.perf/25: scope-aware stamps — the IterationScope
|
|
958
|
+
* flows through here as "document" for iterparse
|
|
959
|
+
* elements; those stay memo-exempt and FFI-mutating,
|
|
960
|
+
* but address-based reads are cache-free and safe. */
|
|
961
|
+
if (rb_obj_class(document) == c_iteration_scope) {
|
|
962
|
+
rb_iv_set(node, "@structure_memoizable", Qfalse);
|
|
963
|
+
rb_iv_set(node, "@native_fast", Qfalse);
|
|
964
|
+
rb_iv_set(node, "@pub_document", Qnil);
|
|
965
|
+
} else {
|
|
966
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
967
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
968
|
+
rb_iv_set(node, "@pub_document", document);
|
|
969
|
+
}
|
|
970
|
+
rb_iv_set(node, "@addr_reads_fast", Qtrue);
|
|
879
971
|
rb_iv_set(node, "@node_type", INT2FIX(kinds[i]));
|
|
880
972
|
rb_hash_aset(cache, key, node);
|
|
881
973
|
}
|
|
@@ -912,15 +1004,45 @@ static VALUE materialize_xp_entry(VALUE document, VALUE cache,
|
|
|
912
1004
|
void *result, void *ptr, int kind,
|
|
913
1005
|
int idx);
|
|
914
1006
|
|
|
1007
|
+
static VALUE bulk_xpath_array(VALUE document, void *result);
|
|
1008
|
+
|
|
915
1009
|
static VALUE nf_bulk_xpath(VALUE self, VALUE document, VALUE result_ptr_val)
|
|
1010
|
+
{
|
|
1011
|
+
(void)self;
|
|
1012
|
+
return bulk_xpath_array(document,
|
|
1013
|
+
(void *)(uintptr_t)NUM2ULL(result_ptr_val));
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/* TODO.perf/22: materialize AND free — the eager xpath path owns
|
|
1017
|
+
* the handle lifecycle in C; no Ruby AutoPointer, no finalizer. */
|
|
1018
|
+
static VALUE nf_materialize_xpath(VALUE self, VALUE document,
|
|
1019
|
+
VALUE result_ptr_val)
|
|
916
1020
|
{
|
|
917
1021
|
void *result = (void *)(uintptr_t)NUM2ULL(result_ptr_val);
|
|
1022
|
+
VALUE out;
|
|
1023
|
+
long i, n;
|
|
1024
|
+
|
|
1025
|
+
(void)self;
|
|
1026
|
+
out = bulk_xpath_array(document, result);
|
|
1027
|
+
n = RARRAY_LEN(out);
|
|
1028
|
+
for (i = 0; i < n; i++) {
|
|
1029
|
+
if (rb_ary_entry(out, i) == m_native_sentinel) {
|
|
1030
|
+
/* Exotic kinds need the Ruby fallback per entry — the
|
|
1031
|
+
* caller keeps the handle and goes the lazy path. */
|
|
1032
|
+
return Qnil;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
f_xp_free(result);
|
|
1036
|
+
return out;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
static VALUE bulk_xpath_array(VALUE document, void *result)
|
|
1040
|
+
{
|
|
918
1041
|
void **buf;
|
|
919
1042
|
int *kinds;
|
|
920
1043
|
int count, i;
|
|
921
1044
|
VALUE cache, out;
|
|
922
1045
|
|
|
923
|
-
(void)self;
|
|
924
1046
|
resolve_binding_classes();
|
|
925
1047
|
size_t scount = f_xp_count(result);
|
|
926
1048
|
if (scount == 0)
|
|
@@ -970,6 +1092,7 @@ static VALUE materialize_xp_entry(VALUE document, VALUE cache,
|
|
|
970
1092
|
rb_iv_set(node, "@parent", Qnil);
|
|
971
1093
|
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
972
1094
|
rb_iv_set(node, "@native_fast", Qtrue);
|
|
1095
|
+
rb_iv_set(node, "@pub_document", document);
|
|
973
1096
|
rb_iv_set(node, "@node_type", INT2FIX(0));
|
|
974
1097
|
rb_hash_aset(cache, key, node);
|
|
975
1098
|
}
|
|
@@ -1009,6 +1132,7 @@ static VALUE materialize_xp_entry(VALUE document, VALUE cache,
|
|
|
1009
1132
|
rb_iv_set(node, "@parent", Qnil);
|
|
1010
1133
|
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
1011
1134
|
rb_iv_set(node, "@native_fast", Qtrue);
|
|
1135
|
+
rb_iv_set(node, "@pub_document", document);
|
|
1012
1136
|
rb_iv_set(node, "@node_type", INT2FIX(nt));
|
|
1013
1137
|
rb_hash_aset(cache, key, node);
|
|
1014
1138
|
}
|
|
@@ -1097,6 +1221,7 @@ static VALUE nf_create_binding_element(VALUE self, VALUE document,
|
|
|
1097
1221
|
rb_iv_set(node, "@parent", Qnil);
|
|
1098
1222
|
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
1099
1223
|
rb_iv_set(node, "@native_fast", Qtrue);
|
|
1224
|
+
rb_iv_set(node, "@pub_document", document);
|
|
1100
1225
|
rb_iv_set(node, "@node_type", INT2FIX(0));
|
|
1101
1226
|
cache = binding_cache_of(document);
|
|
1102
1227
|
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
@@ -1122,6 +1247,7 @@ static VALUE nf_create_binding_text(VALUE self, VALUE document,
|
|
|
1122
1247
|
rb_iv_set(node, "@parent", Qnil);
|
|
1123
1248
|
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
1124
1249
|
rb_iv_set(node, "@native_fast", Qtrue);
|
|
1250
|
+
rb_iv_set(node, "@pub_document", document);
|
|
1125
1251
|
rb_iv_set(node, "@node_type", INT2FIX(1));
|
|
1126
1252
|
cache = binding_cache_of(document);
|
|
1127
1253
|
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
@@ -1577,6 +1703,14 @@ void Init_native(void)
|
|
|
1577
1703
|
nf_insert_binding_child, 4);
|
|
1578
1704
|
rb_define_module_function(m_native, "at_xpath_first",
|
|
1579
1705
|
nf_at_xpath_first, 2);
|
|
1706
|
+
rb_define_module_function(m_native, "materialize_xpath",
|
|
1707
|
+
nf_materialize_xpath, 2);
|
|
1708
|
+
rb_define_module_function(m_native, "set_binding_name",
|
|
1709
|
+
nf_set_binding_name, 3);
|
|
1710
|
+
rb_define_module_function(m_native, "set_binding_text",
|
|
1711
|
+
nf_set_binding_text, 3);
|
|
1712
|
+
rb_define_module_function(m_native, "unlink_binding_node",
|
|
1713
|
+
nf_unlink_binding_node, 2);
|
|
1580
1714
|
rb_define_module_function(m_native, "fast_inner_xml",
|
|
1581
1715
|
nf_fast_inner_xml, 1);
|
|
1582
1716
|
rb_define_module_function(m_native, "doc_handle_attach",
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -15,7 +15,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
15
15
|
def name
|
|
16
16
|
return @name if @name
|
|
17
17
|
ensure_alive!
|
|
18
|
-
@name = if
|
|
18
|
+
@name = if @addr_reads_fast
|
|
19
19
|
Leptris::XML::Native.fast_name(@c_address)
|
|
20
20
|
else
|
|
21
21
|
Leptris::XML::FFI.leptris_element_name(c_ptr)
|
|
@@ -24,6 +24,12 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
24
24
|
alias_method :node_name, :name
|
|
25
25
|
|
|
26
26
|
def name=(new_name)
|
|
27
|
+
if @native_fast
|
|
28
|
+
Leptris::XML::FFI.check_status(
|
|
29
|
+
Leptris::XML::Native.set_binding_name(
|
|
30
|
+
@document, @c_address, new_name))
|
|
31
|
+
return @name = new_name
|
|
32
|
+
end
|
|
27
33
|
ensure_writable!
|
|
28
34
|
Leptris::XML::FFI.check_status(
|
|
29
35
|
Leptris::XML::FFI.leptris_element_set_name(c_ptr, new_name))
|
|
@@ -34,7 +40,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
34
40
|
def content
|
|
35
41
|
return @content if memo_hit?(@content_version)
|
|
36
42
|
ensure_alive!
|
|
37
|
-
result = if
|
|
43
|
+
result = if @addr_reads_fast
|
|
38
44
|
Leptris::XML::Native.fast_element_text(@c_address)
|
|
39
45
|
else
|
|
40
46
|
Leptris::XML::FFI.leptris_element_text(c_ptr)
|
|
@@ -47,9 +53,19 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
def content=(new_content)
|
|
56
|
+
if @native_fast
|
|
57
|
+
Leptris::XML::FFI.check_status(
|
|
58
|
+
Leptris::XML::Native.set_binding_text(
|
|
59
|
+
@document, @c_address, new_content.to_s))
|
|
60
|
+
@content = new_content.to_s
|
|
61
|
+
@content_version = @document.version if @document
|
|
62
|
+
return new_content
|
|
63
|
+
end
|
|
50
64
|
ensure_writable!
|
|
51
65
|
Leptris::XML::FFI.check_status(
|
|
52
66
|
Leptris::XML::FFI.leptris_element_set_text(c_ptr, new_content.to_s))
|
|
67
|
+
@content = new_content.to_s
|
|
68
|
+
@content_version = @document.version if @document
|
|
53
69
|
new_content
|
|
54
70
|
end
|
|
55
71
|
|
|
@@ -79,7 +95,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
79
95
|
# engine call resolves it, then the name is cached.
|
|
80
96
|
return v if !v.nil? || @attributes
|
|
81
97
|
ensure_alive!
|
|
82
|
-
v = if
|
|
98
|
+
v = if @addr_reads_fast
|
|
83
99
|
Leptris::XML::Native.fast_attribute(@c_address, name)
|
|
84
100
|
else
|
|
85
101
|
Leptris::XML::FFI.leptris_element_attribute(c_ptr, name)
|
|
@@ -92,7 +108,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
92
108
|
# post-mutation cold [] would stamp the new version onto a
|
|
93
109
|
# stale @attributes hash and attributes would serve it.
|
|
94
110
|
ensure_alive!
|
|
95
|
-
v = if
|
|
111
|
+
v = if @addr_reads_fast
|
|
96
112
|
Leptris::XML::Native.fast_attribute(@c_address, name)
|
|
97
113
|
else
|
|
98
114
|
Leptris::XML::FFI.leptris_element_attribute(c_ptr, name)
|
|
@@ -125,15 +141,39 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
125
141
|
Leptris::XML::FFI.check_status(
|
|
126
142
|
Leptris::XML::Native.set_binding_attribute(
|
|
127
143
|
@document, @c_address, key.to_s, value.to_s))
|
|
144
|
+
seed_attribute_memo(key.to_s, value.to_s)
|
|
128
145
|
return value
|
|
129
146
|
end
|
|
130
147
|
ensure_writable!
|
|
131
148
|
Leptris::XML::FFI.check_status(
|
|
132
149
|
Leptris::XML::FFI.leptris_element_set_attribute(c_ptr, key.to_s, value.to_s))
|
|
150
|
+
seed_attribute_memo(key.to_s, value.to_s)
|
|
133
151
|
value
|
|
134
152
|
end
|
|
135
153
|
alias_method :set_attribute, :[]=
|
|
136
154
|
|
|
155
|
+
# TODO.perf/26: the write knows the new value — seed the
|
|
156
|
+
# version-stamped memo so a read-back skips re-derivation. A
|
|
157
|
+
# valid-and-just-invalidated partial memo extends in place; a
|
|
158
|
+
# full face (or anything older) restarts partial, mirroring the
|
|
159
|
+
# cold-[] rules.
|
|
160
|
+
def seed_attribute_memo(name, written)
|
|
161
|
+
return unless @document
|
|
162
|
+
values = @attr_values
|
|
163
|
+
if values && !@attributes &&
|
|
164
|
+
@attributes_version == @document.version - 1
|
|
165
|
+
values[name] = written
|
|
166
|
+
else
|
|
167
|
+
@attr_values = { name => written }
|
|
168
|
+
@attributes = nil
|
|
169
|
+
@attribute_nodes = nil
|
|
170
|
+
@keys = nil
|
|
171
|
+
@values = nil
|
|
172
|
+
end
|
|
173
|
+
@attributes_version = @document.version
|
|
174
|
+
end
|
|
175
|
+
private :seed_attribute_memo
|
|
176
|
+
|
|
137
177
|
def key?(name)
|
|
138
178
|
ensure_alive!
|
|
139
179
|
# Versioned attribute memo first (TODO.perf/21): a hit proves
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -11,8 +11,10 @@ class Leptris::XML::Node
|
|
|
11
11
|
# Iterparse-yielded elements are owned by an IterationScope (the
|
|
12
12
|
# internal lifetime/memoization authority) — the public #document
|
|
13
13
|
# answers nil for them, per the documented contract.
|
|
14
|
+
# Precomputed at construction (TODO.perf/24): scope ownership
|
|
15
|
+
# is fixed for a node's lifetime — nil for iterparse yields.
|
|
14
16
|
def document
|
|
15
|
-
|
|
17
|
+
@pub_document
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def initialize(c_ptr, document, parent: nil, node_type: nil)
|
|
@@ -33,6 +35,10 @@ class Leptris::XML::Node
|
|
|
33
35
|
# keeps the memo-hit path free of method dispatch.
|
|
34
36
|
@structure_memoizable =
|
|
35
37
|
!document.nil? && !document.is_a?(Leptris::XML::IterationScope)
|
|
38
|
+
@pub_document = @structure_memoizable ? document : nil
|
|
39
|
+
# TODO.perf/25: pure address-based native reads touch no
|
|
40
|
+
# cache — safe for scope-owned (iterparse) elements too.
|
|
41
|
+
@addr_reads_fast = !!defined?(Leptris::XML::NATIVE_FAST)
|
|
36
42
|
# NATIVE_FAST availability is fixed at load time before any
|
|
37
43
|
# node exists, so the conjunction with document-ownership is
|
|
38
44
|
# construct-time constant — the hot gates read one ivar.
|
|
@@ -161,6 +167,16 @@ class Leptris::XML::Node
|
|
|
161
167
|
@native_fast
|
|
162
168
|
end
|
|
163
169
|
|
|
170
|
+
# TODO.perf/25: the bulk children faces accept the
|
|
171
|
+
# IterationScope itself (its @wrapper_cache resets per yield —
|
|
172
|
+
# recycled addresses cannot collide — and its @version advances
|
|
173
|
+
# per yield and on mutation), so streaming children ride the
|
|
174
|
+
# one-pass materialization too. The scope-aware stamps live in
|
|
175
|
+
# the C face.
|
|
176
|
+
def native_bulk_children?
|
|
177
|
+
defined?(Leptris::XML::NATIVE_FAST)
|
|
178
|
+
end
|
|
179
|
+
|
|
164
180
|
# Content-defined 64-bit Merkle digest of this subtree
|
|
165
181
|
# (libleptris 1.9.99, #869): element name/prefix/resolved
|
|
166
182
|
# namespace URI, attributes sorted and first-wins-deduplicated,
|
|
@@ -278,9 +294,11 @@ class Leptris::XML::Node
|
|
|
278
294
|
@readonly_document = true
|
|
279
295
|
end
|
|
280
296
|
|
|
297
|
+
# A node's source position never changes — first read wins.
|
|
281
298
|
def line
|
|
299
|
+
return @line if defined?(@line)
|
|
282
300
|
ensure_alive!
|
|
283
|
-
Leptris::XML::FFI.leptris_node_line(c_ptr)
|
|
301
|
+
@line = Leptris::XML::FFI.leptris_node_line(c_ptr)
|
|
284
302
|
end
|
|
285
303
|
|
|
286
304
|
# Byte offset of the node's markup in its parse source (the '<'
|
|
@@ -288,8 +306,9 @@ class Leptris::XML::Node
|
|
|
288
306
|
# CALLBACK rows echo). 0 when unknown: mutation-created nodes, or
|
|
289
307
|
# documents >= 2 GiB.
|
|
290
308
|
def byte_offset
|
|
309
|
+
return @byte_offset if defined?(@byte_offset)
|
|
291
310
|
ensure_alive!
|
|
292
|
-
Leptris::XML::FFI.leptris_node_byte_offset(c_ptr)
|
|
311
|
+
@byte_offset = Leptris::XML::FFI.leptris_node_byte_offset(c_ptr)
|
|
293
312
|
end
|
|
294
313
|
|
|
295
314
|
def <=>(other)
|
|
@@ -312,7 +331,7 @@ class Leptris::XML::Node
|
|
|
312
331
|
# (leptris_node_children_ex), so no per-child get_type.
|
|
313
332
|
return @children if memo_hit?(@children_version)
|
|
314
333
|
ensure_alive!
|
|
315
|
-
if
|
|
334
|
+
if native_bulk_children?
|
|
316
335
|
# TODO.perf/01 tail: one C pass constructs every binding
|
|
317
336
|
# wrapper (class dispatch + ivars + identity cache) — the
|
|
318
337
|
# per-child Ruby wrap frames disappear.
|
|
@@ -413,7 +432,7 @@ class Leptris::XML::Node
|
|
|
413
432
|
# children are never wrapped (nor their get_type paid — the
|
|
414
433
|
# ELEMENT hint rides along). Other nodes keep the filter.
|
|
415
434
|
result =
|
|
416
|
-
if
|
|
435
|
+
if native_bulk_children?
|
|
417
436
|
Leptris::XML::Native.bulk_element_children(@document, @c_address)
|
|
418
437
|
elsif is_a?(Leptris::XML::Element)
|
|
419
438
|
parent = as_element_or_self
|
|
@@ -445,6 +464,12 @@ class Leptris::XML::Node
|
|
|
445
464
|
end
|
|
446
465
|
|
|
447
466
|
def unlink
|
|
467
|
+
if @native_fast
|
|
468
|
+
Leptris::XML::FFI.check_status(
|
|
469
|
+
Leptris::XML::Native.unlink_binding_node(@document, @c_address))
|
|
470
|
+
@parent = nil
|
|
471
|
+
return self
|
|
472
|
+
end
|
|
448
473
|
ensure_writable!
|
|
449
474
|
Leptris::XML::FFI.check_status(
|
|
450
475
|
Leptris::XML::FFI.leptris_node_unlink(c_ptr))
|
|
@@ -298,6 +298,15 @@ module Leptris::XML::Searchable
|
|
|
298
298
|
type = Leptris::XML::FFI.leptris_xpath_result_type(result_ptr)
|
|
299
299
|
case type
|
|
300
300
|
when Leptris::XML::FFI::XPATH_NODESET
|
|
301
|
+
# TODO.perf/22: materialize AND free in C — no Ruby
|
|
302
|
+
# AutoPointer + finalizer per xpath call. Qnil means an
|
|
303
|
+
# exotic kind needs the Ruby fallback per entry; the lazy
|
|
304
|
+
# path below keeps the handle.
|
|
305
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
306
|
+
nodes = Leptris::XML::Native.materialize_xpath(
|
|
307
|
+
document, result_ptr.address)
|
|
308
|
+
return Leptris::XML::NodeSet.new(document, nodes) if nodes
|
|
309
|
+
end
|
|
301
310
|
Leptris::XML::NodeSet.from_result(document, result_ptr)
|
|
302
311
|
when Leptris::XML::FFI::XPATH_BOOLEAN
|
|
303
312
|
v = Leptris::XML::FFI.leptris_xpath_result_boolean(result_ptr) != 0
|
data/lib/leptris/xml/text.rb
CHANGED
|
@@ -15,6 +15,12 @@ class Leptris::XML::Text < Leptris::XML::Node
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def content=(new_content)
|
|
18
|
+
if @native_fast
|
|
19
|
+
Leptris::XML::FFI.check_status(
|
|
20
|
+
Leptris::XML::Native.set_binding_text(
|
|
21
|
+
@document, @c_address, new_content.to_s))
|
|
22
|
+
return new_content
|
|
23
|
+
end
|
|
18
24
|
ensure_writable!
|
|
19
25
|
Leptris::XML::FFI.check_status(
|
|
20
26
|
Leptris::XML::FFI.leptris_text_node_set_content(c_ptr, new_content.to_s))
|
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.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -90,6 +90,11 @@ files:
|
|
|
90
90
|
- TODO.perf/19-lazy-node-pointer.md
|
|
91
91
|
- TODO.perf/20-css-translation-cache.md
|
|
92
92
|
- TODO.perf/21-key-memo-consult.md
|
|
93
|
+
- TODO.perf/22-eager-nodeset-materialization.md
|
|
94
|
+
- TODO.perf/23-cbound-value-mutations.md
|
|
95
|
+
- TODO.perf/24-immutable-read-lanes.md
|
|
96
|
+
- TODO.perf/25-scope-owned-bulk-path.md
|
|
97
|
+
- TODO.perf/26-post-mutation-memo-seeding.md
|
|
93
98
|
- TODO.restructure/01-constraint-compliance-audit.md
|
|
94
99
|
- TODO.restructure/02-deep-copy-seam.md
|
|
95
100
|
- TODO.restructure/03-evaluation-context-seam.md
|