moxml 0.5.84 → 0.5.85

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: 0e2ec5341779cd310306935433ce87caceacbf8662ea449582afe042841957a6
4
+ data.tar.gz: 9425324aad5a99169a737a72d8f3fe73c82cd97112faf66fdbdbc016dccba910
5
5
  SHA512:
6
- metadata.gz: decb847bc6cfd50e816c916d98c74b837d63e9e38a9b49a2d964333a141a8fafd9e55291f83a52739403ad99126884c2a37928567ac9f4f8fdaae3ca9df37692
7
- data.tar.gz: 59be5497f4f1a4d439e86b4138be240b1bcabb370bc6d991d79b92aa64e9a40f0243b29f090d4171fd45146593ed27fc2da29ae2a2a72d0f542919e3e5aecf3b
6
+ metadata.gz: 55f4eca7d59c175151bc8cd5217ab238323af351ff4a4dc6caf35fe83847905acaf28c1c2d2906811315282d497fbea0f2e6f241260c9c4cf375e63b26a8d2ed
7
+ data.tar.gz: 0e90df291ceb7d1eb093ea4c204e6e8df0fc8f4d8db2233cc8fd74d7f5f119562aa8e2c14b3239cfbc057c0ed0e0f5f81d505f716951122fd6381c97146b893e
data/README.adoc CHANGED
@@ -681,6 +681,66 @@ doc.parse_diagnostics
681
681
  # => [{ kind: :recover, message: "Attribute a redefined" }]
682
682
  ----
683
683
 
684
+ === Hot-loop SAX consumption (bulk record drain)
685
+
686
+ On the leptris adapter (binding >= 1.9.240), `sax_parse` drains the
687
+ whole document in ONE crossing into a record table. Handlers that
688
+ override `on_sax_records` walk the records directly and materialize
689
+ only the strings they consume — allocations drop from the callback
690
+ path's ~15/element to the consumer-chosen floor (~1/element for a
691
+ name-materializing walker):
692
+
693
+ [source,ruby]
694
+ ----
695
+ class RecordDriver < Moxml::SAX::Handler
696
+ def initialize(builder)
697
+ super()
698
+ @builder = builder
699
+ end
700
+
701
+ def on_sax_records(table)
702
+ idx_stack = []
703
+ name_stack = []
704
+ i = 0
705
+ while i < table.count
706
+ # a parent mismatch is the exact close boundary; end events
707
+ # reconstruct from the records' parent indices
708
+ while !idx_stack.empty? && table.parent(i) != idx_stack.last
709
+ idx_stack.pop
710
+ @builder.end_element(name_stack.pop)
711
+ end
712
+ if table.kind(i).zero? # element
713
+ name = table.view(i)
714
+ attrs = []
715
+ k = table.attr_first(i)
716
+ last = k + table.attr_count_of(i)
717
+ while k < last
718
+ v = table.attr_value(k)
719
+ attrs << [table.attr_name(k),
720
+ table.attr_value_has_ws?(k) ? v.tr("\t\n\r", " ") : v]
721
+ k += 1
722
+ end
723
+ @builder.start_element(name, attrs)
724
+ idx_stack << i
725
+ name_stack << name
726
+ else # text
727
+ @builder.characters(table.view(i))
728
+ end
729
+ i += 1
730
+ end
731
+ @builder.end_element(name_stack.pop) until name_stack.empty?
732
+ :claimed # claimed the table — no callback replay
733
+ end
734
+ end
735
+ ----
736
+
737
+ Measured on the 2 x 226-element bench shape (`rake benchmark:sax`):
738
+ callback recorder 6,835 allocs / 1,735µs; default replay 6,353 /
739
+ 1,991µs; record-driven builder 3,638 / 1,248µs; counting walk 482 /
740
+ 778µs. Documents outside the drain's subset (comments, PIs, CDATA,
741
+ DOCTYPE, xmlns, entities) fall back to the callback path
742
+ transparently.
743
+
684
744
  == Real-world examples
685
745
 
686
746
  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
 
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.85"
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.85
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