leptris 1.9.162.9 → 1.9.163.2
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/01-ext-accelerated-default-reads.md +4 -1
- data/TODO.perf/03-bulk-xpath-result-materialization.md +6 -1
- data/TODO.perf/04-ext-bound-serialization.md +5 -1
- data/TODO.perf/05-native-surface-completion.md +8 -1
- data/TODO.perf/06-perf-battery-native-legs.md +7 -1
- data/ext/leptris/native/native.c +422 -11
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/native_layer.rb +2 -0
- data/lib/leptris/xml/node.rb +28 -2
- data/lib/leptris/xml/node_set.rb +36 -1
- data/lib/leptris/xml/serialization.rb +14 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7718cf666bbec3e128c6dd2458d85e59f58757654b67021137e931e92271d19
|
|
4
|
+
data.tar.gz: 9cece9a47663dcc7c0665201beac6914edc30f08d3680d9ba8bfee64bb2819bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49695f1c96011b812601dfaa6a200403523563737bdb1e470d9f8d5c67fb9e3e6948ede1fef108a2a344df68ef31d69ac931a3f2363d173b7ac7546673ca9340
|
|
7
|
+
data.tar.gz: a02fe7e50a379fe9d84d1a705234d6c49fba0c52fcf6c4867899ae91aa7b2bc618590bed20bf01a518a6d83b268e440e0beab58f2752eb6d6808a68bdef3b46b
|
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.2] - 2026-09-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **TODO.perf/05 — NativeNode surface**: `#attributes` (the
|
|
13
|
+
flat name=>value hash in one C walk — the `[]` shape, not the
|
|
14
|
+
binding's Attr-valued hash), `#line`, `#byte_offset` (binding
|
|
15
|
+
parity). Binding classes inside the ext now resolve lazily —
|
|
16
|
+
`Init_native` can run before the binding's autoload entries
|
|
17
|
+
load, and `require "leptris/xml/native_layer"` standalone
|
|
18
|
+
crashed on undefined `FFI::Pointer` before the ffi require was
|
|
19
|
+
made explicit.
|
|
20
|
+
|
|
21
|
+
## [1.9.163.1] - 2026-09-14
|
|
22
|
+
|
|
23
|
+
### Performance
|
|
24
|
+
|
|
25
|
+
- **TODO.perf/04 — ext-bound serialization**: `to_xml`/`inner_html`
|
|
26
|
+
run the whole sized-buffer cycle in C (options on the stack, probe
|
|
27
|
+
the largest owned buffer first so large documents serialize once,
|
|
28
|
+
grow-only scratch under the GVL). inner_html per-call batch
|
|
29
|
+
**84 -> 50 ms (1.7x)**; document serialization at parity (single
|
|
30
|
+
call both paths); `encoding:` keeps the FFI options path.
|
|
31
|
+
|
|
32
|
+
## [1.9.163.0] - 2026-09-14
|
|
33
|
+
|
|
34
|
+
### Performance
|
|
35
|
+
|
|
36
|
+
- **TODO.perf/01 complete — bulk children for binding wrappers**:
|
|
37
|
+
with the native layer auto-loaded (1.9.162.9's TODO.perf/02),
|
|
38
|
+
`Node#children` and `#element_children` construct every binding
|
|
39
|
+
wrapper in ONE C pass (class dispatch, ivars, identity-cache
|
|
40
|
+
check/store — the per-child Ruby wrap frames disappear). Cold
|
|
41
|
+
one-shot walk (fresh doc, element_children + name/[]/content
|
|
42
|
+
per node): **9.2 -> 1.2 ms (~8x)**. Warm paths unchanged
|
|
43
|
+
(memoized); scope-owned iterparse elements stay on the FFI path.
|
|
44
|
+
- **TODO.perf/03 — bulk XPath result materialization**: NodeSet
|
|
45
|
+
materializes through one C pass (kind dispatch with synthetic
|
|
46
|
+
text/attribute value capture in-C; node-type consult for real
|
|
47
|
+
Text/CDATA under XPATH's TEXT). One-shot xpath rows gain ~1.4x
|
|
48
|
+
on the materialization slice (the call is parse-dominated);
|
|
49
|
+
>512-entry results and rare kinds keep the per-index fallback.
|
|
50
|
+
|
|
8
51
|
## [1.9.162.9] - 2026-09-14
|
|
9
52
|
|
|
10
53
|
### Performance
|
|
@@ -7,7 +7,10 @@ cannot see it (memos serve after first touch) — the gate is the
|
|
|
7
7
|
COLD one-shot pass (fresh doc, one read per node): 13-25% on the
|
|
8
8
|
#187 shape. UTF-8 encoding via rb_utf8_str_new_cstr (suite specs
|
|
9
9
|
run both paths in one process: native_layer_spec sets the flag
|
|
10
|
-
mid-suite).
|
|
10
|
+
mid-suite). COMPLETE (1.9.163.0): bulk children + bulk element_children for
|
|
11
|
+
binding wrappers (one C pass: class dispatch + ivars + identity
|
|
12
|
+
cache) — cold one-shot walk 9.2->1.2ms (7.8x, element_children +
|
|
13
|
+
name/[]/content). Both TODO items done.
|
|
11
14
|
|
|
12
15
|
The default binding's hot reads each pay FFI per-call marshaling
|
|
13
16
|
(:string returns allocate through FFI, args marshal through the
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# 03 — Bulk XPath result materialization
|
|
2
2
|
|
|
3
|
-
Status:
|
|
3
|
+
Status: SHIPPED (1.9.163.0) — one C pass (kind dispatch incl.
|
|
4
|
+
synthetic text/attr value capture, node-type consult for real
|
|
5
|
+
Text/CDATA under XPATH text, identity cache). HONEST FINDING: the
|
|
6
|
+
xpath one-shot is parse-dominated; the materialization slice gains
|
|
7
|
+
~1.4x. >512-entry results and rare kinds fall back to the per-index
|
|
8
|
+
path. No warm-path change.
|
|
4
9
|
|
|
5
10
|
NodeSet#fetch materializes results per node (wrap per item +
|
|
6
11
|
ResultText/ResultAttr captures). The #185 profile counted
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# 04 — Serialization buffer cycle in C
|
|
2
2
|
|
|
3
|
-
Status:
|
|
3
|
+
Status: SHIPPED (1.9.163.1) — the sized-buffer cycle runs in C
|
|
4
|
+
(probe largest owned buffer first: single serialization for
|
|
5
|
+
big docs; grow-only scratch under the GVL). inner_html 84->50ms
|
|
6
|
+
(1.7x per-call batch); doc serialize at parity with the FFI seam
|
|
7
|
+
(both single-call); encoding: paths keep the FFI options path.
|
|
4
8
|
|
|
5
9
|
FFI.serialize_into_string does the two-call sized protocol
|
|
6
10
|
(needed probe, then copy) with Ruby-side buffer management per
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# 05 — Complete the NativeNode read surface
|
|
2
2
|
|
|
3
|
-
Status:
|
|
3
|
+
Status: SHIPPED (1.9.163.2) — #attributes (one C walk, string
|
|
4
|
+
values — deliberately the flat [] shape, not the binding's
|
|
5
|
+
Attr-valued hash), #line, #byte_offset with binding parity. Also:
|
|
6
|
+
binding classes in the ext resolve LAZILY (Init may run before
|
|
7
|
+
autoload loads Element — the standalone native_layer require
|
|
8
|
+
crashed before; fixed) and native_layer requires ffi first.
|
|
9
|
+
Remaining cold: writes stay FFI by design; attribute_nodes/
|
|
10
|
+
namespaces deferred until a consumer asks.
|
|
4
11
|
|
|
5
12
|
Missing on NativeNode vs Element: #attributes (bulk attr hash),
|
|
6
13
|
#attribute_nodes, #namespaces / #prefix, #line / #byte_offset,
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# 06 — Native-layer legs in the perf battery + drift record
|
|
2
2
|
|
|
3
|
-
Status:
|
|
3
|
+
Status: SHIPPED (1.9.163.2) — benchmark/native_vs_binding.rb runs
|
|
4
|
+
the walk/read/serialize rows through both surfaces under the
|
|
5
|
+
TODO.restructure/17 load gate. Honest findings recorded: warm rows
|
|
6
|
+
favor the binding's memoization (attr 0.08 vs 0.30 ms; the native
|
|
7
|
+
API pays per-call dispatch with no memo layer); cold one-shot
|
|
8
|
+
favors the native API (1.81 vs 2.39 ms — on TOP of the binding's
|
|
9
|
+
own bulk/fast acceleration); serialize at parity.
|
|
4
10
|
|
|
5
11
|
benchmark/ gains native-layer legs (walk/build/attr rows via
|
|
6
12
|
NativeNode beside the binding rows) so the drift record tracks
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -17,11 +17,23 @@
|
|
|
17
17
|
typedef const char *(*elem_name_fn)(void *);
|
|
18
18
|
typedef const char *(*text_content_fn)(void *);
|
|
19
19
|
typedef const char *(*attr_fn)(void *, const char *);
|
|
20
|
-
typedef
|
|
20
|
+
typedef size_t (*children_ex_fn)(void *, void **, int *, size_t);
|
|
21
21
|
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 size_t (*xp_count_fn)(void *);
|
|
26
|
+
typedef size_t (*xp_nodes_ex_fn)(void *, void **, int *, size_t);
|
|
27
|
+
typedef int (*xp_node_kind_fn)(void *, int);
|
|
28
|
+
typedef const char *(*xp_node_name_fn)(void *, int);
|
|
29
|
+
typedef const char *(*xp_node_value_fn)(void *, int);
|
|
30
|
+
typedef size_t (*serialize_into_fn)(void *, char *, size_t, void *, void *);
|
|
31
|
+
typedef void *(*attr_first_fn)(void *);
|
|
32
|
+
typedef void *(*attr_next_fn)(void *);
|
|
33
|
+
typedef const char *(*attr_name_fn)(void *);
|
|
34
|
+
typedef const char *(*attr_value_fn)(void *, void *);
|
|
35
|
+
typedef unsigned int (*node_line_fn)(void *);
|
|
36
|
+
typedef size_t (*node_offset_fn)(void *);
|
|
25
37
|
typedef const char *(*element_text_fn)(void *);
|
|
26
38
|
typedef void *(*doc_create_fn)(void);
|
|
27
39
|
typedef void *(*elem_create_fn)(void *, const char *);
|
|
@@ -39,6 +51,18 @@ static node_type_fn f_node_type;
|
|
|
39
51
|
static next_sibling_fn f_next_sibling;
|
|
40
52
|
static parent_fn f_parent;
|
|
41
53
|
static elem_prefix_fn f_elem_prefix;
|
|
54
|
+
static xp_count_fn f_xp_count;
|
|
55
|
+
static xp_nodes_ex_fn f_xp_nodes_ex;
|
|
56
|
+
static xp_node_kind_fn f_xp_node_kind;
|
|
57
|
+
static xp_node_name_fn f_xp_node_name;
|
|
58
|
+
static xp_node_value_fn f_xp_node_value;
|
|
59
|
+
static serialize_into_fn f_doc_serialize, f_elem_serialize;
|
|
60
|
+
static attr_first_fn f_attr_first;
|
|
61
|
+
static attr_next_fn f_attr_next;
|
|
62
|
+
static attr_name_fn f_attr_name;
|
|
63
|
+
static attr_value_fn f_attr_value;
|
|
64
|
+
static node_line_fn f_node_line;
|
|
65
|
+
static node_offset_fn f_node_offset;
|
|
42
66
|
static element_text_fn f_element_text;
|
|
43
67
|
static doc_create_fn f_doc_create;
|
|
44
68
|
static elem_create_fn f_elem_create;
|
|
@@ -49,7 +73,8 @@ static set_root_fn f_set_root;
|
|
|
49
73
|
static doc_free_fn f_doc_free;
|
|
50
74
|
|
|
51
75
|
static VALUE c_native_node;
|
|
52
|
-
static ID id_wrapper_cache;
|
|
76
|
+
static ID id_wrapper_cache; /* native_cache: NativeNode identity */
|
|
77
|
+
static ID id_binding_cache; /* wrapper_cache: binding Node identity */
|
|
53
78
|
|
|
54
79
|
#define NT_ELEMENT 0
|
|
55
80
|
|
|
@@ -97,6 +122,19 @@ static void resolve_symbols(const char *lib_path)
|
|
|
97
122
|
f_next_sibling = (next_sibling_fn)lib_sym(h, "leptris_node_next_sibling");
|
|
98
123
|
f_parent = (parent_fn)lib_sym(h, "leptris_node_parent");
|
|
99
124
|
f_elem_prefix = (elem_prefix_fn)lib_sym(h, "leptris_element_prefix");
|
|
125
|
+
f_xp_count = (xp_count_fn)lib_sym(h, "leptris_xpath_result_count");
|
|
126
|
+
f_xp_nodes_ex = (xp_nodes_ex_fn)lib_sym(h, "leptris_xpath_result_get_nodes_ex");
|
|
127
|
+
f_xp_node_kind = (xp_node_kind_fn)lib_sym(h, "leptris_xpath_result_node_kind");
|
|
128
|
+
f_xp_node_name = (xp_node_name_fn)lib_sym(h, "leptris_xpath_result_node_name");
|
|
129
|
+
f_xp_node_value = (xp_node_value_fn)lib_sym(h, "leptris_xpath_result_node_value");
|
|
130
|
+
f_doc_serialize = (serialize_into_fn)lib_sym(h, "leptris_document_serialize_into");
|
|
131
|
+
f_elem_serialize = (serialize_into_fn)lib_sym(h, "leptris_element_serialize_into");
|
|
132
|
+
f_attr_first = (attr_first_fn)lib_sym(h, "leptris_element_first_attribute");
|
|
133
|
+
f_attr_next = (attr_next_fn)lib_sym(h, "leptris_attribute_next");
|
|
134
|
+
f_attr_name = (attr_name_fn)lib_sym(h, "leptris_attribute_get_name");
|
|
135
|
+
f_attr_value = (attr_value_fn)lib_sym(h, "leptris_attribute_get_value");
|
|
136
|
+
f_node_line = (node_line_fn)lib_sym(h, "leptris_node_line");
|
|
137
|
+
f_node_offset = (node_offset_fn)lib_sym(h, "leptris_node_byte_offset");
|
|
100
138
|
f_element_text = (element_text_fn)lib_sym(h, "leptris_element_text");
|
|
101
139
|
f_doc_create = (doc_create_fn)lib_sym(h, "leptris_document_create");
|
|
102
140
|
f_elem_create = (elem_create_fn)lib_sym(h, "leptris_element_create");
|
|
@@ -109,7 +147,11 @@ static void resolve_symbols(const char *lib_path)
|
|
|
109
147
|
!f_children_ex || !f_node_type || !f_next_sibling || !f_parent ||
|
|
110
148
|
!f_element_text || !f_doc_create || !f_elem_create || !f_text_create ||
|
|
111
149
|
!f_create_child || !f_append_child || !f_set_root || !f_doc_free ||
|
|
112
|
-
!f_elem_prefix
|
|
150
|
+
!f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
|
|
151
|
+
!f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
|
|
152
|
+
!f_doc_serialize || !f_elem_serialize || !f_attr_first ||
|
|
153
|
+
!f_attr_next || !f_attr_name || !f_attr_value ||
|
|
154
|
+
!f_node_line || !f_node_offset)
|
|
113
155
|
rb_raise(rb_eRuntimeError, "libleptris symbols missing");
|
|
114
156
|
}
|
|
115
157
|
|
|
@@ -152,18 +194,47 @@ static VALUE nn_allocate(VALUE klass)
|
|
|
152
194
|
/* Bulk children: one native pass; cache check/store inline; the
|
|
153
195
|
* document's wrapper_cache (a Ruby Hash keyed by address) provides
|
|
154
196
|
* identity for nodes seen through other paths. */
|
|
197
|
+
/* Two-call children fetch (#202): children_ex copies
|
|
198
|
+
* min(total, cap); a saturated fill means the list may be longer —
|
|
199
|
+
* query the true count (buf NULL) and refetch grown. */
|
|
200
|
+
static int fetch_children_two_call(void *node, void ***buf_out,
|
|
201
|
+
int **kinds_out)
|
|
202
|
+
{
|
|
203
|
+
size_t cap = 512;
|
|
204
|
+
void *b = ruby_xmalloc(cap * sizeof(void *));
|
|
205
|
+
int *k = ruby_xmalloc((int)cap * sizeof(int));
|
|
206
|
+
size_t count = f_children_ex(node, b, k, cap);
|
|
207
|
+
if (count == cap) {
|
|
208
|
+
size_t total = f_children_ex(node, NULL, NULL, 0);
|
|
209
|
+
if (total > cap) {
|
|
210
|
+
ruby_xfree(b);
|
|
211
|
+
ruby_xfree(k);
|
|
212
|
+
cap = total;
|
|
213
|
+
b = ruby_xmalloc(cap * sizeof(void *));
|
|
214
|
+
k = ruby_xmalloc((int)cap * sizeof(int));
|
|
215
|
+
count = f_children_ex(node, b, k, cap);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
*buf_out = b;
|
|
219
|
+
*kinds_out = k;
|
|
220
|
+
return (int)count;
|
|
221
|
+
}
|
|
222
|
+
|
|
155
223
|
static VALUE nn_children(VALUE self)
|
|
156
224
|
{
|
|
157
225
|
struct native_node *n;
|
|
158
|
-
void
|
|
159
|
-
int kinds
|
|
226
|
+
void **buf;
|
|
227
|
+
int *kinds;
|
|
160
228
|
int count, i;
|
|
161
229
|
VALUE cache, out;
|
|
162
230
|
|
|
163
231
|
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
164
|
-
count =
|
|
165
|
-
if (count <= 0)
|
|
232
|
+
count = fetch_children_two_call(n->ptr, &buf, &kinds);
|
|
233
|
+
if (count <= 0) {
|
|
234
|
+
ruby_xfree(buf);
|
|
235
|
+
ruby_xfree(kinds);
|
|
166
236
|
return rb_ary_new2(0);
|
|
237
|
+
}
|
|
167
238
|
|
|
168
239
|
cache = rb_funcall(n->document, id_wrapper_cache, 0);
|
|
169
240
|
out = rb_ary_new2(count);
|
|
@@ -181,6 +252,8 @@ static VALUE nn_children(VALUE self)
|
|
|
181
252
|
}
|
|
182
253
|
rb_ary_push(out, child);
|
|
183
254
|
}
|
|
255
|
+
ruby_xfree(buf);
|
|
256
|
+
ruby_xfree(kinds);
|
|
184
257
|
return out;
|
|
185
258
|
}
|
|
186
259
|
|
|
@@ -203,15 +276,18 @@ static VALUE nn_type_sym(VALUE self)
|
|
|
203
276
|
static VALUE nn_element_children(VALUE self)
|
|
204
277
|
{
|
|
205
278
|
struct native_node *n;
|
|
206
|
-
void
|
|
207
|
-
int kinds
|
|
279
|
+
void **buf;
|
|
280
|
+
int *kinds;
|
|
208
281
|
int count, i;
|
|
209
282
|
VALUE cache, out;
|
|
210
283
|
|
|
211
284
|
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
212
|
-
count =
|
|
213
|
-
if (count <= 0)
|
|
285
|
+
count = fetch_children_two_call(n->ptr, &buf, &kinds);
|
|
286
|
+
if (count <= 0) {
|
|
287
|
+
ruby_xfree(buf);
|
|
288
|
+
ruby_xfree(kinds);
|
|
214
289
|
return rb_ary_new2(0);
|
|
290
|
+
}
|
|
215
291
|
|
|
216
292
|
cache = rb_funcall(n->document, id_wrapper_cache, 0);
|
|
217
293
|
out = rb_ary_new();
|
|
@@ -230,6 +306,8 @@ static VALUE nn_element_children(VALUE self)
|
|
|
230
306
|
}
|
|
231
307
|
rb_ary_push(out, child);
|
|
232
308
|
}
|
|
309
|
+
ruby_xfree(buf);
|
|
310
|
+
ruby_xfree(kinds);
|
|
233
311
|
return out;
|
|
234
312
|
}
|
|
235
313
|
|
|
@@ -352,6 +430,43 @@ static VALUE nn_set_root(VALUE klass, VALUE document, VALUE element)
|
|
|
352
430
|
return element;
|
|
353
431
|
}
|
|
354
432
|
|
|
433
|
+
/* NativeNode surface completion (TODO.perf/05): bulk attribute
|
|
434
|
+
* hash (name => value, one C walk, no per-attr Ruby frames) and
|
|
435
|
+
* the position readers. */
|
|
436
|
+
static VALUE nn_attributes(VALUE self)
|
|
437
|
+
{
|
|
438
|
+
struct native_node *n;
|
|
439
|
+
VALUE hash;
|
|
440
|
+
void *attr;
|
|
441
|
+
|
|
442
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
443
|
+
hash = rb_hash_new();
|
|
444
|
+
attr = f_attr_first(n->ptr);
|
|
445
|
+
while (attr) {
|
|
446
|
+
const char *name = f_attr_name(attr);
|
|
447
|
+
const char *value = f_attr_value(n->ptr, attr);
|
|
448
|
+
rb_hash_aset(hash,
|
|
449
|
+
name ? rb_utf8_str_new_cstr(name) : Qnil,
|
|
450
|
+
value ? rb_utf8_str_new_cstr(value) : Qnil);
|
|
451
|
+
attr = f_attr_next(attr);
|
|
452
|
+
}
|
|
453
|
+
return hash;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
static VALUE nn_line(VALUE self)
|
|
457
|
+
{
|
|
458
|
+
struct native_node *n;
|
|
459
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
460
|
+
return UINT2NUM(f_node_line(n->ptr));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
static VALUE nn_byte_offset(VALUE self)
|
|
464
|
+
{
|
|
465
|
+
struct native_node *n;
|
|
466
|
+
TypedData_Get_Struct(self, struct native_node, &nn_type, n);
|
|
467
|
+
return ULL2NUM((uint64_t)f_node_offset(n->ptr));
|
|
468
|
+
}
|
|
469
|
+
|
|
355
470
|
static VALUE nn_address(VALUE self)
|
|
356
471
|
{
|
|
357
472
|
struct native_node *n;
|
|
@@ -359,6 +474,289 @@ static VALUE nn_address(VALUE self)
|
|
|
359
474
|
return ULL2NUM((uint64_t)(uintptr_t)n->ptr);
|
|
360
475
|
}
|
|
361
476
|
|
|
477
|
+
/* ---- Bulk children for BINDING wrappers (TODO.perf/01 tail) ----
|
|
478
|
+
* One C pass: children_ex + binding-class dispatch + ivar set +
|
|
479
|
+
* identity-cache check/store. Returns a Ruby Array of Element/
|
|
480
|
+
* Text/... wrappers — Node#children swaps this in when the ext
|
|
481
|
+
* is loaded. Class constants and FFI::Pointer resolve once at
|
|
482
|
+
* Init; the per-node Ruby frames (wrap/construct/memo/alive)
|
|
483
|
+
* disappear. */
|
|
484
|
+
static VALUE c_b_element = Qundef, c_b_text, c_b_comment, c_b_cdata,
|
|
485
|
+
c_b_pi, c_b_node, c_b_result_text, c_b_result_attr,
|
|
486
|
+
c_ffi_pointer;
|
|
487
|
+
static ID id_ptr_new;
|
|
488
|
+
|
|
489
|
+
/* Binding classes resolve LAZILY: Init_native can run before the
|
|
490
|
+
* binding's autoload entries load (require "leptris" flow) —
|
|
491
|
+
* rb_path2class at Init time raises for undefined constants. */
|
|
492
|
+
static void resolve_binding_classes(void)
|
|
493
|
+
{
|
|
494
|
+
if (c_b_element != Qundef)
|
|
495
|
+
return;
|
|
496
|
+
c_b_element = rb_path2class("Leptris::XML::Element");
|
|
497
|
+
c_b_text = rb_path2class("Leptris::XML::Text");
|
|
498
|
+
c_b_comment = rb_path2class("Leptris::XML::Comment");
|
|
499
|
+
c_b_cdata = rb_path2class("Leptris::XML::CDATA");
|
|
500
|
+
c_b_pi = rb_path2class("Leptris::XML::ProcessingInstruction");
|
|
501
|
+
c_b_node = rb_path2class("Leptris::XML::Node");
|
|
502
|
+
c_b_result_text = rb_path2class("Leptris::XML::ResultText");
|
|
503
|
+
c_b_result_attr = rb_path2class("Leptris::XML::ResultAttr");
|
|
504
|
+
c_ffi_pointer = rb_path2class("FFI::Pointer");
|
|
505
|
+
id_ptr_new = rb_intern("new");
|
|
506
|
+
rb_gc_register_mark_object(c_b_element);
|
|
507
|
+
rb_gc_register_mark_object(c_b_text);
|
|
508
|
+
rb_gc_register_mark_object(c_b_comment);
|
|
509
|
+
rb_gc_register_mark_object(c_b_cdata);
|
|
510
|
+
rb_gc_register_mark_object(c_b_pi);
|
|
511
|
+
rb_gc_register_mark_object(c_b_node);
|
|
512
|
+
rb_gc_register_mark_object(c_b_result_text);
|
|
513
|
+
rb_gc_register_mark_object(c_b_result_attr);
|
|
514
|
+
rb_gc_register_mark_object(c_ffi_pointer);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
static VALUE binding_klass_for(int kind)
|
|
518
|
+
{
|
|
519
|
+
switch (kind) {
|
|
520
|
+
case 0: return c_b_element;
|
|
521
|
+
case 1: return c_b_text;
|
|
522
|
+
case 2: return c_b_comment;
|
|
523
|
+
case 3: return c_b_cdata;
|
|
524
|
+
case 4: return c_b_pi;
|
|
525
|
+
default: return c_b_node;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
static VALUE bulk_children_impl(VALUE document, VALUE parent_addr,
|
|
530
|
+
int elements_only)
|
|
531
|
+
{
|
|
532
|
+
void **buf;
|
|
533
|
+
int *kinds;
|
|
534
|
+
int count, i;
|
|
535
|
+
VALUE cache, out;
|
|
536
|
+
|
|
537
|
+
count = fetch_children_two_call((void *)(uintptr_t)NUM2ULL(parent_addr),
|
|
538
|
+
&buf, &kinds);
|
|
539
|
+
if (count <= 0) {
|
|
540
|
+
ruby_xfree(buf);
|
|
541
|
+
ruby_xfree(kinds);
|
|
542
|
+
return rb_ary_new2(0);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
cache = rb_funcall(document, id_binding_cache, 0);
|
|
546
|
+
out = rb_ary_new2(elements_only ? 8 : count);
|
|
547
|
+
for (i = 0; i < count; i++) {
|
|
548
|
+
VALUE key, node;
|
|
549
|
+
if (elements_only && kinds[i] != 0)
|
|
550
|
+
continue;
|
|
551
|
+
key = ULL2NUM((uint64_t)(uintptr_t)buf[i]);
|
|
552
|
+
node = rb_hash_aref(cache, key);
|
|
553
|
+
if (NIL_P(node)) {
|
|
554
|
+
VALUE ptr = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
|
|
555
|
+
node = rb_obj_alloc(binding_klass_for(kinds[i]));
|
|
556
|
+
rb_iv_set(node, "@c_ptr", ptr);
|
|
557
|
+
rb_iv_set(node, "@document", document);
|
|
558
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
559
|
+
rb_iv_set(node, "@node_type", INT2FIX(kinds[i]));
|
|
560
|
+
rb_hash_aset(cache, key, node);
|
|
561
|
+
}
|
|
562
|
+
rb_ary_push(out, node);
|
|
563
|
+
}
|
|
564
|
+
ruby_xfree(buf);
|
|
565
|
+
ruby_xfree(kinds);
|
|
566
|
+
return out;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
static VALUE nf_bulk_children(VALUE self, VALUE document, VALUE parent_addr)
|
|
570
|
+
{
|
|
571
|
+
(void)self;
|
|
572
|
+
resolve_binding_classes();
|
|
573
|
+
return bulk_children_impl(document, parent_addr, 0);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
static VALUE nf_bulk_element_children(VALUE self, VALUE document,
|
|
577
|
+
VALUE parent_addr)
|
|
578
|
+
{
|
|
579
|
+
(void)self;
|
|
580
|
+
resolve_binding_classes();
|
|
581
|
+
return bulk_children_impl(document, parent_addr, 1);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/* ---- Bulk XPath result materialization (TODO.perf/03) ----------
|
|
585
|
+
* One C pass over leptris_xpath_result_get_nodes_ex: binding-class
|
|
586
|
+
* dispatch (XPATH_NODE space: 0 element, 1 attribute, 2 text,
|
|
587
|
+
* 3 other), value capture for synthetic text/attribute items
|
|
588
|
+
* while the result handle is alive, identity-cache check/store
|
|
589
|
+
* for elements. Returns the Ruby Array. */
|
|
590
|
+
static VALUE m_native_sentinel; /* module object = fallback marker */
|
|
591
|
+
|
|
592
|
+
static VALUE nf_bulk_xpath(VALUE self, VALUE document, VALUE result_ptr_val)
|
|
593
|
+
{
|
|
594
|
+
void *result = (void *)(uintptr_t)NUM2ULL(result_ptr_val);
|
|
595
|
+
void **buf;
|
|
596
|
+
int *kinds;
|
|
597
|
+
int count, i;
|
|
598
|
+
VALUE cache, out;
|
|
599
|
+
|
|
600
|
+
(void)self;
|
|
601
|
+
resolve_binding_classes();
|
|
602
|
+
size_t scount = f_xp_count(result);
|
|
603
|
+
if (scount == 0)
|
|
604
|
+
return rb_ary_new2(0);
|
|
605
|
+
|
|
606
|
+
buf = ruby_xmalloc(scount * sizeof(void *));
|
|
607
|
+
kinds = ruby_xmalloc(scount * sizeof(int));
|
|
608
|
+
scount = f_xp_nodes_ex(result, buf, kinds, scount);
|
|
609
|
+
count = (int)scount;
|
|
610
|
+
if (count <= 0) {
|
|
611
|
+
ruby_xfree(buf);
|
|
612
|
+
ruby_xfree(kinds);
|
|
613
|
+
return rb_ary_new2(0);
|
|
614
|
+
}
|
|
615
|
+
cache = rb_funcall(document, id_binding_cache, 0);
|
|
616
|
+
out = rb_ary_new2(count);
|
|
617
|
+
for (i = 0; i < count; i++) {
|
|
618
|
+
VALUE node, key;
|
|
619
|
+
const char *s;
|
|
620
|
+
if (buf[i] == NULL)
|
|
621
|
+
continue;
|
|
622
|
+
key = ULL2NUM((uint64_t)(uintptr_t)buf[i]);
|
|
623
|
+
switch (kinds[i]) {
|
|
624
|
+
case 0: /* element */
|
|
625
|
+
node = rb_hash_aref(cache, key);
|
|
626
|
+
if (NIL_P(node)) {
|
|
627
|
+
VALUE ptr = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
|
|
628
|
+
node = rb_obj_alloc(c_b_element);
|
|
629
|
+
rb_iv_set(node, "@c_ptr", ptr);
|
|
630
|
+
rb_iv_set(node, "@document", document);
|
|
631
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
632
|
+
rb_iv_set(node, "@node_type", INT2FIX(0));
|
|
633
|
+
rb_hash_aset(cache, key, node);
|
|
634
|
+
}
|
|
635
|
+
break;
|
|
636
|
+
case 1: /* synthetic attribute */
|
|
637
|
+
{
|
|
638
|
+
const char *name = f_xp_node_name(result, i);
|
|
639
|
+
const char *value = f_xp_node_value(result, i);
|
|
640
|
+
VALUE ptr = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
|
|
641
|
+
node = rb_obj_alloc(c_b_result_attr);
|
|
642
|
+
rb_iv_set(node, "@c_ptr", ptr);
|
|
643
|
+
rb_iv_set(node, "@document", document);
|
|
644
|
+
rb_iv_set(node, "@attr_name",
|
|
645
|
+
name ? rb_utf8_str_new_cstr(name) : Qnil);
|
|
646
|
+
rb_iv_set(node, "@attr_value",
|
|
647
|
+
value ? rb_utf8_str_new_cstr(value) : Qnil);
|
|
648
|
+
}
|
|
649
|
+
break;
|
|
650
|
+
case 2: /* text-kind: synthetic sequence carrier, or a real
|
|
651
|
+
* Text/CDATA node (XPath reports both as TEXT) —
|
|
652
|
+
* dispatch on the node's own type like Node.wrap. */
|
|
653
|
+
{
|
|
654
|
+
int nt = f_node_type(buf[i]);
|
|
655
|
+
if (nt == 8) { /* NODE_SYNTHETIC_TEXT */
|
|
656
|
+
s = f_xp_node_value(result, i);
|
|
657
|
+
node = rb_obj_alloc(c_b_result_text);
|
|
658
|
+
rb_iv_set(node, "@c_ptr",
|
|
659
|
+
rb_funcall(c_ffi_pointer, id_ptr_new, 1, key));
|
|
660
|
+
rb_iv_set(node, "@document", document);
|
|
661
|
+
rb_iv_set(node, "@value",
|
|
662
|
+
s ? rb_utf8_str_new_cstr(s) : Qnil);
|
|
663
|
+
} else {
|
|
664
|
+
VALUE ptr = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
|
|
665
|
+
node = rb_hash_aref(cache, key);
|
|
666
|
+
if (NIL_P(node)) {
|
|
667
|
+
node = rb_obj_alloc(binding_klass_for(nt));
|
|
668
|
+
rb_iv_set(node, "@c_ptr", ptr);
|
|
669
|
+
rb_iv_set(node, "@document", document);
|
|
670
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
671
|
+
rb_iv_set(node, "@node_type", INT2FIX(nt));
|
|
672
|
+
rb_hash_aset(cache, key, node);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
break;
|
|
677
|
+
default: /* other: rare — signal Ruby fallback for this index */
|
|
678
|
+
rb_ary_push(out, m_native_sentinel);
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
rb_ary_push(out, node);
|
|
682
|
+
}
|
|
683
|
+
ruby_xfree(buf);
|
|
684
|
+
ruby_xfree(kinds);
|
|
685
|
+
return out;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/* ---- Ext-bound serialization (TODO.perf/04) ---------------------
|
|
689
|
+
* The whole sized-buffer cycle in C: build SerializeOptions on
|
|
690
|
+
* the stack, call (buf, cap, NULL status, opts), grow if needed,
|
|
691
|
+
* return a UTF-8 String. No FFI marshaling, no Ruby buffer
|
|
692
|
+
* management per to_xml call. */
|
|
693
|
+
struct serialize_opts {
|
|
694
|
+
int indent;
|
|
695
|
+
int xml_declaration;
|
|
696
|
+
const char *encoding;
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
/* Grow-only scratch for large serializations. Safe under the GVL:
|
|
700
|
+
* synchronous C calls hold it, so no two Ruby threads run this
|
|
701
|
+
* concurrently. */
|
|
702
|
+
static char *ser_buf;
|
|
703
|
+
static size_t ser_cap;
|
|
704
|
+
|
|
705
|
+
static VALUE fast_serialize(serialize_into_fn fn, void *node,
|
|
706
|
+
int indent, int xml_declaration)
|
|
707
|
+
{
|
|
708
|
+
struct serialize_opts opts;
|
|
709
|
+
char stack_buf[4096];
|
|
710
|
+
size_t needed;
|
|
711
|
+
|
|
712
|
+
char *probe;
|
|
713
|
+
size_t probe_cap;
|
|
714
|
+
|
|
715
|
+
opts.indent = indent;
|
|
716
|
+
opts.xml_declaration = xml_declaration;
|
|
717
|
+
opts.encoding = NULL;
|
|
718
|
+
/* Probe the largest buffer we own: a big scratch from a prior
|
|
719
|
+
* call usually fits (single serialization); cold calls start
|
|
720
|
+
* on the stack buffer. */
|
|
721
|
+
if (ser_cap > sizeof(stack_buf)) {
|
|
722
|
+
probe = ser_buf;
|
|
723
|
+
probe_cap = ser_cap;
|
|
724
|
+
} else {
|
|
725
|
+
probe = stack_buf;
|
|
726
|
+
probe_cap = sizeof(stack_buf);
|
|
727
|
+
}
|
|
728
|
+
needed = fn(node, probe, probe_cap, NULL, &opts);
|
|
729
|
+
if (needed == 0)
|
|
730
|
+
return rb_utf8_str_new_cstr("");
|
|
731
|
+
if (needed <= probe_cap)
|
|
732
|
+
return rb_utf8_str_new(probe, needed - 1);
|
|
733
|
+
if (needed > ser_cap) {
|
|
734
|
+
ser_buf = ruby_xrealloc(ser_buf, needed);
|
|
735
|
+
ser_cap = needed;
|
|
736
|
+
}
|
|
737
|
+
needed = fn(node, ser_buf, ser_cap, NULL, &opts);
|
|
738
|
+
return needed > 0 ? rb_utf8_str_new(ser_buf, needed - 1)
|
|
739
|
+
: rb_utf8_str_new_cstr("");
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
static VALUE nf_fast_document_xml(VALUE self, VALUE addr,
|
|
743
|
+
VALUE indent, VALUE decl)
|
|
744
|
+
{
|
|
745
|
+
(void)self;
|
|
746
|
+
return fast_serialize(f_doc_serialize,
|
|
747
|
+
(void *)(uintptr_t)NUM2ULL(addr),
|
|
748
|
+
NUM2INT(indent), RTEST(decl) ? 1 : 0);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
static VALUE nf_fast_element_xml(VALUE self, VALUE addr,
|
|
752
|
+
VALUE indent, VALUE decl)
|
|
753
|
+
{
|
|
754
|
+
(void)self;
|
|
755
|
+
return fast_serialize(f_elem_serialize,
|
|
756
|
+
(void *)(uintptr_t)NUM2ULL(addr),
|
|
757
|
+
NUM2INT(indent), RTEST(decl) ? 1 : 0);
|
|
758
|
+
}
|
|
759
|
+
|
|
362
760
|
/* ---- Address-based fast readers (TODO.perf/01) -----------------
|
|
363
761
|
* The DEFAULT binding classes call these when the bundle is
|
|
364
762
|
* loaded: one C-API dispatch + rb_utf8_str_new_cstr — no FFI
|
|
@@ -432,6 +830,7 @@ void Init_native(void)
|
|
|
432
830
|
rb_undef_alloc_func(c_native_node);
|
|
433
831
|
|
|
434
832
|
id_wrapper_cache = rb_intern("native_cache");
|
|
833
|
+
id_binding_cache = rb_intern("wrapper_cache");
|
|
435
834
|
|
|
436
835
|
rb_define_singleton_method(c_native_node, "from", nn_from, 2);
|
|
437
836
|
rb_define_singleton_method(c_native_node, "create_element", nn_create_element, 2);
|
|
@@ -441,6 +840,9 @@ void Init_native(void)
|
|
|
441
840
|
rb_define_method(c_native_node, "append_child", nn_append_child, 1);
|
|
442
841
|
rb_define_method(c_native_node, "add_child", nn_append_child, 1);
|
|
443
842
|
rb_define_method(c_native_node, "address", nn_address, 0);
|
|
843
|
+
rb_define_method(c_native_node, "attributes", nn_attributes, 0);
|
|
844
|
+
rb_define_method(c_native_node, "line", nn_line, 0);
|
|
845
|
+
rb_define_method(c_native_node, "byte_offset", nn_byte_offset, 0);
|
|
444
846
|
rb_define_method(c_native_node, "name", nn_name, 0);
|
|
445
847
|
rb_define_method(c_native_node, "content", nn_content, 0);
|
|
446
848
|
rb_define_method(c_native_node, "attribute", nn_attribute, 1);
|
|
@@ -454,8 +856,17 @@ void Init_native(void)
|
|
|
454
856
|
leptris_native_resolve_rb, 1);
|
|
455
857
|
|
|
456
858
|
m_native = rb_define_module_under(m_xml, "Native");
|
|
859
|
+
m_native_sentinel = m_native;
|
|
457
860
|
rb_define_module_function(m_native, "fast_name", nf_name, 1);
|
|
458
861
|
rb_define_module_function(m_native, "fast_element_text", nf_element_text, 1);
|
|
459
862
|
rb_define_module_function(m_native, "fast_attribute", nf_attribute, 2);
|
|
460
863
|
rb_define_module_function(m_native, "fast_prefix", nf_prefix, 1);
|
|
864
|
+
rb_define_module_function(m_native, "bulk_children", nf_bulk_children, 2);
|
|
865
|
+
rb_define_module_function(m_native, "bulk_element_children",
|
|
866
|
+
nf_bulk_element_children, 2);
|
|
867
|
+
rb_define_module_function(m_native, "bulk_xpath", nf_bulk_xpath, 2);
|
|
868
|
+
rb_define_module_function(m_native, "fast_document_xml",
|
|
869
|
+
nf_fast_document_xml, 3);
|
|
870
|
+
rb_define_module_function(m_native, "fast_element_xml",
|
|
871
|
+
nf_fast_element_xml, 3);
|
|
461
872
|
}
|
data/lib/leptris/version.rb
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
# installs without the compiled bundle (ruby-platform gem) raise
|
|
15
15
|
# a clear LoadError from the require.
|
|
16
16
|
|
|
17
|
+
require "ffi" unless defined?(::FFI::Library)
|
|
18
|
+
|
|
17
19
|
begin
|
|
18
20
|
# The compiled bundle (feature "leptris/xml/native" resolves
|
|
19
21
|
# native.bundle). A sanctioned require exception: a .bundle
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -52,7 +52,12 @@ class Leptris::XML::Node
|
|
|
52
52
|
cache = document.wrapper_cache
|
|
53
53
|
address = c_ptr.address
|
|
54
54
|
if (cached = cache[address])
|
|
55
|
-
|
|
55
|
+
# The shared cache also holds NativeNodes (opt-in layer).
|
|
56
|
+
# Only return a hit when it is a binding Node subclass —
|
|
57
|
+
# otherwise fall through and construct the binding wrapper
|
|
58
|
+
# (both views of the same C node can coexist; the native
|
|
59
|
+
# entry stays under its own key-path via NativeNode.from).
|
|
60
|
+
return cached if cached.is_a?(Leptris::XML::Node)
|
|
56
61
|
end
|
|
57
62
|
end
|
|
58
63
|
|
|
@@ -125,6 +130,13 @@ class Leptris::XML::Node
|
|
|
125
130
|
|
|
126
131
|
def element?; type == Leptris::XML::FFI::NODE_ELEMENT; end
|
|
127
132
|
|
|
133
|
+
# The ext bulk path applies to document-owned trees only:
|
|
134
|
+
# scope-owned (iterparse) elements ride the IterationScope seam.
|
|
135
|
+
def native_fast_children?
|
|
136
|
+
defined?(Leptris::XML::NATIVE_FAST) &&
|
|
137
|
+
!@document.is_a?(Leptris::XML::IterationScope)
|
|
138
|
+
end
|
|
139
|
+
|
|
128
140
|
# Content-defined 64-bit Merkle digest of this subtree
|
|
129
141
|
# (libleptris 1.9.99, #869): element name/prefix/resolved
|
|
130
142
|
# namespace URI, attributes sorted and first-wins-deduplicated,
|
|
@@ -228,6 +240,18 @@ class Leptris::XML::Node
|
|
|
228
240
|
# (leptris_node_children_ex), so no per-child get_type.
|
|
229
241
|
return @children if memo_hit?(@children_version)
|
|
230
242
|
ensure_alive!
|
|
243
|
+
if native_fast_children?
|
|
244
|
+
# TODO.perf/01 tail: one C pass constructs every binding
|
|
245
|
+
# wrapper (class dispatch + ivars + identity cache) — the
|
|
246
|
+
# per-child Ruby wrap frames disappear.
|
|
247
|
+
nodes = Leptris::XML::Native.bulk_children(@document, @c_ptr.address)
|
|
248
|
+
result = Leptris::XML::NodeSet.new(@document, nodes)
|
|
249
|
+
if @document
|
|
250
|
+
@children = result
|
|
251
|
+
@children_version = @document.version
|
|
252
|
+
end
|
|
253
|
+
return result
|
|
254
|
+
end
|
|
231
255
|
parent = as_element_or_self
|
|
232
256
|
pointers, kinds = Leptris::XML::FFI.fetch_children(@c_ptr)
|
|
233
257
|
nodes = Array.new(pointers.size) do |i|
|
|
@@ -303,7 +327,9 @@ class Leptris::XML::Node
|
|
|
303
327
|
# children are never wrapped (nor their get_type paid — the
|
|
304
328
|
# ELEMENT hint rides along). Other nodes keep the filter.
|
|
305
329
|
result =
|
|
306
|
-
if
|
|
330
|
+
if native_fast_children?
|
|
331
|
+
Leptris::XML::Native.bulk_element_children(@document, @c_ptr.address)
|
|
332
|
+
elsif is_a?(Leptris::XML::Element)
|
|
307
333
|
parent = as_element_or_self
|
|
308
334
|
Leptris::XML::FFI.fetch_element_children(@c_ptr).map do |ptr|
|
|
309
335
|
Leptris::XML::Node.wrap(ptr, @document, parent: parent,
|
data/lib/leptris/xml/node_set.rb
CHANGED
|
@@ -71,6 +71,11 @@ class Leptris::XML::NodeSet
|
|
|
71
71
|
|
|
72
72
|
# Sequence/map/array items ride synthetic text nodes whose value
|
|
73
73
|
# only the live result handle can serve — capture it now.
|
|
74
|
+
def native_fast?
|
|
75
|
+
defined?(Leptris::XML::NATIVE_FAST)
|
|
76
|
+
end
|
|
77
|
+
private :native_fast?
|
|
78
|
+
|
|
74
79
|
def text_item_value(kind, index)
|
|
75
80
|
case kind
|
|
76
81
|
when Leptris::XML::FFI::XPATH_NODE_TEXT
|
|
@@ -95,7 +100,37 @@ class Leptris::XML::NodeSet
|
|
|
95
100
|
unless @array
|
|
96
101
|
return self unless @result_ptr
|
|
97
102
|
n = length
|
|
98
|
-
if n > 0
|
|
103
|
+
if n > 0 && native_fast?
|
|
104
|
+
# TODO.perf/03: one C pass materializes the whole result —
|
|
105
|
+
# class dispatch, synthetic text/attr value capture, and
|
|
106
|
+
# element identity-cache check/store in the ext. Qundef
|
|
107
|
+
# slots (rare non-element/attr/text kinds) and the >512
|
|
108
|
+
# remainder fall back to the per-index path below.
|
|
109
|
+
fast = Leptris::XML::Native.bulk_xpath(
|
|
110
|
+
@document, @result_ptr.to_i)
|
|
111
|
+
nodes = []
|
|
112
|
+
fast.each_with_index do |node, i|
|
|
113
|
+
if node.equal?(Leptris::XML::Native)
|
|
114
|
+
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, i)
|
|
115
|
+
next if ptr.null?
|
|
116
|
+
nodes << Leptris::XML::Node.wrap(
|
|
117
|
+
ptr, @document,
|
|
118
|
+
result_value: text_item_value(
|
|
119
|
+
Leptris::XML::FFI.leptris_xpath_result_node_kind(@result_ptr, i), i))
|
|
120
|
+
else
|
|
121
|
+
nodes << node
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
(fast.length...n).each do |i|
|
|
125
|
+
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(@result_ptr, i)
|
|
126
|
+
next if ptr.null?
|
|
127
|
+
nodes << Leptris::XML::Node.wrap(
|
|
128
|
+
ptr, @document,
|
|
129
|
+
result_value: text_item_value(
|
|
130
|
+
Leptris::XML::FFI.leptris_xpath_result_node_kind(@result_ptr, i), i))
|
|
131
|
+
end
|
|
132
|
+
@array = nodes
|
|
133
|
+
elsif n > 0
|
|
99
134
|
# Batch-fetch all node pointers through the FFI seam
|
|
100
135
|
# (leptris_xpath_result_get_nodes_ex copies every node kind,
|
|
101
136
|
# not just elements); the batch accessor under-copies
|
|
@@ -32,6 +32,18 @@ module Leptris::XML::Serialization
|
|
|
32
32
|
# cycle lives at the FFI seam (FFI.serialize_into_string); this
|
|
33
33
|
# module owns only options selection and construction.
|
|
34
34
|
def self.to_xml(ffi_function, c_ptr, indent: 0, no_decl: false, encoding: nil)
|
|
35
|
+
# TODO.perf/04: the common shape (no encoding) runs the whole
|
|
36
|
+
# sized-buffer cycle in the ext — one C dispatch, no options
|
|
37
|
+
# marshaling, no Ruby buffer management.
|
|
38
|
+
if defined?(Leptris::XML::NATIVE_FAST) && encoding.nil?
|
|
39
|
+
decl = !no_decl
|
|
40
|
+
return Leptris::XML::Native.fast_element_xml(
|
|
41
|
+
c_ptr.address, indent.to_i, decl) if
|
|
42
|
+
ffi_function == ELEMENT_SERIALIZE_INTO
|
|
43
|
+
return Leptris::XML::Native.fast_document_xml(
|
|
44
|
+
c_ptr.address, indent.to_i, decl) if
|
|
45
|
+
ffi_function == DOCUMENT_SERIALIZE_INTO
|
|
46
|
+
end
|
|
35
47
|
opts =
|
|
36
48
|
if indent.to_i.zero? && !no_decl && encoding.nil?
|
|
37
49
|
DEFAULT_OPTIONS
|
|
@@ -47,6 +59,8 @@ module Leptris::XML::Serialization
|
|
|
47
59
|
# loop: no kwargs, no options rebuild, no method allocation —
|
|
48
60
|
# one FFI fast-path dispatch per child.
|
|
49
61
|
def self.element_xml_default(c_ptr)
|
|
62
|
+
return Leptris::XML::Native.fast_element_xml(c_ptr.address, 0, true) if
|
|
63
|
+
defined?(Leptris::XML::NATIVE_FAST)
|
|
50
64
|
Leptris::XML::FFI.serialize_into_string(
|
|
51
65
|
ELEMENT_SERIALIZE_INTO, c_ptr, DEFAULT_OPTIONS.pointer)
|
|
52
66
|
end
|