leptris 1.9.163.4 → 1.9.163.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 +43 -0
- data/TODO.perf/07-native-read-floor.md +31 -0
- data/TODO.perf/08-version-coherent-native-mutations.md +24 -0
- data/TODO.perf/09-adoption-lift-fast-path.md +38 -0
- data/TODO.perf/10-bulk-attribute-materialization.md +20 -0
- data/TODO.perf/11-moxml-gap-battery.md +18 -0
- data/ext/leptris/native/native.c +397 -21
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/document.rb +20 -1
- data/lib/leptris/xml/element.rb +60 -4
- 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: fc446877d41a4725feaa4ac5a451398395c2b74888ecbe6b89bd15c07586c5b1
|
|
4
|
+
data.tar.gz: e86fb2e1ad7af3a32ca07f5fdb93936b88c2910d61618f4c5e3afb9fe8828a21
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49705259500bd74522dbfbdd2f9703249eed074148f73d7207f430072e531194740232ba6caa27eb7e21d5a67a14f25a06b9fe5b82f1e10fb6b65e394c1f7c2a
|
|
7
|
+
data.tar.gz: fbea087c956ce35ba6e30f274db4c9db2f7b194e61e4f78633a9510fe85d6151c35f4758ade380d8a1397d261756fbd415d78b0587b628965b8ca82b9179af5f
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,49 @@ All notable changes to Leptris will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.9.163.5] - 2026-09-15
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **C-bound mutations for the programmatic-build path (#204 asks
|
|
13
|
+
2-3)**: `Element#add_child` and `Element#[]=` dispatch to the
|
|
14
|
+
native layer when auto-enabled — one C entry runs the
|
|
15
|
+
readonly/liveness gates, the provable no-op namespace-lift
|
|
16
|
+
predicate, the version bump, and the engine write. Children
|
|
17
|
+
that DO need a namespace lift (resolved or declared namespaces)
|
|
18
|
+
fall back to the full Ruby path unchanged. Measured (shared
|
|
19
|
+
host, load 12-19): binding add_child on a fresh parent
|
|
20
|
+
1678ns -> 506ns; the ask-3 shape (create x2 + attach x2) went
|
|
21
|
+
from the 4.1us baseline to ~1.2-1.3us — at or past the raw
|
|
22
|
+
Nokogiri row (~1.5-1.6us).
|
|
23
|
+
- **Native read floors (#204 ask 1)**: `NativeNode#[]` and
|
|
24
|
+
`NativeNode#content` now keep version-stamped memos (the same
|
|
25
|
+
invalidation discipline as binding memos, ADR 0003) — repeat
|
|
26
|
+
reads are two ivar reads and a compare, no C call, no string
|
|
27
|
+
mint; mutations through either surface drop them via the shared
|
|
28
|
+
document version.
|
|
29
|
+
- **Adoption-lift guard ordering (#204 ask 3)**: the provable
|
|
30
|
+
no-op check moved ahead of the target's in-scope namespace
|
|
31
|
+
materialization at every mutation site (`add_child`,
|
|
32
|
+
`prepend_child`, sibling inserts, `root=`) — a fresh parent no
|
|
33
|
+
longer pays the namespace collection FFI round-trips per
|
|
34
|
+
attach. Non-element children skip the lift outright.
|
|
35
|
+
- **Ivar-based document state in the native layer**: the C faces
|
|
36
|
+
read `@c_address`, `@wrapper_cache`/`@native_cache`, `@version`,
|
|
37
|
+
`@readonly` directly instead of rb_funcall dispatching
|
|
38
|
+
(`Document` maintains `@c_address` as the Integer twin of
|
|
39
|
+
`@c_ptr`, nil'ed on free); document create/mutate faces shed
|
|
40
|
+
~300ns of method-dispatch overhead each.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- `Leptris::XML::Native.append_binding_child` /
|
|
45
|
+
`set_binding_attribute` module faces and
|
|
46
|
+
`Element.skip_adoption_lift?` (the provable no-op predicate,
|
|
47
|
+
public so specs can pin its exact semantics).
|
|
48
|
+
- moxml gap-table rows (per-operation binding floors, build row)
|
|
49
|
+
in `benchmark/native_vs_binding.rb` (TODO.perf/11).
|
|
50
|
+
|
|
8
51
|
## [1.9.163.4] - 2026-09-15
|
|
9
52
|
|
|
10
53
|
### Fixed
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# 07 — Native read floor: shave the TypedData []/content dispatch (#204 ask 1)
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.5)
|
|
4
|
+
|
|
5
|
+
moxml's 2x mandate: `NativeNode#[]` at 110ns needs <= ~70ns (their
|
|
6
|
+
wrapper keeps ~24ns); `#content` 85ns needs <= 58ns. The budget
|
|
7
|
+
dies in the C entry itself: TypedData fetch + StringValueCStr's
|
|
8
|
+
embedded-NUL scan (memchr over the name) + rb_utf8_str_new_cstr
|
|
9
|
+
(strlen + string mint). The string mint (~50ns) is irreducible
|
|
10
|
+
(the C string must be copied into a Ruby String) — the shaves:
|
|
11
|
+
|
|
12
|
+
- StringValueCStr -> StringValue + RSTRING_PTR for wire names we
|
|
13
|
+
control the mint of (attribute names cannot contain NULs);
|
|
14
|
+
- single type-check fetch (RTYPEDDATA_DATA + one pointer compare
|
|
15
|
+
against nn_type instead of the generic check);
|
|
16
|
+
- measure honestly: report how close to 70/58 each landed.
|
|
17
|
+
|
|
18
|
+
## Outcome (1.9.163.5)
|
|
19
|
+
|
|
20
|
+
Repeat reads no longer re-enter the C reader at all: NativeNode
|
|
21
|
+
keeps a version-stamped single-slot memo (struct fields, pointer
|
|
22
|
+
compare against the document's Fixnum version) plus a hash-memo
|
|
23
|
+
second layer for other repeat names; content memoizes in one
|
|
24
|
+
slot. `StringValueCStr` -> `StringValue`+`RSTRING_PTR` landed for
|
|
25
|
+
attribute names. Measured (shared host, load 12-19, so treat as
|
|
26
|
+
upper bounds; #204's table was load <6): `#content` 85 -> 42-55ns
|
|
27
|
+
(budget 58 met), `#[]` 110 -> ~140ns under load — the remaining
|
|
28
|
+
residue is the argument mint + TypedData fetch; a clean-load run
|
|
29
|
+
of benchmark/native_vs_binding.rb's gap rows is the honest gate.
|
|
30
|
+
`#name` 78 -> ~63-73ns. One-shot reads still pay the string mint
|
|
31
|
+
(irreducible: the C string must be copied).
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# 08 — Version-coherent native mutations (#204 ask 2)
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.5)
|
|
4
|
+
|
|
5
|
+
The native builder factories (create_element/text/child, add_child,
|
|
6
|
+
set_root) do not advance the binding Document's mutation version —
|
|
7
|
+
a binding children read after a native attach serves the STALE
|
|
8
|
+
memo. Fix in the ext: every mutating entry calls the document's
|
|
9
|
+
advance_version (cached ID) after the C mutation, and the readonly
|
|
10
|
+
gate before it. Mutations are cold relative to reads, so the two
|
|
11
|
+
rb_funcall hops are fine. Gate: build via native factories, read
|
|
12
|
+
via binding children/element_children — lists must agree.
|
|
13
|
+
|
|
14
|
+
## Outcome (1.9.163.5)
|
|
15
|
+
|
|
16
|
+
document_advance_version moved from two rb_funcall hops to direct
|
|
17
|
+
ivar reads/writes (@readonly Qtrue check, @version Fixnum bump) —
|
|
18
|
+
no method dispatch on the mutation path. Every NativeNode
|
|
19
|
+
mutating entry (create_child, append_child/add_child, set_root)
|
|
20
|
+
and the new C-bound binding faces (append_binding_child,
|
|
21
|
+
set_binding_attribute) run the same gate: UseAfterFreeError via
|
|
22
|
+
the nil @c_address twin, ReadOnlyError via @readonly, then the
|
|
23
|
+
bump. Gate verified: native append -> binding children/content
|
|
24
|
+
serve fresh values; readonly raises through every native entry.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# 09 — Adoption-lift fast path + binding create via C (#204 ask 3, #149 continuation)
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.5)
|
|
4
|
+
|
|
5
|
+
Binding create x2 + attach measured 4.1us vs Nokogiri 1.5us. The
|
|
6
|
+
#178/#208 namespace lift + pruning runs on EVERY attach and costs
|
|
7
|
+
several FFI round-trips (own definitions, prefix read, in-scope
|
|
8
|
+
walk) even when it is a provable no-op. Add a one-dispatch C
|
|
9
|
+
predicate `Native.ns_lift_needed?(addr)`: false when the node
|
|
10
|
+
carries no namespace declarations AND its name has no prefix —
|
|
11
|
+
the overwhelmingly common programmatic-build shape. add_child and
|
|
12
|
+
friends skip the lift entirely on false. Also route binding
|
|
13
|
+
create_element/create_text_node through the ext (one C call +
|
|
14
|
+
binding-wrapper construction in C — no FFI marshaling) when
|
|
15
|
+
auto-enabled.
|
|
16
|
+
|
|
17
|
+
## Outcome (1.9.163.5)
|
|
18
|
+
|
|
19
|
+
Three layers landed:
|
|
20
|
+
1. `Element.skip_adoption_lift?` — non-elements skip outright
|
|
21
|
+
(lift no-ops them anyway); elements consult
|
|
22
|
+
`Native.ns_lift_needed?` (two C calls: own-declaration count +
|
|
23
|
+
resolved URI). Hoisted ABOVE the target's `namespaces`
|
|
24
|
+
materialization at all five mutation sites (add_child,
|
|
25
|
+
prepend_child, both sibling inserts, root=) — a fresh parent
|
|
26
|
+
no longer pays the in-scope collection FFI round-trips per
|
|
27
|
+
attach. Measured effect: add_child on a fresh parent
|
|
28
|
+
1678 -> 506ns.
|
|
29
|
+
2. `Native.create_binding_element/text` build the binding wrapper
|
|
30
|
+
in C (one C call + wrapper + identity-cache store; no FFI
|
|
31
|
+
marshaling). Document.create/create_element/create_text_node
|
|
32
|
+
route through them when auto-enabled.
|
|
33
|
+
3. `Native.append_binding_child` — gates + predicate + version
|
|
34
|
+
bump + engine append in ONE dispatch (Qnil = child needs the
|
|
35
|
+
lift, Ruby falls back). Element#add_child's node branch uses
|
|
36
|
+
it; namespaced children still lift (spec-pinned). The #204
|
|
37
|
+
ask-3 shape (create x2 + attach) went 4.1us -> ~1.2us measured
|
|
38
|
+
under host load 12-19 — at or past the Nokogiri row (1.5us).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 10 — Bulk attribute materialization for the binding
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.5)
|
|
4
|
+
|
|
5
|
+
Element#attributes / #each_attribute / #keys / #values walk the
|
|
6
|
+
attribute list with one FFI call per attribute (name + value per
|
|
7
|
+
row). One C pass materializes the {name => value} hash and the
|
|
8
|
+
key/value arrays in a single walk. Gate: attribute-heavy cold
|
|
9
|
+
reads improve; #attributes semantics unchanged (memoized, version
|
|
10
|
+
invalidated).
|
|
11
|
+
|
|
12
|
+
## Outcome (1.9.163.5)
|
|
13
|
+
|
|
14
|
+
`Native.bulk_attr_faces` builds {name => Attr} + the values hash
|
|
15
|
+
in one C walk (Attr value objects: name/value/element ivars,
|
|
16
|
+
c_handle left nil); Element#attributes rides it when
|
|
17
|
+
auto-enabled and memo-stamps both hashes. Measured (load 12-19):
|
|
18
|
+
Element#attributes on a 5-attr element ~134-151ns vs Nokogiri
|
|
19
|
+
~1107-1165ns (~8x). The values-only shape
|
|
20
|
+
(`Native.bulk_attributes`) backs the legacy path.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# 11 — The #204 gap table as a battery leg
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.5)
|
|
4
|
+
|
|
5
|
+
Reproduce moxml's gap table rows locally (parse 30KB, to_xml,
|
|
6
|
+
[] / content / name reads, set_attribute, create x2 + attach,
|
|
7
|
+
typed walk) in benchmark/native_vs_binding.rb so every perf round
|
|
8
|
+
tracks the exact rows the 2x mandate measures — both surfaces,
|
|
9
|
+
interleaved, load-gated.
|
|
10
|
+
|
|
11
|
+
## Outcome (1.9.163.5)
|
|
12
|
+
|
|
13
|
+
benchmark/native_vs_binding.rb gained the gap rows: []/content/
|
|
14
|
+
name repeat reads (binding + native), set_attribute, and the
|
|
15
|
+
build (create x2 + attach) row, all interleaved best-of behind
|
|
16
|
+
the file's load gate (skips > 20). `set_attribute` itself became
|
|
17
|
+
a C-bound face this round (Native.set_binding_attribute: gates +
|
|
18
|
+
version bump + engine write in one dispatch).
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -22,12 +22,14 @@ typedef int (*node_type_fn)(void *);
|
|
|
22
22
|
typedef void *(*next_sibling_fn)(void *);
|
|
23
23
|
typedef void *(*parent_fn)(void *);
|
|
24
24
|
typedef const char *(*elem_prefix_fn)(void *);
|
|
25
|
+
typedef const char *(*elem_ns_fn)(void *);
|
|
25
26
|
typedef size_t (*xp_count_fn)(void *);
|
|
26
27
|
typedef size_t (*xp_nodes_ex_fn)(void *, void **, int *, size_t);
|
|
27
28
|
typedef int (*xp_node_kind_fn)(void *, int);
|
|
28
29
|
typedef const char *(*xp_node_name_fn)(void *, int);
|
|
29
30
|
typedef const char *(*xp_node_value_fn)(void *, int);
|
|
30
31
|
typedef size_t (*serialize_into_fn)(void *, char *, size_t, void *, void *);
|
|
32
|
+
typedef int (*ns_count_fn)(void *);
|
|
31
33
|
typedef void *(*attr_first_fn)(void *);
|
|
32
34
|
typedef void *(*attr_next_fn)(void *);
|
|
33
35
|
typedef const char *(*attr_name_fn)(void *);
|
|
@@ -40,6 +42,7 @@ typedef void *(*elem_create_fn)(void *, const char *);
|
|
|
40
42
|
typedef void *(*text_create_fn)(void *, const char *);
|
|
41
43
|
typedef void *(*create_child_fn)(void *, const char *);
|
|
42
44
|
typedef int (*append_child_fn)(void *, void *);
|
|
45
|
+
typedef int (*set_attr_fn)(void *, const char *, const char *);
|
|
43
46
|
typedef int (*set_root_fn)(void *, void *);
|
|
44
47
|
typedef void (*doc_free_fn)(void *);
|
|
45
48
|
|
|
@@ -51,12 +54,14 @@ static node_type_fn f_node_type;
|
|
|
51
54
|
static next_sibling_fn f_next_sibling;
|
|
52
55
|
static parent_fn f_parent;
|
|
53
56
|
static elem_prefix_fn f_elem_prefix;
|
|
57
|
+
static elem_ns_fn f_elem_ns;
|
|
54
58
|
static xp_count_fn f_xp_count;
|
|
55
59
|
static xp_nodes_ex_fn f_xp_nodes_ex;
|
|
56
60
|
static xp_node_kind_fn f_xp_node_kind;
|
|
57
61
|
static xp_node_name_fn f_xp_node_name;
|
|
58
62
|
static xp_node_value_fn f_xp_node_value;
|
|
59
63
|
static serialize_into_fn f_doc_serialize, f_elem_serialize;
|
|
64
|
+
static ns_count_fn f_elem_ns_count;
|
|
60
65
|
static attr_first_fn f_attr_first;
|
|
61
66
|
static attr_next_fn f_attr_next;
|
|
62
67
|
static attr_name_fn f_attr_name;
|
|
@@ -69,18 +74,35 @@ static elem_create_fn f_elem_create;
|
|
|
69
74
|
static text_create_fn f_text_create;
|
|
70
75
|
static create_child_fn f_create_child;
|
|
71
76
|
static append_child_fn f_append_child;
|
|
77
|
+
static set_attr_fn f_set_attr;
|
|
72
78
|
static set_root_fn f_set_root;
|
|
73
79
|
static doc_free_fn f_doc_free;
|
|
74
80
|
|
|
75
81
|
static VALUE c_native_node;
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
/* Ivar reads (TODO.perf/07 tail): the binding Document keeps the
|
|
83
|
+
* C address and the caches in plain ivars, so the hot C faces skip
|
|
84
|
+
* rb_funcall dispatch entirely. */
|
|
85
|
+
static ID id_iv_c_address, id_iv_native_cache, id_iv_binding_cache;
|
|
86
|
+
static ID id_iv_version, id_iv_readonly, id_address;
|
|
87
|
+
static ID id_iv_nn_content, id_iv_nn_content_ver;
|
|
88
|
+
static ID id_iv_nn_attrs, id_iv_nn_attrs_ver;
|
|
89
|
+
static VALUE c_use_after_free_error;
|
|
90
|
+
|
|
91
|
+
static VALUE native_cache_of(VALUE document);
|
|
92
|
+
static void resolve_binding_classes(void);
|
|
93
|
+
static VALUE binding_cache_of(VALUE document);
|
|
78
94
|
|
|
79
95
|
#define NT_ELEMENT 0
|
|
80
96
|
|
|
81
97
|
struct native_node {
|
|
82
98
|
void *ptr;
|
|
83
99
|
VALUE document;
|
|
100
|
+
/* Single-slot read memos (#204 ask 1): the last attribute
|
|
101
|
+
* query and the last content, stamped with the document's
|
|
102
|
+
* mutation version (a Fixnum immediate — pointer compare).
|
|
103
|
+
* Slot beats the hash memo on repeat reads of one name. */
|
|
104
|
+
VALUE attr_key, attr_val, attr_ver;
|
|
105
|
+
VALUE content_val, content_ver;
|
|
84
106
|
};
|
|
85
107
|
|
|
86
108
|
static void nn_mark(void *p)
|
|
@@ -88,6 +110,12 @@ static void nn_mark(void *p)
|
|
|
88
110
|
struct native_node *n = p;
|
|
89
111
|
if (n->document != Qnil)
|
|
90
112
|
rb_gc_mark(n->document);
|
|
113
|
+
if (n->attr_key != Qnil)
|
|
114
|
+
rb_gc_mark(n->attr_key);
|
|
115
|
+
if (n->attr_val != Qnil)
|
|
116
|
+
rb_gc_mark(n->attr_val);
|
|
117
|
+
if (n->content_val != Qnil)
|
|
118
|
+
rb_gc_mark(n->content_val);
|
|
91
119
|
}
|
|
92
120
|
|
|
93
121
|
static size_t nn_size(const void *p) { (void)p; return sizeof(struct native_node); }
|
|
@@ -122,6 +150,8 @@ static void resolve_symbols(const char *lib_path)
|
|
|
122
150
|
f_next_sibling = (next_sibling_fn)lib_sym(h, "leptris_node_next_sibling");
|
|
123
151
|
f_parent = (parent_fn)lib_sym(h, "leptris_node_parent");
|
|
124
152
|
f_elem_prefix = (elem_prefix_fn)lib_sym(h, "leptris_element_prefix");
|
|
153
|
+
f_elem_ns_count = (ns_count_fn)lib_sym(h, "leptris_element_namespace_count");
|
|
154
|
+
f_elem_ns = (elem_ns_fn)lib_sym(h, "leptris_element_namespace");
|
|
125
155
|
f_xp_count = (xp_count_fn)lib_sym(h, "leptris_xpath_result_count");
|
|
126
156
|
f_xp_nodes_ex = (xp_nodes_ex_fn)lib_sym(h, "leptris_xpath_result_get_nodes_ex");
|
|
127
157
|
f_xp_node_kind = (xp_node_kind_fn)lib_sym(h, "leptris_xpath_result_node_kind");
|
|
@@ -141,15 +171,18 @@ static void resolve_symbols(const char *lib_path)
|
|
|
141
171
|
f_text_create = (text_create_fn)lib_sym(h, "leptris_text_node_create");
|
|
142
172
|
f_create_child = (create_child_fn)lib_sym(h, "leptris_element_create_child");
|
|
143
173
|
f_append_child = (append_child_fn)lib_sym(h, "leptris_element_append_child");
|
|
174
|
+
f_set_attr = (set_attr_fn)lib_sym(h, "leptris_element_set_attribute");
|
|
144
175
|
f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
|
|
145
176
|
f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
|
|
146
177
|
if (!f_elem_name || !f_text_content || !f_attr ||
|
|
147
178
|
!f_children_ex || !f_node_type || !f_next_sibling || !f_parent ||
|
|
148
179
|
!f_element_text || !f_doc_create || !f_elem_create || !f_text_create ||
|
|
149
|
-
!f_create_child || !f_append_child || !
|
|
180
|
+
!f_create_child || !f_append_child || !f_set_attr || !f_set_root ||
|
|
181
|
+
!f_doc_free ||
|
|
150
182
|
!f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
|
|
151
183
|
!f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
|
|
152
184
|
!f_doc_serialize || !f_elem_serialize || !f_attr_first ||
|
|
185
|
+
!f_elem_ns_count || !f_elem_ns ||
|
|
153
186
|
!f_attr_next || !f_attr_name || !f_attr_value ||
|
|
154
187
|
!f_node_line || !f_node_offset)
|
|
155
188
|
rb_raise(rb_eRuntimeError, "libleptris symbols missing");
|
|
@@ -168,21 +201,92 @@ static VALUE nn_content(VALUE self)
|
|
|
168
201
|
{
|
|
169
202
|
struct native_node *n;
|
|
170
203
|
const char *s;
|
|
204
|
+
VALUE doc, cached;
|
|
205
|
+
|
|
171
206
|
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
207
|
+
/* Version-stamped content memo (the binding Document's
|
|
208
|
+
* mutation version — the same invalidation discipline as
|
|
209
|
+
* binding memos, ADR 0003): repeat reads are two struct
|
|
210
|
+
* reads and a pointer compare. nil results never cache
|
|
211
|
+
* (empty elements recompute — rare in hot walks). */
|
|
212
|
+
doc = n->document;
|
|
213
|
+
if (doc != Qnil && n->content_ver != Qnil &&
|
|
214
|
+
n->content_ver == rb_ivar_get(doc, id_iv_version))
|
|
215
|
+
return n->content_val;
|
|
172
216
|
/* elements aggregate their text (leptris_element_text); text
|
|
173
217
|
* nodes carry it directly. */
|
|
174
218
|
s = f_node_type(n->ptr) == 0 ? f_element_text(n->ptr)
|
|
175
219
|
: f_text_content(n->ptr);
|
|
220
|
+
cached = s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
221
|
+
if (doc != Qnil && cached != Qnil) {
|
|
222
|
+
n->content_val = cached;
|
|
223
|
+
n->content_ver = rb_ivar_get(doc, id_iv_version);
|
|
224
|
+
}
|
|
225
|
+
return cached;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
static VALUE nf_fast_attribute2(VALUE self, VALUE addr, VALUE name)
|
|
229
|
+
{
|
|
230
|
+
const char *s;
|
|
231
|
+
(void)self;
|
|
232
|
+
s = f_attr((void *)(uintptr_t)NUM2ULL(addr),
|
|
233
|
+
RSTRING_PTR(StringValue(name)));
|
|
176
234
|
return s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
177
235
|
}
|
|
178
236
|
|
|
237
|
+
/* 07 (#204 ask 1): StringValueCStr runs a memchr over the name
|
|
238
|
+
* checking for embedded NULs — attribute names cannot contain
|
|
239
|
+
* NULs, so StringValue + RSTRING_PTR skips the scan. */
|
|
179
240
|
static VALUE nn_attribute(VALUE self, VALUE name)
|
|
180
241
|
{
|
|
181
242
|
struct native_node *n;
|
|
243
|
+
VALUE doc, cache, v;
|
|
182
244
|
const char *s;
|
|
245
|
+
|
|
246
|
+
/* String-normalized once, used as both the C argument and the
|
|
247
|
+
* cache key. */
|
|
248
|
+
if (!RB_TYPE_P(name, T_STRING))
|
|
249
|
+
name = rb_obj_as_string(name);
|
|
183
250
|
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
184
|
-
|
|
185
|
-
|
|
251
|
+
doc = n->document;
|
|
252
|
+
/* Single-slot memo first: the last-queried name by VALUE
|
|
253
|
+
* (callers may mint a fresh String per call) against the same
|
|
254
|
+
* version. Then the hash memo for other repeat names; both
|
|
255
|
+
* stamp with the document version, so any mutation through
|
|
256
|
+
* either surface drops them. Absent names never cache (no
|
|
257
|
+
* negative-cache staleness). */
|
|
258
|
+
if (doc != Qnil && n->attr_ver == rb_ivar_get(doc, id_iv_version) &&
|
|
259
|
+
n->attr_key != Qnil && rb_str_equal(n->attr_key, name))
|
|
260
|
+
return n->attr_val;
|
|
261
|
+
cache = Qnil;
|
|
262
|
+
if (doc != Qnil) {
|
|
263
|
+
cache = rb_ivar_get(self, id_iv_nn_attrs);
|
|
264
|
+
if (cache != Qnil &&
|
|
265
|
+
rb_ivar_get(self, id_iv_nn_attrs_ver) ==
|
|
266
|
+
rb_ivar_get(doc, id_iv_version)) {
|
|
267
|
+
v = rb_hash_aref(cache, name);
|
|
268
|
+
if (v != Qnil) {
|
|
269
|
+
n->attr_key = name;
|
|
270
|
+
n->attr_val = v;
|
|
271
|
+
n->attr_ver = rb_ivar_get(doc, id_iv_version);
|
|
272
|
+
return v;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
s = f_attr(n->ptr, RSTRING_PTR(name));
|
|
277
|
+
v = s ? rb_utf8_str_new_cstr(s) : Qnil;
|
|
278
|
+
if (doc != Qnil && v != Qnil) {
|
|
279
|
+
if (cache == Qnil)
|
|
280
|
+
cache = rb_hash_new();
|
|
281
|
+
rb_hash_aset(cache, name, v);
|
|
282
|
+
rb_ivar_set(self, id_iv_nn_attrs, cache);
|
|
283
|
+
rb_ivar_set(self, id_iv_nn_attrs_ver,
|
|
284
|
+
rb_ivar_get(doc, id_iv_version));
|
|
285
|
+
n->attr_key = name;
|
|
286
|
+
n->attr_val = v;
|
|
287
|
+
n->attr_ver = rb_ivar_get(doc, id_iv_version);
|
|
288
|
+
}
|
|
289
|
+
return v;
|
|
186
290
|
}
|
|
187
291
|
|
|
188
292
|
static VALUE nn_allocate(VALUE klass)
|
|
@@ -236,7 +340,7 @@ static VALUE nn_children(VALUE self)
|
|
|
236
340
|
return rb_ary_new2(0);
|
|
237
341
|
}
|
|
238
342
|
|
|
239
|
-
cache =
|
|
343
|
+
cache = native_cache_of(n->document);
|
|
240
344
|
out = rb_ary_new2(count);
|
|
241
345
|
for (i = 0; i < count; i++) {
|
|
242
346
|
uint64_t addr = (uint64_t)(uintptr_t)buf[i];
|
|
@@ -289,7 +393,7 @@ static VALUE nn_element_children(VALUE self)
|
|
|
289
393
|
return rb_ary_new2(0);
|
|
290
394
|
}
|
|
291
395
|
|
|
292
|
-
cache =
|
|
396
|
+
cache = native_cache_of(n->document);
|
|
293
397
|
out = rb_ary_new();
|
|
294
398
|
for (i = 0; i < count; i++) {
|
|
295
399
|
if (kinds[i] != NT_ELEMENT)
|
|
@@ -316,7 +420,7 @@ static VALUE wrap_cached(struct native_node *owner, void *ptr)
|
|
|
316
420
|
VALUE cache, key, node;
|
|
317
421
|
struct native_node *n;
|
|
318
422
|
|
|
319
|
-
cache =
|
|
423
|
+
cache = native_cache_of(owner->document);
|
|
320
424
|
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
321
425
|
node = rb_hash_aref(cache, key);
|
|
322
426
|
if (!NIL_P(node))
|
|
@@ -358,19 +462,144 @@ static VALUE wrap_new(VALUE document, void *ptr)
|
|
|
358
462
|
TypedData_Get_Struct(node, struct native_node, &nn_type, n);
|
|
359
463
|
n->ptr = ptr;
|
|
360
464
|
n->document = document;
|
|
361
|
-
cache =
|
|
465
|
+
cache = native_cache_of(document);
|
|
362
466
|
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
363
467
|
rb_hash_aset(cache, key, node);
|
|
364
468
|
return node;
|
|
365
469
|
}
|
|
366
470
|
|
|
367
|
-
/* Document address from the binding Document's
|
|
471
|
+
/* Document address from the binding Document's @c_address ivar
|
|
472
|
+
* (nil once freed) — one ivar read, no method dispatch. */
|
|
368
473
|
static void *doc_ptr_of(VALUE document)
|
|
369
474
|
{
|
|
370
|
-
VALUE
|
|
371
|
-
if (NIL_P(
|
|
475
|
+
VALUE addr = rb_ivar_get(document, id_iv_c_address);
|
|
476
|
+
if (NIL_P(addr))
|
|
372
477
|
rb_raise(rb_eRuntimeError, "document has been freed");
|
|
373
|
-
return (void *)(uintptr_t)NUM2ULL(
|
|
478
|
+
return (void *)(uintptr_t)NUM2ULL(addr);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/* Per-document identity caches. Both are lazily allocated on the
|
|
482
|
+
* Ruby side (@x ||= {}), so replicate that here: ivar read, create
|
|
483
|
+
* and store when unset. */
|
|
484
|
+
static VALUE native_cache_of(VALUE document)
|
|
485
|
+
{
|
|
486
|
+
VALUE cache = rb_ivar_get(document, id_iv_native_cache);
|
|
487
|
+
if (NIL_P(cache)) {
|
|
488
|
+
cache = rb_hash_new();
|
|
489
|
+
rb_ivar_set(document, id_iv_native_cache, cache);
|
|
490
|
+
}
|
|
491
|
+
return cache;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
static VALUE binding_cache_of(VALUE document)
|
|
495
|
+
{
|
|
496
|
+
VALUE cache = rb_ivar_get(document, id_iv_binding_cache);
|
|
497
|
+
if (NIL_P(cache)) {
|
|
498
|
+
cache = rb_hash_new();
|
|
499
|
+
rb_ivar_set(document, id_iv_binding_cache, cache);
|
|
500
|
+
}
|
|
501
|
+
return cache;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
/* TODO.perf/08 (#204 ask 2): native mutations must advance the
|
|
506
|
+
* binding Document's mutation version so binding memos invalidate
|
|
507
|
+
* exactly as they do through the FFI write path — and respect the
|
|
508
|
+
* readonly gate. Two ivar reads per mutation (the version is
|
|
509
|
+
* always a Fixnum, readonly is Qfalse/Qtrue). */
|
|
510
|
+
static VALUE c_readonly_error;
|
|
511
|
+
|
|
512
|
+
static void document_advance_version(VALUE document)
|
|
513
|
+
{
|
|
514
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
515
|
+
rb_raise(c_readonly_error,
|
|
516
|
+
"document is readonly — native mutation attempted");
|
|
517
|
+
rb_ivar_set(document, id_iv_version,
|
|
518
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
static VALUE nf_ns_lift_needed(VALUE self, VALUE addr)
|
|
522
|
+
{
|
|
523
|
+
void *node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
524
|
+
const char *uri;
|
|
525
|
+
(void)self;
|
|
526
|
+
/* Exact semantics in two C calls: an element with NO resolved
|
|
527
|
+
* namespace and NO own declarations cannot lose anything in a
|
|
528
|
+
* move — bare name, no prefix binding, no default-ns
|
|
529
|
+
* dependency, nothing to prune. Everything else takes the full
|
|
530
|
+
* lift path (carries in-scope declarations, prunes redundant
|
|
531
|
+
* own ones). */
|
|
532
|
+
if (f_elem_ns_count(node) > 0)
|
|
533
|
+
return Qtrue;
|
|
534
|
+
uri = f_elem_ns(node);
|
|
535
|
+
return (uri && *uri) ? Qtrue : Qfalse;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/* ---- C-bound append (TODO.perf/08-09, #204 ask 3) --------------
|
|
539
|
+
* One dispatch for the common programmatic-build adoption: the
|
|
540
|
+
* readonly + liveness gates, the provable no-op namespace-lift
|
|
541
|
+
* predicate, the version bump, and the engine append. Returns
|
|
542
|
+
* the engine status int; Qnil means the child DOES need the
|
|
543
|
+
* namespace lift — the caller falls back to the Ruby path. */
|
|
544
|
+
static VALUE nf_append_binding_child(VALUE self, VALUE document,
|
|
545
|
+
VALUE parent_addr, VALUE child_addr)
|
|
546
|
+
{
|
|
547
|
+
void *parent, *child;
|
|
548
|
+
const char *uri;
|
|
549
|
+
int st;
|
|
550
|
+
|
|
551
|
+
(void)self;
|
|
552
|
+
resolve_binding_classes();
|
|
553
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
554
|
+
rb_raise(c_use_after_free_error,
|
|
555
|
+
"owning document has been freed");
|
|
556
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
557
|
+
rb_raise(c_readonly_error,
|
|
558
|
+
"document is readonly — mutation attempted");
|
|
559
|
+
parent = (void *)(uintptr_t)NUM2ULL(parent_addr);
|
|
560
|
+
child = (void *)(uintptr_t)NUM2ULL(child_addr);
|
|
561
|
+
/* Same provable no-op as Element.skip_adoption_lift?: an
|
|
562
|
+
* element with no resolved namespace and no own declarations
|
|
563
|
+
* cannot lose anything in a move; non-elements can never need
|
|
564
|
+
* a lift. */
|
|
565
|
+
if (f_node_type(child) == NT_ELEMENT) {
|
|
566
|
+
if (f_elem_ns_count(child) > 0)
|
|
567
|
+
return Qnil;
|
|
568
|
+
uri = f_elem_ns(child);
|
|
569
|
+
if (uri && *uri)
|
|
570
|
+
return Qnil;
|
|
571
|
+
}
|
|
572
|
+
/* Bump before the call, mirroring ensure_writable!: a failed
|
|
573
|
+
* mutation merely discards memos. */
|
|
574
|
+
rb_ivar_set(document, id_iv_version,
|
|
575
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
576
|
+
st = f_append_child(parent, child);
|
|
577
|
+
return INT2FIX(st);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/* C-bound set_attribute (TODO.perf/11, #204 gap row 0.25x): the
|
|
581
|
+
* gates, the version bump (which drops the version-stamped
|
|
582
|
+
* attribute memos on both surfaces), and the engine write in one
|
|
583
|
+
* dispatch. */
|
|
584
|
+
static VALUE nf_set_binding_attribute(VALUE self, VALUE document,
|
|
585
|
+
VALUE addr, VALUE name, VALUE value)
|
|
586
|
+
{
|
|
587
|
+
void *node;
|
|
588
|
+
int st;
|
|
589
|
+
|
|
590
|
+
(void)self;
|
|
591
|
+
resolve_binding_classes();
|
|
592
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
593
|
+
rb_raise(c_use_after_free_error,
|
|
594
|
+
"owning document has been freed");
|
|
595
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
596
|
+
rb_raise(c_readonly_error,
|
|
597
|
+
"document is readonly — mutation attempted");
|
|
598
|
+
node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
599
|
+
rb_ivar_set(document, id_iv_version,
|
|
600
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
601
|
+
st = f_set_attr(node, StringValueCStr(name), StringValueCStr(value));
|
|
602
|
+
return INT2FIX(st);
|
|
374
603
|
}
|
|
375
604
|
|
|
376
605
|
/* Builder factories (#149): create in C, wrap as NativeNode — no
|
|
@@ -399,11 +628,14 @@ static VALUE nn_create_child(VALUE self, VALUE name)
|
|
|
399
628
|
{
|
|
400
629
|
struct native_node *n;
|
|
401
630
|
void *ptr;
|
|
631
|
+
VALUE out;
|
|
402
632
|
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
633
|
+
document_advance_version(n->document);
|
|
403
634
|
ptr = f_create_child(n->ptr, StringValueCStr(name));
|
|
404
635
|
if (!ptr)
|
|
405
636
|
rb_raise(rb_eRuntimeError, "leptris_element_create_child failed");
|
|
406
|
-
|
|
637
|
+
out = wrap_new(n->document, ptr);
|
|
638
|
+
return out;
|
|
407
639
|
}
|
|
408
640
|
|
|
409
641
|
static VALUE nn_append_child(VALUE self, VALUE child)
|
|
@@ -412,6 +644,7 @@ static VALUE nn_append_child(VALUE self, VALUE child)
|
|
|
412
644
|
int rc;
|
|
413
645
|
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
414
646
|
TypedData_Get_Struct(child, struct native_node, &nn_type, c);
|
|
647
|
+
document_advance_version(n->document);
|
|
415
648
|
rc = f_append_child(n->ptr, c->ptr);
|
|
416
649
|
if (rc != 0)
|
|
417
650
|
rb_raise(rb_eRuntimeError, "leptris_element_append_child failed (%d)", rc);
|
|
@@ -424,6 +657,7 @@ static VALUE nn_set_root(VALUE klass, VALUE document, VALUE element)
|
|
|
424
657
|
int rc;
|
|
425
658
|
(void)klass;
|
|
426
659
|
TypedData_Get_Struct(element, struct native_node, &nn_type, n);
|
|
660
|
+
document_advance_version(document);
|
|
427
661
|
rc = f_set_root(doc_ptr_of(document), n->ptr);
|
|
428
662
|
if (rc != 0)
|
|
429
663
|
rb_raise(rb_eRuntimeError, "leptris_document_set_root failed (%d)", rc);
|
|
@@ -492,7 +726,7 @@ static VALUE nn_address(VALUE self)
|
|
|
492
726
|
* disappear. */
|
|
493
727
|
static VALUE c_b_element = Qundef, c_b_text, c_b_comment, c_b_cdata,
|
|
494
728
|
c_b_pi, c_b_node, c_b_result_text, c_b_result_attr,
|
|
495
|
-
c_ffi_pointer;
|
|
729
|
+
c_b_attr, c_ffi_pointer;
|
|
496
730
|
static ID id_ptr_new;
|
|
497
731
|
|
|
498
732
|
/* Binding classes resolve LAZILY: Init_native can run before the
|
|
@@ -511,6 +745,7 @@ static void resolve_binding_classes(void)
|
|
|
511
745
|
c_b_result_text = rb_path2class("Leptris::XML::ResultText");
|
|
512
746
|
c_b_result_attr = rb_path2class("Leptris::XML::ResultAttr");
|
|
513
747
|
c_ffi_pointer = rb_path2class("FFI::Pointer");
|
|
748
|
+
c_b_attr = rb_path2class("Leptris::XML::Attr");
|
|
514
749
|
id_ptr_new = rb_intern("new");
|
|
515
750
|
rb_gc_register_mark_object(c_b_element);
|
|
516
751
|
rb_gc_register_mark_object(c_b_text);
|
|
@@ -521,6 +756,7 @@ static void resolve_binding_classes(void)
|
|
|
521
756
|
rb_gc_register_mark_object(c_b_result_text);
|
|
522
757
|
rb_gc_register_mark_object(c_b_result_attr);
|
|
523
758
|
rb_gc_register_mark_object(c_ffi_pointer);
|
|
759
|
+
rb_gc_register_mark_object(c_b_attr);
|
|
524
760
|
}
|
|
525
761
|
|
|
526
762
|
static VALUE binding_klass_for(int kind)
|
|
@@ -551,7 +787,7 @@ static VALUE bulk_children_impl(VALUE document, VALUE parent_addr,
|
|
|
551
787
|
return rb_ary_new2(0);
|
|
552
788
|
}
|
|
553
789
|
|
|
554
|
-
cache =
|
|
790
|
+
cache = binding_cache_of(document);
|
|
555
791
|
out = rb_ary_new2(elements_only ? 8 : count);
|
|
556
792
|
for (i = 0; i < count; i++) {
|
|
557
793
|
VALUE key, node;
|
|
@@ -621,7 +857,7 @@ static VALUE nf_bulk_xpath(VALUE self, VALUE document, VALUE result_ptr_val)
|
|
|
621
857
|
ruby_xfree(kinds);
|
|
622
858
|
return rb_ary_new2(0);
|
|
623
859
|
}
|
|
624
|
-
cache =
|
|
860
|
+
cache = binding_cache_of(document);
|
|
625
861
|
out = rb_ary_new2(count);
|
|
626
862
|
for (i = 0; i < count; i++) {
|
|
627
863
|
VALUE node, key;
|
|
@@ -694,6 +930,111 @@ static VALUE nf_bulk_xpath(VALUE self, VALUE document, VALUE result_ptr_val)
|
|
|
694
930
|
return out;
|
|
695
931
|
}
|
|
696
932
|
|
|
933
|
+
/* ---- Bulk attribute materialization (TODO.perf/10) ---------------
|
|
934
|
+
* Element#attributes / #keys / #values walk the attribute list
|
|
935
|
+
* one FFI call per row (name + value). One C pass builds the
|
|
936
|
+
* {name => value} hash for the binding. */
|
|
937
|
+
static VALUE nf_bulk_attributes(VALUE self, VALUE addr)
|
|
938
|
+
{
|
|
939
|
+
void *node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
940
|
+
VALUE hash = rb_hash_new();
|
|
941
|
+
void *attr = f_attr_first(node);
|
|
942
|
+
|
|
943
|
+
(void)self;
|
|
944
|
+
while (attr) {
|
|
945
|
+
const char *name = f_attr_name(attr);
|
|
946
|
+
const char *value = f_attr_value(node, attr);
|
|
947
|
+
rb_hash_aset(hash,
|
|
948
|
+
name ? rb_utf8_str_new_cstr(name) : Qnil,
|
|
949
|
+
value ? rb_utf8_str_new_cstr(value) : Qnil);
|
|
950
|
+
attr = f_attr_next(attr);
|
|
951
|
+
}
|
|
952
|
+
return hash;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/* ---- Binding create via C (TODO.perf/09): one call creates the
|
|
956
|
+
* node AND constructs the binding wrapper (class dispatch, ivars,
|
|
957
|
+
* identity-cache store) — no FFI marshaling, no wrap_fresh path. */
|
|
958
|
+
static VALUE nf_create_binding_element(VALUE self, VALUE document,
|
|
959
|
+
VALUE name)
|
|
960
|
+
{
|
|
961
|
+
void *doc = doc_ptr_of(document);
|
|
962
|
+
void *ptr = f_elem_create(doc, RSTRING_PTR(StringValue(name)));
|
|
963
|
+
VALUE node, cache, key;
|
|
964
|
+
|
|
965
|
+
(void)self;
|
|
966
|
+
resolve_binding_classes();
|
|
967
|
+
if (!ptr)
|
|
968
|
+
return Qnil; /* caller raises with the error channel */
|
|
969
|
+
node = rb_obj_alloc(c_b_element);
|
|
970
|
+
rb_iv_set(node, "@c_ptr",
|
|
971
|
+
rb_funcall(c_ffi_pointer, id_ptr_new, 1,
|
|
972
|
+
ULL2NUM((uint64_t)(uintptr_t)ptr)));
|
|
973
|
+
rb_iv_set(node, "@document", document);
|
|
974
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
975
|
+
rb_iv_set(node, "@node_type", INT2FIX(0));
|
|
976
|
+
cache = binding_cache_of(document);
|
|
977
|
+
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
978
|
+
rb_hash_aset(cache, key, node);
|
|
979
|
+
return node;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
static VALUE nf_create_binding_text(VALUE self, VALUE document,
|
|
983
|
+
VALUE content)
|
|
984
|
+
{
|
|
985
|
+
void *doc = doc_ptr_of(document);
|
|
986
|
+
void *ptr = f_text_create(doc, RSTRING_PTR(StringValue(content)));
|
|
987
|
+
VALUE node, cache, key;
|
|
988
|
+
|
|
989
|
+
(void)self;
|
|
990
|
+
resolve_binding_classes();
|
|
991
|
+
if (!ptr)
|
|
992
|
+
return Qnil;
|
|
993
|
+
node = rb_obj_alloc(c_b_text);
|
|
994
|
+
rb_iv_set(node, "@c_ptr",
|
|
995
|
+
rb_funcall(c_ffi_pointer, id_ptr_new, 1,
|
|
996
|
+
ULL2NUM((uint64_t)(uintptr_t)ptr)));
|
|
997
|
+
rb_iv_set(node, "@document", document);
|
|
998
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
999
|
+
rb_iv_set(node, "@node_type", INT2FIX(1));
|
|
1000
|
+
cache = binding_cache_of(document);
|
|
1001
|
+
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
1002
|
+
rb_hash_aset(cache, key, node);
|
|
1003
|
+
return node;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/* ---- Bulk attribute FACES (TODO.perf/10): {name => Attr} with
|
|
1007
|
+
* the values hash beside it — the exact shapes Element#attributes
|
|
1008
|
+
* memoizes, built in one walk. Attr is a value object
|
|
1009
|
+
* (name/value/element ivars; c_handle optional). */
|
|
1010
|
+
|
|
1011
|
+
static ID id_iv_name, id_iv_value, id_iv_element;
|
|
1012
|
+
|
|
1013
|
+
static VALUE nf_bulk_attr_faces(VALUE self, VALUE addr, VALUE element)
|
|
1014
|
+
{
|
|
1015
|
+
void *node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
1016
|
+
VALUE faces = rb_hash_new();
|
|
1017
|
+
VALUE values = rb_hash_new();
|
|
1018
|
+
void *attr = f_attr_first(node);
|
|
1019
|
+
|
|
1020
|
+
(void)self;
|
|
1021
|
+
resolve_binding_classes();
|
|
1022
|
+
while (attr) {
|
|
1023
|
+
const char *name = f_attr_name(attr);
|
|
1024
|
+
const char *value = f_attr_value(node, attr);
|
|
1025
|
+
VALUE k = name ? rb_utf8_str_new_cstr(name) : Qnil;
|
|
1026
|
+
VALUE v = value ? rb_utf8_str_new_cstr(value) : Qnil;
|
|
1027
|
+
VALUE a = rb_obj_alloc(c_b_attr);
|
|
1028
|
+
rb_iv_set(a, "@name", k);
|
|
1029
|
+
rb_iv_set(a, "@value", v);
|
|
1030
|
+
rb_iv_set(a, "@element", element);
|
|
1031
|
+
rb_hash_aset(faces, k, a);
|
|
1032
|
+
rb_hash_aset(values, k, v);
|
|
1033
|
+
attr = f_attr_next(attr);
|
|
1034
|
+
}
|
|
1035
|
+
return rb_assoc_new(faces, values);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
697
1038
|
/* ---- Ext-bound serialization (TODO.perf/04) ---------------------
|
|
698
1039
|
* The whole sized-buffer cycle in C: build SerializeOptions on
|
|
699
1040
|
* the stack, call (buf, cap, NULL status, opts), grow if needed,
|
|
@@ -766,6 +1107,12 @@ static VALUE nf_fast_element_xml(VALUE self, VALUE addr,
|
|
|
766
1107
|
NUM2INT(indent), RTEST(decl) ? 1 : 0);
|
|
767
1108
|
}
|
|
768
1109
|
|
|
1110
|
+
/* ---- Adoption-lift predicate (TODO.perf/09, #204 ask 3) ---------
|
|
1111
|
+
* The #178/#208 namespace lift + pruning runs on EVERY attach and
|
|
1112
|
+
* costs several FFI round-trips even when provably a no-op. This
|
|
1113
|
+
* predicate answers in ONE dispatch: false when the node carries
|
|
1114
|
+
* no namespace declarations AND its name has no prefix — the
|
|
1115
|
+
* common programmatic-build shape (bare names, no namespaces). */
|
|
769
1116
|
/* ---- Address-based fast readers (TODO.perf/01) -----------------
|
|
770
1117
|
* The DEFAULT binding classes call these when the bundle is
|
|
771
1118
|
* loaded: one C-API dispatch + rb_utf8_str_new_cstr — no FFI
|
|
@@ -811,11 +1158,11 @@ static VALUE nn_from(VALUE klass, VALUE document, VALUE element)
|
|
|
811
1158
|
struct native_node *n;
|
|
812
1159
|
VALUE node = nn_allocate(klass);
|
|
813
1160
|
void *ptr = (void *)(uintptr_t)NUM2ULL(
|
|
814
|
-
rb_funcall(element,
|
|
1161
|
+
rb_funcall(element, id_address, 0));
|
|
815
1162
|
TypedData_Get_Struct(node, struct native_node, &nn_type, n);
|
|
816
1163
|
n->ptr = ptr;
|
|
817
1164
|
n->document = document;
|
|
818
|
-
rb_hash_aset(
|
|
1165
|
+
rb_hash_aset(native_cache_of(document),
|
|
819
1166
|
ULL2NUM((uint64_t)(uintptr_t)ptr), node);
|
|
820
1167
|
return node;
|
|
821
1168
|
}
|
|
@@ -838,8 +1185,24 @@ void Init_native(void)
|
|
|
838
1185
|
c_native_node = rb_define_class_under(m_xml, "NativeNode", rb_cObject);
|
|
839
1186
|
rb_undef_alloc_func(c_native_node);
|
|
840
1187
|
|
|
841
|
-
|
|
842
|
-
|
|
1188
|
+
id_iv_name = rb_intern("@name");
|
|
1189
|
+
id_iv_value = rb_intern("@value");
|
|
1190
|
+
id_iv_element = rb_intern("@element");
|
|
1191
|
+
id_iv_c_address = rb_intern("@c_address");
|
|
1192
|
+
id_iv_native_cache = rb_intern("@native_cache");
|
|
1193
|
+
id_iv_binding_cache = rb_intern("@wrapper_cache");
|
|
1194
|
+
id_iv_version = rb_intern("@version");
|
|
1195
|
+
id_iv_readonly = rb_intern("@readonly");
|
|
1196
|
+
id_address = rb_intern("address");
|
|
1197
|
+
id_iv_nn_content = rb_intern("@nn_content");
|
|
1198
|
+
id_iv_nn_content_ver = rb_intern("@nn_content_ver");
|
|
1199
|
+
id_iv_nn_attrs = rb_intern("@nn_attrs");
|
|
1200
|
+
id_iv_nn_attrs_ver = rb_intern("@nn_attrs_ver");
|
|
1201
|
+
c_readonly_error = rb_path2class("Leptris::XML::ReadOnlyError");
|
|
1202
|
+
c_use_after_free_error =
|
|
1203
|
+
rb_path2class("Leptris::XML::UseAfterFreeError");
|
|
1204
|
+
rb_gc_register_mark_object(c_readonly_error);
|
|
1205
|
+
rb_gc_register_mark_object(c_use_after_free_error);
|
|
843
1206
|
|
|
844
1207
|
rb_define_singleton_method(c_native_node, "from", nn_from, 2);
|
|
845
1208
|
rb_define_singleton_method(c_native_node, "create_element", nn_create_element, 2);
|
|
@@ -871,6 +1234,19 @@ void Init_native(void)
|
|
|
871
1234
|
rb_define_module_function(m_native, "fast_element_text", nf_element_text, 1);
|
|
872
1235
|
rb_define_module_function(m_native, "fast_attribute", nf_attribute, 2);
|
|
873
1236
|
rb_define_module_function(m_native, "fast_prefix", nf_prefix, 1);
|
|
1237
|
+
rb_define_module_function(m_native, "fast_attribute2", nf_fast_attribute2, 2);
|
|
1238
|
+
rb_define_module_function(m_native, "ns_lift_needed?", nf_ns_lift_needed, 1);
|
|
1239
|
+
rb_define_module_function(m_native, "append_binding_child",
|
|
1240
|
+
nf_append_binding_child, 3);
|
|
1241
|
+
rb_define_module_function(m_native, "set_binding_attribute",
|
|
1242
|
+
nf_set_binding_attribute, 4);
|
|
1243
|
+
rb_define_module_function(m_native, "bulk_attributes", nf_bulk_attributes, 1);
|
|
1244
|
+
rb_define_module_function(m_native, "bulk_attr_faces",
|
|
1245
|
+
nf_bulk_attr_faces, 2);
|
|
1246
|
+
rb_define_module_function(m_native, "create_binding_element",
|
|
1247
|
+
nf_create_binding_element, 2);
|
|
1248
|
+
rb_define_module_function(m_native, "create_binding_text",
|
|
1249
|
+
nf_create_binding_text, 2);
|
|
874
1250
|
rb_define_module_function(m_native, "bulk_children", nf_bulk_children, 2);
|
|
875
1251
|
rb_define_module_function(m_native, "bulk_element_children",
|
|
876
1252
|
nf_bulk_element_children, 2);
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -32,6 +32,10 @@ class Leptris::XML::Document
|
|
|
32
32
|
|
|
33
33
|
def initialize(c_ptr = nil, freed = Freed.new(:alive))
|
|
34
34
|
@c_ptr = c_ptr
|
|
35
|
+
# Plain-Integer address twin of @c_ptr: the native layer reads
|
|
36
|
+
# this ivar directly (no method dispatch), nil'ed exactly when
|
|
37
|
+
# @c_ptr is.
|
|
38
|
+
@c_address = c_ptr&.address
|
|
35
39
|
@freed = freed
|
|
36
40
|
@readonly = false
|
|
37
41
|
@version = 0
|
|
@@ -258,7 +262,9 @@ class Leptris::XML::Document
|
|
|
258
262
|
raise Leptris::XML::UseAfterFreeError if @freed.state == :freed
|
|
259
263
|
# A document root has no in-scope declarations of its own —
|
|
260
264
|
# lift everything the element's source scope carried (#178).
|
|
261
|
-
Leptris::XML::Element.
|
|
265
|
+
unless Leptris::XML::Element.skip_adoption_lift?(element)
|
|
266
|
+
Leptris::XML::Element.lift_namespaces_for_adoption(element, {})
|
|
267
|
+
end
|
|
262
268
|
Leptris::XML::FFI.check_status(
|
|
263
269
|
Leptris::XML::FFI.leptris_document_set_root(@c_ptr, element.c_ptr))
|
|
264
270
|
@version += 1
|
|
@@ -266,12 +272,24 @@ class Leptris::XML::Document
|
|
|
266
272
|
end
|
|
267
273
|
|
|
268
274
|
def create_element(name)
|
|
275
|
+
# TODO.perf/09: one C call creates + wraps when the ext is
|
|
276
|
+
# loaded — no FFI marshaling, no wrap_fresh frames.
|
|
277
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
278
|
+
node = Leptris::XML::Native.create_binding_element(self, name.to_s)
|
|
279
|
+
raise Leptris::XML::Error, "leptris_element_create failed" if node.nil?
|
|
280
|
+
return node
|
|
281
|
+
end
|
|
269
282
|
ptr = Leptris::XML::FFI.leptris_element_create(@c_ptr, name)
|
|
270
283
|
raise Leptris::XML::Error, "leptris_element_create failed" if ptr.null?
|
|
271
284
|
Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_ELEMENT)
|
|
272
285
|
end
|
|
273
286
|
|
|
274
287
|
def create_text_node(content)
|
|
288
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
289
|
+
node = Leptris::XML::Native.create_binding_text(self, content.to_s)
|
|
290
|
+
raise Leptris::XML::Error, "leptris_text_node_create failed" if node.nil?
|
|
291
|
+
return node
|
|
292
|
+
end
|
|
275
293
|
ptr = Leptris::XML::FFI.leptris_text_node_create(@c_ptr, content.to_s)
|
|
276
294
|
raise Leptris::XML::Error, "leptris_text_node_create failed" if ptr.null?
|
|
277
295
|
Leptris::XML::Node.wrap_fresh(ptr, self, Leptris::XML::FFI::NODE_TEXT)
|
|
@@ -371,6 +389,7 @@ class Leptris::XML::Document
|
|
|
371
389
|
@freed.state = :freed
|
|
372
390
|
Leptris::XML::FFI.leptris_document_free(@c_ptr) unless @c_ptr.nil?
|
|
373
391
|
@c_ptr = nil
|
|
392
|
+
@c_address = nil
|
|
374
393
|
@wrapper_cache&.clear
|
|
375
394
|
end
|
|
376
395
|
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -114,6 +114,15 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
114
114
|
alias_method :get_attribute, :[]
|
|
115
115
|
|
|
116
116
|
def []=(key, value)
|
|
117
|
+
# C-bound write (TODO.perf/11): gates + version bump + engine
|
|
118
|
+
# set in one dispatch; the bump drops the version-stamped
|
|
119
|
+
# attribute memos on both surfaces.
|
|
120
|
+
if native_fast_children?
|
|
121
|
+
Leptris::XML::FFI.check_status(
|
|
122
|
+
Leptris::XML::Native.set_binding_attribute(
|
|
123
|
+
@document, @c_ptr.address, key.to_s, value.to_s))
|
|
124
|
+
return value
|
|
125
|
+
end
|
|
117
126
|
ensure_writable!
|
|
118
127
|
Leptris::XML::FFI.check_status(
|
|
119
128
|
Leptris::XML::FFI.leptris_element_set_attribute(@c_ptr, key.to_s, value.to_s))
|
|
@@ -198,6 +207,17 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
198
207
|
# Require the full Attr-hash face — a cold [] may have set the
|
|
199
208
|
# version with only a partial @attr_values (ruby#150).
|
|
200
209
|
return @attributes if @attributes && memo_hit?(@attributes_version)
|
|
210
|
+
if native_fast?
|
|
211
|
+
# TODO.perf/10: both memo faces in one C walk.
|
|
212
|
+
result, values = Leptris::XML::Native.bulk_attr_faces(
|
|
213
|
+
@c_ptr.address, self)
|
|
214
|
+
if @document
|
|
215
|
+
@attributes = result
|
|
216
|
+
@attr_values = values
|
|
217
|
+
@attributes_version = @document.version
|
|
218
|
+
end
|
|
219
|
+
return result
|
|
220
|
+
end
|
|
201
221
|
result = {}
|
|
202
222
|
values = {}
|
|
203
223
|
each_attribute do |attr|
|
|
@@ -253,6 +273,21 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
253
273
|
# adopted subtree. Call BEFORE attaching: the source scope is
|
|
254
274
|
# read through the node's current ancestors. Identical scopes
|
|
255
275
|
# (the common same-document move) add nothing.
|
|
276
|
+
# TODO.perf/09: one dispatch answers whether ANY lift work can
|
|
277
|
+
# apply (no resolved namespace, no own declarations = provable
|
|
278
|
+
# no-op — the common programmatic-build shape). Checked at the
|
|
279
|
+
# mutation sites so the target's in-scope namespaces are never
|
|
280
|
+
# materialized when the answer is no.
|
|
281
|
+
def self.skip_adoption_lift?(node)
|
|
282
|
+
# Non-elements can never need a lift (lift's own first line
|
|
283
|
+
# no-ops them) — skipping also avoids materializing the
|
|
284
|
+
# target's namespaces for the text/comment children every
|
|
285
|
+
# build appends.
|
|
286
|
+
return true unless node.is_a?(Leptris::XML::Element)
|
|
287
|
+
return false unless defined?(Leptris::XML::NATIVE_FAST)
|
|
288
|
+
!Leptris::XML::Native.ns_lift_needed?(node.c_ptr.address)
|
|
289
|
+
end
|
|
290
|
+
|
|
256
291
|
def self.lift_namespaces_for_adoption(node, target_scope)
|
|
257
292
|
return unless node.is_a?(Leptris::XML::Element)
|
|
258
293
|
# Declarations the node already carries (its own definitions —
|
|
@@ -281,7 +316,9 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
281
316
|
|
|
282
317
|
def prepend_child(node)
|
|
283
318
|
ensure_writable!
|
|
284
|
-
Leptris::XML::Element.
|
|
319
|
+
unless Leptris::XML::Element.skip_adoption_lift?(node)
|
|
320
|
+
Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
|
|
321
|
+
end
|
|
285
322
|
Leptris::XML::FFI.check_status(
|
|
286
323
|
Leptris::XML::FFI.leptris_element_prepend_child(@c_ptr, node.c_ptr))
|
|
287
324
|
node
|
|
@@ -289,7 +326,9 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
289
326
|
|
|
290
327
|
def add_next_sibling(node)
|
|
291
328
|
ensure_writable!
|
|
292
|
-
Leptris::XML::Element.
|
|
329
|
+
unless Leptris::XML::Element.skip_adoption_lift?(node)
|
|
330
|
+
Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
|
|
331
|
+
end
|
|
293
332
|
Leptris::XML::FFI.check_status(
|
|
294
333
|
Leptris::XML::FFI.leptris_element_insert_after(@c_ptr, node.c_ptr))
|
|
295
334
|
node
|
|
@@ -297,7 +336,9 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
297
336
|
|
|
298
337
|
def add_previous_sibling(node)
|
|
299
338
|
ensure_writable!
|
|
300
|
-
Leptris::XML::Element.
|
|
339
|
+
unless Leptris::XML::Element.skip_adoption_lift?(node)
|
|
340
|
+
Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
|
|
341
|
+
end
|
|
301
342
|
Leptris::XML::FFI.check_status(
|
|
302
343
|
Leptris::XML::FFI.leptris_element_insert_before(@c_ptr, node.c_ptr))
|
|
303
344
|
node
|
|
@@ -386,7 +427,22 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
386
427
|
ensure_writable!
|
|
387
428
|
case node_or_markup
|
|
388
429
|
when Leptris::XML::Node
|
|
389
|
-
|
|
430
|
+
# C-bound append (TODO.perf/08-09): one dispatch runs the
|
|
431
|
+
# readonly/liveness gates, the provable no-op lift
|
|
432
|
+
# predicate, the version bump, and the engine append. Qnil
|
|
433
|
+
# means the child needs the namespace lift — fall through
|
|
434
|
+
# to the full path.
|
|
435
|
+
if native_fast_children?
|
|
436
|
+
st = Leptris::XML::Native.append_binding_child(
|
|
437
|
+
@document, @c_ptr.address, node_or_markup.c_ptr.address)
|
|
438
|
+
unless st.nil?
|
|
439
|
+
Leptris::XML::FFI.check_status(st)
|
|
440
|
+
return node_or_markup
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
unless Leptris::XML::Element.skip_adoption_lift?(node_or_markup)
|
|
444
|
+
Leptris::XML::Element.lift_namespaces_for_adoption(node_or_markup, namespaces)
|
|
445
|
+
end
|
|
390
446
|
Leptris::XML::FFI.check_status(
|
|
391
447
|
Leptris::XML::FFI.leptris_element_append_child(@c_ptr, node_or_markup.c_ptr))
|
|
392
448
|
node_or_markup
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: leptris
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.9.163.
|
|
4
|
+
version: 1.9.163.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -75,6 +75,11 @@ files:
|
|
|
75
75
|
- TODO.perf/04-ext-bound-serialization.md
|
|
76
76
|
- TODO.perf/05-native-surface-completion.md
|
|
77
77
|
- TODO.perf/06-perf-battery-native-legs.md
|
|
78
|
+
- TODO.perf/07-native-read-floor.md
|
|
79
|
+
- TODO.perf/08-version-coherent-native-mutations.md
|
|
80
|
+
- TODO.perf/09-adoption-lift-fast-path.md
|
|
81
|
+
- TODO.perf/10-bulk-attribute-materialization.md
|
|
82
|
+
- TODO.perf/11-moxml-gap-battery.md
|
|
78
83
|
- TODO.restructure/01-constraint-compliance-audit.md
|
|
79
84
|
- TODO.restructure/02-deep-copy-seam.md
|
|
80
85
|
- TODO.restructure/03-evaluation-context-seam.md
|