leptris 1.9.163.6 → 1.9.163.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/TODO.perf/16-at-xpath-single-result-seam.md +38 -0
- data/TODO.perf/17-precomputed-fast-path-flags.md +35 -0
- data/TODO.perf/18-inner-html-one-c-pass.md +32 -0
- data/ext/leptris/native/native.c +252 -60
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/element.rb +6 -1
- data/lib/leptris/xml/node.rb +7 -2
- data/lib/leptris/xml/searchable.rb +8 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a191c85c5149a0f263ca7d7ee5f8ec63b58570f2f56ecd9c3cfc6333a974eff0
|
|
4
|
+
data.tar.gz: 7f126b1b774331af49faaea1b2dadffa7e8a170a47b917a9c3ea03d89377bbee
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 47c7ae31a849c74b9dc651439de4b55d23a038098ed7b6dbfc8ff6ec2b740d45dfb39bb3ee42ac0b0d8a7ef467f03807e77c5cba70dfa5246a957f24c901726e
|
|
7
|
+
data.tar.gz: 8530bd56c3a373dd9bff555ed3fae96212db9792de187be21a002081be24e961601816016a3dc899482b61527424b818910cfeb85288156e95f5cd96fa7e4d8a
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ 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.7] - 2026-09-15
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **at_xpath single-result seam (TODO.perf/16)**: nodeset entry-0
|
|
13
|
+
materialization (identity cache, kind dispatch, attribute/text
|
|
14
|
+
value capture) plus the result free run as one C dispatch —
|
|
15
|
+
repeat `at_xpath` measured 3.7µs (pre-cache baseline) → ~1.3µs
|
|
16
|
+
across TODO.perf/15+16 (2.8x). Non-nodeset results keep the
|
|
17
|
+
exact Ruby scalar path.
|
|
18
|
+
- **inner_html in one C pass (TODO.perf/18)**: the child chain,
|
|
19
|
+
per-kind serialization (elements through the engine's
|
|
20
|
+
serialize_into, text escaped with the binding's entity set,
|
|
21
|
+
CDATA/comments/PIs wrapped), and the buffer growth all in C —
|
|
22
|
+
byte-identical to the Ruby loop (spec-pinned per kind and past
|
|
23
|
+
the growth floor); ~160ns/child vs ~500-800ns/child.
|
|
24
|
+
- **Precomputed fast-path flags (TODO.perf/17)**: `@native_fast`
|
|
25
|
+
at construction (Ruby and every C site) replaces the
|
|
26
|
+
`defined?`+`scope_owned?` method chain at every gate.
|
|
27
|
+
`Element#[]=` reads attribute names via StringValue+RSTRING_PTR
|
|
28
|
+
(values keep the embedded-NUL raise); the row measured
|
|
29
|
+
454ns → 236ns (~Nokogiri parity in the same run).
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- `Native.at_xpath_first` / `Native.fast_inner_xml` module faces;
|
|
34
|
+
`materialize_xp_entry` shared by the bulk and single-result
|
|
35
|
+
paths.
|
|
36
|
+
|
|
8
37
|
## [1.9.163.6] - 2026-09-15
|
|
9
38
|
|
|
10
39
|
### Changed
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# 16 — C-bound at_xpath single-result seam
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.7)
|
|
4
|
+
|
|
5
|
+
at_xpath is the hottest query shape (adapters picking one node),
|
|
6
|
+
and its result path is five FFI calls per hit: result_type,
|
|
7
|
+
get_node(0), node_kind(0), Node.wrap (pointer + construct), and
|
|
8
|
+
result_free — plus the Ruby case dispatch between them. The bulk
|
|
9
|
+
machinery from TODO.perf/03 already knows how to materialize a
|
|
10
|
+
result entry as a binding wrapper in C (identity cache, kind
|
|
11
|
+
dispatch, attribute/text value capture).
|
|
12
|
+
|
|
13
|
+
Add Native.at_xpath_first(document, result_ptr): one C dispatch
|
|
14
|
+
checks the result type; nodesets materialize entry 0 through the
|
|
15
|
+
bulk machinery (get_nodes_ex with capacity 1 carries pointer +
|
|
16
|
+
kind in one call), free the result handle, and return the binding
|
|
17
|
+
node (or Qnil on empty); non-nodeset results return Qundef so the
|
|
18
|
+
existing Ruby scalar path keeps its exact semantics. Searchable's
|
|
19
|
+
wrap_xpath_first_result routes through the face when native is
|
|
20
|
+
enabled.
|
|
21
|
+
|
|
22
|
+
Gates: at_xpath parity for elements, attributes (ResultAttr name/
|
|
23
|
+
value capture), text-kind results, empty results, and scalars;
|
|
24
|
+
the result handle always freed exactly once.
|
|
25
|
+
|
|
26
|
+
## Outcome (1.9.163.7)
|
|
27
|
+
|
|
28
|
+
Native.at_xpath_first(document, result_ptr): one dispatch — type
|
|
29
|
+
check, entry-0 materialization through the extracted
|
|
30
|
+
materialize_xp_entry (shared with the TODO.perf/03 bulk loop:
|
|
31
|
+
identity cache, kind dispatch, attribute/text value capture),
|
|
32
|
+
result free. Non-nodeset results return the Native module object
|
|
33
|
+
(Qundef must never cross into Ruby) and keep the exact Ruby
|
|
34
|
+
scalar path. Measured: repeat at_xpath 3.7µs baseline -> 1.9µs
|
|
35
|
+
(TODO.perf/15) -> ~1.3µs with the seam (2.8x total, load 8-30;
|
|
36
|
+
the same-run Nokogiri comparison on this box is anomalous and not
|
|
37
|
+
reported). Specs pin element identity, attribute capture, text
|
|
38
|
+
kinds, nil misses, scalars, and xpath().first agreement.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# 17 — Precomputed fast-path flags + set_attribute tightening
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.7)
|
|
4
|
+
|
|
5
|
+
The native guards on hot paths are method chains:
|
|
6
|
+
native_fast_children? resolves as defined?(NATIVE_FAST) plus
|
|
7
|
+
scope_owned? (another method) — paid per mutation ([]=, add_child,
|
|
8
|
+
the insert family) and per cold read fill. @structure_memoizable
|
|
9
|
+
(TODO.perf/13) already carries "document-owned and alive" as an
|
|
10
|
+
ivar; NATIVE_FAST availability is fixed at load time before any
|
|
11
|
+
node exists, so the conjunction is construct-time constant.
|
|
12
|
+
|
|
13
|
+
Add @native_fast to every wrapper construction (Ruby initialize
|
|
14
|
+
and every C construction site): one ivar read replaces the chain
|
|
15
|
+
at every gate. Fold in the set_attribute shaves: the C face reads
|
|
16
|
+
attribute NAMES via StringValue+RSTRING_PTR (names cannot contain
|
|
17
|
+
NULs — same rationale as nn_attribute); VALUES keep StringValueCStr
|
|
18
|
+
so an embedded-NUL value still raises rather than truncating.
|
|
19
|
+
|
|
20
|
+
Gates: suite green in both modes (the FFI fallback semantics must
|
|
21
|
+
be unchanged for nodes constructed before a late native require —
|
|
22
|
+
worst case they stay on the FFI path); set_attribute row improves.
|
|
23
|
+
|
|
24
|
+
## Outcome (1.9.163.7)
|
|
25
|
+
|
|
26
|
+
@native_fast computed once at construction (Ruby initialize and
|
|
27
|
+
every C construction site — the bundle running implies the
|
|
28
|
+
constant), replacing the defined?+scope_owned? method chain at
|
|
29
|
+
every gate (native_fast?/native_fast_children? are ivar readers
|
|
30
|
+
now). []= additionally reads attribute names via
|
|
31
|
+
StringValue+RSTRING_PTR (values keep StringValueCStr — an
|
|
32
|
+
embedded-NUL value still raises; spec-pinned). Measured (load
|
|
33
|
+
17-31): binding []= 454ns mandate baseline -> 236ns (~2x on the
|
|
34
|
+
row; 0.88x vs Nokogiri's 209ns in the same run — effectively
|
|
35
|
+
parity).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# 18 — inner_html in one C pass
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.163.7)
|
|
4
|
+
|
|
5
|
+
Element#inner_html walks children (bulk since TODO.perf/01) and
|
|
6
|
+
then serializes EACH child from Ruby: element children get one
|
|
7
|
+
ext dispatch each, text/comment/PI children are string-built in
|
|
8
|
+
Ruby and joined. An N-child element pays N dispatches plus the
|
|
9
|
+
case ladder plus the join.
|
|
10
|
+
|
|
11
|
+
Bind the whole pass in C: iterate the child chain once, appending
|
|
12
|
+
each child's serialization into one growable buffer — elements
|
|
13
|
+
through leptris_element_serialize_into (the fast_serialize
|
|
14
|
+
protocol), text nodes XML-escaped (& < > — the binding's
|
|
15
|
+
escape_text set), CDATA wrapped raw, comments and PIs wrapped.
|
|
16
|
+
One dispatch, one string mint, no Ruby per child.
|
|
17
|
+
|
|
18
|
+
Gates: inner_html parity for every child kind (elements, text
|
|
19
|
+
with entities, CDATA, comments, PIs, mixed), empty elements,
|
|
20
|
+
large children past the 4096-byte stack probe (growth path).
|
|
21
|
+
|
|
22
|
+
## Outcome (1.9.163.7)
|
|
23
|
+
|
|
24
|
+
Native.fast_inner_xml: one pass over the child chain into a
|
|
25
|
+
grow-only scratch — elements through leptris_element_serialize_into
|
|
26
|
+
(needed-1 protocol, grow+retry past capacity), text escaped with
|
|
27
|
+
the binding's entity set (& < > \r), CDATA raw-wrapped, comments
|
|
28
|
+
and PIs wrapped (PI data leading-whitespace-stripped, read_pi_data
|
|
29
|
+
parity). Byte-identical to the Ruby loop for every child kind and
|
|
30
|
+
past the 4KB growth floor (specs). Measured (load 17-31): 30-child
|
|
31
|
+
inner_html ~4.9µs (~160ns/child vs the loop's ~500-800ns/child
|
|
32
|
+
estimate).
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -76,6 +76,15 @@ static create_child_fn f_create_child;
|
|
|
76
76
|
static append_child_fn f_append_child;
|
|
77
77
|
static append_child_fn f_prepend_child, f_insert_after, f_insert_before;
|
|
78
78
|
static set_attr_fn f_set_attr;
|
|
79
|
+
typedef int (*xp_type_fn)(void *);
|
|
80
|
+
typedef void (*xp_free_fn)(void *);
|
|
81
|
+
typedef void *(*first_child_fn)(void *);
|
|
82
|
+
typedef const char *(*node_str_fn)(void *);
|
|
83
|
+
static xp_type_fn f_xp_type;
|
|
84
|
+
static xp_free_fn f_xp_free;
|
|
85
|
+
static first_child_fn f_first_child;
|
|
86
|
+
static node_str_fn f_comment_content, f_cdata_content, f_pi_target,
|
|
87
|
+
f_pi_data;
|
|
79
88
|
static set_root_fn f_set_root;
|
|
80
89
|
static doc_free_fn f_doc_free;
|
|
81
90
|
|
|
@@ -176,6 +185,13 @@ static void resolve_symbols(const char *lib_path)
|
|
|
176
185
|
f_insert_after = (append_child_fn)lib_sym(h, "leptris_element_insert_after");
|
|
177
186
|
f_insert_before = (append_child_fn)lib_sym(h, "leptris_element_insert_before");
|
|
178
187
|
f_set_attr = (set_attr_fn)lib_sym(h, "leptris_element_set_attribute");
|
|
188
|
+
f_xp_type = (xp_type_fn)lib_sym(h, "leptris_xpath_result_type");
|
|
189
|
+
f_xp_free = (xp_free_fn)lib_sym(h, "leptris_xpath_result_free");
|
|
190
|
+
f_first_child = (first_child_fn)lib_sym(h, "leptris_node_first_child");
|
|
191
|
+
f_comment_content = (node_str_fn)lib_sym(h, "leptris_comment_node_get_content");
|
|
192
|
+
f_cdata_content = (node_str_fn)lib_sym(h, "leptris_cdata_node_get_content");
|
|
193
|
+
f_pi_target = (node_str_fn)lib_sym(h, "leptris_pi_node_get_target");
|
|
194
|
+
f_pi_data = (node_str_fn)lib_sym(h, "leptris_pi_node_get_data");
|
|
179
195
|
f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
|
|
180
196
|
f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
|
|
181
197
|
if (!f_elem_name || !f_text_content || !f_attr ||
|
|
@@ -183,6 +199,9 @@ static void resolve_symbols(const char *lib_path)
|
|
|
183
199
|
!f_element_text || !f_doc_create || !f_elem_create || !f_text_create ||
|
|
184
200
|
!f_create_child || !f_append_child || !f_prepend_child ||
|
|
185
201
|
!f_insert_after || !f_insert_before || !f_set_attr || !f_set_root ||
|
|
202
|
+
!f_xp_type || !f_xp_free || !f_first_child ||
|
|
203
|
+
!f_comment_content || !f_cdata_content || !f_pi_target ||
|
|
204
|
+
!f_pi_data ||
|
|
186
205
|
!f_doc_free ||
|
|
187
206
|
!f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
|
|
188
207
|
!f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
|
|
@@ -603,7 +622,11 @@ static VALUE nf_set_binding_attribute(VALUE self, VALUE document,
|
|
|
603
622
|
node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
604
623
|
rb_ivar_set(document, id_iv_version,
|
|
605
624
|
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
606
|
-
|
|
625
|
+
/* Attribute names cannot contain NULs (StringValueCStr's
|
|
626
|
+
* memchr is a wasted scan); VALUES keep the scan so an
|
|
627
|
+
* embedded-NUL value raises rather than truncates. */
|
|
628
|
+
st = f_set_attr(node, RSTRING_PTR(StringValue(name)),
|
|
629
|
+
StringValueCStr(value));
|
|
607
630
|
return INT2FIX(st);
|
|
608
631
|
}
|
|
609
632
|
|
|
@@ -853,6 +876,7 @@ static VALUE bulk_children_impl(VALUE document, VALUE parent_addr,
|
|
|
853
876
|
rb_iv_set(node, "@document", document);
|
|
854
877
|
rb_iv_set(node, "@parent", Qnil);
|
|
855
878
|
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
879
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
856
880
|
rb_iv_set(node, "@node_type", INT2FIX(kinds[i]));
|
|
857
881
|
rb_hash_aset(cache, key, node);
|
|
858
882
|
}
|
|
@@ -885,6 +909,9 @@ static VALUE nf_bulk_element_children(VALUE self, VALUE document,
|
|
|
885
909
|
* while the result handle is alive, identity-cache check/store
|
|
886
910
|
* for elements. Returns the Ruby Array. */
|
|
887
911
|
static VALUE m_native_sentinel; /* module object = fallback marker */
|
|
912
|
+
static VALUE materialize_xp_entry(VALUE document, VALUE cache,
|
|
913
|
+
void *result, void *ptr, int kind,
|
|
914
|
+
int idx);
|
|
888
915
|
|
|
889
916
|
static VALUE nf_bulk_xpath(VALUE self, VALUE document, VALUE result_ptr_val)
|
|
890
917
|
{
|
|
@@ -912,76 +939,124 @@ static VALUE nf_bulk_xpath(VALUE self, VALUE document, VALUE result_ptr_val)
|
|
|
912
939
|
cache = binding_cache_of(document);
|
|
913
940
|
out = rb_ary_new2(count);
|
|
914
941
|
for (i = 0; i < count; i++) {
|
|
915
|
-
VALUE node, key;
|
|
916
|
-
const char *s;
|
|
917
942
|
if (buf[i] == NULL)
|
|
918
943
|
continue;
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
944
|
+
rb_ary_push(out, materialize_xp_entry(document, cache, result,
|
|
945
|
+
buf[i], kinds[i], i));
|
|
946
|
+
}
|
|
947
|
+
ruby_xfree(buf);
|
|
948
|
+
ruby_xfree(kinds);
|
|
949
|
+
return out;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/* Materializes ONE result entry as a binding wrapper (or the
|
|
953
|
+
* module sentinel for the rare kinds the bulk path defers to
|
|
954
|
+
* Ruby). Shared by the bulk materializer (TODO.perf/03) and the
|
|
955
|
+
* at_xpath single-result seam (TODO.perf/16). */
|
|
956
|
+
static VALUE materialize_xp_entry(VALUE document, VALUE cache,
|
|
957
|
+
void *result, void *ptr, int kind,
|
|
958
|
+
int idx)
|
|
959
|
+
{
|
|
960
|
+
VALUE node, key;
|
|
961
|
+
const char *s;
|
|
962
|
+
|
|
963
|
+
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
964
|
+
switch (kind) {
|
|
965
|
+
case 0: /* element */
|
|
966
|
+
node = rb_hash_aref(cache, key);
|
|
967
|
+
if (NIL_P(node)) {
|
|
968
|
+
VALUE p = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
|
|
969
|
+
node = rb_obj_alloc(c_b_element);
|
|
970
|
+
rb_iv_set(node, "@c_ptr", p);
|
|
971
|
+
rb_iv_set(node, "@document", document);
|
|
972
|
+
rb_iv_set(node, "@parent", Qnil);
|
|
973
|
+
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
974
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
975
|
+
rb_iv_set(node, "@node_type", INT2FIX(0));
|
|
976
|
+
rb_hash_aset(cache, key, node);
|
|
977
|
+
}
|
|
978
|
+
return node;
|
|
979
|
+
case 1: /* synthetic attribute */
|
|
980
|
+
{
|
|
981
|
+
const char *name = f_xp_node_name(result, idx);
|
|
982
|
+
const char *value = f_xp_node_value(result, idx);
|
|
983
|
+
VALUE p = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
|
|
984
|
+
node = rb_obj_alloc(c_b_result_attr);
|
|
985
|
+
rb_iv_set(node, "@c_ptr", p);
|
|
986
|
+
rb_iv_set(node, "@document", document);
|
|
987
|
+
rb_iv_set(node, "@attr_name",
|
|
988
|
+
name ? rb_utf8_str_new_cstr(name) : Qnil);
|
|
989
|
+
rb_iv_set(node, "@attr_value",
|
|
990
|
+
value ? rb_utf8_str_new_cstr(value) : Qnil);
|
|
991
|
+
}
|
|
992
|
+
return node;
|
|
993
|
+
case 2: /* text-kind: synthetic sequence carrier, or a real
|
|
994
|
+
* Text/CDATA node (XPath reports both as TEXT) —
|
|
995
|
+
* dispatch on the node's own type like Node.wrap. */
|
|
996
|
+
{
|
|
997
|
+
int nt = f_node_type(ptr);
|
|
998
|
+
if (nt == 8) { /* NODE_SYNTHETIC_TEXT */
|
|
999
|
+
s = f_xp_node_value(result, idx);
|
|
1000
|
+
node = rb_obj_alloc(c_b_result_text);
|
|
1001
|
+
rb_iv_set(node, "@c_ptr",
|
|
1002
|
+
rb_funcall(c_ffi_pointer, id_ptr_new, 1, key));
|
|
1003
|
+
rb_iv_set(node, "@document", document);
|
|
1004
|
+
rb_iv_set(node, "@value",
|
|
1005
|
+
s ? rb_utf8_str_new_cstr(s) : Qnil);
|
|
1006
|
+
return node;
|
|
1007
|
+
}
|
|
922
1008
|
node = rb_hash_aref(cache, key);
|
|
923
1009
|
if (NIL_P(node)) {
|
|
924
|
-
VALUE
|
|
925
|
-
node = rb_obj_alloc(
|
|
926
|
-
rb_iv_set(node, "@c_ptr",
|
|
1010
|
+
VALUE p = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
|
|
1011
|
+
node = rb_obj_alloc(binding_klass_for(nt));
|
|
1012
|
+
rb_iv_set(node, "@c_ptr", p);
|
|
927
1013
|
rb_iv_set(node, "@document", document);
|
|
928
1014
|
rb_iv_set(node, "@parent", Qnil);
|
|
929
1015
|
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
930
|
-
rb_iv_set(node, "@
|
|
1016
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
1017
|
+
rb_iv_set(node, "@node_type", INT2FIX(nt));
|
|
931
1018
|
rb_hash_aset(cache, key, node);
|
|
932
1019
|
}
|
|
933
|
-
break;
|
|
934
|
-
case 1: /* synthetic attribute */
|
|
935
|
-
{
|
|
936
|
-
const char *name = f_xp_node_name(result, i);
|
|
937
|
-
const char *value = f_xp_node_value(result, i);
|
|
938
|
-
VALUE ptr = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
|
|
939
|
-
node = rb_obj_alloc(c_b_result_attr);
|
|
940
|
-
rb_iv_set(node, "@c_ptr", ptr);
|
|
941
|
-
rb_iv_set(node, "@document", document);
|
|
942
|
-
rb_iv_set(node, "@attr_name",
|
|
943
|
-
name ? rb_utf8_str_new_cstr(name) : Qnil);
|
|
944
|
-
rb_iv_set(node, "@attr_value",
|
|
945
|
-
value ? rb_utf8_str_new_cstr(value) : Qnil);
|
|
946
|
-
}
|
|
947
|
-
break;
|
|
948
|
-
case 2: /* text-kind: synthetic sequence carrier, or a real
|
|
949
|
-
* Text/CDATA node (XPath reports both as TEXT) —
|
|
950
|
-
* dispatch on the node's own type like Node.wrap. */
|
|
951
|
-
{
|
|
952
|
-
int nt = f_node_type(buf[i]);
|
|
953
|
-
if (nt == 8) { /* NODE_SYNTHETIC_TEXT */
|
|
954
|
-
s = f_xp_node_value(result, i);
|
|
955
|
-
node = rb_obj_alloc(c_b_result_text);
|
|
956
|
-
rb_iv_set(node, "@c_ptr",
|
|
957
|
-
rb_funcall(c_ffi_pointer, id_ptr_new, 1, key));
|
|
958
|
-
rb_iv_set(node, "@document", document);
|
|
959
|
-
rb_iv_set(node, "@value",
|
|
960
|
-
s ? rb_utf8_str_new_cstr(s) : Qnil);
|
|
961
|
-
} else {
|
|
962
|
-
VALUE ptr = rb_funcall(c_ffi_pointer, id_ptr_new, 1, key);
|
|
963
|
-
node = rb_hash_aref(cache, key);
|
|
964
|
-
if (NIL_P(node)) {
|
|
965
|
-
node = rb_obj_alloc(binding_klass_for(nt));
|
|
966
|
-
rb_iv_set(node, "@c_ptr", ptr);
|
|
967
|
-
rb_iv_set(node, "@document", document);
|
|
968
|
-
rb_iv_set(node, "@parent", Qnil);
|
|
969
|
-
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
970
|
-
rb_iv_set(node, "@node_type", INT2FIX(nt));
|
|
971
|
-
rb_hash_aset(cache, key, node);
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
break;
|
|
976
|
-
default: /* other: rare — signal Ruby fallback for this index */
|
|
977
|
-
rb_ary_push(out, m_native_sentinel);
|
|
978
|
-
continue;
|
|
979
1020
|
}
|
|
980
|
-
|
|
1021
|
+
return node;
|
|
1022
|
+
default: /* other: rare — signal Ruby fallback for this entry */
|
|
1023
|
+
return m_native_sentinel;
|
|
981
1024
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/* ---- at_xpath single-result seam (TODO.perf/16) ----------------
|
|
1028
|
+
* One dispatch for the hottest query shape: type check, entry-0
|
|
1029
|
+
* materialization through the bulk machinery, result free.
|
|
1030
|
+
* Non-nodeset results return Qundef — the caller keeps the exact
|
|
1031
|
+
* Ruby scalar path (and owns the free there). */
|
|
1032
|
+
static VALUE nf_at_xpath_first(VALUE self, VALUE document,
|
|
1033
|
+
VALUE result_ptr_val)
|
|
1034
|
+
{
|
|
1035
|
+
void *result = (void *)(uintptr_t)NUM2ULL(result_ptr_val);
|
|
1036
|
+
void *buf;
|
|
1037
|
+
int kind;
|
|
1038
|
+
size_t scount;
|
|
1039
|
+
VALUE node;
|
|
1040
|
+
|
|
1041
|
+
(void)self;
|
|
1042
|
+
resolve_binding_classes();
|
|
1043
|
+
if (f_xp_type(result) != 0) /* XPATH_NODESET */
|
|
1044
|
+
return m_native_sentinel; /* caller keeps the scalar path */
|
|
1045
|
+
scount = f_xp_count(result);
|
|
1046
|
+
if (scount == 0) {
|
|
1047
|
+
f_xp_free(result);
|
|
1048
|
+
return Qnil;
|
|
1049
|
+
}
|
|
1050
|
+
buf = NULL;
|
|
1051
|
+
scount = f_xp_nodes_ex(result, &buf, &kind, 1);
|
|
1052
|
+
if (scount == 0 || buf == NULL) {
|
|
1053
|
+
f_xp_free(result);
|
|
1054
|
+
return Qnil;
|
|
1055
|
+
}
|
|
1056
|
+
node = materialize_xp_entry(document, binding_cache_of(document),
|
|
1057
|
+
result, buf, kind, 0);
|
|
1058
|
+
f_xp_free(result);
|
|
1059
|
+
return node;
|
|
985
1060
|
}
|
|
986
1061
|
|
|
987
1062
|
/* ---- Bulk attribute materialization (TODO.perf/10) ---------------
|
|
@@ -1027,6 +1102,7 @@ static VALUE nf_create_binding_element(VALUE self, VALUE document,
|
|
|
1027
1102
|
rb_iv_set(node, "@document", document);
|
|
1028
1103
|
rb_iv_set(node, "@parent", Qnil);
|
|
1029
1104
|
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
1105
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
1030
1106
|
rb_iv_set(node, "@node_type", INT2FIX(0));
|
|
1031
1107
|
cache = binding_cache_of(document);
|
|
1032
1108
|
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
@@ -1052,6 +1128,7 @@ static VALUE nf_create_binding_text(VALUE self, VALUE document,
|
|
|
1052
1128
|
rb_iv_set(node, "@document", document);
|
|
1053
1129
|
rb_iv_set(node, "@parent", Qnil);
|
|
1054
1130
|
rb_iv_set(node, "@structure_memoizable", Qtrue);
|
|
1131
|
+
rb_iv_set(node, "@native_fast", Qtrue);
|
|
1055
1132
|
rb_iv_set(node, "@node_type", INT2FIX(1));
|
|
1056
1133
|
cache = binding_cache_of(document);
|
|
1057
1134
|
key = ULL2NUM((uint64_t)(uintptr_t)ptr);
|
|
@@ -1163,6 +1240,117 @@ static VALUE nf_fast_element_xml(VALUE self, VALUE addr,
|
|
|
1163
1240
|
NUM2INT(indent), RTEST(decl) ? 1 : 0);
|
|
1164
1241
|
}
|
|
1165
1242
|
|
|
1243
|
+
/* ---- inner_html in one C pass (TODO.perf/18) --------------------
|
|
1244
|
+
* Serializes the receiver's children into one growable buffer:
|
|
1245
|
+
* elements via leptris_element_serialize_into (the same opts the
|
|
1246
|
+
* Ruby loop passed), text XML-escaped with the binding's entity
|
|
1247
|
+
* set (& < > \r), CDATA/comments/PIs wrapped raw — byte-identical
|
|
1248
|
+
* to Element#inner_html's per-child loop. */
|
|
1249
|
+
static char *inner_buf;
|
|
1250
|
+
static size_t inner_cap;
|
|
1251
|
+
|
|
1252
|
+
static void inner_ensure(size_t need)
|
|
1253
|
+
{
|
|
1254
|
+
if (need > inner_cap) {
|
|
1255
|
+
inner_cap = need < 64 ? 64 : need;
|
|
1256
|
+
inner_buf = ruby_xrealloc(inner_buf, inner_cap);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
static size_t inner_append_raw(const char *s, size_t len, size_t at)
|
|
1261
|
+
{
|
|
1262
|
+
inner_ensure(at + len + 1);
|
|
1263
|
+
if (len)
|
|
1264
|
+
memcpy(inner_buf + at, s, len);
|
|
1265
|
+
return at + len;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
static size_t inner_append_escaped(const char *s, size_t at)
|
|
1269
|
+
{
|
|
1270
|
+
for (; s && *s; s++) {
|
|
1271
|
+
const char *repl;
|
|
1272
|
+
size_t rl;
|
|
1273
|
+
switch (*s) {
|
|
1274
|
+
case '&': repl = "&"; rl = 5; break;
|
|
1275
|
+
case '<': repl = "<"; rl = 4; break;
|
|
1276
|
+
case '>': repl = ">"; rl = 4; break;
|
|
1277
|
+
case '\r': repl = "
"; rl = 5; break;
|
|
1278
|
+
default: repl = NULL; rl = 0; break;
|
|
1279
|
+
}
|
|
1280
|
+
if (repl) {
|
|
1281
|
+
at = inner_append_raw(repl, rl, at);
|
|
1282
|
+
} else {
|
|
1283
|
+
inner_ensure(at + 2);
|
|
1284
|
+
inner_buf[at++] = *s;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return at;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
static VALUE nf_fast_inner_xml(VALUE self, VALUE addr)
|
|
1291
|
+
{
|
|
1292
|
+
void *node = (void *)(uintptr_t)NUM2ULL(addr);
|
|
1293
|
+
void *child;
|
|
1294
|
+
struct serialize_opts opts;
|
|
1295
|
+
size_t at = 0, needed;
|
|
1296
|
+
|
|
1297
|
+
(void)self;
|
|
1298
|
+
opts.indent = 0;
|
|
1299
|
+
opts.xml_declaration = 1; /* matches element_xml_default */
|
|
1300
|
+
opts.encoding = NULL;
|
|
1301
|
+
inner_ensure(4096);
|
|
1302
|
+
for (child = f_first_child(node); child != NULL;
|
|
1303
|
+
child = f_next_sibling(child)) {
|
|
1304
|
+
switch (f_node_type(child)) {
|
|
1305
|
+
case 0: /* element */
|
|
1306
|
+
needed = f_elem_serialize(child, inner_buf + at,
|
|
1307
|
+
inner_cap - at, NULL, &opts);
|
|
1308
|
+
if (needed > 0) {
|
|
1309
|
+
if (needed > inner_cap - at) {
|
|
1310
|
+
inner_ensure(at + needed);
|
|
1311
|
+
needed = f_elem_serialize(child, inner_buf + at,
|
|
1312
|
+
inner_cap - at, NULL, &opts);
|
|
1313
|
+
}
|
|
1314
|
+
at += needed - 1; /* size includes the NUL */
|
|
1315
|
+
}
|
|
1316
|
+
break;
|
|
1317
|
+
case 1: /* text */
|
|
1318
|
+
at = inner_append_escaped(f_text_content(child), at);
|
|
1319
|
+
break;
|
|
1320
|
+
case 2: /* comment */
|
|
1321
|
+
at = inner_append_raw("<!--", 4, at);
|
|
1322
|
+
at = inner_append_escaped(f_comment_content(child), at);
|
|
1323
|
+
at = inner_append_raw("-->", 3, at);
|
|
1324
|
+
break;
|
|
1325
|
+
case 3: { /* CDATA */
|
|
1326
|
+
const char *c = f_cdata_content(child);
|
|
1327
|
+
at = inner_append_raw("<![CDATA[", 9, at);
|
|
1328
|
+
at = inner_append_raw(c, c ? strlen(c) : 0, at);
|
|
1329
|
+
at = inner_append_raw("]]>", 3, at);
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
case 4: { /* PI — the binding strips leading whitespace from
|
|
1333
|
+
* the data before joining (read_pi_data parity). */
|
|
1334
|
+
const char *data = f_pi_data(child);
|
|
1335
|
+
while (data && (*data == ' ' || *data == '\t' ||
|
|
1336
|
+
*data == '\r' || *data == '\n'))
|
|
1337
|
+
data++;
|
|
1338
|
+
at = inner_append_raw("<?", 2, at);
|
|
1339
|
+
at = inner_append_escaped(f_pi_target(child), at);
|
|
1340
|
+
if (data && *data) {
|
|
1341
|
+
at = inner_append_raw(" ", 1, at);
|
|
1342
|
+
at = inner_append_escaped(data, at);
|
|
1343
|
+
}
|
|
1344
|
+
at = inner_append_raw("?>", 2, at);
|
|
1345
|
+
break;
|
|
1346
|
+
}
|
|
1347
|
+
default:
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return rb_utf8_str_new(inner_buf, at);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1166
1354
|
/* ---- Adoption-lift predicate (TODO.perf/09, #204 ask 3) ---------
|
|
1167
1355
|
* The #178/#208 namespace lift + pruning runs on EVERY attach and
|
|
1168
1356
|
* costs several FFI round-trips even when provably a no-op. This
|
|
@@ -1394,6 +1582,10 @@ void Init_native(void)
|
|
|
1394
1582
|
nf_set_binding_attribute, 4);
|
|
1395
1583
|
rb_define_module_function(m_native, "insert_binding_child",
|
|
1396
1584
|
nf_insert_binding_child, 4);
|
|
1585
|
+
rb_define_module_function(m_native, "at_xpath_first",
|
|
1586
|
+
nf_at_xpath_first, 2);
|
|
1587
|
+
rb_define_module_function(m_native, "fast_inner_xml",
|
|
1588
|
+
nf_fast_inner_xml, 1);
|
|
1397
1589
|
rb_define_module_function(m_native, "doc_handle_attach",
|
|
1398
1590
|
nf_doc_handle_attach, 1);
|
|
1399
1591
|
rb_define_module_function(m_native, "doc_handle_release",
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -9,7 +9,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
9
9
|
# rules ride the IterationScope seam — the ext fast path is for
|
|
10
10
|
# document-owned trees.
|
|
11
11
|
def native_fast?
|
|
12
|
-
|
|
12
|
+
@native_fast
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def name
|
|
@@ -609,6 +609,11 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
609
609
|
# of #inner_text, which returns the unescaped text content.
|
|
610
610
|
def inner_html
|
|
611
611
|
ensure_alive!
|
|
612
|
+
# One C pass (TODO.perf/18): the child chain, per-kind
|
|
613
|
+
# serialization, and the escape set all in one dispatch —
|
|
614
|
+
# byte-identical to the Ruby loop below.
|
|
615
|
+
return Leptris::XML::Native.fast_inner_xml(@c_ptr.address) if
|
|
616
|
+
@native_fast
|
|
612
617
|
children.map do |child|
|
|
613
618
|
case child
|
|
614
619
|
when Leptris::XML::Element
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -27,6 +27,11 @@ class Leptris::XML::Node
|
|
|
27
27
|
# keeps the memo-hit path free of method dispatch.
|
|
28
28
|
@structure_memoizable =
|
|
29
29
|
!document.nil? && !document.is_a?(Leptris::XML::IterationScope)
|
|
30
|
+
# NATIVE_FAST availability is fixed at load time before any
|
|
31
|
+
# node exists, so the conjunction with document-ownership is
|
|
32
|
+
# construct-time constant — the hot gates read one ivar.
|
|
33
|
+
@native_fast = @structure_memoizable &&
|
|
34
|
+
defined?(Leptris::XML::NATIVE_FAST) ? true : false
|
|
30
35
|
# wrap() already calls leptris_node_get_type for dispatch; reusing
|
|
31
36
|
# the result makes every predicate and #type call FFI-free.
|
|
32
37
|
@node_type = node_type
|
|
@@ -145,9 +150,9 @@ class Leptris::XML::Node
|
|
|
145
150
|
|
|
146
151
|
# The ext bulk path applies to document-owned trees only:
|
|
147
152
|
# scope-owned (iterparse) elements ride the IterationScope seam.
|
|
153
|
+
# Precomputed at construction (TODO.perf/17) — one ivar read.
|
|
148
154
|
def native_fast_children?
|
|
149
|
-
|
|
150
|
-
!@document.is_a?(Leptris::XML::IterationScope)
|
|
155
|
+
@native_fast
|
|
151
156
|
end
|
|
152
157
|
|
|
153
158
|
# Content-defined 64-bit Merkle digest of this subtree
|
|
@@ -236,6 +236,14 @@ module Leptris::XML::Searchable
|
|
|
236
236
|
# AutoPointer, one fewer FFI than xpath().first; scalars keep the
|
|
237
237
|
# full-wrapper semantics.
|
|
238
238
|
def self.wrap_xpath_first_result(document, result_ptr)
|
|
239
|
+
# C seam (TODO.perf/16): nodesets materialize entry 0, free
|
|
240
|
+
# the handle, and return in one dispatch. The module object
|
|
241
|
+
# back means non-nodeset — the scalar path below owns it.
|
|
242
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
243
|
+
first = Leptris::XML::Native.at_xpath_first(
|
|
244
|
+
document, result_ptr.address)
|
|
245
|
+
return first unless first.equal?(Leptris::XML::Native)
|
|
246
|
+
end
|
|
239
247
|
type = Leptris::XML::FFI.leptris_xpath_result_type(result_ptr)
|
|
240
248
|
if type == Leptris::XML::FFI::XPATH_NODESET
|
|
241
249
|
ptr = Leptris::XML::FFI.leptris_xpath_result_get_node(result_ptr, 0)
|
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.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -84,6 +84,9 @@ files:
|
|
|
84
84
|
- TODO.perf/13-structural-memos.md
|
|
85
85
|
- TODO.perf/14-insert-family-c-bound.md
|
|
86
86
|
- TODO.perf/15-compiled-expression-cache.md
|
|
87
|
+
- TODO.perf/16-at-xpath-single-result-seam.md
|
|
88
|
+
- TODO.perf/17-precomputed-fast-path-flags.md
|
|
89
|
+
- TODO.perf/18-inner-html-one-c-pass.md
|
|
87
90
|
- TODO.restructure/01-constraint-compliance-audit.md
|
|
88
91
|
- TODO.restructure/02-deep-copy-seam.md
|
|
89
92
|
- TODO.restructure/03-evaluation-context-seam.md
|