leptris 1.9.174.4 → 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: 3052fa875df89194fa904610e20786deaf2076de881242382afe9bbc9fd7fa0d
4
- data.tar.gz: 86806a382948d948d2ec5c2550302a8ae52b976edd224aaff4c130f3a4606f3e
3
+ metadata.gz: ec76aed6663e2ae97d677c09e8b05486eb6e0543da0f92640a81a87d49c38565
4
+ data.tar.gz: bd881b0e87ff21939925f879abfced736f304a5396f675013a2bc8e2f220a89c
5
5
  SHA512:
6
- metadata.gz: 81e9fe81a94ddd6d82ef044569b8d43ff5c663ca41cf2a59798660638e4f73cfc23f7aabf4eb3123757e3590d0d7524438f378c6b1bd447a2b39af95bb0089d5
7
- data.tar.gz: e161ced4c4ab3634ae6a4fa6ebb0e95e970e1634b72a9d1b4ef5e1ad3800461170a2893bb89477c404f22b992813b7abf66abc083272e851c341affc699488fc
6
+ metadata.gz: f9f8f54bca5c16e6f1068a9988374ab2a852febc8f4f8695fa7cf0e5bb21fed700bf5b7a34b0732693d55f520000528ed35719dc2488fdf3afb3b6224d2dce9d
7
+ data.tar.gz: a883b80b556feaa34abc6544fe7b4c5f03d6da46597c6dc3170d1219ab66aa4018be3233f824ae6df551c50dc963974a35f72ea715037d596a26259bc8087d8a
data/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ 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
+
8
25
  ## [1.9.174.4] - 2026-09-15
9
26
 
10
27
  ### Changed
@@ -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).
@@ -874,6 +874,7 @@ static VALUE nn_address(VALUE self)
874
874
  static VALUE c_b_element = Qundef, c_b_text, c_b_comment, c_b_cdata,
875
875
  c_b_pi, c_b_node, c_b_result_text, c_b_result_attr,
876
876
  c_b_attr, c_ffi_pointer, c_b_document, c_b_freed;
877
+ static VALUE c_iteration_scope;
877
878
  static ID id_ptr_new;
878
879
 
879
880
  /* Binding classes resolve LAZILY: Init_native can run before the
@@ -895,6 +896,7 @@ static void resolve_binding_classes(void)
895
896
  c_b_attr = rb_path2class("Leptris::XML::Attr");
896
897
  c_b_document = rb_path2class("Leptris::XML::Document");
897
898
  c_b_freed = rb_path2class("Leptris::XML::Document::Freed");
899
+ c_iteration_scope = rb_path2class("Leptris::XML::IterationScope");
898
900
  id_ptr_new = rb_intern("new");
899
901
  rb_gc_register_mark_object(c_b_element);
900
902
  rb_gc_register_mark_object(c_b_text);
@@ -908,6 +910,7 @@ static void resolve_binding_classes(void)
908
910
  rb_gc_register_mark_object(c_b_attr);
909
911
  rb_gc_register_mark_object(c_b_document);
910
912
  rb_gc_register_mark_object(c_b_freed);
913
+ rb_gc_register_mark_object(c_iteration_scope);
911
914
  }
912
915
 
913
916
  static VALUE binding_klass_for(int kind)
@@ -951,9 +954,20 @@ static VALUE bulk_children_impl(VALUE document, VALUE parent_addr,
951
954
  rb_iv_set(node, "@c_address", key);
952
955
  rb_iv_set(node, "@document", document);
953
956
  rb_iv_set(node, "@parent", Qnil);
954
- rb_iv_set(node, "@structure_memoizable", Qtrue);
955
- 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);
956
968
  rb_iv_set(node, "@pub_document", document);
969
+ }
970
+ rb_iv_set(node, "@addr_reads_fast", Qtrue);
957
971
  rb_iv_set(node, "@node_type", INT2FIX(kinds[i]));
958
972
  rb_hash_aset(cache, key, node);
959
973
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.174.4"
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)
@@ -40,7 +40,7 @@ class Leptris::XML::Element < Leptris::XML::Node
40
40
  def content
41
41
  return @content if memo_hit?(@content_version)
42
42
  ensure_alive!
43
- result = if native_fast?
43
+ result = if @addr_reads_fast
44
44
  Leptris::XML::Native.fast_element_text(@c_address)
45
45
  else
46
46
  Leptris::XML::FFI.leptris_element_text(c_ptr)
@@ -57,11 +57,15 @@ class Leptris::XML::Element < Leptris::XML::Node
57
57
  Leptris::XML::FFI.check_status(
58
58
  Leptris::XML::Native.set_binding_text(
59
59
  @document, @c_address, new_content.to_s))
60
+ @content = new_content.to_s
61
+ @content_version = @document.version if @document
60
62
  return new_content
61
63
  end
62
64
  ensure_writable!
63
65
  Leptris::XML::FFI.check_status(
64
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
65
69
  new_content
66
70
  end
67
71
 
@@ -91,7 +95,7 @@ class Leptris::XML::Element < Leptris::XML::Node
91
95
  # engine call resolves it, then the name is cached.
92
96
  return v if !v.nil? || @attributes
93
97
  ensure_alive!
94
- v = if native_fast?
98
+ v = if @addr_reads_fast
95
99
  Leptris::XML::Native.fast_attribute(@c_address, name)
96
100
  else
97
101
  Leptris::XML::FFI.leptris_element_attribute(c_ptr, name)
@@ -104,7 +108,7 @@ class Leptris::XML::Element < Leptris::XML::Node
104
108
  # post-mutation cold [] would stamp the new version onto a
105
109
  # stale @attributes hash and attributes would serve it.
106
110
  ensure_alive!
107
- v = if native_fast?
111
+ v = if @addr_reads_fast
108
112
  Leptris::XML::Native.fast_attribute(@c_address, name)
109
113
  else
110
114
  Leptris::XML::FFI.leptris_element_attribute(c_ptr, name)
@@ -137,15 +141,39 @@ class Leptris::XML::Element < Leptris::XML::Node
137
141
  Leptris::XML::FFI.check_status(
138
142
  Leptris::XML::Native.set_binding_attribute(
139
143
  @document, @c_address, key.to_s, value.to_s))
144
+ seed_attribute_memo(key.to_s, value.to_s)
140
145
  return value
141
146
  end
142
147
  ensure_writable!
143
148
  Leptris::XML::FFI.check_status(
144
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)
145
151
  value
146
152
  end
147
153
  alias_method :set_attribute, :[]=
148
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
+
149
177
  def key?(name)
150
178
  ensure_alive!
151
179
  # Versioned attribute memo first (TODO.perf/21): a hit proves
@@ -36,6 +36,9 @@ class Leptris::XML::Node
36
36
  @structure_memoizable =
37
37
  !document.nil? && !document.is_a?(Leptris::XML::IterationScope)
38
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)
39
42
  # NATIVE_FAST availability is fixed at load time before any
40
43
  # node exists, so the conjunction with document-ownership is
41
44
  # construct-time constant — the hot gates read one ivar.
@@ -164,6 +167,16 @@ class Leptris::XML::Node
164
167
  @native_fast
165
168
  end
166
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
+
167
180
  # Content-defined 64-bit Merkle digest of this subtree
168
181
  # (libleptris 1.9.99, #869): element name/prefix/resolved
169
182
  # namespace URI, attributes sorted and first-wins-deduplicated,
@@ -318,7 +331,7 @@ class Leptris::XML::Node
318
331
  # (leptris_node_children_ex), so no per-child get_type.
319
332
  return @children if memo_hit?(@children_version)
320
333
  ensure_alive!
321
- if native_fast_children?
334
+ if native_bulk_children?
322
335
  # TODO.perf/01 tail: one C pass constructs every binding
323
336
  # wrapper (class dispatch + ivars + identity cache) — the
324
337
  # per-child Ruby wrap frames disappear.
@@ -419,7 +432,7 @@ class Leptris::XML::Node
419
432
  # children are never wrapped (nor their get_type paid — the
420
433
  # ELEMENT hint rides along). Other nodes keep the filter.
421
434
  result =
422
- if native_fast_children?
435
+ if native_bulk_children?
423
436
  Leptris::XML::Native.bulk_element_children(@document, @c_address)
424
437
  elsif is_a?(Leptris::XML::Element)
425
438
  parent = as_element_or_self
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
4
+ version: 1.9.174.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -93,6 +93,8 @@ files:
93
93
  - TODO.perf/22-eager-nodeset-materialization.md
94
94
  - TODO.perf/23-cbound-value-mutations.md
95
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
96
98
  - TODO.restructure/01-constraint-compliance-audit.md
97
99
  - TODO.restructure/02-deep-copy-seam.md
98
100
  - TODO.restructure/03-evaluation-context-seam.md