moxml 0.5.23 → 0.5.25

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: 4c88f88d6ab8eafea4b11c8007e7b667e1413bfc60715142ef280af00c1909de
4
- data.tar.gz: 8302e755e10fa582f9349c443fc91ed63ddd684ce24c45f0baedd88f0541c16f
3
+ metadata.gz: 7962623114db23fce306ee38745c51ccf85fe8333c60a274322eb3d5a6dd6270
4
+ data.tar.gz: 00b614eed4a674d4ad43a1cbf1f4dd0483de2e929b7e78728c4aab7106662b56
5
5
  SHA512:
6
- metadata.gz: b7067b38f81a13bd29e1c542f66bd99636f594f91a754997744419549e259c0267a69daea3ab147410e425f243e9ddc807441860df8d91f00644b93f5608fd2e
7
- data.tar.gz: 662df0f63b4600b923eeedf4747b04bbd71a3a0cd732d25f2ca4fccaf6b563b710f09d8fd470497434962aa024c1e9354f718ebd8b793649a4aa06033149852b
6
+ metadata.gz: 9e49d1c70a4332def347256eccf22f7defa256eaec8290987a73079daa0bcfed0f141f4862a9b457fbbc6f606be5386c3d5717e35e767b1088d2e7f88e0fde5d
7
+ data.tar.gz: d3ad61616f0b7bf163fef6333c59595bbce7b40dc96fd01c7a64f8bf2e8db2419102f3633444d1a54453bff7f6ad170051f75c9756e1c406233e25716426f3c7
@@ -54,7 +54,8 @@ jobs:
54
54
  - name: Run rubocop
55
55
  run: bundle exec rubocop
56
56
 
57
- - name: Run performance gates
57
+ - name: Run performance gates (YJIT)
58
58
  env:
59
59
  RUN_PERFORMANCE: "1"
60
+ RUBY_YJIT_ENABLE: "1"
60
61
  run: bundle exec rake spec:performance
@@ -33,3 +33,34 @@ doc.xpath('//title')
33
33
  root.xpath('./*/title')
34
34
  ----
35
35
 
36
+
37
+ === Run with YJIT
38
+
39
+ The wrapper layer is Ruby-dispatch-heavy — exactly what YJIT compiles
40
+ well. Enable it for any throughput-sensitive workload (Ruby >= 3.3;
41
+ production-ready since then):
42
+
43
+ [source,bash]
44
+ ----
45
+ RUBY_YJIT_ENABLE=1 ruby your_app.rb # or: ruby --yjit
46
+ ----
47
+
48
+ Measured against raw engine calls on a 52KB fixture (leptris,
49
+ min-of-N, 100 warm reads): without YJIT the facade adds +23% on tree
50
+ walks and +43% on warm attribute reads; **with YJIT those become
51
+ -12% (faster than the binding's own walk) and +10%**, and `.name`
52
+ reads reach parity. Parse, serialize and duplicate are C-bound
53
+ either way. No semantic surface changes — it is a runtime flag.
54
+
55
+ Consumers on CRuby 3.3+ (canon's pretty-print pipeline, batch
56
+ conversion) should treat it as the default; moxml's own CI
57
+ performance gates run with YJIT enabled.
58
+
59
+ === Prefer bulk paths for per-node conversion
60
+
61
+ Per-node Ruby iteration pays the wrapper tax per element. When the
62
+ goal is conversion rather than manipulation, the bulk paths move the
63
+ loop into C: `materialize_fields` streams the flattened record
64
+ fields from one C call and measures faster than an equivalent
65
+ raw-Nokogiri DOM walk. See
66
+ link:conversion-apis[Conversion and memory lifecycle APIs].
@@ -10,6 +10,7 @@ module Moxml
10
10
  # Mutation return contract (Adapter::Base): returns the native
11
11
  # to keep tracking — same object for in-place adapters, fresh
12
12
  # object for value-object adapters (leptris).
13
+ context.bump_namespace_scope_generation
13
14
  @native = adapter.set_attribute_name(@native, new_name)
14
15
  end
15
16
 
@@ -32,6 +33,7 @@ module Moxml
32
33
  end
33
34
 
34
35
  def value=(new_value)
36
+ context.bump_namespace_scope_generation
35
37
  adapter.set_attribute_value(@native, new_value)
36
38
  end
37
39
 
data/lib/moxml/element.rb CHANGED
@@ -3,10 +3,14 @@
3
3
  module Moxml
4
4
  class Element < Node
5
5
  def name
6
- adapter.node_name(@native)
6
+ # The adapter read is an FFI call plus (on several adapters) a
7
+ # defensive string copy; names only change through name= and
8
+ # native adoption, both of which clear this.
9
+ @name ||= adapter.node_name(@native)
7
10
  end
8
11
 
9
12
  def name=(value)
13
+ @name = nil
10
14
  adapter.set_node_name(@native, value)
11
15
  end
12
16
 
@@ -42,6 +46,7 @@ module Moxml
42
46
  # attributes only; prefixed names replace by namespace URI and
43
47
  # require a declared prefix. Cache coherence is the resolver's.
44
48
  def []=(name, value)
49
+ context.bump_namespace_scope_generation
45
50
  Moxml::AttributeResolver.assign(self, name, normalize_xml_value(value))
46
51
  end
47
52
 
@@ -49,13 +54,30 @@ module Moxml
49
54
  # bare names match only no-namespace attributes; prefixed names
50
55
  # resolve through in-scope declarations to expanded names.
51
56
  def [](name)
52
- Moxml::AttributeResolver.resolve(self, name)&.value
57
+ cache = attribute_read_cache
58
+ key = name.to_s
59
+ unless cache.key?(key)
60
+ cache[key] = Moxml::AttributeResolver.resolve(self, key)
61
+ end
62
+ cache[key]&.value
53
63
  end
54
64
 
55
65
  def attribute(name)
56
66
  Moxml::AttributeResolver.resolve(self, name)
57
67
  end
58
68
 
69
+ # Resolution reads the element.s in-scope namespaces, so the
70
+ # cache rides the context.s namespace-scope generation clock —
71
+ # every attribute or namespace mutation anywhere bumps it.
72
+ def attribute_read_cache
73
+ generation = context.namespace_scope_generation
74
+ if @attribute_cache_generation != generation
75
+ @attribute_cache = {}
76
+ @attribute_cache_generation = generation
77
+ end
78
+ @attribute_cache
79
+ end
80
+
59
81
  # Returns attribute value by name (used by XPath engine)
60
82
  def get(attr_name)
61
83
  self[attr_name]
@@ -70,6 +92,7 @@ module Moxml
70
92
  end
71
93
 
72
94
  def remove_attribute(name)
95
+ context.bump_namespace_scope_generation
73
96
  Moxml::AttributeResolver.remove(self, name)
74
97
  self
75
98
  end
@@ -223,9 +246,13 @@ module Moxml
223
246
  children
224
247
  end
225
248
 
226
- # Called by Attribute#remove to invalidate the cached attributes
249
+ # Called by Attribute#remove and the attribute mutators. Clears
250
+ # the attribute list and the resolved-read cache, and bumps the
251
+ # context generation so cross-wrapper reads recompute.
227
252
  def invalidate_attribute_cache!
228
253
  @attributes = nil
254
+ @attribute_cache = nil
255
+ context.bump_namespace_scope_generation
229
256
  end
230
257
 
231
258
  # Clear the namespace caches and bump the context's scope
data/lib/moxml/entity.rb CHANGED
@@ -49,6 +49,34 @@ module Moxml
49
49
  # marker gsub's full-buffer copy entirely.
50
50
  NON_STANDARD_ENTITY_RE = /&(?!amp;|lt;|gt;|quot;|apos;)(#{NAME_PATTERN});/
51
51
 
52
+ # True when a NON-standard named entity exists somewhere (the
53
+ # gsub would then be needed). The leptris binding's C probe
54
+ # (leptris-ruby#124) answers in one pass at memcmp speed; it is
55
+ # resolved lazily because adapters load on demand. Otherwise:
56
+ # the cheap `&` pre-filter, then the Ruby regex.
57
+ def nonstandard_entity?(str)
58
+ probe = c_entity_probe
59
+ if probe
60
+ probe.call(str, str.bytesize) == 1
61
+ else
62
+ str.include?("&") && str.match?(NON_STANDARD_ENTITY_RE)
63
+ end
64
+ end
65
+
66
+ def c_entity_probe
67
+ if @c_entity_probe.nil?
68
+ @c_entity_probe =
69
+ if defined?(::Leptris::XML::FFI) &&
70
+ ::Leptris::XML::FFI.respond_to?(:leptris_str_has_nonstandard_entity)
71
+ ::Leptris::XML::FFI.method(:leptris_str_has_nonstandard_entity)
72
+ else
73
+ false
74
+ end
75
+ end
76
+ @c_entity_probe || nil
77
+ end
78
+ module_function :nonstandard_entity?, :c_entity_probe
79
+
52
80
  NAMED_DECODE_MAP = {
53
81
  "amp" => "&", "lt" => "<", "gt" => ">",
54
82
  "quot" => '"', "apos" => "'"
@@ -87,11 +115,12 @@ module Moxml
87
115
  # Fast path: no `&` means no entity references to mark — skip
88
116
  # the regex scan and string allocation entirely. The vast
89
117
  # majority of XML payloads contain no entity references.
90
- return [str, false] unless str.include?("&")
91
118
  # Second fast path: only predefined entities — the gsub would
92
119
  # pass every match through unchanged, so skip its full-buffer
93
- # copy too.
94
- return [str, false] unless str.match?(NON_STANDARD_ENTITY_RE)
120
+ # copy too. When the leptris binding is loaded, its C probe
121
+ # (leptris-ruby#124) answers in one memcmp-class pass; the
122
+ # binding loads lazily, so the probe is resolved on first use.
123
+ return [str, false] unless nonstandard_entity?(str)
95
124
 
96
125
  marked = false
97
126
  processed = str.gsub(NAME_RE) do |match|
data/lib/moxml/node.rb CHANGED
@@ -25,10 +25,18 @@ module Moxml
25
25
  context.unregister_wrapper(@native)
26
26
  @native = new_native
27
27
  context.register_wrapper(new_native, self)
28
+ clear_native_memo!
28
29
  end
29
30
  self
30
31
  end
31
32
 
33
+ # Wrapper-local memos derived from the native (name, attribute
34
+ # reads) die with the native they were computed from.
35
+ def clear_native_memo!
36
+ @name = nil
37
+ @attribute_cache = nil
38
+ end
39
+
32
40
  def document
33
41
  Document.wrap(adapter.document(@native), context)
34
42
  end
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.23"
4
+ VERSION = "0.5.25"
5
5
  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.23
4
+ version: 0.5.25
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-01 00:00:00.000000000 Z
11
+ date: 2026-09-03 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