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 +4 -4
- data/.github/workflows/rake.yml +2 -1
- data/docs/_pages/performance.adoc +31 -0
- data/lib/moxml/attribute.rb +2 -0
- data/lib/moxml/element.rb +30 -3
- data/lib/moxml/entity.rb +32 -3
- data/lib/moxml/node.rb +8 -0
- data/lib/moxml/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7962623114db23fce306ee38745c51ccf85fe8333c60a274322eb3d5a6dd6270
|
|
4
|
+
data.tar.gz: 00b614eed4a674d4ad43a1cbf1f4dd0483de2e929b7e78728c4aab7106662b56
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9e49d1c70a4332def347256eccf22f7defa256eaec8290987a73079daa0bcfed0f141f4862a9b457fbbc6f606be5386c3d5717e35e767b1088d2e7f88e0fde5d
|
|
7
|
+
data.tar.gz: d3ad61616f0b7bf163fef6333c59595bbce7b40dc96fd01c7a64f8bf2e8db2419102f3633444d1a54453bff7f6ad170051f75c9756e1c406233e25716426f3c7
|
data/.github/workflows/rake.yml
CHANGED
|
@@ -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].
|
data/lib/moxml/attribute.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
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.
|
|
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-
|
|
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
|