leptris 1.9.311.1 → 1.9.311.3
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 +35 -0
- data/ext/leptris/native/native.c +26 -2
- data/lib/leptris/vendor/arm64-darwin/libleptris.dylib +0 -0
- data/lib/leptris/vendor/x86_64-linux-musl/libleptris.so +0 -0
- data/lib/leptris/version.rb +1 -1
- data/lib/leptris/xml/document.rb +12 -2
- data/lib/leptris/xml/element.rb +7 -10
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a004384f7927ff6e71fd0277c1920ca6890a69c9dee52f36f267c85807cf8b0b
|
|
4
|
+
data.tar.gz: a02220a1a36d2016d947f35065d204a92811fc99559b8e5d596f0c5278d4089a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f599ca78c88649f4778c81644947dc939fcbfe2db06179f4b7785d62bb46ce3ea4fc86ccf6b589d5f83b06ea1ab3e0686a1427fba31ad27ca7b67c39b6993a2
|
|
7
|
+
data.tar.gz: d1030e40b0903052901e5a245b17531f44ad7ee718daefc303644e7bebc630debea0594bd930273fdcaabe0c9745951263965706e54fdf5ffb583f2bc58bd4e1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
+
## [1.9.311.3] - 2026-10-06
|
|
2
|
+
|
|
3
|
+
### Performance
|
|
4
|
+
|
|
5
|
+
- **Same-document splices stop paying the cross-document sweep
|
|
6
|
+
(#374, the fresh-doc 0.80x follow-up)**: every fast splice path
|
|
7
|
+
is reached only after `foreign_to?` said no — the child's
|
|
8
|
+
document IS this one — so the post-splice
|
|
9
|
+
`invalidate_cross_document!` (a method call plus two ivar reads
|
|
10
|
+
that always returned at the equal check) was dead weight on the
|
|
11
|
+
hottest construction loop. Removed from the add_child /
|
|
12
|
+
prepend_child / sibling-insert fast paths and their FFI tails
|
|
13
|
+
(the foreign and batch paths keep their sweeps); `add_child` no
|
|
14
|
+
longer runs the Ruby-side readonly/version gate twice when the
|
|
15
|
+
C face self-gates. Measured under load with interleaved
|
|
16
|
+
CPU-time medians (3 rounds, 15k appends): add_child 1.69 →
|
|
17
|
+
1.41 µs/call (~16%), programmatic build ~14%.
|
|
18
|
+
|
|
19
|
+
## [1.9.311.2] - 2026-10-06
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Absorbed sources alive at exit no longer double-free the pool
|
|
24
|
+
(#386)**: `Document#absorb`'s handle-only contract was enforced
|
|
25
|
+
only by the explicit `#free` — the C lifetime handle's dfree
|
|
26
|
+
and the no-native Ruby finalizer still called
|
|
27
|
+
`leptris_document_free`, destroying a pool already owned (and
|
|
28
|
+
released) by the absorber. Libraries that pin adopted-source
|
|
29
|
+
documents (moxml's lifetime pins) aborted at exit-time
|
|
30
|
+
finalization (`leptris_pool_destroy` → `free_small_botch`). The
|
|
31
|
+
absorbed flag now lives where every release path reads it: the
|
|
32
|
+
shared `Freed` struct (Ruby finalizer) and the C handle
|
|
33
|
+
(`doc_handle_mark_absorbed`; dfree is handle-only when the pool
|
|
34
|
+
moved). Both paths pinned by subprocess regression specs.
|
|
35
|
+
|
|
1
36
|
## [1.9.311.1] - 2026-10-06
|
|
2
37
|
|
|
3
38
|
### Fixed
|
data/ext/leptris/native/native.c
CHANGED
|
@@ -2682,15 +2682,21 @@ static VALUE nf_fast_inner_xml(VALUE self, VALUE addr)
|
|
|
2682
2682
|
* Ruby-finalizer path (which stays for LEPTRIS_NO_NATIVE). */
|
|
2683
2683
|
struct doc_handle {
|
|
2684
2684
|
void *doc;
|
|
2685
|
+
/* Pool ownership transferred to an absorber (engine #1548):
|
|
2686
|
+
* the destination's free performs the real release, so this
|
|
2687
|
+
* handle's dfree is HANDLE-ONLY (#386: malloc_zone_error /
|
|
2688
|
+
* free_small_botch when an absorbed source survived to
|
|
2689
|
+
* exit-time finalization after its destination's release). */
|
|
2690
|
+
int absorbed;
|
|
2685
2691
|
};
|
|
2686
2692
|
|
|
2687
2693
|
static void dh_free(void *p)
|
|
2688
2694
|
{
|
|
2689
2695
|
struct doc_handle *h = p;
|
|
2690
|
-
if (h->doc) {
|
|
2696
|
+
if (h->doc && !h->absorbed) {
|
|
2691
2697
|
f_doc_free(h->doc);
|
|
2692
|
-
h->doc = NULL;
|
|
2693
2698
|
}
|
|
2699
|
+
h->doc = NULL;
|
|
2694
2700
|
}
|
|
2695
2701
|
|
|
2696
2702
|
static size_t dh_size(const void *p)
|
|
@@ -2888,6 +2894,22 @@ static VALUE nf_doc_handle_release(VALUE self, VALUE document)
|
|
|
2888
2894
|
return Qnil;
|
|
2889
2895
|
}
|
|
2890
2896
|
|
|
2897
|
+
/* Absorption bookkeeping (#386, engine #1548): Document#absorb
|
|
2898
|
+
* calls this on the SOURCE right after the engine transfers pool
|
|
2899
|
+
* ownership — the handle's dfree becomes handle-only, matching
|
|
2900
|
+
* the Ruby-side Freed flag the no-native finalizer reads. */
|
|
2901
|
+
static VALUE nf_doc_handle_mark_absorbed(VALUE self, VALUE document)
|
|
2902
|
+
{
|
|
2903
|
+
VALUE handle = rb_ivar_get(document, id_iv_doc_handle);
|
|
2904
|
+
(void)self;
|
|
2905
|
+
if (handle != Qnil) {
|
|
2906
|
+
struct doc_handle *h;
|
|
2907
|
+
TypedData_Get_Struct(handle, struct doc_handle, &dh_type, h);
|
|
2908
|
+
h->absorbed = 1;
|
|
2909
|
+
}
|
|
2910
|
+
return Qnil;
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2891
2913
|
/* The full Document.create in one dispatch: engine create,
|
|
2892
2914
|
* binding wrapper (ivar-seeded, bypassing initialize), and the
|
|
2893
2915
|
* lifetime handle. Returns Qnil when the engine refuses. */
|
|
@@ -3158,6 +3180,8 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
|
|
|
3158
3180
|
nf_doc_handle_attach, 1);
|
|
3159
3181
|
rb_define_module_function(m_native, "doc_handle_release",
|
|
3160
3182
|
nf_doc_handle_release, 1);
|
|
3183
|
+
rb_define_module_function(m_native, "doc_handle_mark_absorbed",
|
|
3184
|
+
nf_doc_handle_mark_absorbed, 1);
|
|
3161
3185
|
rb_define_module_function(m_native, "create_binding_document",
|
|
3162
3186
|
nf_create_binding_document, 0);
|
|
3163
3187
|
c_doc_handle = rb_define_class_under(m_xml, "DocHandle", rb_cObject);
|
|
Binary file
|
|
Binary file
|
data/lib/leptris/version.rb
CHANGED
data/lib/leptris/xml/document.rb
CHANGED
|
@@ -11,7 +11,9 @@ class Leptris::XML::Document
|
|
|
11
11
|
# the explicit `free` path and the finalizer. This eliminates the
|
|
12
12
|
# double-free that FFI::AutoPointer's release proc caused when
|
|
13
13
|
# `Document#free` was called explicitly and then GC ran.
|
|
14
|
-
|
|
14
|
+
# +absorbed+ (engine #1548, #386): pool ownership moved to an
|
|
15
|
+
# absorber — the finalizer must never call the engine free.
|
|
16
|
+
Freed = Struct.new(:state, :absorbed) # state: :alive | :freed
|
|
15
17
|
|
|
16
18
|
# Mutation version: advanced by every data mutation (via
|
|
17
19
|
# Node#ensure_writable!, root=, add_pi). Node memos stamp the
|
|
@@ -286,7 +288,7 @@ class Leptris::XML::Document
|
|
|
286
288
|
|
|
287
289
|
def self.finalizer(address, freed)
|
|
288
290
|
proc do
|
|
289
|
-
next if freed.state == :freed
|
|
291
|
+
next if freed.state == :freed || freed.absorbed
|
|
290
292
|
freed.state = :freed
|
|
291
293
|
Leptris::XML::FFI.leptris_document_free(::FFI::Pointer.new(address))
|
|
292
294
|
end
|
|
@@ -591,8 +593,16 @@ class Leptris::XML::Document
|
|
|
591
593
|
# objects by hand): the absorbed document's #free skips the
|
|
592
594
|
# engine release, and splice seams skip source-side removals
|
|
593
595
|
# because absorbed splices MOVE the node (nothing to remove).
|
|
596
|
+
# The flag is mirrored into every release path (#386): the
|
|
597
|
+
# shared Freed struct (the no-native Ruby finalizer reads it)
|
|
598
|
+
# and the C lifetime handle (dh_free must be handle-only when
|
|
599
|
+
# the pool belongs to the absorber).
|
|
594
600
|
def mark_absorbed_into(destination)
|
|
595
601
|
@absorbed_into = destination
|
|
602
|
+
@freed.absorbed = true
|
|
603
|
+
if defined?(Leptris::XML::NATIVE_FAST)
|
|
604
|
+
Leptris::XML::Native.doc_handle_mark_absorbed(self)
|
|
605
|
+
end
|
|
596
606
|
end
|
|
597
607
|
|
|
598
608
|
def absorbed_into?(destination)
|
data/lib/leptris/xml/element.rb
CHANGED
|
@@ -446,7 +446,6 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
446
446
|
if native_fast_children?
|
|
447
447
|
unless Leptris::XML::Native.insert_binding_child(
|
|
448
448
|
@document, @c_address, node.c_ptr.address, 1).nil?
|
|
449
|
-
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
450
449
|
return node
|
|
451
450
|
end
|
|
452
451
|
end
|
|
@@ -456,7 +455,6 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
456
455
|
end
|
|
457
456
|
Leptris::XML::FFI.check_status(
|
|
458
457
|
Leptris::XML::FFI.leptris_element_prepend_child(c_ptr, node.c_ptr))
|
|
459
|
-
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
460
458
|
node
|
|
461
459
|
end
|
|
462
460
|
|
|
@@ -481,7 +479,6 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
481
479
|
if native_fast_children?
|
|
482
480
|
unless Leptris::XML::Native.insert_binding_child(
|
|
483
481
|
@document, @c_address, node.c_ptr.address, 2).nil?
|
|
484
|
-
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
485
482
|
return node
|
|
486
483
|
end
|
|
487
484
|
end
|
|
@@ -491,7 +488,6 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
491
488
|
end
|
|
492
489
|
Leptris::XML::FFI.check_status(
|
|
493
490
|
Leptris::XML::FFI.leptris_element_insert_after(c_ptr, node.c_ptr))
|
|
494
|
-
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
495
491
|
node
|
|
496
492
|
end
|
|
497
493
|
|
|
@@ -516,7 +512,6 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
516
512
|
if native_fast_children?
|
|
517
513
|
unless Leptris::XML::Native.insert_binding_child(
|
|
518
514
|
@document, @c_address, node.c_ptr.address, 3).nil?
|
|
519
|
-
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
520
515
|
return node
|
|
521
516
|
end
|
|
522
517
|
end
|
|
@@ -526,7 +521,6 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
526
521
|
end
|
|
527
522
|
Leptris::XML::FFI.check_status(
|
|
528
523
|
Leptris::XML::FFI.leptris_element_insert_before(c_ptr, node.c_ptr))
|
|
529
|
-
Leptris::XML::Node.invalidate_cross_document!(node, @document)
|
|
530
524
|
node
|
|
531
525
|
end
|
|
532
526
|
|
|
@@ -672,7 +666,6 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
672
666
|
end
|
|
673
667
|
|
|
674
668
|
def add_child(node_or_markup)
|
|
675
|
-
ensure_writable!
|
|
676
669
|
case node_or_markup
|
|
677
670
|
when Leptris::XML::Node
|
|
678
671
|
# Foreign-child adoption (1.9.304, #1528): the engine
|
|
@@ -686,22 +679,24 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
686
679
|
# readonly/liveness gates, the provable no-op lift
|
|
687
680
|
# predicate, the version bump, and the engine append. Qnil
|
|
688
681
|
# means the child needs the namespace lift — fall through
|
|
689
|
-
# to the full path.
|
|
682
|
+
# to the full path. Same-document children cannot reach
|
|
683
|
+
# here from another document (foreign ones routed above),
|
|
684
|
+
# so there is no cross-document sweep to run (#374): the
|
|
685
|
+
# C-side version bump invalidates this document's memos.
|
|
690
686
|
if native_fast_children?
|
|
691
687
|
# The face raises on failure (TODO.perf/36); Qnil = the
|
|
692
688
|
# child needs the lift — the full path handles it.
|
|
693
689
|
unless Leptris::XML::Native.append_binding_child(
|
|
694
690
|
@document, @c_address, node_or_markup.c_ptr.address).nil?
|
|
695
|
-
Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
|
|
696
691
|
return node_or_markup
|
|
697
692
|
end
|
|
698
693
|
end
|
|
694
|
+
ensure_writable!
|
|
699
695
|
unless Leptris::XML::Element.skip_adoption_lift?(node_or_markup)
|
|
700
696
|
Leptris::XML::Element.lift_namespaces_for_adoption(node_or_markup, namespaces)
|
|
701
697
|
end
|
|
702
698
|
Leptris::XML::FFI.check_status(
|
|
703
699
|
Leptris::XML::FFI.leptris_element_append_child(c_ptr, node_or_markup.c_ptr))
|
|
704
|
-
Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
|
|
705
700
|
node_or_markup
|
|
706
701
|
when String
|
|
707
702
|
# TODO.perf/34: one dispatch parses the markup and appends
|
|
@@ -721,6 +716,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
721
716
|
# path (it re-fails fast)
|
|
722
717
|
Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
|
|
723
718
|
end
|
|
719
|
+
ensure_writable!
|
|
724
720
|
frag = Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
|
|
725
721
|
added = []
|
|
726
722
|
frag.children.each do |n|
|
|
@@ -741,6 +737,7 @@ class Leptris::XML::Element < Leptris::XML::Node
|
|
|
741
737
|
# original is removed after (the source loses the node, as
|
|
742
738
|
# every pre-304 caller observed).
|
|
743
739
|
def append_installed(node)
|
|
740
|
+
ensure_writable!
|
|
744
741
|
unless Leptris::XML::Element.skip_adoption_lift?(node)
|
|
745
742
|
Leptris::XML::Element.lift_namespaces_for_adoption(node, namespaces)
|
|
746
743
|
end
|