leptris 1.9.174.3 → 1.9.174.5

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