leptris 1.9.174.8 → 1.9.174.9

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: e3bbbd00177ef272738c9d0d9562beb33cc2ed100145bc843785fd56ae0c5f22
4
- data.tar.gz: c47af3b6769fb4e9cb233128df91b36bf9cecae4d23e06b9f94ad60caf9d498a
3
+ metadata.gz: 73130c8bf549007d80bf610c22bc831df382f5fe972316835a6dc806885929c5
4
+ data.tar.gz: 3b6bfb23c72a2ed3532eb64886c9f36603975b5d601aa693f6b3ba821bb19de2
5
5
  SHA512:
6
- metadata.gz: e951276162615da4fdecfd7d500bdd9ddd2a378188190d9edf14341713243f1ce802fa2a129f5afb07d676ce2977e7d0e59e44b474dce656432cdb57d99b5fac
7
- data.tar.gz: 812ae80ffb569ae41fa0734858a8f487f31f3a22689fc221686b801cf1922e69d4331615a678e1dd5a5589c2c464b6fe6ae51c4504b365448ea63a6ad6215628
6
+ metadata.gz: 621d0f62f7869c1a4b0c63212dce8694f51cc1914a3f115551b7e91c1f5b2bc0af9a33dcb2baa4d462fc5a12f72225a228dee59f7e4a7f70ed5173e0afe6932b
7
+ data.tar.gz: 2a389319493ace9979ea863853b75c56149f73dcc5e204c362bd8bdbaacd335d94a58bd9bb3d4ed83f6c83df3331b53549bb462fa19980fb996a8c2798c3fbdc
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.9] - 2026-09-16
9
+
10
+ ### Changed
11
+
12
+ - **Fragment fast lane (TODO.perf/34)**: `add_child(String)` runs
13
+ one C dispatch for the whole markup add — fragment parse +
14
+ every child append + a single readonly gate and version bump
15
+ (the move-during-iteration hazard handled: appending detaches,
16
+ so the walk captures next before each move). The fragment parse
17
+ face returns an address (no status MemoryPointer, no Pointer);
18
+ `DocumentFragment#children` rides the bulk face; parse failures
19
+ fall back to the legacy path for the exact error.
20
+ - **Document.parse default path as one C dispatch (TODO.perf/35)**:
21
+ engine parse + ivar-seeded wrapper + lifetime handle in one
22
+ call — small parses measured 15.6µs → 6.8µs (~2.3x, under host
23
+ load ~100; the shape is self-contained).
24
+
8
25
  ## [1.9.174.8] - 2026-09-16
9
26
 
10
27
  ### Fixed
@@ -0,0 +1,41 @@
1
+ # 34 — Fragment fast lane: C parse, bulk children, one-shot markup append
2
+
3
+ Status: DONE (1.9.174.9)
4
+
5
+ The add_child(String) path — moxml's markup-builder shape — pays
6
+ three layers per call: parse_fragment_with_status allocates a
7
+ MemoryPointer for the status out-param, DocumentFragment#children
8
+ runs the FFI fetch+wrap loop (never moved to the bulk faces), and
9
+ each child appends through its own dispatch.
10
+
11
+ Bind the lane: a C parse face returns the fragment's address
12
+ (Integer — no Pointer, no MemoryPointer); the fragment stores
13
+ @c_address with a lazy c_ptr; children rides the bulk face
14
+ (fragment nodes live in the document pool — the document's cache
15
+ and version discipline apply); and a one-shot append face parses
16
+ the markup, walks the fragment's children, and appends every one
17
+ with a single readonly gate + version bump. Parse failures return
18
+ a marker; the Ruby fallback re-runs the old path for the exact
19
+ error message (the rare path, cheap to re-fail).
20
+
21
+ Gates: add_child(String) semantics identical (multiple top-level
22
+ nodes, text, PIs, namespaces lifted per the Node path); fragment
23
+ children identical through the bulk face; suite both modes.
24
+
25
+ ## Outcome (1.9.174.9)
26
+
27
+ Three layers landed: the fragment parse face answers the
28
+ fragment ADDRESS (no status MemoryPointer, no Pointer mint —
29
+ failures fall back to the FFI pair for the exact error);
30
+ DocumentFragment stores @c_address with a lazy c_ptr and
31
+ children() rides the bulk face (fragment nodes live in the
32
+ document pool — the document's cache and version discipline
33
+ apply); and Element#add_child(String) runs ONE dispatch for the
34
+ whole markup add (parse + per-child append + single gate/bump —
35
+ with the move-during-iteration hazard caught by the suite:
36
+ appending detaches the child, so the walk captures next BEFORE
37
+ each move). -1 re-runs the legacy path for parse errors;
38
+ -1000-st routes through check_status. The markup row needs a
39
+ quiet-host battery number (this box sat at load 100+ today);
40
+ small-parse measured 6.8µs native vs 15.6µs FFI (~2.3x) under
41
+ load 100.
@@ -0,0 +1,24 @@
1
+ # 35 — Document.parse default path as one C dispatch
2
+
3
+ Status: DONE (1.9.174.9)
4
+
5
+ The default parse path (no options, no recover — the adapter
6
+ default) is already allocation-free at the Ruby level (#187),
7
+ but still pays FFI marshaling, the wrap frames, the Freed and
8
+ Pointer constructions, and the handle attach across three Ruby
9
+ layers. One C face runs leptris_parse_string + the full document
10
+ wrapper (ivar-seeded) + lifetime handle and returns it; failure
11
+ raises the same ParseError shape via last_error read in C.
12
+
13
+ Gates: parse defaults identical (errors, readonly flag, root
14
+ access); the options/recover paths untouched; suite both modes.
15
+
16
+ ## Outcome (1.9.174.9)
17
+
18
+ Native.parse_binding_document: leptris_parse_string + the full
19
+ ivar-seeded wrapper + lifetime handle in one dispatch (shared
20
+ build_binding_document helper); Document.parse's default path
21
+ (no options, no recover) routes through it, raising the same
22
+ ParseError shape via last_error on failure. Options/recover
23
+ paths untouched. Measured (load ~100, self-contained shape):
24
+ small parse 15.6µs -> 6.8µs (~2.3x).
@@ -94,6 +94,9 @@ typedef void (*visit_fn)(void *, visit_cb_fn, void *);
94
94
  typedef void (*free_str_fn)(void *);
95
95
  typedef const char *(*node_xpath_fn)(void *);
96
96
  typedef void *(*elem_copy_fn)(void *, void *);
97
+ typedef void *(*parse_str_fn)(const char *, size_t, void *);
98
+ typedef void *(*parse_frag_fn)(const char *, size_t, void *, int *);
99
+ typedef const char *(*last_err_fn)(void);
97
100
  static set_str_fn f_elem_set_name, f_elem_set_text, f_text_set_content;
98
101
  static node_unlink_fn f_node_unlink;
99
102
  static traverse_fn f_node_traverse;
@@ -101,6 +104,9 @@ static visit_fn f_node_visit;
101
104
  static free_str_fn f_free_str;
102
105
  static node_xpath_fn f_node_xpath;
103
106
  static elem_copy_fn f_elem_copy;
107
+ static parse_str_fn f_parse_str;
108
+ static parse_frag_fn f_parse_frag;
109
+ static last_err_fn f_last_err;
104
110
  static set_root_fn f_set_root;
105
111
  static doc_free_fn f_doc_free;
106
112
 
@@ -221,6 +227,9 @@ static void resolve_symbols(const char *lib_path)
221
227
  f_free_str = (free_str_fn)lib_sym(h, "leptris_free_string");
222
228
  f_node_xpath = (node_xpath_fn)lib_sym(h, "leptris_node_get_xpath");
223
229
  f_elem_copy = (elem_copy_fn)lib_sym(h, "leptris_element_copy");
230
+ f_parse_str = (parse_str_fn)lib_sym(h, "leptris_parse_string");
231
+ f_parse_frag = (parse_frag_fn)lib_sym(h, "leptris_parse_fragment");
232
+ f_last_err = (last_err_fn)lib_sym(h, "leptris_last_error");
224
233
  f_set_root = (set_root_fn)lib_sym(h, "leptris_document_set_root");
225
234
  f_doc_free = (doc_free_fn)lib_sym(h, "leptris_document_free");
226
235
  if (!f_elem_name || !f_text_content || !f_attr ||
@@ -233,7 +242,8 @@ static void resolve_symbols(const char *lib_path)
233
242
  !f_pi_data || !f_elem_set_name || !f_elem_set_text ||
234
243
  !f_text_set_content || !f_node_unlink ||
235
244
  !f_node_traverse || !f_node_visit || !f_free_str ||
236
- !f_node_xpath || !f_elem_copy ||
245
+ !f_node_xpath || !f_elem_copy || !f_parse_str ||
246
+ !f_parse_frag || !f_last_err ||
237
247
  !f_doc_free ||
238
248
  !f_elem_prefix || !f_xp_count || !f_xp_nodes_ex ||
239
249
  !f_xp_node_kind || !f_xp_node_name || !f_xp_node_value ||
@@ -1874,6 +1884,102 @@ static VALUE nf_copy_binding_element(VALUE self, VALUE document,
1874
1884
  return new_doc;
1875
1885
  }
1876
1886
 
1887
+ /* Shared document-wrapper construction from a C document
1888
+ * pointer: ivar-seeded wrapper + lifetime handle. Returns the
1889
+ * wrapper. (TODO.perf/35 — the create face inlines the same
1890
+ * steps.) */
1891
+ static VALUE build_binding_document(void *doc)
1892
+ {
1893
+ VALUE new_doc, doc_addr, handle, freed;
1894
+ struct doc_handle *h;
1895
+
1896
+ doc_addr = ULL2NUM((uint64_t)(uintptr_t)doc);
1897
+ new_doc = rb_obj_alloc(c_b_document);
1898
+ rb_iv_set(new_doc, "@c_ptr",
1899
+ rb_funcall(c_ffi_pointer, id_ptr_new, 1, doc_addr));
1900
+ rb_iv_set(new_doc, "@c_address", doc_addr);
1901
+ freed = rb_funcall(c_b_freed, id_freed_new, 1, ID2SYM(id_alive));
1902
+ rb_iv_set(new_doc, "@freed", freed);
1903
+ rb_iv_set(new_doc, "@readonly", Qfalse);
1904
+ rb_iv_set(new_doc, "@version", INT2FIX(0));
1905
+ handle = TypedData_Make_Struct(c_doc_handle, struct doc_handle,
1906
+ &dh_type, h);
1907
+ h->doc = doc;
1908
+ rb_ivar_set(new_doc, id_iv_doc_handle, handle);
1909
+ return new_doc;
1910
+ }
1911
+
1912
+ /* Document.parse default path in one dispatch (TODO.perf/35):
1913
+ * leptris_parse_string + wrapper + handle. Qnil = parse failure
1914
+ * (the caller raises with the same message shape). */
1915
+ static VALUE nf_parse_binding_document(VALUE self, VALUE xml)
1916
+ {
1917
+ void *doc;
1918
+
1919
+ (void)self;
1920
+ resolve_binding_classes();
1921
+ doc = f_parse_str(RSTRING_PTR(StringValue(xml)),
1922
+ (size_t)RSTRING_LEN(StringValue(xml)), NULL);
1923
+ return doc ? build_binding_document(doc) : Qnil;
1924
+ }
1925
+
1926
+ /* Fragment lane (TODO.perf/34): parse the markup against the
1927
+ * document; returns the fragment's ADDRESS, or Qfalse when the
1928
+ * parse fails (status is not surfaced — the caller re-runs the
1929
+ * FFI path for the exact error). */
1930
+ static VALUE nf_parse_fragment_addr(VALUE self, VALUE document,
1931
+ VALUE xml)
1932
+ {
1933
+ void *frag;
1934
+ int status;
1935
+
1936
+ (void)self;
1937
+ resolve_binding_classes();
1938
+ frag = f_parse_frag(RSTRING_PTR(StringValue(xml)),
1939
+ (size_t)RSTRING_LEN(StringValue(xml)),
1940
+ doc_ptr_of(document), &status);
1941
+ return frag ? ULL2NUM((uint64_t)(uintptr_t)frag) : Qfalse;
1942
+ }
1943
+
1944
+ /* One-shot markup append (TODO.perf/34): parse the markup, walk
1945
+ * the fragment's children, append each to the parent — one
1946
+ * readonly gate + version bump for the whole batch. Returns the
1947
+ * appended count; -1 = parse failure; status codes surface as
1948
+ * negatives (-1000 - st) for the caller's check_status path. */
1949
+ static VALUE nf_append_markup(VALUE self, VALUE document,
1950
+ VALUE parent_addr, VALUE xml)
1951
+ {
1952
+ void *frag, *parent, *child, *next_child;
1953
+ int status, count = 0;
1954
+
1955
+ (void)self;
1956
+ resolve_binding_classes();
1957
+ if (NIL_P(rb_ivar_get(document, id_iv_c_address)))
1958
+ rb_raise(c_use_after_free_error,
1959
+ "owning document has been freed");
1960
+ if (rb_ivar_get(document, id_iv_readonly) == Qtrue)
1961
+ rb_raise(c_readonly_error,
1962
+ "document is readonly — mutation attempted");
1963
+ frag = f_parse_frag(RSTRING_PTR(StringValue(xml)),
1964
+ (size_t)RSTRING_LEN(StringValue(xml)),
1965
+ doc_ptr_of(document), &status);
1966
+ if (!frag)
1967
+ return INT2FIX(-1);
1968
+ parent = (void *)(uintptr_t)NUM2ULL(parent_addr);
1969
+ rb_ivar_set(document, id_iv_version,
1970
+ LONG2FIX(FIX2LONG(rb_ivar_get(document, id_iv_version)) + 1));
1971
+ /* Appending DETACHES the child from the fragment — capture
1972
+ * the next sibling before each move or the walk corrupts. */
1973
+ for (child = f_first_child(frag); child != NULL; child = next_child) {
1974
+ next_child = f_next_sibling(child);
1975
+ status = f_append_child(parent, child);
1976
+ if (status != 0)
1977
+ return INT2FIX(-1000 - status);
1978
+ count++;
1979
+ }
1980
+ return INT2FIX(count);
1981
+ }
1982
+
1877
1983
  static VALUE nf_doc_handle_attach(VALUE self, VALUE document)
1878
1984
  {
1879
1985
  struct doc_handle *h;
@@ -2097,6 +2203,12 @@ void Init_native(void)
2097
2203
  nf_copy_binding_element, 2);
2098
2204
  rb_define_module_function(m_native, "set_binding_root",
2099
2205
  nf_set_binding_root, 2);
2206
+ rb_define_module_function(m_native, "parse_binding_document",
2207
+ nf_parse_binding_document, 1);
2208
+ rb_define_module_function(m_native, "parse_fragment_addr",
2209
+ nf_parse_fragment_addr, 2);
2210
+ rb_define_module_function(m_native, "append_markup",
2211
+ nf_append_markup, 3);
2100
2212
  rb_define_module_function(m_native, "fast_inner_xml",
2101
2213
  nf_fast_inner_xml, 1);
2102
2214
  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.8"
4
+ VERSION = "1.9.174.9"
5
5
  end
@@ -70,6 +70,15 @@ class Leptris::XML::Document
70
70
  # ParseOptions.new here would only discover flags==0 (#187 —
71
71
  # per-call overhead is most of a small-document parse).
72
72
  if options.nil? && !recover
73
+ # TODO.perf/35: parse + wrapper + lifetime handle in one C
74
+ # dispatch; failure raises through the shared path below.
75
+ if defined?(Leptris::XML::NATIVE_FAST)
76
+ doc = Leptris::XML::Native.parse_binding_document(xml)
77
+ raise Leptris::XML::ParseError,
78
+ "leptris_parse_string failed: " +
79
+ Leptris::XML::FFI.leptris_last_error.to_s if doc.nil?
80
+ return doc.tap { |d| d.readonly! if readonly }
81
+ end
73
82
  raw = Leptris::XML::FFI.leptris_parse_string(xml, xml.bytesize, nil)
74
83
  if raw.null?
75
84
  raise Leptris::XML::ParseError,
@@ -9,12 +9,32 @@ class Leptris::XML::DocumentFragment
9
9
 
10
10
  attr_reader :document, :c_ptr
11
11
 
12
- def initialize(document, c_ptr)
12
+ def initialize(document, c_ptr, c_address: nil)
13
13
  @document = document
14
14
  @c_ptr = c_ptr
15
+ # TODO.perf/34: the address may arrive before any Pointer
16
+ # exists (the C parse face returns an Integer).
17
+ @c_address = c_address || c_ptr&.address
18
+ end
19
+
20
+ # Integer twin — reading it never materializes a Pointer.
21
+ def c_address
22
+ @c_address
23
+ end
24
+
25
+ def c_ptr
26
+ @c_ptr ||= ::FFI::Pointer.new(@c_address)
15
27
  end
16
28
 
17
29
  def self.parse(xml, document)
30
+ # TODO.perf/34: the C face answers the fragment address with
31
+ # no status MemoryPointer and no Pointer mint; failures fall
32
+ # back to the FFI pair for the exact error.
33
+ if defined?(Leptris::XML::NATIVE_FAST)
34
+ addr = Leptris::XML::Native.parse_fragment_addr(
35
+ document, xml.to_s)
36
+ return new(document, nil, c_address: addr) if addr != false
37
+ end
18
38
  raw, status = Leptris::XML::FFI.parse_fragment_with_status(
19
39
  xml.to_s, document.c_ptr)
20
40
  if raw.null?
@@ -25,6 +45,11 @@ class Leptris::XML::DocumentFragment
25
45
  end
26
46
 
27
47
  def children
48
+ # TODO.perf/34: one C pass (fragment nodes live in the
49
+ # document pool — the document's cache and version apply).
50
+ if defined?(Leptris::XML::NATIVE_FAST)
51
+ return Leptris::XML::Native.bulk_children(@document, @c_address)
52
+ end
28
53
  pointers, kinds = Leptris::XML::FFI.fetch_children(c_ptr)
29
54
  nodes = Array.new(pointers.size) do |i|
30
55
  Leptris::XML::Node.wrap(pointers[i], @document, node_type: kinds[i])
@@ -555,6 +555,25 @@ class Leptris::XML::Element < Leptris::XML::Node
555
555
  Leptris::XML::Node.invalidate_cross_document!(node_or_markup, @document)
556
556
  node_or_markup
557
557
  when String
558
+ # TODO.perf/34: one dispatch parses the markup and appends
559
+ # every fragment child (single gate + bump). Negative
560
+ # returns: -1 re-runs the FFI path for the exact parse
561
+ # error; -1000-st routes through check_status.
562
+ if native_fast_children?
563
+ r = Leptris::XML::Native.append_markup(
564
+ @document, @c_address, node_or_markup)
565
+ if r.is_a?(Integer) && r >= 0
566
+ return Leptris::XML::NodeSet.new(@document, []) if r.zero?
567
+ return Leptris::XML::NodeSet.new(@document, last_added(r))
568
+ end
569
+ if r == -1
570
+ # parse failure: reproduce the exact error via the
571
+ # legacy path (it re-fails fast)
572
+ Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
573
+ else
574
+ Leptris::XML::FFI.check_status(-r - 1000)
575
+ end
576
+ end
558
577
  frag = Leptris::XML::DocumentFragment.parse(node_or_markup, @document)
559
578
  added = []
560
579
  frag.children.each do |n|
@@ -567,6 +586,14 @@ class Leptris::XML::Element < Leptris::XML::Node
567
586
  raise ArgumentError, "add_child expects a Node or String, got #{node_or_markup.class}"
568
587
  end
569
588
  end
589
+ # The markup-append face appends in fragment order; the added
590
+ # nodes are this element's last +count+ children.
591
+ def last_added(count)
592
+ kids = children.to_a
593
+ kids.last(count)
594
+ end
595
+ private :last_added
596
+
570
597
  alias_method :<<, :add_child
571
598
 
572
599
  def namespace
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.8
4
+ version: 1.9.174.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -102,6 +102,8 @@ files:
102
102
  - TODO.perf/31-battery-rounds-4-8.md
103
103
  - TODO.perf/32-clean-host-floor-table.md
104
104
  - TODO.perf/33-cbound-root-set.md
105
+ - TODO.perf/34-fragment-fast-lane.md
106
+ - TODO.perf/35-parse-default-c-face.md
105
107
  - TODO.restructure/01-constraint-compliance-audit.md
106
108
  - TODO.restructure/02-deep-copy-seam.md
107
109
  - TODO.restructure/03-evaluation-context-seam.md