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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a00351d2185cc9432a2a675feb686a11e616e9bfc4ed930ed5c3737e7eee8cbf
4
- data.tar.gz: 93aa8c74620619e74b7a28a4f83569a2154df36c600edd0f8126cf2129d39bda
3
+ metadata.gz: a004384f7927ff6e71fd0277c1920ca6890a69c9dee52f36f267c85807cf8b0b
4
+ data.tar.gz: a02220a1a36d2016d947f35065d204a92811fc99559b8e5d596f0c5278d4089a
5
5
  SHA512:
6
- metadata.gz: 169251ef7965e3ff58646406f37bbebc1308732d7e5dc3ab400b1e766df5142bde0d887acba8e3eb2eb1b22ef5f71dc934ca76b8f01c7baff96f2ee55ab41872
7
- data.tar.gz: 9e84022d8a35c5135b1fdf7f7b60531c4ba2231f9f462c48b9051de2f268b84bb9a042d2d1298f3e06aa74898364a71c6a25b57010d81c265f44da571112b74a
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
@@ -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);
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.311.1"
4
+ VERSION = "1.9.311.3"
5
5
  end
@@ -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
- Freed = Struct.new(:state) # state: :alive | :freed
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)
@@ -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
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.311.1
4
+ version: 1.9.311.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.