leptris 1.9.174.8 → 1.9.174.10
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 +37 -0
- data/Rakefile +6 -1
- data/TODO.perf/34-fragment-fast-lane.md +41 -0
- data/TODO.perf/35-parse-default-c-face.md +24 -0
- data/TODO.perf/36-raise-in-c-serializer-encoding.md +34 -0
- data/TODO.perf/37-document-lazy-pointer.md +28 -0
- data/ext/leptris/native/native.c +168 -18
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/document.rb +23 -3
- data/lib/leptris/xml/document_fragment.rb +26 -1
- data/lib/leptris/xml/element.rb +41 -25
- data/lib/leptris/xml/node.rb +1 -2
- data/lib/leptris/xml/searchable.rb +8 -0
- data/lib/leptris/xml/serialization.rb +8 -7
- data/lib/leptris/xml/text.rb +2 -3
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f0e347192431e6ed3ff15bed0fca55fb103ba7307a33d889014e72c3ece0dba8
|
|
4
|
+
data.tar.gz: f7edb28afa62063bbc1f0ab2cfcc349619a8944249f69f41b40e054e016b5b45
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 660309e07c7ce84a4a68bdca9c46a1970f0af6a1bd2e316acca600de60cf54cbd445cd0814232593dd1752f59965d7df8ede74c14da977005b48cbec4ec276fd
|
|
7
|
+
data.tar.gz: ffe8c0e4f6fc98f4bda50187b3c740e278aae6c82ec384f1ac6843673276b916910a1ab72a4c740a69e0006992418c83eb92280a619b18fac69c8568f5b7639e
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,43 @@ All notable changes to Leptris will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.9.174.10] - 2026-09-16
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Status raises in C (TODO.perf/36)**: every C mutation face
|
|
13
|
+
raises `Leptris::XML::Error` with the exact `status_message`
|
|
14
|
+
format — the Ruby `check_status` dispatch leaves the fast
|
|
15
|
+
mutation paths (and the success/lift-marker collision it
|
|
16
|
+
briefly introduced — Qtrue on success now — was caught by the
|
|
17
|
+
bottom-up construction spec). `to_xml(encoding:)` rides the ext
|
|
18
|
+
serializer face (encoding VALUE, nil → NULL).
|
|
19
|
+
- **Document lazy Pointer (TODO.perf/37)**: `#c_ptr` materializes
|
|
20
|
+
once over `@c_address` (freed docs answer nil); the parse/create
|
|
21
|
+
faces skip the Pointer mint entirely. The ffi gem rejects
|
|
22
|
+
Integers for typedef'd pointer params — the search paths keep
|
|
23
|
+
the lazy Pointer.
|
|
24
|
+
- **Atomic bundle vendoring**: replacing a mapped `native.bundle`
|
|
25
|
+
in place gets the next loader SIGKILLed (CODESIGNING Invalid
|
|
26
|
+
Page) — the Rakefile now cp+mv's atomically.
|
|
27
|
+
|
|
28
|
+
## [1.9.174.9] - 2026-09-16
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **Fragment fast lane (TODO.perf/34)**: `add_child(String)` runs
|
|
33
|
+
one C dispatch for the whole markup add — fragment parse +
|
|
34
|
+
every child append + a single readonly gate and version bump
|
|
35
|
+
(the move-during-iteration hazard handled: appending detaches,
|
|
36
|
+
so the walk captures next before each move). The fragment parse
|
|
37
|
+
face returns an address (no status MemoryPointer, no Pointer);
|
|
38
|
+
`DocumentFragment#children` rides the bulk face; parse failures
|
|
39
|
+
fall back to the legacy path for the exact error.
|
|
40
|
+
- **Document.parse default path as one C dispatch (TODO.perf/35)**:
|
|
41
|
+
engine parse + ivar-seeded wrapper + lifetime handle in one
|
|
42
|
+
call — small parses measured 15.6µs → 6.8µs (~2.3x, under host
|
|
43
|
+
load ~100; the shape is self-contained).
|
|
44
|
+
|
|
8
45
|
## [1.9.174.8] - 2026-09-16
|
|
9
46
|
|
|
10
47
|
### Fixed
|
data/Rakefile
CHANGED
|
@@ -172,7 +172,12 @@ task :compile do
|
|
|
172
172
|
# Build-log proof of the linkage contract: libSystem only.
|
|
173
173
|
puts `otool -L #{bundle}`
|
|
174
174
|
end
|
|
175
|
-
|
|
175
|
+
# Atomic replace: overwriting a bundle another process still
|
|
176
|
+
# has mapped gets that loader SIGKILLed (CODESIGNING Invalid
|
|
177
|
+
# Page) — cp to a temp name, then mv.
|
|
178
|
+
dest = "lib/leptris/xml/#{File.basename(bundle)}"
|
|
179
|
+
cp(bundle, "#{dest}.new")
|
|
180
|
+
mv("#{dest}.new", dest)
|
|
176
181
|
puts "Vendored native layer (#{File.basename(bundle)}) into lib/leptris/xml/"
|
|
177
182
|
end
|
|
178
183
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# 34 — Fragment fast lane: C parse, bulk children, one-shot markup append
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.9)
|
|
4
|
+
|
|
5
|
+
The add_child(String) path — moxml's markup-builder shape — pays
|
|
6
|
+
three layers per call: parse_fragment_with_status allocates a
|
|
7
|
+
MemoryPointer for the status out-param, DocumentFragment#children
|
|
8
|
+
runs the FFI fetch+wrap loop (never moved to the bulk faces), and
|
|
9
|
+
each child appends through its own dispatch.
|
|
10
|
+
|
|
11
|
+
Bind the lane: a C parse face returns the fragment's address
|
|
12
|
+
(Integer — no Pointer, no MemoryPointer); the fragment stores
|
|
13
|
+
@c_address with a lazy c_ptr; children rides the bulk face
|
|
14
|
+
(fragment nodes live in the document pool — the document's cache
|
|
15
|
+
and version discipline apply); and a one-shot append face parses
|
|
16
|
+
the markup, walks the fragment's children, and appends every one
|
|
17
|
+
with a single readonly gate + version bump. Parse failures return
|
|
18
|
+
a marker; the Ruby fallback re-runs the old path for the exact
|
|
19
|
+
error message (the rare path, cheap to re-fail).
|
|
20
|
+
|
|
21
|
+
Gates: add_child(String) semantics identical (multiple top-level
|
|
22
|
+
nodes, text, PIs, namespaces lifted per the Node path); fragment
|
|
23
|
+
children identical through the bulk face; suite both modes.
|
|
24
|
+
|
|
25
|
+
## Outcome (1.9.174.9)
|
|
26
|
+
|
|
27
|
+
Three layers landed: the fragment parse face answers the
|
|
28
|
+
fragment ADDRESS (no status MemoryPointer, no Pointer mint —
|
|
29
|
+
failures fall back to the FFI pair for the exact error);
|
|
30
|
+
DocumentFragment stores @c_address with a lazy c_ptr and
|
|
31
|
+
children() rides the bulk face (fragment nodes live in the
|
|
32
|
+
document pool — the document's cache and version discipline
|
|
33
|
+
apply); and Element#add_child(String) runs ONE dispatch for the
|
|
34
|
+
whole markup add (parse + per-child append + single gate/bump —
|
|
35
|
+
with the move-during-iteration hazard caught by the suite:
|
|
36
|
+
appending detaches the child, so the walk captures next BEFORE
|
|
37
|
+
each move). -1 re-runs the legacy path for parse errors;
|
|
38
|
+
-1000-st routes through check_status. The markup row needs a
|
|
39
|
+
quiet-host battery number (this box sat at load 100+ today);
|
|
40
|
+
small-parse measured 6.8µs native vs 15.6µs FFI (~2.3x) under
|
|
41
|
+
load 100.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# 35 — Document.parse default path as one C dispatch
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.9)
|
|
4
|
+
|
|
5
|
+
The default parse path (no options, no recover — the adapter
|
|
6
|
+
default) is already allocation-free at the Ruby level (#187),
|
|
7
|
+
but still pays FFI marshaling, the wrap frames, the Freed and
|
|
8
|
+
Pointer constructions, and the handle attach across three Ruby
|
|
9
|
+
layers. One C face runs leptris_parse_string + the full document
|
|
10
|
+
wrapper (ivar-seeded) + lifetime handle and returns it; failure
|
|
11
|
+
raises the same ParseError shape via last_error read in C.
|
|
12
|
+
|
|
13
|
+
Gates: parse defaults identical (errors, readonly flag, root
|
|
14
|
+
access); the options/recover paths untouched; suite both modes.
|
|
15
|
+
|
|
16
|
+
## Outcome (1.9.174.9)
|
|
17
|
+
|
|
18
|
+
Native.parse_binding_document: leptris_parse_string + the full
|
|
19
|
+
ivar-seeded wrapper + lifetime handle in one dispatch (shared
|
|
20
|
+
build_binding_document helper); Document.parse's default path
|
|
21
|
+
(no options, no recover) routes through it, raising the same
|
|
22
|
+
ParseError shape via last_error on failure. Options/recover
|
|
23
|
+
paths untouched. Measured (load ~100, self-contained shape):
|
|
24
|
+
small parse 15.6µs -> 6.8µs (~2.3x).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# 36 — Status raises in C + encoding on the serializer faces
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.10)
|
|
4
|
+
|
|
5
|
+
Every C mutation face returns INT2FIX(st) for a Ruby check_status
|
|
6
|
+
dispatch (~30ns per mutation, plus a whole class of
|
|
7
|
+
"forgot-to-check" bug the compiler cannot catch). With
|
|
8
|
+
leptris_status_string and leptris_last_error both dlsym'd, the
|
|
9
|
+
faces raise Leptris::XML::Error in C with the exact
|
|
10
|
+
status_message format ("base" or "base (detail)") — call sites
|
|
11
|
+
shrink to the bare face call.
|
|
12
|
+
|
|
13
|
+
The ext serializer faces skip the encoding-tagged shape entirely
|
|
14
|
+
(to_xml(encoding: ...) still builds the options struct through
|
|
15
|
+
FFI): the faces gain an encoding VALUE (nil -> NULL, the Ruby
|
|
16
|
+
string's bytes stay valid across the synchronous engine call).
|
|
17
|
+
|
|
18
|
+
Gates: every mutation's failure message byte-identical to
|
|
19
|
+
check_status; encoding-tagged to_xml output identical to the FFI
|
|
20
|
+
path (declaration, no anchor lifetime issues); suite both modes.
|
|
21
|
+
|
|
22
|
+
## Outcome (1.9.174.10)
|
|
23
|
+
|
|
24
|
+
All seven mutation faces raise Leptris::XML::Error in C with the
|
|
25
|
+
exact status_message format (status_string + optional
|
|
26
|
+
" (last_error)") — the Ruby check_status dispatch disappears from
|
|
27
|
+
every fast mutation. TRAP FOUND: the faces' success return became
|
|
28
|
+
Qnil, colliding with the lift-needed marker — every fast mutation
|
|
29
|
+
fell through to the slow path and re-applied (order-corrupting
|
|
30
|
+
for inserts; the v180 bottom-up spec caught it). Success now
|
|
31
|
+
returns Qtrue. The serializer faces take an encoding VALUE (nil
|
|
32
|
+
-> NULL; the Ruby string's bytes live across the synchronous
|
|
33
|
+
call), and to_xml(encoding:) rides the ext face instead of the
|
|
34
|
+
options-struct FFI path.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# 37 — Document lazy Pointer + address-based xpath contexts
|
|
2
|
+
|
|
3
|
+
Status: DONE (1.9.174.10)
|
|
4
|
+
|
|
5
|
+
Documents still mint an FFI::Pointer eagerly (the parse/create
|
|
6
|
+
faces pay the funcall + one allocation + its GC visit per
|
|
7
|
+
document). Make #c_ptr lazy like Node's (attr_reader becomes a
|
|
8
|
+
materializer over @c_address), skip the Pointer in
|
|
9
|
+
build_binding_document, and feed the search paths Integer
|
|
10
|
+
addresses (the ffi gem converts Integers for :pointer params) so
|
|
11
|
+
per-xpath document Pointers never materialize at all.
|
|
12
|
+
|
|
13
|
+
Gates: c_ptr stability and public API unchanged; every FFI
|
|
14
|
+
document consumer still works (options paths, c14n, fragment
|
|
15
|
+
fallback); suite both modes.
|
|
16
|
+
|
|
17
|
+
## Outcome (1.9.174.10)
|
|
18
|
+
|
|
19
|
+
Document#c_ptr materializes lazily over @c_address (freed docs
|
|
20
|
+
answer nil — never re-materialize); build_binding_document skips
|
|
21
|
+
the Pointer funcall (one alloc + ~200ns per document). TRAP
|
|
22
|
+
FOUND: this ffi version does NOT convert Integers for typedef'd
|
|
23
|
+
pointer params — passing c_address into the compiled-eval calls
|
|
24
|
+
failed with ":pointer argument is not a valid pointer"; the
|
|
25
|
+
search paths keep passing the (now lazy, once-per-document)
|
|
26
|
+
Pointer. Also: replacing a mapped native.bundle in place gets
|
|
27
|
+
the next loader SIGKILLed (CODESIGNING Invalid Page) — the
|
|
28
|
+
vendoring cp is now an atomic cp+mv.
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -94,6 +94,10 @@ typedef void (*visit_fn)(void *, visit_cb_fn, void *);
|
|
|
94
94
|
typedef void (*free_str_fn)(void *);
|
|
95
95
|
typedef const char *(*node_xpath_fn)(void *);
|
|
96
96
|
typedef void *(*elem_copy_fn)(void *, void *);
|
|
97
|
+
typedef void *(*parse_str_fn)(const char *, size_t, void *);
|
|
98
|
+
typedef void *(*parse_frag_fn)(const char *, size_t, void *, int *);
|
|
99
|
+
typedef const char *(*last_err_fn)(void);
|
|
100
|
+
typedef const char *(*status_str_fn)(int);
|
|
97
101
|
static set_str_fn f_elem_set_name, f_elem_set_text, f_text_set_content;
|
|
98
102
|
static node_unlink_fn f_node_unlink;
|
|
99
103
|
static traverse_fn f_node_traverse;
|
|
@@ -101,6 +105,10 @@ static visit_fn f_node_visit;
|
|
|
101
105
|
static free_str_fn f_free_str;
|
|
102
106
|
static node_xpath_fn f_node_xpath;
|
|
103
107
|
static elem_copy_fn f_elem_copy;
|
|
108
|
+
static parse_str_fn f_parse_str;
|
|
109
|
+
static parse_frag_fn f_parse_frag;
|
|
110
|
+
static last_err_fn f_last_err;
|
|
111
|
+
static status_str_fn f_status_str;
|
|
104
112
|
static set_root_fn f_set_root;
|
|
105
113
|
static doc_free_fn f_doc_free;
|
|
106
114
|
|
|
@@ -119,8 +127,10 @@ static void resolve_binding_classes(void);
|
|
|
119
127
|
static VALUE binding_cache_of(VALUE document);
|
|
120
128
|
static VALUE binding_klass_for(int kind);
|
|
121
129
|
static VALUE c_iteration_scope;
|
|
130
|
+
static VALUE c_leptris_error;
|
|
122
131
|
static VALUE c_b_document, c_b_freed, c_ffi_pointer;
|
|
123
132
|
static ID id_ptr_new, id_freed_new, id_alive;
|
|
133
|
+
static void check_status_c(int st);
|
|
124
134
|
|
|
125
135
|
#define NT_ELEMENT 0
|
|
126
136
|
|
|
@@ -221,6 +231,10 @@ static void resolve_symbols(const char *lib_path)
|
|
|
221
231
|
f_free_str = (free_str_fn)lib_sym(h, "leptris_free_string");
|
|
222
232
|
f_node_xpath = (node_xpath_fn)lib_sym(h, "leptris_node_get_xpath");
|
|
223
233
|
f_elem_copy = (elem_copy_fn)lib_sym(h, "leptris_element_copy");
|
|
234
|
+
f_parse_str = (parse_str_fn)lib_sym(h, "leptris_parse_string");
|
|
235
|
+
f_parse_frag = (parse_frag_fn)lib_sym(h, "leptris_parse_fragment");
|
|
236
|
+
f_last_err = (last_err_fn)lib_sym(h, "leptris_last_error");
|
|
237
|
+
f_status_str = (status_str_fn)lib_sym(h, "leptris_status_string");
|
|
224
238
|
f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
|
|
225
239
|
f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
|
|
226
240
|
if (!f_elem_name || !f_text_content || !f_attr ||
|
|
@@ -233,7 +247,8 @@ static void resolve_symbols(const char *lib_path)
|
|
|
233
247
|
!f_pi_data || !f_elem_set_name || !f_elem_set_text ||
|
|
234
248
|
!f_text_set_content || !f_node_unlink ||
|
|
235
249
|
!f_node_traverse || !f_node_visit || !f_free_str ||
|
|
236
|
-
!f_node_xpath || !f_elem_copy ||
|
|
250
|
+
!f_node_xpath || !f_elem_copy || !f_parse_str ||
|
|
251
|
+
!f_parse_frag || !f_last_err || !f_status_str ||
|
|
237
252
|
!f_doc_free ||
|
|
238
253
|
!f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
|
|
239
254
|
!f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
|
|
@@ -630,7 +645,8 @@ static VALUE nf_append_binding_child(VALUE self, VALUE document,
|
|
|
630
645
|
rb_ivar_set(document, id_iv_version,
|
|
631
646
|
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
632
647
|
st = f_append_child(parent, child);
|
|
633
|
-
|
|
648
|
+
check_status_c(st);
|
|
649
|
+
return Qtrue;
|
|
634
650
|
}
|
|
635
651
|
|
|
636
652
|
/* C-bound set_attribute (TODO.perf/11, #204 gap row 0.25x): the
|
|
@@ -659,7 +675,8 @@ static VALUE nf_set_binding_attribute(VALUE self, VALUE document,
|
|
|
659
675
|
* embedded-NUL value raises rather than truncates. */
|
|
660
676
|
st = f_set_attr(node, RSTRING_PTR(StringValue(name)),
|
|
661
677
|
StringValueCStr(value));
|
|
662
|
-
|
|
678
|
+
check_status_c(st);
|
|
679
|
+
return Qtrue;
|
|
663
680
|
}
|
|
664
681
|
|
|
665
682
|
/* C-bound value mutations (TODO.perf/23): the same gates + bump
|
|
@@ -682,7 +699,8 @@ static VALUE nf_set_binding_name(VALUE self, VALUE document,
|
|
|
682
699
|
rb_ivar_set(document, id_iv_version,
|
|
683
700
|
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
684
701
|
st = f_elem_set_name(node, StringValueCStr(name));
|
|
685
|
-
|
|
702
|
+
check_status_c(st);
|
|
703
|
+
return Qtrue;
|
|
686
704
|
}
|
|
687
705
|
|
|
688
706
|
/* Element text or text-node content — kind-dispatched in C. */
|
|
@@ -706,7 +724,8 @@ static VALUE nf_set_binding_text(VALUE self, VALUE document,
|
|
|
706
724
|
st = f_node_type(node) == NT_ELEMENT
|
|
707
725
|
? f_elem_set_text(node, StringValueCStr(content))
|
|
708
726
|
: f_text_set_content(node, StringValueCStr(content));
|
|
709
|
-
|
|
727
|
+
check_status_c(st);
|
|
728
|
+
return Qtrue;
|
|
710
729
|
}
|
|
711
730
|
|
|
712
731
|
static VALUE nf_unlink_binding_node(VALUE self, VALUE document,
|
|
@@ -727,7 +746,8 @@ static VALUE nf_unlink_binding_node(VALUE self, VALUE document,
|
|
|
727
746
|
rb_ivar_set(document, id_iv_version,
|
|
728
747
|
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
729
748
|
st = f_node_unlink(node);
|
|
730
|
-
|
|
749
|
+
check_status_c(st);
|
|
750
|
+
return Qtrue;
|
|
731
751
|
}
|
|
732
752
|
|
|
733
753
|
/* C-bound root= (TODO.perf/33): gates + bump + engine set_root
|
|
@@ -751,7 +771,8 @@ static VALUE nf_set_binding_root(VALUE self, VALUE document,
|
|
|
751
771
|
rb_ivar_set(document, id_iv_version,
|
|
752
772
|
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
753
773
|
st = f_set_root(doc_ptr_of(document), node);
|
|
754
|
-
|
|
774
|
+
check_status_c(st);
|
|
775
|
+
return Qtrue;
|
|
755
776
|
}
|
|
756
777
|
|
|
757
778
|
/* ---- C-yield traversal (TODO.perf/27) ----------------------------
|
|
@@ -1064,7 +1085,8 @@ static VALUE nf_insert_binding_child(VALUE self, VALUE document,
|
|
|
1064
1085
|
rb_raise(rb_eArgError, "invalid insert mode %d", m);
|
|
1065
1086
|
return Qnil;
|
|
1066
1087
|
}
|
|
1067
|
-
|
|
1088
|
+
check_status_c(st);
|
|
1089
|
+
return Qtrue;
|
|
1068
1090
|
}
|
|
1069
1091
|
|
|
1070
1092
|
/* Builder factories (#149): create in C, wrap as NativeNode — no
|
|
@@ -1215,6 +1237,7 @@ static void resolve_binding_classes(void)
|
|
|
1215
1237
|
c_b_document = rb_path2class("Leptris::XML::Document");
|
|
1216
1238
|
c_b_freed = rb_path2class("Leptris::XML::Document::Freed");
|
|
1217
1239
|
c_iteration_scope = rb_path2class("Leptris::XML::IterationScope");
|
|
1240
|
+
c_leptris_error = rb_path2class("Leptris::XML::Error");
|
|
1218
1241
|
id_ptr_new = rb_intern("new");
|
|
1219
1242
|
rb_gc_register_mark_object(c_b_element);
|
|
1220
1243
|
rb_gc_register_mark_object(c_b_text);
|
|
@@ -1229,6 +1252,7 @@ static void resolve_binding_classes(void)
|
|
|
1229
1252
|
rb_gc_register_mark_object(c_b_document);
|
|
1230
1253
|
rb_gc_register_mark_object(c_b_freed);
|
|
1231
1254
|
rb_gc_register_mark_object(c_iteration_scope);
|
|
1255
|
+
rb_gc_register_mark_object(c_leptris_error);
|
|
1232
1256
|
}
|
|
1233
1257
|
|
|
1234
1258
|
static VALUE binding_klass_for(int kind)
|
|
@@ -1625,7 +1649,8 @@ static char *ser_buf;
|
|
|
1625
1649
|
static size_t ser_cap;
|
|
1626
1650
|
|
|
1627
1651
|
static VALUE fast_serialize(serialize_into_fn fn, void *node,
|
|
1628
|
-
int indent, int xml_declaration
|
|
1652
|
+
int indent, int xml_declaration,
|
|
1653
|
+
VALUE encoding)
|
|
1629
1654
|
{
|
|
1630
1655
|
struct serialize_opts opts;
|
|
1631
1656
|
char stack_buf[4096];
|
|
@@ -1636,7 +1661,11 @@ static VALUE fast_serialize(serialize_into_fn fn, void *node,
|
|
|
1636
1661
|
|
|
1637
1662
|
opts.indent = indent;
|
|
1638
1663
|
opts.xml_declaration = xml_declaration;
|
|
1639
|
-
|
|
1664
|
+
/* The Ruby string's bytes stay valid across the synchronous
|
|
1665
|
+
* engine call (TODO.perf/36). */
|
|
1666
|
+
opts.encoding = NIL_P(encoding)
|
|
1667
|
+
? NULL
|
|
1668
|
+
: StringValueCStr(encoding);
|
|
1640
1669
|
/* Probe the largest buffer we own: a big scratch from a prior
|
|
1641
1670
|
* call usually fits (single serialization); cold calls start
|
|
1642
1671
|
* on the stack buffer. */
|
|
@@ -1662,21 +1691,25 @@ static VALUE fast_serialize(serialize_into_fn fn, void *node,
|
|
|
1662
1691
|
}
|
|
1663
1692
|
|
|
1664
1693
|
static VALUE nf_fast_document_xml(VALUE self, VALUE addr,
|
|
1665
|
-
VALUE indent, VALUE decl
|
|
1694
|
+
VALUE indent, VALUE decl,
|
|
1695
|
+
VALUE encoding)
|
|
1666
1696
|
{
|
|
1667
1697
|
(void)self;
|
|
1668
1698
|
return fast_serialize(f_doc_serialize,
|
|
1669
1699
|
(void *)(uintptr_t)NUM2ULL(addr),
|
|
1670
|
-
NUM2INT(indent), RTEST(decl) ? 1 : 0
|
|
1700
|
+
NUM2INT(indent), RTEST(decl) ? 1 : 0,
|
|
1701
|
+
encoding);
|
|
1671
1702
|
}
|
|
1672
1703
|
|
|
1673
1704
|
static VALUE nf_fast_element_xml(VALUE self, VALUE addr,
|
|
1674
|
-
VALUE indent, VALUE decl
|
|
1705
|
+
VALUE indent, VALUE decl,
|
|
1706
|
+
VALUE encoding)
|
|
1675
1707
|
{
|
|
1676
1708
|
(void)self;
|
|
1677
1709
|
return fast_serialize(f_elem_serialize,
|
|
1678
1710
|
(void *)(uintptr_t)NUM2ULL(addr),
|
|
1679
|
-
NUM2INT(indent), RTEST(decl) ? 1 : 0
|
|
1711
|
+
NUM2INT(indent), RTEST(decl) ? 1 : 0,
|
|
1712
|
+
encoding);
|
|
1680
1713
|
}
|
|
1681
1714
|
|
|
1682
1715
|
/* ---- inner_html in one C pass (TODO.perf/18) --------------------
|
|
@@ -1851,8 +1884,6 @@ static VALUE nf_copy_binding_element(VALUE self, VALUE document,
|
|
|
1851
1884
|
return Qnil;
|
|
1852
1885
|
doc_addr = ULL2NUM((uint64_t)(uintptr_t)doc);
|
|
1853
1886
|
new_doc = rb_obj_alloc(c_b_document);
|
|
1854
|
-
rb_iv_set(new_doc, "@c_ptr",
|
|
1855
|
-
rb_funcall(c_ffi_pointer, id_ptr_new, 1, doc_addr));
|
|
1856
1887
|
rb_iv_set(new_doc, "@c_address", doc_addr);
|
|
1857
1888
|
freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
|
|
1858
1889
|
rb_iv_set(new_doc, "@freed", freed);
|
|
@@ -1874,6 +1905,119 @@ static VALUE nf_copy_binding_element(VALUE self, VALUE document,
|
|
|
1874
1905
|
return new_doc;
|
|
1875
1906
|
}
|
|
1876
1907
|
|
|
1908
|
+
/* Shared document-wrapper construction from a C document
|
|
1909
|
+
* pointer: ivar-seeded wrapper + lifetime handle. Returns the
|
|
1910
|
+
* wrapper. (TODO.perf/35 — the create face inlines the same
|
|
1911
|
+
* steps.) */
|
|
1912
|
+
static VALUE build_binding_document(void *doc)
|
|
1913
|
+
{
|
|
1914
|
+
VALUE new_doc, doc_addr, handle, freed;
|
|
1915
|
+
struct doc_handle *h;
|
|
1916
|
+
|
|
1917
|
+
doc_addr = ULL2NUM((uint64_t)(uintptr_t)doc);
|
|
1918
|
+
new_doc = rb_obj_alloc(c_b_document);
|
|
1919
|
+
rb_iv_set(new_doc, "@c_ptr",
|
|
1920
|
+
rb_funcall(c_ffi_pointer, id_ptr_new, 1, doc_addr));
|
|
1921
|
+
rb_iv_set(new_doc, "@c_address", doc_addr);
|
|
1922
|
+
freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
|
|
1923
|
+
rb_iv_set(new_doc, "@freed", freed);
|
|
1924
|
+
rb_iv_set(new_doc, "@readonly", Qfalse);
|
|
1925
|
+
rb_iv_set(new_doc, "@version", INT2FIX(0));
|
|
1926
|
+
handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
|
|
1927
|
+
&dh_type, h);
|
|
1928
|
+
h->doc = doc;
|
|
1929
|
+
rb_ivar_set(new_doc, id_iv_doc_handle, handle);
|
|
1930
|
+
return new_doc;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
/* check_status in C (TODO.perf/36): the exact status_message
|
|
1934
|
+
* format — "base" or "base (detail)" — so the mutation faces
|
|
1935
|
+
* raise instead of returning codes for a Ruby dispatch. */
|
|
1936
|
+
static void check_status_c(int st)
|
|
1937
|
+
{
|
|
1938
|
+
const char *base, *detail;
|
|
1939
|
+
|
|
1940
|
+
if (st == 0)
|
|
1941
|
+
return;
|
|
1942
|
+
base = f_status_str(st);
|
|
1943
|
+
detail = f_last_err();
|
|
1944
|
+
if (detail && *detail)
|
|
1945
|
+
rb_raise(c_leptris_error, "%s (%s)",
|
|
1946
|
+
base ? base : "?", detail);
|
|
1947
|
+
rb_raise(c_leptris_error, "%s", base ? base : "?");
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
/* Document.parse default path in one dispatch (TODO.perf/35):
|
|
1951
|
+
* leptris_parse_string + wrapper + handle. Qnil = parse failure
|
|
1952
|
+
* (the caller raises with the same message shape). */
|
|
1953
|
+
static VALUE nf_parse_binding_document(VALUE self, VALUE xml)
|
|
1954
|
+
{
|
|
1955
|
+
void *doc;
|
|
1956
|
+
|
|
1957
|
+
(void)self;
|
|
1958
|
+
resolve_binding_classes();
|
|
1959
|
+
doc = f_parse_str(RSTRING_PTR(StringValue(xml)),
|
|
1960
|
+
(size_t)RSTRING_LEN(StringValue(xml)), NULL);
|
|
1961
|
+
return doc ? build_binding_document(doc) : Qnil;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/* Fragment lane (TODO.perf/34): parse the markup against the
|
|
1965
|
+
* document; returns the fragment's ADDRESS, or Qfalse when the
|
|
1966
|
+
* parse fails (status is not surfaced — the caller re-runs the
|
|
1967
|
+
* FFI path for the exact error). */
|
|
1968
|
+
static VALUE nf_parse_fragment_addr(VALUE self, VALUE document,
|
|
1969
|
+
VALUE xml)
|
|
1970
|
+
{
|
|
1971
|
+
void *frag;
|
|
1972
|
+
int status;
|
|
1973
|
+
|
|
1974
|
+
(void)self;
|
|
1975
|
+
resolve_binding_classes();
|
|
1976
|
+
frag = f_parse_frag(RSTRING_PTR(StringValue(xml)),
|
|
1977
|
+
(size_t)RSTRING_LEN(StringValue(xml)),
|
|
1978
|
+
doc_ptr_of(document), &status);
|
|
1979
|
+
return frag ? ULL2NUM((uint64_t)(uintptr_t)frag) : Qfalse;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
/* One-shot markup append (TODO.perf/34): parse the markup, walk
|
|
1983
|
+
* the fragment's children, append each to the parent — one
|
|
1984
|
+
* readonly gate + version bump for the whole batch. Returns the
|
|
1985
|
+
* appended count; -1 = parse failure; status codes surface as
|
|
1986
|
+
* negatives (-1000 - st) for the caller's check_status path. */
|
|
1987
|
+
static VALUE nf_append_markup(VALUE self, VALUE document,
|
|
1988
|
+
VALUE parent_addr, VALUE xml)
|
|
1989
|
+
{
|
|
1990
|
+
void *frag, *parent, *child, *next_child;
|
|
1991
|
+
int status, count = 0;
|
|
1992
|
+
|
|
1993
|
+
(void)self;
|
|
1994
|
+
resolve_binding_classes();
|
|
1995
|
+
if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
|
|
1996
|
+
rb_raise(c_use_after_free_error,
|
|
1997
|
+
"owning document has been freed");
|
|
1998
|
+
if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
|
|
1999
|
+
rb_raise(c_readonly_error,
|
|
2000
|
+
"document is readonly — mutation attempted");
|
|
2001
|
+
frag = f_parse_frag(RSTRING_PTR(StringValue(xml)),
|
|
2002
|
+
(size_t)RSTRING_LEN(StringValue(xml)),
|
|
2003
|
+
doc_ptr_of(document), &status);
|
|
2004
|
+
if (!frag)
|
|
2005
|
+
return INT2FIX(-1);
|
|
2006
|
+
parent = (void *)(uintptr_t)NUM2ULL(parent_addr);
|
|
2007
|
+
rb_ivar_set(document, id_iv_version,
|
|
2008
|
+
LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
|
|
2009
|
+
/* Appending DETACHES the child from the fragment — capture
|
|
2010
|
+
* the next sibling before each move or the walk corrupts. */
|
|
2011
|
+
for (child = f_first_child(frag); child != NULL; child = next_child) {
|
|
2012
|
+
next_child = f_next_sibling(child);
|
|
2013
|
+
status = f_append_child(parent, child);
|
|
2014
|
+
if (status != 0)
|
|
2015
|
+
check_status_c(status);
|
|
2016
|
+
count++;
|
|
2017
|
+
}
|
|
2018
|
+
return INT2FIX(count);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
1877
2021
|
static VALUE nf_doc_handle_attach(VALUE self, VALUE document)
|
|
1878
2022
|
{
|
|
1879
2023
|
struct doc_handle *h;
|
|
@@ -2097,6 +2241,12 @@ void Init_native(void)
|
|
|
2097
2241
|
nf_copy_binding_element, 2);
|
|
2098
2242
|
rb_define_module_function(m_native, "set_binding_root",
|
|
2099
2243
|
nf_set_binding_root, 2);
|
|
2244
|
+
rb_define_module_function(m_native, "parse_binding_document",
|
|
2245
|
+
nf_parse_binding_document, 1);
|
|
2246
|
+
rb_define_module_function(m_native, "parse_fragment_addr",
|
|
2247
|
+
nf_parse_fragment_addr, 2);
|
|
2248
|
+
rb_define_module_function(m_native, "append_markup",
|
|
2249
|
+
nf_append_markup, 3);
|
|
2100
2250
|
rb_define_module_function(m_native, "fast_inner_xml",
|
|
2101
2251
|
nf_fast_inner_xml, 1);
|
|
2102
2252
|
rb_define_module_function(m_native, "doc_handle_attach",
|
|
@@ -2119,7 +2269,7 @@ void Init_native(void)
|
|
|
2119
2269
|
nf_bulk_element_children, 2);
|
|
2120
2270
|
rb_define_module_function(m_native, "bulk_xpath", nf_bulk_xpath, 2);
|
|
2121
2271
|
rb_define_module_function(m_native, "fast_document_xml",
|
|
2122
|
-
nf_fast_document_xml,
|
|
2272
|
+
nf_fast_document_xml, 4);
|
|
2123
2273
|
rb_define_module_function(m_native, "fast_element_xml",
|
|
2124
|
-
nf_fast_element_xml,
|
|
2274
|
+
nf_fast_element_xml, 4);
|
|
2125
2275
|
}
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require "ffi"
|
|
4
4
|
|
|
5
5
|
class Leptris::XML::Document
|
|
6
|
-
attr_reader :c_ptr
|
|
7
6
|
|
|
8
7
|
# @api private
|
|
9
8
|
# Internal flag container shared between the Document instance and its
|
|
@@ -57,6 +56,19 @@ class Leptris::XML::Document
|
|
|
57
56
|
# document for trees that are freed before any wrap.
|
|
58
57
|
end
|
|
59
58
|
|
|
59
|
+
# TODO.perf/37: the Integer address is canonical; the Pointer
|
|
60
|
+
# materializes only when read (parse/create faces skip it).
|
|
61
|
+
# A freed document answers nil — never re-materialize a stale
|
|
62
|
+
# or absent address.
|
|
63
|
+
def c_ptr
|
|
64
|
+
@c_ptr ||= (@c_address && ::FFI::Pointer.new(@c_address))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The Integer twin of #c_ptr.
|
|
68
|
+
def c_address
|
|
69
|
+
@c_address
|
|
70
|
+
end
|
|
71
|
+
|
|
60
72
|
def wrapper_cache
|
|
61
73
|
@wrapper_cache ||= {}
|
|
62
74
|
end
|
|
@@ -70,6 +82,15 @@ class Leptris::XML::Document
|
|
|
70
82
|
# ParseOptions.new here would only discover flags==0 (#187 —
|
|
71
83
|
# per-call overhead is most of a small-document parse).
|
|
72
84
|
if options.nil? && !recover
|
|
85
|
+
# TODO.perf/35: parse + wrapper + lifetime handle in one C
|
|
86
|
+
# dispatch; failure raises through the shared path below.
|
|
87
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
88
|
+
doc = Leptris::XML::Native.parse_binding_document(xml)
|
|
89
|
+
raise Leptris::XML::ParseError,
|
|
90
|
+
"leptris_parse_string failed: " +
|
|
91
|
+
Leptris::XML::FFI.leptris_last_error.to_s if doc.nil?
|
|
92
|
+
return doc.tap { |d| d.readonly! if readonly }
|
|
93
|
+
end
|
|
73
94
|
raw = Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, nil)
|
|
74
95
|
if raw.null?
|
|
75
96
|
raise Leptris::XML::ParseError,
|
|
@@ -299,8 +320,7 @@ class Leptris::XML::Document
|
|
|
299
320
|
# dispatch (the FFI call plus the c_ptr materialization fed
|
|
300
321
|
# ~5% of a fresh-doc build).
|
|
301
322
|
if defined?(Leptris::XML::NATIVE_FAST)
|
|
302
|
-
Leptris::XML::
|
|
303
|
-
Leptris::XML::Native.set_binding_root(self, element.c_address))
|
|
323
|
+
Leptris::XML::Native.set_binding_root(self, element.c_address)
|
|
304
324
|
else
|
|
305
325
|
Leptris::XML::FFI.check_status(
|
|
306
326
|
Leptris::XML::FFI.leptris_document_set_root(c_ptr, element.c_ptr))
|
|
@@ -9,12 +9,32 @@ class Leptris::XML::DocumentFragment
|
|
|
9
9
|
|
|
10
10
|
attr_reader :document, :c_ptr
|
|
11
11
|
|
|
12
|
-
def initialize(document, c_ptr)
|
|
12
|
+
def initialize(document, c_ptr, c_address: nil)
|
|
13
13
|
@document = document
|
|
14
14
|
@c_ptr = c_ptr
|
|
15
|
+
# TODO.perf/34: the address may arrive before any Pointer
|
|
16
|
+
# exists (the C parse face returns an Integer).
|
|
17
|
+
@c_address = c_address || c_ptr&.address
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Integer twin — reading it never materializes a Pointer.
|
|
21
|
+
def c_address
|
|
22
|
+
@c_address
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def c_ptr
|
|
26
|
+
@c_ptr ||= ::FFI::Pointer.new(@c_address)
|
|
15
27
|
end
|
|
16
28
|
|
|
17
29
|
def self.parse(xml, document)
|
|
30
|
+
# TODO.perf/34: the C face answers the fragment address with
|
|
31
|
+
# no status MemoryPointer and no Pointer mint; failures fall
|
|
32
|
+
# back to the FFI pair for the exact error.
|
|
33
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
34
|
+
addr = Leptris::XML::Native.parse_fragment_addr(
|
|
35
|
+
document, xml.to_s)
|
|
36
|
+
return new(document, nil, c_address: addr) if addr != false
|
|
37
|
+
end
|
|
18
38
|
raw, status = Leptris::XML::FFI.parse_fragment_with_status(
|
|
19
39
|
xml.to_s, document.c_ptr)
|
|
20
40
|
if raw.null?
|
|
@@ -25,6 +45,11 @@ class Leptris::XML::DocumentFragment
|
|
|
25
45
|
end
|
|
26
46
|
|
|
27
47
|
def children
|
|
48
|
+
# TODO.perf/34: one C pass (fragment nodes live in the
|
|
49
|
+
# document pool — the document's cache and version apply).
|
|
50
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
51
|
+
return Leptris::XML::Native.bulk_children(@document, @c_address)
|
|
52
|
+
end
|
|
28
53
|
pointers, kinds = Leptris::XML::FFI.fetch_children(c_ptr)
|
|
29
54
|
nodes = Array.new(pointers.size) do |i|
|
|
30
55
|
Leptris::XML::Node.wrap(pointers[i], @document, node_type: kinds[i])
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -25,9 +25,8 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
25
25
|
|
|
26
26
|
def name=(new_name)
|
|
27
27
|
if @native_fast
|
|
28
|
-
Leptris::XML::
|
|
29
|
-
|
|
30
|
-
@document, @c_address, new_name))
|
|
28
|
+
Leptris::XML::Native.set_binding_name(
|
|
29
|
+
@document, @c_address, new_name)
|
|
31
30
|
return @name = new_name
|
|
32
31
|
end
|
|
33
32
|
ensure_writable!
|
|
@@ -54,9 +53,8 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
54
53
|
|
|
55
54
|
def content=(new_content)
|
|
56
55
|
if @native_fast
|
|
57
|
-
Leptris::XML::
|
|
58
|
-
|
|
59
|
-
@document, @c_address, new_content.to_s))
|
|
56
|
+
Leptris::XML::Native.set_binding_text(
|
|
57
|
+
@document, @c_address, new_content.to_s)
|
|
60
58
|
@content = new_content.to_s
|
|
61
59
|
@content_version = @document.version if @document
|
|
62
60
|
return new_content
|
|
@@ -138,9 +136,8 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
138
136
|
# set in one dispatch; the bump drops the version-stamped
|
|
139
137
|
# attribute memos on both surfaces.
|
|
140
138
|
if native_fast_children?
|
|
141
|
-
Leptris::XML::
|
|
142
|
-
|
|
143
|
-
@document, @c_address, key.to_s, value.to_s))
|
|
139
|
+
Leptris::XML::Native.set_binding_attribute(
|
|
140
|
+
@document, @c_address, key.to_s, value.to_s)
|
|
144
141
|
seed_attribute_memo(key.to_s, value.to_s)
|
|
145
142
|
return value
|
|
146
143
|
end
|
|
@@ -374,10 +371,8 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
374
371
|
# bump + engine insert in one dispatch; Qnil = the child needs
|
|
375
372
|
# the namespace lift — the full path below handles it.
|
|
376
373
|
if native_fast_children?
|
|
377
|
-
|
|
378
|
-
@document, @c_address, node.c_ptr.address, 1)
|
|
379
|
-
unless st.nil?
|
|
380
|
-
Leptris::XML::FFI.check_status(st)
|
|
374
|
+
unless Leptris::XML::Native.insert_binding_child(
|
|
375
|
+
@document, @c_address, node.c_ptr.address, 1).nil?
|
|
381
376
|
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
382
377
|
return node
|
|
383
378
|
end
|
|
@@ -397,10 +392,8 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
397
392
|
# bump + engine insert in one dispatch; Qnil = the child needs
|
|
398
393
|
# the namespace lift — the full path below handles it.
|
|
399
394
|
if native_fast_children?
|
|
400
|
-
|
|
401
|
-
@document, @c_address, node.c_ptr.address, 2)
|
|
402
|
-
unless st.nil?
|
|
403
|
-
Leptris::XML::FFI.check_status(st)
|
|
395
|
+
unless Leptris::XML::Native.insert_binding_child(
|
|
396
|
+
@document, @c_address, node.c_ptr.address, 2).nil?
|
|
404
397
|
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
405
398
|
return node
|
|
406
399
|
end
|
|
@@ -420,10 +413,8 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
420
413
|
# bump + engine insert in one dispatch; Qnil = the child needs
|
|
421
414
|
# the namespace lift — the full path below handles it.
|
|
422
415
|
if native_fast_children?
|
|
423
|
-
|
|
424
|
-
@document, @c_address, node.c_ptr.address, 3)
|
|
425
|
-
unless st.nil?
|
|
426
|
-
Leptris::XML::FFI.check_status(st)
|
|
416
|
+
unless Leptris::XML::Native.insert_binding_child(
|
|
417
|
+
@document, @c_address, node.c_ptr.address, 3).nil?
|
|
427
418
|
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
428
419
|
return node
|
|
429
420
|
end
|
|
@@ -539,10 +530,10 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
539
530
|
# means the child needs the namespace lift — fall through
|
|
540
531
|
# to the full path.
|
|
541
532
|
if native_fast_children?
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
unless
|
|
545
|
-
|
|
533
|
+
# The face raises on failure (TODO.perf/36); Qnil = the
|
|
534
|
+
# child needs the lift — the full path handles it.
|
|
535
|
+
unless Leptris::XML::Native.append_binding_child(
|
|
536
|
+
@document, @c_address, node_or_markup.c_ptr.address).nil?
|
|
546
537
|
Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
|
|
547
538
|
return node_or_markup
|
|
548
539
|
end
|
|
@@ -555,6 +546,23 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
555
546
|
Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
|
|
556
547
|
node_or_markup
|
|
557
548
|
when String
|
|
549
|
+
# TODO.perf/34: one dispatch parses the markup and appends
|
|
550
|
+
# every fragment child (single gate + bump). Negative
|
|
551
|
+
# returns: -1 re-runs the FFI path for the exact parse
|
|
552
|
+
# error; -1000-st routes through check_status.
|
|
553
|
+
if native_fast_children?
|
|
554
|
+
r = Leptris::XML::Native.append_markup(
|
|
555
|
+
@document, @c_address, node_or_markup)
|
|
556
|
+
if r != -1
|
|
557
|
+
# Appends raise in C on status failures (TODO.perf/36);
|
|
558
|
+
# -1 alone means the parse failed.
|
|
559
|
+
return Leptris::XML::NodeSet.new(@document, []) if r.zero?
|
|
560
|
+
return Leptris::XML::NodeSet.new(@document, last_added(r))
|
|
561
|
+
end
|
|
562
|
+
# parse failure: reproduce the exact error via the legacy
|
|
563
|
+
# path (it re-fails fast)
|
|
564
|
+
Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
|
|
565
|
+
end
|
|
558
566
|
frag = Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
|
|
559
567
|
added = []
|
|
560
568
|
frag.children.each do |n|
|
|
@@ -567,6 +575,14 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
567
575
|
raise ArgumentError, "add_child expects a Node or String, got #{node_or_markup.class}"
|
|
568
576
|
end
|
|
569
577
|
end
|
|
578
|
+
# The markup-append face appends in fragment order; the added
|
|
579
|
+
# nodes are this element's last +count+ children.
|
|
580
|
+
def last_added(count)
|
|
581
|
+
kids = children.to_a
|
|
582
|
+
kids.last(count)
|
|
583
|
+
end
|
|
584
|
+
private :last_added
|
|
585
|
+
|
|
570
586
|
alias_method :<<, :add_child
|
|
571
587
|
|
|
572
588
|
def namespace
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -486,8 +486,7 @@ class Leptris::XML::Node
|
|
|
486
486
|
|
|
487
487
|
def unlink
|
|
488
488
|
if @native_fast
|
|
489
|
-
Leptris::XML::
|
|
490
|
-
Leptris::XML::Native.unlink_binding_node(@document, @c_address))
|
|
489
|
+
Leptris::XML::Native.unlink_binding_node(@document, @c_address)
|
|
491
490
|
@parent = nil
|
|
492
491
|
return self
|
|
493
492
|
end
|
|
@@ -55,6 +55,10 @@ module Leptris::XML::Searchable
|
|
|
55
55
|
expr = paths.one? && paths.first.is_a?(String) ? paths.first
|
|
56
56
|
: paths.join(" | ")
|
|
57
57
|
|
|
58
|
+
# TODO.perf/37: the document's Pointer is lazy (materializes
|
|
59
|
+
# once per document lifetime, not per parse); the ffi gem
|
|
60
|
+
# does NOT convert Integers for typedef'd pointer params, so
|
|
61
|
+
# the Pointer object is still what crosses here.
|
|
58
62
|
doc_ptr = is_a?(Leptris::XML::Document) ? c_ptr : document.c_ptr
|
|
59
63
|
context_ptr = is_a?(Leptris::XML::Document) ? nil : c_ptr
|
|
60
64
|
|
|
@@ -100,6 +104,10 @@ module Leptris::XML::Searchable
|
|
|
100
104
|
expr = paths.one? && paths.first.is_a?(String) ? paths.first
|
|
101
105
|
: paths.join(" | ")
|
|
102
106
|
|
|
107
|
+
# TODO.perf/37: the document's Pointer is lazy (materializes
|
|
108
|
+
# once per document lifetime, not per parse); the ffi gem
|
|
109
|
+
# does NOT convert Integers for typedef'd pointer params, so
|
|
110
|
+
# the Pointer object is still what crosses here.
|
|
103
111
|
doc_ptr = is_a?(Leptris::XML::Document) ? c_ptr : document.c_ptr
|
|
104
112
|
context_ptr = is_a?(Leptris::XML::Document) ? nil : c_ptr
|
|
105
113
|
|
|
@@ -32,16 +32,16 @@ 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
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
if defined?(Leptris::XML::NATIVE_FAST)
|
|
35
|
+
# TODO.perf/04+36: the whole sized-buffer cycle in the ext —
|
|
36
|
+
# one C dispatch, no options marshaling, no Ruby buffer
|
|
37
|
+
# management; encoding rides the face (nil -> no override).
|
|
38
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
39
39
|
decl = !no_decl
|
|
40
40
|
return Leptris::XML::Native.fast_element_xml(
|
|
41
|
-
c_ptr.address, indent.to_i, decl) if
|
|
41
|
+
c_ptr.address, indent.to_i, decl, encoding) if
|
|
42
42
|
ffi_function == ELEMENT_SERIALIZE_INTO
|
|
43
43
|
return Leptris::XML::Native.fast_document_xml(
|
|
44
|
-
c_ptr.address, indent.to_i, decl) if
|
|
44
|
+
c_ptr.address, indent.to_i, decl, encoding) if
|
|
45
45
|
ffi_function == DOCUMENT_SERIALIZE_INTO
|
|
46
46
|
end
|
|
47
47
|
opts =
|
|
@@ -59,7 +59,8 @@ module Leptris::XML::Serialization
|
|
|
59
59
|
# loop: no kwargs, no options rebuild, no method allocation —
|
|
60
60
|
# one FFI fast-path dispatch per child.
|
|
61
61
|
def self.element_xml_default(c_ptr)
|
|
62
|
-
return Leptris::XML::Native.fast_element_xml(c_ptr.address, 0, true
|
|
62
|
+
return Leptris::XML::Native.fast_element_xml(c_ptr.address, 0, true,
|
|
63
|
+
nil) if
|
|
63
64
|
defined?(Leptris::XML::NATIVE_FAST)
|
|
64
65
|
Leptris::XML::FFI.serialize_into_string(
|
|
65
66
|
ELEMENT_SERIALIZE_INTO, c_ptr, DEFAULT_OPTIONS.pointer)
|
data/lib/leptris/xml/text.rb
CHANGED
|
@@ -20,9 +20,8 @@ class Leptris::XML::Text < Leptris::XML::Node
|
|
|
20
20
|
|
|
21
21
|
def content=(new_content)
|
|
22
22
|
if @native_fast
|
|
23
|
-
Leptris::XML::
|
|
24
|
-
|
|
25
|
-
@document, @c_address, new_content.to_s))
|
|
23
|
+
Leptris::XML::Native.set_binding_text(
|
|
24
|
+
@document, @c_address, new_content.to_s)
|
|
26
25
|
return new_content
|
|
27
26
|
end
|
|
28
27
|
ensure_writable!
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: leptris
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.9.174.
|
|
4
|
+
version: 1.9.174.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -102,6 +102,10 @@ files:
|
|
|
102
102
|
- TODO.perf/31-battery-rounds-4-8.md
|
|
103
103
|
- TODO.perf/32-clean-host-floor-table.md
|
|
104
104
|
- TODO.perf/33-cbound-root-set.md
|
|
105
|
+
- TODO.perf/34-fragment-fast-lane.md
|
|
106
|
+
- TODO.perf/35-parse-default-c-face.md
|
|
107
|
+
- TODO.perf/36-raise-in-c-serializer-encoding.md
|
|
108
|
+
- TODO.perf/37-document-lazy-pointer.md
|
|
105
109
|
- TODO.restructure/01-constraint-compliance-audit.md
|
|
106
110
|
- TODO.restructure/02-deep-copy-seam.md
|
|
107
111
|
- TODO.restructure/03-evaluation-context-seam.md
|