lutaml-model 0.8.34 → 0.8.35

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: 65361e31ef80bfd3600c0bc3ea4241c2b9a9e9d227b1772b9bd23f3324e92ad7
4
- data.tar.gz: da258d5127e14511cd77bd1ca44e6ad1647aaaf445cc14a92102096f180a82a3
3
+ metadata.gz: 0a5f36f080c15bb2639142a36b424853d26458732c867963dbf7a99d6a1cd189
4
+ data.tar.gz: 3159ba755e2cc317e5e9b3785b2be1cffb08e424b2bd792bca34f20c853d25be
5
5
  SHA512:
6
- metadata.gz: 2d5ece44cac496100e30c2d4f3c5ca3934f656bd81352ee9215e57c2a4b8563f0a7a8ae3c155909eaf132efcce992e1954731b89cd79808fcf8c948555b06abd
7
- data.tar.gz: 9db5fc0ac630ad33336250d06a953357d8a2d60bb0f0cae037d5a2d9d6cc12da2fca2937aa5b73c5b490852381995937fad226049e51fd167906e900084fa837
6
+ metadata.gz: e83080b05e3277310d103e84c5f63fda7e963e25bb1cdc764934a1d931e388f212b51f97c5a02182966b3e376a97d00fc30fa396568a644100b5745064d1fd1a
7
+ data.tar.gz: f59d08e3915dacf3a7da3c0bfbc2e9b300cb7c66177c6dd4455870349fc4f2cb892d8d34a1028b74f2660b87e21980b44a4e941329fcf8a69687fde9a7d336f0
data/.rspec CHANGED
@@ -1,3 +1,4 @@
1
1
  --format documentation
2
2
  --color
3
3
  --require spec_helper
4
+ --tag ~benchmark
@@ -52,11 +52,13 @@ module Lutaml
52
52
  # Public reader for compiled rule plans: does this type class carry a
53
53
  # custom from_xml/from_json (answer static, probed once)?
54
54
  def custom_from_probe?(type)
55
- TypeProbeCache.cache[type] ||= begin
56
- base = Lutaml::Model::Type::Value.singleton_class
55
+ cache = TypeProbeCache.cache
56
+ return cache[type] if cache.key?(type)
57
+
58
+ base = Lutaml::Model::Type::Value.singleton_class
59
+ cache[type] =
57
60
  type.method(:from_xml).owner != base ||
58
- type.method(:from_json).owner != base
59
- end
61
+ type.method(:from_json).owner != base
60
62
  end
61
63
  end
62
64
 
@@ -80,15 +80,22 @@ module Lutaml
80
80
  # @param parent_register [Symbol, String, nil] The parent's register/context ID
81
81
  # @return [Symbol, nil] The register ID to use for the child
82
82
  def self.resolve_for_child(child_class, parent_register)
83
- cache_key = [child_class.object_id, parent_register]
84
-
85
83
  if Lutaml::Model.opal?
86
- return RESOLVE_CACHE[cache_key] if RESOLVE_CACHE.key?(cache_key)
84
+ per_class = RESOLVE_CACHE[child_class] # rubocop:todo Style/IdenticalConditionalBranches
85
+ return per_class[parent_register] if per_class&.key?(parent_register)
87
86
 
88
- RESOLVE_CACHE[cache_key] =
87
+ (RESOLVE_CACHE[child_class] ||= {})[parent_register] =
89
88
  _resolve_for_child_uncached(child_class, parent_register)
90
89
  else
91
- RESOLVE_CACHE.compute_if_absent(cache_key) do
90
+ # Nested maps: class → register → result. Every hop keys on a
91
+ # stable object (Class / Symbol), so the hot lookup allocates
92
+ # nothing — the flat [object_id, register] array key cost one
93
+ # Array per call, six per parsed element.
94
+ per_class = RESOLVE_CACHE[child_class] # rubocop:todo Style/IdenticalConditionalBranches
95
+ per_class ||= RESOLVE_CACHE.compute_if_absent(child_class) do
96
+ Concurrent::Map.new
97
+ end
98
+ per_class.compute_if_absent(parent_register) do
92
99
  _resolve_for_child_uncached(child_class, parent_register)
93
100
  end
94
101
  end
@@ -174,25 +174,31 @@ module Lutaml
174
174
  # @param register [Symbol, Register, nil] The register
175
175
  # @return [Mapping] The resolved mapping
176
176
  def get_or_build_mapping(model_class, format, register)
177
- # Performance: Use fast array key instead of symbol construction
177
+ # Performance: nested class format register maps keyed on
178
+ # stable objects; the flat array key allocated per call.
178
179
  register_id = extract_register_id(register)
179
- key = [model_class.object_id, format, register_id]
180
180
 
181
- # Fast path: native Ruby uses Concurrent::Map, Opal uses Hash.
182
- cached = @mappings[key]
181
+ per_class = @mappings[model_class]
182
+ per_format = per_class&.[](format)
183
+ cached = per_format&.[](register_id)
183
184
  return cached if cached
184
185
 
185
- # Build mapping OUTSIDE any lock to avoid deadlock
186
- # (ensure_mappings_imported! may recursively call get_or_build_mapping)
187
186
  mapping = model_class.mappings[format]
188
187
  mapping = mapping || model_class.default_mappings(format)
189
188
 
190
189
  mapping.ensure_mappings_imported!(register_id)
191
190
 
192
- @mappings[key] = mapping
191
+ per_class = (@mappings[model_class] ||= new_inner_map)
192
+ (per_class[format] ||= new_inner_map)[register_id] = mapping
193
193
  mapping
194
194
  end
195
195
 
196
+ # Inner cache level container matching the outer map's engine
197
+ # (Concurrent::Map under GVL-threaded MRI, plain Hash under Opal).
198
+ def new_inner_map
199
+ Lutaml::Model.opal? ? {} : ::Concurrent::Map.new
200
+ end
201
+
196
202
  # Clear all cached data (transformations and mappings).
197
203
  #
198
204
  # This is useful for testing or when configuration changes dynamically.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.34"
5
+ VERSION = "0.8.35"
6
6
  end
7
7
  end
@@ -64,8 +64,8 @@ module Lutaml
64
64
  name = adapter_class.name_of(node)
65
65
  super(
66
66
  node,
67
- Hash(attributes),
68
- Array(children),
67
+ attributes || NO_ATTRIBUTES,
68
+ children || NO_CHILDREN,
69
69
  text,
70
70
  name: name,
71
71
  parent_document: parent,
@@ -146,8 +146,19 @@ module Lutaml
146
146
  end
147
147
  end
148
148
 
149
+ # Shared frozen empties: the parse path hands these straight into
150
+ # XmlElement (which never mutates the containers it is given), so
151
+ # attribute-less elements — the majority in real documents — build
152
+ # nothing here.
153
+ NO_ATTRIBUTES = {}.freeze # rubocop:todo Lint/UselessConstantScoping
154
+ NO_CHILDREN = [].freeze # rubocop:todo Lint/UselessConstantScoping
155
+ EMPTY_ATTRIBUTES = [{}, nil].freeze # rubocop:todo Lint/UselessConstantScoping
156
+
149
157
  def node_attributes_with_order(node)
150
- return [{}, nil] unless node.is_a?(Moxml::Element)
158
+ return EMPTY_ATTRIBUTES unless node.is_a?(Moxml::Element)
159
+
160
+ attrs = node.attributes
161
+ return EMPTY_ATTRIBUTES if attrs.respond_to?(:empty?) && attrs.empty?
151
162
 
152
163
  order = []
153
164
  hash = node.attributes.each_with_object({}) do |attr, h|
@@ -174,7 +185,7 @@ module Lutaml
174
185
  end
175
186
 
176
187
  def parse_children(node, default_namespace: nil)
177
- return [] unless node.children
188
+ return NO_CHILDREN unless node.children
178
189
 
179
190
  # Non-element children (text/cdata/comment/PI) stay RAW moxml
180
191
  # nodes in the list — the hot scalar parse path reads text off
@@ -197,6 +208,8 @@ module Lutaml
197
208
  public
198
209
 
199
210
  def children
211
+ return @children if @children.empty?
212
+
200
213
  unless @non_element_children_wrapped
201
214
  @children.map! do |child|
202
215
  if child.is_a?(Moxml::Node) && !child.is_a?(Moxml::Element)
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+ require "benchmark/ips"
5
+
6
+ # Adapter-comparison benchmark, runnable on demand:
7
+ # bundle exec rspec --tag benchmark spec/benchmarks/
8
+ # Tagged out of the default suite (.rsex --tag ~benchmark); asserts
9
+ # nothing about absolute speed — machines differ — but prints i/s and
10
+ # allocations/op so regressions are visible when run deliberately.
11
+ RSpec.describe "XML adapter benchmark", :benchmark do
12
+ Item = Class.new(Lutaml::Model::Serializable) do # rubocop:todo Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
13
+ attribute :id, :integer
14
+ attribute :name, :string
15
+ attribute :tags, :string, collection: true
16
+
17
+ xml do
18
+ element "item"
19
+ map_attribute "id", to: :id
20
+ map_element "name", to: :name
21
+ map_element "tag", to: :tags
22
+ end
23
+ end
24
+
25
+ Root = Class.new(Lutaml::Model::Serializable) do # rubocop:todo Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
26
+ attribute :item, Item, collection: true
27
+
28
+ xml do
29
+ element "root"
30
+ map_element "item", to: :item
31
+ end
32
+ end
33
+
34
+ XML = +"<root>" # rubocop:todo Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
35
+ 1_000.times { |i| XML << %(<item id="#{i}"><name>Item #{i}</name><tag>a</tag><tag>b</tag></item>) }
36
+ XML << "</root>"
37
+
38
+ def allocations_of
39
+ GC.start
40
+ before = GC.stat(:total_allocated_objects)
41
+ yield
42
+ GC.stat(:total_allocated_objects) - before
43
+ end
44
+
45
+ %i[nokogiri ox oga rexml leptris].each do |adapter|
46
+ it "#{adapter}: from_xml/to_xml i/s and allocations" do
47
+ model = nil
48
+ Lutaml::Model::Config.with_adapter(xml: adapter) do
49
+ begin
50
+ Lutaml::Model::Config.adapter_for(:xml)
51
+ rescue Lutaml::Model::UnknownAdapterTypeError
52
+ skip "#{adapter} adapter unavailable on this platform"
53
+ end
54
+ report = Benchmark.ips(time: 2, warmup: 1, quiet: true) do |x|
55
+ x.report("parse") { model = Root.from_xml(XML) }
56
+ x.report("serialize") { model.to_xml }
57
+ end
58
+ parse_ips = report.entries.first.ips
59
+ serialize_ips = report.entries.last.ips
60
+
61
+ allocs = allocations_of { Root.from_xml(XML) }
62
+ puts format( # rubocop:todo RSpec/Output
63
+ "%<a>-9s parse %<p>7.1f i/s serialize %<s>7.1f i/s %<al>8d allocs/op",
64
+ a: adapter, p: parse_ips, s: serialize_ips, al: allocs,
65
+ )
66
+ expect(model.item.size).to eq(1_000)
67
+ end
68
+ end
69
+ end
70
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.34
4
+ version: 0.8.35
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -971,6 +971,7 @@ files:
971
971
  - sig/lutaml/model.rbs
972
972
  - spec/address_person_spec.rb
973
973
  - spec/address_spec.rb
974
+ - spec/benchmarks/xml_parsing_benchmark_spec.rb
974
975
  - spec/ceramic_spec.rb
975
976
  - spec/fixtures/address.rb
976
977
  - spec/fixtures/ceramic.rb