moxml 0.5.84 → 0.5.86

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: ff836e409dd2cf77368c7b26015e57de1b0af5e0678489e6e10452a6c70cc61a
4
- data.tar.gz: a2a09b561505f90fc624089e925c3b41d4ab867668b00fd35f1132787e9dbfde
3
+ metadata.gz: 24155b1be6da49c7e24b3be5bef79caeac8e76f19c006329382330dee7d4e259
4
+ data.tar.gz: 0f8b8736e98239902ad1dfe0deba7f7c4b22f61400ae6628ebcc8e5484ef319b
5
5
  SHA512:
6
- metadata.gz: decb847bc6cfd50e816c916d98c74b837d63e9e38a9b49a2d964333a141a8fafd9e55291f83a52739403ad99126884c2a37928567ac9f4f8fdaae3ca9df37692
7
- data.tar.gz: 59be5497f4f1a4d439e86b4138be240b1bcabb370bc6d991d79b92aa64e9a40f0243b29f090d4171fd45146593ed27fc2da29ae2a2a72d0f542919e3e5aecf3b
6
+ metadata.gz: 4a1a93ce53b2dee189b74c608f930483c4a0ecffaeed03a570486bede424e6cf9244d7262e038ba8cb0a5a2d3b2d2c99711a47fac53f5abdc87640957a9ae379
7
+ data.tar.gz: 5b997e1cf171d4f6c08bfedb2fb9cb0caa02e022d491e26a63e1e613fe8340972e8629cfe28e158d23ee1c24956d4e54d541af8b76c44f1be6ca25d9e7aa0d76
data/README.adoc CHANGED
@@ -102,8 +102,13 @@ entity references (`entity_mode: :keep`), 1.9.193.4+ native plan rows
102
102
  (`Moxml::Plan`), 1.9.194.1+ hot-read aliases, 1.9.201.1+ the C struct
103
103
  executor (`Moxml::StructPlan`; the floor is the varargs-mint fix),
104
104
  1.9.204+ native document-level parts (created DOCTYPEs as native
105
- nodes, document-PI removal, declaration clear). Current
106
- recommendation: 1.9.228+ (the #318 text-set fix train; carries
105
+ nodes, document-PI removal, declaration clear), 1.9.240+ the bulk
106
+ SAX record drain (`Handler#on_sax_records`) — and 1.9.242+ is
107
+ REQUIRED for that drain to be CORRECT: 1.9.240/241 drain documents
108
+ containing duplicate attributes without surfacing the fatal error
109
+ (moxml#281), so the drain arms only on 1.9.242+, where such
110
+ documents fall back to the callback path and the error fires.
111
+ Current recommendation: 1.9.242+ (the record-drain train; carries
107
112
  the plan-ABI surface — bound plan rows, typed scalars, fused
108
113
  Descriptor#materialize — plus the 1.9.217+ borrowed-text-run and
109
114
  1.9.221+ memory wins).
@@ -681,6 +686,66 @@ doc.parse_diagnostics
681
686
  # => [{ kind: :recover, message: "Attribute a redefined" }]
682
687
  ----
683
688
 
689
+ === Hot-loop SAX consumption (bulk record drain)
690
+
691
+ On the leptris adapter (binding >= 1.9.240), `sax_parse` drains the
692
+ whole document in ONE crossing into a record table. Handlers that
693
+ override `on_sax_records` walk the records directly and materialize
694
+ only the strings they consume — allocations drop from the callback
695
+ path's ~15/element to the consumer-chosen floor (~1/element for a
696
+ name-materializing walker):
697
+
698
+ [source,ruby]
699
+ ----
700
+ class RecordDriver < Moxml::SAX::Handler
701
+ def initialize(builder)
702
+ super()
703
+ @builder = builder
704
+ end
705
+
706
+ def on_sax_records(table)
707
+ idx_stack = []
708
+ name_stack = []
709
+ i = 0
710
+ while i < table.count
711
+ # a parent mismatch is the exact close boundary; end events
712
+ # reconstruct from the records' parent indices
713
+ while !idx_stack.empty? && table.parent(i) != idx_stack.last
714
+ idx_stack.pop
715
+ @builder.end_element(name_stack.pop)
716
+ end
717
+ if table.kind(i).zero? # element
718
+ name = table.view(i)
719
+ attrs = []
720
+ k = table.attr_first(i)
721
+ last = k + table.attr_count_of(i)
722
+ while k < last
723
+ v = table.attr_value(k)
724
+ attrs << [table.attr_name(k),
725
+ table.attr_value_has_ws?(k) ? v.tr("\t\n\r", " ") : v]
726
+ k += 1
727
+ end
728
+ @builder.start_element(name, attrs)
729
+ idx_stack << i
730
+ name_stack << name
731
+ else # text
732
+ @builder.characters(table.view(i))
733
+ end
734
+ i += 1
735
+ end
736
+ @builder.end_element(name_stack.pop) until name_stack.empty?
737
+ :claimed # claimed the table — no callback replay
738
+ end
739
+ end
740
+ ----
741
+
742
+ Measured on the 2 x 226-element bench shape (`rake benchmark:sax`):
743
+ callback recorder 6,835 allocs / 1,735µs; default replay 6,353 /
744
+ 1,991µs; record-driven builder 3,638 / 1,248µs; counting walk 482 /
745
+ 778µs. Documents outside the drain's subset (comments, PIs, CDATA,
746
+ DOCTYPE, xmlns, entities) fall back to the callback path
747
+ transparently.
748
+
684
749
  == Real-world examples
685
750
 
686
751
  Practical, runnable examples demonstrating Moxml usage in common scenarios are
@@ -41,6 +41,53 @@ module SaxBenchHandlers
41
41
  :claimed
42
42
  end
43
43
  end
44
+
45
+ # The builder-protocol shape (canon's comparison driver): walk the
46
+ # records but materialize the full event payload — name, attr
47
+ # name/value pairs, text — exactly what the callback protocol
48
+ # delivers, minus the recorder's double marshaling and the Hash.
49
+ class RecordBuilderHandler < Moxml::SAX::Handler
50
+ attr_reader :events
51
+
52
+ def initialize
53
+ super
54
+ @events = 0
55
+ end
56
+
57
+ def on_sax_records(table)
58
+ idx_stack = []
59
+ name_stack = []
60
+ i = 0
61
+ count = table.count
62
+ while i < count
63
+ while !idx_stack.empty? && table.parent(i) != idx_stack.last
64
+ idx_stack.pop
65
+ name_stack.pop
66
+ @events += 1
67
+ end
68
+ if table.kind(i).zero?
69
+ name = table.view(i)
70
+ k = table.attr_first(i)
71
+ last = k + table.attr_count_of(i)
72
+ while k < last
73
+ table.attr_name(k)
74
+ v = table.attr_value(k)
75
+ v = v.tr("\t\n\r", " ") if table.attr_value_has_ws?(k)
76
+ @events += 1
77
+ k += 1
78
+ end
79
+ idx_stack << i
80
+ name_stack << name
81
+ else
82
+ table.view(i)
83
+ @events += 1
84
+ end
85
+ i += 1
86
+ end
87
+ @events += name_stack.size
88
+ :claimed
89
+ end
90
+ end
44
91
  end
45
92
 
46
93
  CTX = Moxml.new(:leptris).freeze # rubocop:disable Lint/ConstantDefinitionInBlock
@@ -68,6 +115,8 @@ handler = SaxBenchHandlers::NullHandler.new
68
115
  3.times { DOCS.each { |doc| Leptris::XML::SAX::Parser.new(BRIDGE.new(handler)).parse(doc) } }
69
116
  walker = SaxBenchHandlers::RecordWalkHandler.new
70
117
  3.times { DOCS.each { |doc| Moxml::Adapter::Leptris.sax_parse(doc, walker) } }
118
+ builder = SaxBenchHandlers::RecordBuilderHandler.new
119
+ 3.times { DOCS.each { |doc| Moxml::Adapter::Leptris.sax_parse(doc, builder) } }
71
120
 
72
121
  recorder_allocs = allocations do
73
122
  DOCS.each { |doc| Leptris::XML::SAX::Parser.new(BRIDGE.new(handler)).parse(doc) }
@@ -78,13 +127,18 @@ end
78
127
  walk_allocs = allocations do
79
128
  DOCS.each { |doc| Moxml::Adapter::Leptris.sax_parse(doc, walker) }
80
129
  end
130
+ builder_allocs = allocations do
131
+ DOCS.each { |doc| Moxml::Adapter::Leptris.sax_parse(doc, builder) }
132
+ end
81
133
 
82
134
  replay_time = cpu_micros { 5.times { DOCS.each { |doc| Moxml::Adapter::Leptris.sax_parse(doc, handler) } } }
83
135
  recorder_time = cpu_micros { 5.times { DOCS.each { |doc| Leptris::XML::SAX::Parser.new(BRIDGE.new(handler)).parse(doc) } } }
84
136
  walk_time = cpu_micros { 5.times { DOCS.each { |doc| Moxml::Adapter::Leptris.sax_parse(doc, walker) } } }
137
+ builder_time = cpu_micros { 5.times { DOCS.each { |doc| Moxml::Adapter::Leptris.sax_parse(doc, builder) } } }
85
138
 
86
139
  puts format("recorder %<allocs>6d allocs %<time>6dµs", allocs: recorder_allocs, time: recorder_time / 5)
87
140
  puts format("drain replay %<allocs>6d allocs %<time>6dµs", allocs: replay_allocs, time: replay_time / 5)
141
+ puts format("drain builder-walk %<allocs>6d allocs %<time>6dµs", allocs: builder_allocs, time: builder_time / 5)
88
142
  puts format("drain records-walk %<allocs>6d allocs %<time>6dµs", allocs: walk_allocs, time: walk_time / 5)
89
143
  puts "(#{walker.elements} elements walked cumulatively; 452 per rep)"
90
144
 
@@ -132,12 +132,15 @@ module Moxml
132
132
 
133
133
  # Bulk SAX record drain (leptris#1298): leptris_sax_records_parse
134
134
  # + Leptris::XML::SAX::Records — one crossing per document,
135
- # handlers replay or walk the table. Ships in the binding ride
136
- # 1.9.240.0 (1.9.239.0 predates the walker — the gate must name
137
- # the carrying release, not the engine surface). Dev floor pins
138
- # ~> 1.9.238; older bindings take the callback Parser below.
135
+ # handlers replay or walk the table. Ships in binding 1.9.242.0:
136
+ # 1.9.240/241 carry the drain WITHOUT the duplicate-attribute
137
+ # bail (moxml#281 — the record table has no error channel, so a
138
+ # fatal dup-attr document parses with on_error never firing),
139
+ # so the gate names the first release where the drain is
140
+ # CORRECT, not merely present. Dev floor pins ~> 1.9.238;
141
+ # older bindings take the callback Parser below.
139
142
  NATIVE_SAX_RECORDS =
140
- Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.240")
143
+ Gem::Version.new(::Leptris::VERSION) >= Gem::Version.new("1.9.242")
141
144
 
142
145
  # Defined unconditionally: the body self-guards, and call
143
146
  # sites must never depend on the layer having loaded
data/lib/moxml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.84"
4
+ VERSION = "0.5.86"
5
5
  end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # Worked hot-loop adoption (moxml#279 follow-up): a builder-protocol
6
+ # driver — the shape canon's comparison lane uses — consuming the
7
+ # bulk record table directly via on_sax_records instead of the
8
+ # callback replay. The walker materializes only the strings the
9
+ # builder consumes (one name/value/text get_string each) and
10
+ # reconstructs end events from the records' parent indices, so its
11
+ # event stream is identical to the callback path while allocations
12
+ # land at the consumer-chosen floor.
13
+ #
14
+ # Docs: README "Hot-loop SAX consumption".
15
+ module RecordDriverSpec
16
+ class Builder
17
+ attr_accessor :events
18
+
19
+ def initialize
20
+ @events = []
21
+ end
22
+
23
+ def start_element(name, attrs)
24
+ @events << [:start, name, attrs]
25
+ end
26
+
27
+ def end_element(name)
28
+ @events << [:end, name]
29
+ end
30
+
31
+ def characters(text)
32
+ @events << [:chars, text]
33
+ end
34
+ end
35
+
36
+ class RecordDriver < Moxml::SAX::Handler
37
+ def initialize(builder)
38
+ super()
39
+ @builder = builder
40
+ end
41
+
42
+ def on_sax_records(table)
43
+ idx_stack = []
44
+ name_stack = []
45
+ i = 0
46
+ count = table.count
47
+ while i < count
48
+ while !idx_stack.empty? && table.parent(i) != idx_stack.last
49
+ idx_stack.pop
50
+ @builder.end_element(name_stack.pop)
51
+ end
52
+ if table.kind(i).zero?
53
+ name = table.view(i)
54
+ attrs = []
55
+ k = table.attr_first(i)
56
+ last = k + table.attr_count_of(i)
57
+ while k < last
58
+ v = table.attr_value(k)
59
+ attrs << [table.attr_name(k),
60
+ table.attr_value_has_ws?(k) ? v.tr("\t\n\r", " ") : v]
61
+ k += 1
62
+ end
63
+ @builder.start_element(name, attrs)
64
+ idx_stack << i
65
+ name_stack << name
66
+ else
67
+ @builder.characters(table.view(i))
68
+ end
69
+ i += 1
70
+ end
71
+ @builder.end_element(name_stack.pop) until name_stack.empty?
72
+ :claimed
73
+ end
74
+ end
75
+
76
+ class CallbackDriver < Moxml::SAX::Handler
77
+ attr_reader :events
78
+
79
+ def initialize
80
+ super
81
+ @events = []
82
+ end
83
+
84
+ def on_start_element(name, attributes = {}, _namespaces = {})
85
+ @events << [:start, name, attributes.to_a]
86
+ end
87
+
88
+ def on_end_element(name)
89
+ @events << [:end, name]
90
+ end
91
+
92
+ def on_characters(text)
93
+ @events << [:chars, text]
94
+ end
95
+ end
96
+ end
97
+
98
+ RSpec.describe "record-driven builder protocol",
99
+ if: defined?(Leptris::XML::SAX::Records) do
100
+ let(:context) { Moxml.new(:leptris) }
101
+ let(:xml) do
102
+ '<r a="x y"><item n="1">t1</item><item n="2">t2<b>k</b></item><last/></r>'
103
+ end
104
+
105
+ it "feeds the builder the same stream the callback path produces" do
106
+ record_events = RecordDriverSpec::Builder.new
107
+ driver = RecordDriverSpec::RecordDriver.new(record_events)
108
+ context.sax_parse(xml, driver)
109
+ walked = record_events.events
110
+
111
+ callback_events = RecordDriverSpec::Builder.new
112
+ callback = RecordDriverSpec::CallbackDriver.new
113
+ callback_events.events = []
114
+ bridge = Moxml::Adapter::Leptris::LeptrisSAXBridge.new(callback)
115
+ Leptris::XML::SAX::Parser.new(bridge).parse(xml)
116
+
117
+ expect(walked).to eq(callback.events)
118
+ expect(walked).to include([:start, "item", [["n", "1"]]])
119
+ expect(walked).to include([:start, "r", [["a", "x y"]]])
120
+ end
121
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: moxml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.84
4
+ version: 0.5.86
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-25 00:00:00.000000000 Z
11
+ date: 2026-09-26 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Moxml is a unified XML manipulation library that provides a common API
@@ -438,6 +438,7 @@ files:
438
438
  - spec/moxml/adapter/entity_restoration_spec.rb
439
439
  - spec/moxml/adapter/headed_ox_spec.rb
440
440
  - spec/moxml/adapter/leptris_doc_parts_spec.rb
441
+ - spec/moxml/adapter/leptris_record_driver_spec.rb
441
442
  - spec/moxml/adapter/leptris_sax_dup_attr_spec.rb
442
443
  - spec/moxml/adapter/leptris_sax_records_spec.rb
443
444
  - spec/moxml/adapter/leptris_spec.rb