lutaml-model 0.8.48 → 0.8.50
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/performance.yml +47 -1
- data/docs/_guides/native-engines.adoc +30 -0
- data/lib/lutaml/model/serialize/initialization.rb +53 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/plan_compiler.rb +8 -1
- data/lib/lutaml/xml/plan_hydrator.rb +5 -1
- data/spec/lutaml/model/serializable_instantiate_spec.rb +75 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +77 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f2f88962ec380d731bd7a31ce5ff62f9aec9dcef4b7783aebfb128b6b5c86bbb
|
|
4
|
+
data.tar.gz: b282edc8ccf91c2381315e58a34d54a15642153e673ba31bbbd46c084c7634c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f92f932e37bed4d355b3a41c1d2599bf23a5f0907f2ba18967f27cb5a572a4be11d080d54d0a73787055b6e9143414928e87086f2269f42acfb77665e9cccda6
|
|
7
|
+
data.tar.gz: a5801daa0947b0a2308fc720412d0add3cd5956f6a902cf20aa62b0ece6d883ec801dc4c4112319ab161a5d68934d54aac0253d601515823d04440df52011400
|
|
@@ -55,4 +55,50 @@ jobs:
|
|
|
55
55
|
run: rake vendor:compile
|
|
56
56
|
|
|
57
57
|
- name: Run performance benchmarks
|
|
58
|
-
run: bundle exec rake performance:compare
|
|
58
|
+
run: bundle exec rake performance:compare
|
|
59
|
+
# TODO.max-perf/36: YJIT moves exactly the interpretive paths the
|
|
60
|
+
# model layer still runs. Sequential off/on probes on one runner give
|
|
61
|
+
# a directly comparable pair per commit - a measurement leg, not a
|
|
62
|
+
# gate. The plan fast path shrinks the YJIT win; watch both.
|
|
63
|
+
performance-yjit:
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
timeout-minutes: 15
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v6
|
|
68
|
+
with:
|
|
69
|
+
submodules: "recursive"
|
|
70
|
+
|
|
71
|
+
- name: Set up Ruby
|
|
72
|
+
uses: ruby/setup-ruby@v1
|
|
73
|
+
with:
|
|
74
|
+
ruby-version: '3.4'
|
|
75
|
+
bundler-cache: false
|
|
76
|
+
|
|
77
|
+
- name: Install ragel
|
|
78
|
+
run: sudo apt-get update && sudo apt-get install -y ragel
|
|
79
|
+
|
|
80
|
+
- name: Install ruby-ll (for rake vendor:prepare)
|
|
81
|
+
run: gem install ruby-ll --no-document
|
|
82
|
+
|
|
83
|
+
- name: Generate ragel / ruby-ll outputs in vendored forks
|
|
84
|
+
run: rake vendor:prepare
|
|
85
|
+
|
|
86
|
+
- name: Configure bundler
|
|
87
|
+
run: |
|
|
88
|
+
mkdir -p .bundle
|
|
89
|
+
echo "---" > .bundle/config
|
|
90
|
+
echo 'BUNDLE_BUILD__RUBY___LL: "--with-cflags=-std=gnu17"' >> .bundle/config
|
|
91
|
+
|
|
92
|
+
- name: Install dependencies
|
|
93
|
+
run: bundle install
|
|
94
|
+
|
|
95
|
+
- name: Compile liboga / libll native extensions
|
|
96
|
+
run: rake vendor:compile
|
|
97
|
+
|
|
98
|
+
- name: Probe YJIT off
|
|
99
|
+
run: bundle exec rake performance:probe
|
|
100
|
+
|
|
101
|
+
- name: Probe YJIT on
|
|
102
|
+
env:
|
|
103
|
+
RUBY_YJIT_ENABLE: "1"
|
|
104
|
+
run: bundle exec rake performance:probe
|
|
@@ -31,6 +31,36 @@ leptris and yeptris ship prebuilt platform gems for Linux and macOS
|
|
|
31
31
|
every platform, including Windows. When a gem is absent, lutaml-model
|
|
32
32
|
automatically falls back to the standard engine for that format.
|
|
33
33
|
|
|
34
|
+
== YJIT doubles the model pipeline
|
|
35
|
+
|
|
36
|
+
The model layer's own work — mapping dispatch, casts, hydration — is
|
|
37
|
+
plain Ruby and exactly the code shape YJIT compiles well; the native
|
|
38
|
+
engines' C work is unaffected either way. On a controlled runner
|
|
39
|
+
(ubuntu-latest, Ruby 3.4, the repository's performance probe):
|
|
40
|
+
|
|
41
|
+
|===
|
|
42
|
+
| op | YJIT off | YJIT on | change
|
|
43
|
+
|
|
44
|
+
| from_yaml | 75.1 i/s | 129.7 i/s | +73%
|
|
45
|
+
| to_yaml | 67.3 i/s | 145.1 i/s | +116%
|
|
46
|
+
| from_json | 73.2 i/s | 128.4 i/s | +75%
|
|
47
|
+
| to_json | 115.4 i/s | 255.5 i/s | +121%
|
|
48
|
+
|===
|
|
49
|
+
|
|
50
|
+
Enable it like any Ruby application does — this is the application's
|
|
51
|
+
call, never a gem's:
|
|
52
|
+
|
|
53
|
+
[source,sh]
|
|
54
|
+
----
|
|
55
|
+
RUBY_YJIT_ENABLE=1 ruby app.rb
|
|
56
|
+
# or in code, before heavy work:
|
|
57
|
+
RubyVM::YJIT.enable
|
|
58
|
+
----
|
|
59
|
+
|
|
60
|
+
The opt-in XML plan fast path (see below) moves work out of Ruby and
|
|
61
|
+
into the engine, which shrinks the YJIT win — enabling both is still
|
|
62
|
+
net faster than either alone.
|
|
63
|
+
|
|
34
64
|
== Selecting adapters explicitly
|
|
35
65
|
|
|
36
66
|
The engines register as ordinary adapter types, so explicit selection and
|
|
@@ -350,6 +350,59 @@ module Lutaml
|
|
|
350
350
|
instance
|
|
351
351
|
end
|
|
352
352
|
|
|
353
|
+
# Fast bulk constructor for deserialization-heavy callers
|
|
354
|
+
# (native extensions building object trees bottom-up).
|
|
355
|
+
#
|
|
356
|
+
# Allocates without running #initialize and applies each present
|
|
357
|
+
# attribute through its compiled writer, which casts the value
|
|
358
|
+
# and marks it as explicitly set — so defaults, to_hash output,
|
|
359
|
+
# and using_default? behave exactly as if the instance had been
|
|
360
|
+
# produced by from_hash. Values may be primitives or already
|
|
361
|
+
# built instances (instances pass through casting unchanged);
|
|
362
|
+
# absent keys keep their defaults. Unknown keys raise.
|
|
363
|
+
#
|
|
364
|
+
# @param attrs [Hash] attribute names (String or Symbol) to
|
|
365
|
+
# pre-cast values
|
|
366
|
+
# @param register [Symbol, nil] The register context
|
|
367
|
+
# @return [Object] The hydrated instance
|
|
368
|
+
def instantiate(attrs = {}, register = nil)
|
|
369
|
+
instance = allocate_for_deserialization(register)
|
|
370
|
+
register_id = instance.lutaml_register
|
|
371
|
+
given = {}
|
|
372
|
+
attrs.each do |key, value|
|
|
373
|
+
name = key.to_sym
|
|
374
|
+
next if name == :lutaml_register
|
|
375
|
+
|
|
376
|
+
unless attributes(register_id).key?(name)
|
|
377
|
+
raise Error, "unknown attribute '#{name}' for #{self}"
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
given[name] = value
|
|
381
|
+
end
|
|
382
|
+
attributes(register_id).each do |name, attr|
|
|
383
|
+
next if attr.derived?
|
|
384
|
+
|
|
385
|
+
if given.key?(name)
|
|
386
|
+
instance.public_send(:"#{name}=", given[name])
|
|
387
|
+
else
|
|
388
|
+
# Absent keys seed their default explicitly, mirroring
|
|
389
|
+
# initialize_attributes: every mapped attribute ends up
|
|
390
|
+
# set (nil when no default), never left as the
|
|
391
|
+
# uninitialized sentinel — readers and formatters touch
|
|
392
|
+
# absent attributes freely.
|
|
393
|
+
default = attr.default_value(register_id, instance)
|
|
394
|
+
value = if Lutaml::Model::Utils.uninitialized?(default)
|
|
395
|
+
nil
|
|
396
|
+
else
|
|
397
|
+
attr.cast_value(default, register_id)
|
|
398
|
+
end
|
|
399
|
+
instance.public_send(:"#{name}=", value)
|
|
400
|
+
instance.using_default_for(name)
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
instance
|
|
404
|
+
end
|
|
405
|
+
|
|
353
406
|
# Define register-specific attribute methods on the class itself.
|
|
354
407
|
#
|
|
355
408
|
# Called once per (class, register) combination. Replaces per-instance
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -200,7 +200,14 @@ module Lutaml
|
|
|
200
200
|
|
|
201
201
|
def compilable_mapping?(mapping)
|
|
202
202
|
mapping.root_element &&
|
|
203
|
-
!(mapping.respond_to?(:root_mappings) && mapping.root_mappings)
|
|
203
|
+
!(mapping.respond_to?(:root_mappings) && mapping.root_mappings) &&
|
|
204
|
+
# lutaml-model#88: when_attribute partitions route same-name
|
|
205
|
+
# occurrences by attribute value — name-keyed plan rows would
|
|
206
|
+
# hydrate every occurrence into EVERY partition attribute
|
|
207
|
+
# (verified double-capture under the flag). The interpretive
|
|
208
|
+
# filter is the only correct path until the descriptor ABI
|
|
209
|
+
# grows row predicates.
|
|
210
|
+
mapping.mappings.none?(&:when_attribute?)
|
|
204
211
|
end
|
|
205
212
|
|
|
206
213
|
# Attribute for a rule — delegate rules resolve against their
|
|
@@ -30,7 +30,11 @@ module Lutaml
|
|
|
30
30
|
|
|
31
31
|
child_kwargs[name] = []
|
|
32
32
|
end
|
|
33
|
-
|
|
33
|
+
# TODO.max-perf/33: the bulk constructor — allocate +
|
|
34
|
+
# compiled writers, from_hash semantics — measured ~30x on
|
|
35
|
+
# hydration-heavy corpora versus the generic constructor.
|
|
36
|
+
instance = model_class.instantiate(attr_kwargs.merge(child_kwargs),
|
|
37
|
+
register)
|
|
34
38
|
instance.lutaml_parent = parent if parent
|
|
35
39
|
instance.lutaml_root ||= parent&.lutaml_root || parent
|
|
36
40
|
instance.element_order = PlanOrder.build(node) if node && plan[:ordered]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require_relative "../../../lib/lutaml/model"
|
|
5
|
+
|
|
6
|
+
module InstantiateMapping
|
|
7
|
+
class Part < Lutaml::Model::Serializable
|
|
8
|
+
attribute :label, :string
|
|
9
|
+
attribute :count, :integer
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class Widget < Lutaml::Model::Serializable
|
|
13
|
+
attribute :name, :string
|
|
14
|
+
attribute :flag, :boolean
|
|
15
|
+
attribute :part, Part
|
|
16
|
+
attribute :tags, :string, collection: true
|
|
17
|
+
|
|
18
|
+
key_value do
|
|
19
|
+
map "name", to: :name
|
|
20
|
+
map "flag", to: :flag
|
|
21
|
+
map "part", to: :part
|
|
22
|
+
map "tags", to: :tags
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
RSpec.describe "Serializable.instantiate" do
|
|
28
|
+
it "hydrates attributes from primitives" do
|
|
29
|
+
widget = InstantiateMapping::Widget.instantiate("name" => "w", "flag" => true,
|
|
30
|
+
"tags" => %w[a b])
|
|
31
|
+
expect(widget.name).to eq("w")
|
|
32
|
+
expect(widget.flag).to be(true)
|
|
33
|
+
expect(widget.tags).to eq(%w[a b])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "accepts pre-built instances for typed attributes" do
|
|
37
|
+
part = InstantiateMapping::Part.instantiate(label: "p", count: 2)
|
|
38
|
+
widget = InstantiateMapping::Widget.instantiate(name: "w", part: part)
|
|
39
|
+
|
|
40
|
+
expect(widget.part).to equal(part)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "keeps defaults for absent keys and reports them as defaults" do
|
|
44
|
+
widget = InstantiateMapping::Widget.instantiate
|
|
45
|
+
# Absent scalars read as the uninitialized sentinel, exactly like the
|
|
46
|
+
# XML fast path; serialization omits them either way.
|
|
47
|
+
expect(Lutaml::Model::Utils.uninitialized?(widget.name) ||
|
|
48
|
+
widget.name.nil?).to be(true)
|
|
49
|
+
expect(widget).to be_using_default(:name)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "marks set attributes as explicitly set" do
|
|
53
|
+
widget = InstantiateMapping::Widget.instantiate(name: "w")
|
|
54
|
+
expect(widget).not_to be_using_default(:name)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "serializes identically to from_hash" do
|
|
58
|
+
input = { "name" => "w", "flag" => false,
|
|
59
|
+
"part" => { "label" => "p", "count" => 3 },
|
|
60
|
+
"tags" => ["x"] }
|
|
61
|
+
from_hash = InstantiateMapping::Widget.from_hash(input)
|
|
62
|
+
|
|
63
|
+
part = InstantiateMapping::Part.instantiate(input["part"])
|
|
64
|
+
fast = InstantiateMapping::Widget.instantiate(
|
|
65
|
+
name: "w", flag: false, part: part, tags: ["x"],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
expect(fast.to_hash).to eq(from_hash.to_hash)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "raises on unknown attribute names" do
|
|
72
|
+
expect { InstantiateMapping::Widget.instantiate(bogus: 1) }
|
|
73
|
+
.to raise_error(Lutaml::Model::Error, /unknown attribute/)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -517,6 +517,83 @@ RSpec.describe "XML plan fast path" do
|
|
|
517
517
|
expect(model.to_xml).to eq(interpretive)
|
|
518
518
|
end
|
|
519
519
|
|
|
520
|
+
# lutaml-model#88: name-keyed plan rows would hydrate every
|
|
521
|
+
# same-name occurrence into EVERY partition attribute (verified
|
|
522
|
+
# double-capture) — discriminator models must take the interpretive
|
|
523
|
+
# path, and a partitioned CHILD model must opt its parent out too.
|
|
524
|
+
it "falls back to the interpretive path for when_attribute models" do
|
|
525
|
+
component = Class.new(Lutaml::Model::Serializable) do
|
|
526
|
+
attribute :text, :string
|
|
527
|
+
|
|
528
|
+
xml do
|
|
529
|
+
element "component"
|
|
530
|
+
map_element "text", to: :text
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
stub_const("PlanFastPath::Component", component)
|
|
534
|
+
|
|
535
|
+
partitioned = Class.new(Lutaml::Model::Serializable) do
|
|
536
|
+
attribute :guidance, component, collection: true
|
|
537
|
+
attribute :purpose, component, collection: true
|
|
538
|
+
|
|
539
|
+
xml do
|
|
540
|
+
element "requirement"
|
|
541
|
+
map_element "component", when_attribute: "type",
|
|
542
|
+
to: { "guidance" => :guidance,
|
|
543
|
+
"purpose" => :purpose }
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
stub_const("PlanFastPath::Partitioned", partitioned)
|
|
547
|
+
|
|
548
|
+
req = partitioned.from_xml(<<~XML)
|
|
549
|
+
<requirement>
|
|
550
|
+
<component type="guidance"><text>g1</text></component>
|
|
551
|
+
<component type="purpose"><text>p1</text></component>
|
|
552
|
+
<component type="guidance"><text>g2</text></component>
|
|
553
|
+
</requirement>
|
|
554
|
+
XML
|
|
555
|
+
|
|
556
|
+
expect(req.guidance.map(&:text)).to eq(%w[g1 g2])
|
|
557
|
+
expect(req.purpose.map(&:text)).to eq(["p1"])
|
|
558
|
+
|
|
559
|
+
expect(Lutaml::Xml::PlanCompiler.compile(partitioned,
|
|
560
|
+
:default)).to be_nil
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
it "opts a parent out when a child model partitions with when_attribute" do
|
|
564
|
+
component = Class.new(Lutaml::Model::Serializable) do
|
|
565
|
+
attribute :text, :string
|
|
566
|
+
|
|
567
|
+
xml do
|
|
568
|
+
element "component"
|
|
569
|
+
map_element "text", to: :text
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
inner = Class.new(Lutaml::Model::Serializable) do
|
|
573
|
+
attribute :guidance, component, collection: true
|
|
574
|
+
|
|
575
|
+
xml do
|
|
576
|
+
element "req"
|
|
577
|
+
map_element "component", to: :guidance,
|
|
578
|
+
when_attribute: { "type" => "guidance" }
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
outer = Class.new(Lutaml::Model::Serializable) do
|
|
582
|
+
attribute :req, inner
|
|
583
|
+
|
|
584
|
+
xml do
|
|
585
|
+
element "holder"
|
|
586
|
+
map_element "req", to: :req
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
stub_const("PlanFastPath::Nested", outer)
|
|
590
|
+
|
|
591
|
+
doc = outer.from_xml(
|
|
592
|
+
"<holder><req><component type=\"guidance\"><text>g1</text></component></req></holder>",
|
|
593
|
+
)
|
|
594
|
+
expect(doc.req.guidance.map(&:text)).to eq(["g1"])
|
|
595
|
+
end
|
|
596
|
+
|
|
520
597
|
it "round-trips through both fast paths" do
|
|
521
598
|
item = Class.new(Lutaml::Model::Serializable) do
|
|
522
599
|
attribute :id, :integer
|
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.
|
|
4
|
+
version: 0.8.50
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -1797,6 +1797,7 @@ files:
|
|
|
1797
1797
|
- spec/lutaml/model/schema/yaml_schema_spec.rb
|
|
1798
1798
|
- spec/lutaml/model/sequence_choice_compositor_spec.rb
|
|
1799
1799
|
- spec/lutaml/model/sequence_spec.rb
|
|
1800
|
+
- spec/lutaml/model/serializable_instantiate_spec.rb
|
|
1800
1801
|
- spec/lutaml/model/serializable_spec.rb
|
|
1801
1802
|
- spec/lutaml/model/serializable_validation_spec.rb
|
|
1802
1803
|
- spec/lutaml/model/serialize/builder_spec.rb
|