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 +4 -4
- data/CHANGELOG.md +17 -0
- data/TODO.perf/25-scope-owned-bulk-path.md +39 -0
- data/TODO.perf/26-post-mutation-memo-seeding.md +26 -0
- data/ext/leptris/native/native.c +16 -2
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/element.rb +32 -4
- data/lib/leptris/xml/node.rb +15 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ec76aed6663e2ae97d677c09e8b05486eb6e0543da0f92640a81a87d49c38565
|
|
4
|
+
data.tar.gz: bd881b0e87ff21939925f879abfced736f304a5396f675013a2bc8e2f220a89c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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).
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -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
|
-
|
|
955
|
-
|
|
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
|
}
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
data/lib/leptris/xml/node.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
+
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
|