xmi 0.5.0 → 0.5.2

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: 7ef7e586a3a637179d25efce69e62366184380659e0aa757a5c57327e8c190aa
4
- data.tar.gz: c8331b279ba8c54c8333dc4c954a2812558a35f844811c641ed49922fbe11492
3
+ metadata.gz: 1ac4f03d86ac8abffeaecaf956bcd77ab9a3bb2ed6348a12f3ccb2bd587f2eec
4
+ data.tar.gz: 7f0051b7bbb867c4c7d82ad95fb435a4f5e5d9bb2b76e1297ded8953682489c1
5
5
  SHA512:
6
- metadata.gz: 36141b75f57006f831f6c9e1081f73a737a08e79bd281f295de59a39ff495ef91fa0437899842158d49688b0b08f8b8a5801ade3c6a17f0dbbbce93b1cf4d07d
7
- data.tar.gz: 5bbd57fbae92032dbf081532f905974bf89f1faafa1c07a49d40d794050972a1d11ffeaff8b361fe3256f662a6e30a9ec855b14a372a9ef77ecf037d2817fd33
6
+ metadata.gz: 99a98d34c015c29e2170695e0e3918a98ec9a4308097a32886bd0af401808172b367c2e2da020713dc0ff2db5581c9336b4a4a64e3d3b39b225f4df5cde0296d
7
+ data.tar.gz: 060447e7ae2275a40e4881616dc104032c491ae4cc3bb0707bcfbcafb7bf93a068ff6cda7da3cbeb62c37c3ef5d8e34f66a1dfd1a11206b509e99d3af27a7d8e
data/.rubocop_todo.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2026-03-22 01:00:16 UTC using RuboCop version 1.85.1.
3
+ # on 2026-04-14 03:59:00 UTC using RuboCop version 1.86.1.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
@@ -11,78 +11,68 @@ Gemspec/RequiredRubyVersion:
11
11
  Exclude:
12
12
  - 'xmi.gemspec'
13
13
 
14
- # Offense count: 8
15
- # This cop supports safe autocorrection (--autocorrect).
16
- # Configuration parameters: Width, EnforcedStyleAlignWith, AllowedPatterns.
17
- # SupportedStylesAlignWith: start_of_line, relative_to_receiver
18
- Layout/IndentationWidth:
19
- Exclude:
20
- - 'scripts-xmi-profile/profile_xmi_simple.rb'
21
-
22
- # Offense count: 67
14
+ # Offense count: 72
23
15
  # This cop supports safe autocorrection (--autocorrect).
24
16
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
25
17
  # URISchemes: http, https
26
18
  Layout/LineLength:
27
19
  Enabled: false
28
20
 
29
- # Offense count: 2
30
- # This cop supports safe autocorrection (--autocorrect).
31
- # Configuration parameters: EnforcedStyle, IndentationWidth.
32
- # SupportedStyles: aligned, indented, indented_relative_to_receiver
33
- Layout/MultilineMethodCallIndentation:
34
- Exclude:
35
- - 'spec/xmi/sparx/sparx_root_citygml_spec.rb'
36
-
37
- # Offense count: 3
38
- # This cop supports safe autocorrection (--autocorrect).
39
- # Configuration parameters: AllowInHeredoc.
40
- Layout/TrailingWhitespace:
21
+ # Offense count: 5
22
+ # Configuration parameters: AllowedMethods.
23
+ # AllowedMethods: enums
24
+ Lint/ConstantDefinitionInBlock:
41
25
  Exclude:
42
- - 'scripts-xmi-profile/profile_xmi_simple.rb'
43
- - 'spec/xmi/versioning_spec.rb'
26
+ - 'spec/performance/xmi_parsing_spec.rb'
44
27
 
45
- # Offense count: 2
28
+ # Offense count: 12
46
29
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
47
30
  Metrics/AbcSize:
48
31
  Exclude:
32
+ - 'lib/tasks/benchmark_runner.rb'
33
+ - 'lib/tasks/performance_comparator.rb'
34
+ - 'lib/tasks/performance_helpers.rb'
49
35
  - 'lib/xmi/ea_root.rb'
50
36
  - 'lib/xmi/version_registry.rb'
37
+ - 'spec/performance/xmi_parsing_spec.rb'
51
38
 
52
- # Offense count: 94
39
+ # Offense count: 95
53
40
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
54
41
  # AllowedMethods: refine
55
42
  Metrics/BlockLength:
56
43
  Max: 143
57
44
 
58
- # Offense count: 12
45
+ # Offense count: 1
46
+ # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
47
+ Metrics/CyclomaticComplexity:
48
+ Exclude:
49
+ - 'lib/tasks/performance_helpers.rb'
50
+
51
+ # Offense count: 26
59
52
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
60
53
  Metrics/MethodLength:
61
- Max: 25
54
+ Max: 33
62
55
 
63
- # Offense count: 1
56
+ # Offense count: 2
64
57
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
65
58
  Metrics/PerceivedComplexity:
66
59
  Exclude:
60
+ - 'lib/tasks/performance_helpers.rb'
67
61
  - 'lib/xmi/version_registry.rb'
68
62
 
69
- # Offense count: 13
63
+ # Offense count: 19
70
64
  # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
71
65
  # SupportedStyles: snake_case, normalcase, non_integer
72
66
  # AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
73
67
  Naming/VariableNumber:
74
68
  Exclude:
69
+ - 'lib/tasks/benchmark_runner.rb'
75
70
  - 'lib/xmi/v20110701.rb'
76
71
  - 'lib/xmi/v20131001.rb'
77
72
  - 'lib/xmi/v20161101.rb'
73
+ - 'spec/performance/xmi_parsing_spec.rb'
78
74
  - 'spec/xmi/versioning_spec.rb'
79
75
 
80
- # Offense count: 1
81
- # This cop supports unsafe autocorrection (--autocorrect-all).
82
- Performance/TimesMap:
83
- Exclude:
84
- - 'benchmark_parse.rb'
85
-
86
76
  # Offense count: 1
87
77
  RSpec/BeforeAfterAll:
88
78
  Exclude:
@@ -99,37 +89,26 @@ RSpec/ContextWording:
99
89
  - 'spec/xmi/sparx/sparx_root_gml_spec.rb'
100
90
  - 'spec/xmi/sparx/sparx_root_mdg_spec.rb'
101
91
 
102
- # Offense count: 3
92
+ # Offense count: 4
103
93
  # Configuration parameters: IgnoredMetadata.
104
94
  RSpec/DescribeClass:
105
95
  Exclude:
96
+ - 'spec/performance/xmi_parsing_spec.rb'
106
97
  - 'spec/xmi/edge_cases_spec.rb'
107
98
  - 'spec/xmi/namespace_aliases_spec.rb'
108
99
  - 'spec/xmi/versioning_spec.rb'
109
100
 
110
- # Offense count: 4
111
- # This cop supports unsafe autocorrection (--autocorrect-all).
112
- # Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants.
113
- # SupportedStyles: described_class, explicit
114
- RSpec/DescribedClass:
115
- Exclude:
116
- - 'spec/xmi/sparx/sparx_root_citygml_spec.rb'
117
-
118
101
  # Offense count: 26
119
102
  # Configuration parameters: CountAsOne.
120
103
  RSpec/ExampleLength:
121
- Max: 31
104
+ Max: 33
122
105
 
123
- # Offense count: 2
124
- # This cop supports safe autocorrection (--autocorrect).
125
- # Configuration parameters: CustomTransform, IgnoredWords, DisallowedExamples.
126
- # DisallowedExamples: works
127
- RSpec/ExampleWording:
106
+ # Offense count: 5
107
+ RSpec/LeakyConstantDeclaration:
128
108
  Exclude:
129
- - 'spec/xmi/sparx/sparx_root_eauml_spec.rb'
130
- - 'spec/xmi/sparx/sparx_root_gml_spec.rb'
109
+ - 'spec/performance/xmi_parsing_spec.rb'
131
110
 
132
- # Offense count: 35
111
+ # Offense count: 36
133
112
  RSpec/MultipleExpectations:
134
113
  Max: 8
135
114
 
@@ -138,31 +117,15 @@ RSpec/MultipleExpectations:
138
117
  RSpec/MultipleMemoizedHelpers:
139
118
  Max: 7
140
119
 
141
- # Offense count: 7
120
+ # Offense count: 8
142
121
  # Configuration parameters: AllowedGroups.
143
122
  RSpec/NestedGroups:
144
123
  Max: 4
145
124
 
146
- # Offense count: 1
147
- # This cop supports safe autocorrection (--autocorrect).
148
- # Configuration parameters: EnforcedStyle.
149
- # SupportedStyles: trailing_conditional, ternary
150
- Style/EmptyStringInsideInterpolation:
151
- Exclude:
152
- - 'scripts-xmi-profile/profile_xmi_simple.rb'
153
-
154
- # Offense count: 5
125
+ # Offense count: 4
155
126
  # Configuration parameters: AllowedClasses.
156
127
  Style/OneClassPerFile:
157
128
  Exclude:
129
+ - 'lib/tasks/benchmark_runner.rb'
158
130
  - 'lib/xmi.rb'
159
131
  - 'lib/xmi/namespace/dynamic.rb'
160
- - 'spec/xmi/sparx/shared_contexts.rb'
161
-
162
- # Offense count: 1
163
- # This cop supports safe autocorrection (--autocorrect).
164
- # Configuration parameters: EnforcedStyleForMultiline.
165
- # SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma
166
- Style/TrailingCommaInArguments:
167
- Exclude:
168
- - 'spec/xmi/sparx/sparx_root_citygml_spec.rb'
data/Gemfile CHANGED
@@ -5,6 +5,8 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in xmi.gemspec
6
6
  gemspec
7
7
 
8
+ gem "benchmark"
9
+ gem "benchmark-ips"
8
10
  gem "canon"
9
11
  gem "lutaml-model", github: "lutaml/lutaml-model", ref: "main"
10
12
  gem "rake"
data/Rakefile CHANGED
@@ -9,4 +9,6 @@ require "rubocop/rake_task"
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
12
+ Dir.glob("lib/tasks/**/*.rake").each { |r| load r }
13
+
12
14
  task default: %i[spec rubocop]
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark/ips"
4
+
5
+ # Ensure lib/ is on the load path regardless of tmp location
6
+ lib_path = File.expand_path(File.join(__dir__, "..", "..", "lib"))
7
+ $LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
8
+
9
+ require "xmi"
10
+
11
+ # Pretty terminal formatting for benchmark output
12
+ module Term
13
+ CLEAR = "\e[0m"
14
+ BOLD = "\e[1m"
15
+ DIM = "\e[2m"
16
+ RED = "\e[31m"
17
+ GREEN = "\e[32m"
18
+ YELLOW = "\e[33m"
19
+ CYAN = "\e[36m"
20
+ MAGENTA = "\e[35m"
21
+
22
+ HL = "─"
23
+ VL = "│"
24
+ TL = "┌"
25
+ TR = "┐"
26
+ BL = "└"
27
+ BR = "┘"
28
+
29
+ def self.header(title, color: CYAN)
30
+ width = 78
31
+ line = HL * width
32
+ puts
33
+ puts "#{color}#{TL}#{line}#{TR}#{CLEAR}"
34
+ puts "#{color}#{VL}#{CLEAR} #{BOLD}#{color}#{title}#{CLEAR}#{' ' * (width - title.length - 4)}#{color}#{VL}#{CLEAR}"
35
+ puts "#{color}#{BL}#{line}#{BR}#{CLEAR}"
36
+ end
37
+
38
+ def self.sep(char: HL, width: 78)
39
+ puts "#{DIM}#{char * width}#{CLEAR}"
40
+ end
41
+
42
+ def self.env_info(ruby_version, platform)
43
+ puts
44
+ puts " #{DIM}Environment:#{CLEAR}"
45
+ puts " #{VL} Ruby #{ruby_version} on #{platform}#{' ' * (60 - ruby_version.length - platform.length)}#{VL}"
46
+ puts " #{DIM}#{BL}#{HL * 76}#{BR}#{CLEAR}"
47
+ puts
48
+ end
49
+
50
+ def self.category(title, icon:, description:, failure_means:,
51
+ compare_against: nil)
52
+ puts
53
+ puts "#{CYAN}#{VL}#{CLEAR} #{BOLD}#{MAGENTA}#{icon} #{title}#{CLEAR}"
54
+ puts
55
+ puts " #{DIM}#{description}#{CLEAR}"
56
+ puts
57
+
58
+ if compare_against
59
+ puts " #{CYAN}Comparing against:#{CLEAR} #{compare_against}"
60
+ puts
61
+ end
62
+
63
+ puts " #{YELLOW}⚠️ Failure means:#{CLEAR} #{failure_means}"
64
+ puts
65
+ sep(width: 76)
66
+ puts
67
+ end
68
+ end
69
+
70
+ class BenchmarkRunner
71
+ REPO_ROOT = File.expand_path(File.join(__dir__, "..", ".."))
72
+
73
+ # Benchmark configuration
74
+ DEFAULT_RUN_TIME = 5
75
+ DEFAULT_WARMUP = 2
76
+
77
+ # Category definitions with descriptions
78
+ CATEGORIES = {
79
+ xmi_parsing: {
80
+ name: "XMI Parsing",
81
+ icon: "📄",
82
+ description: "XMI parsing performance tests. Measures how quickly we can convert XMI files into Ruby objects.",
83
+ failure_means: "Slow XMI parsing impacts all downstream operations. A regression here means users will experience delays when processing XMI documents.",
84
+ compare_against: "Previous branch (main).",
85
+ },
86
+ }.freeze
87
+
88
+ # Test definitions
89
+ BENCHMARKS = {
90
+ xmi_parsing: [
91
+ { name: "XMI 2.4.2 (small)", method: :xmi_parse_242_small,
92
+ desc: "XMI 2.4.2 ~100KB file" },
93
+ { name: "XMI 2.4.2 (medium)", method: :xmi_parse_242_medium,
94
+ desc: "XMI 2.4.2 ~500KB file with extensions" },
95
+ { name: "XMI 2.4.2 (large)", method: :xmi_parse_242_large,
96
+ desc: "XMI 2.4.2 ~3.5MB file" },
97
+ { name: "XMI 2.5.1", method: :xmi_parse_251,
98
+ desc: "XMI 2.5.1 ~100KB file" },
99
+ ],
100
+ }.freeze
101
+
102
+ # Test data - fixture paths
103
+ FIXTURES = {
104
+ xmi_parse_242_small: "spec/fixtures/xmi-v2-4-2-default.xmi",
105
+ xmi_parse_242_medium: "spec/fixtures/xmi-v2-4-2-default-with-citygml.xmi",
106
+ xmi_parse_242_large: "spec/fixtures/full-242.xmi",
107
+ xmi_parse_251: "spec/fixtures/ea-xmi-2.5.1.xmi",
108
+ }.freeze
109
+
110
+ def initialize(run_time: nil, warmup: nil, benchmark: nil)
111
+ @run_time = run_time || DEFAULT_RUN_TIME
112
+ @warmup = warmup || DEFAULT_WARMUP
113
+ @benchmark = benchmark
114
+ @results = {}
115
+ @env_shown = false
116
+ @all_results = []
117
+ end
118
+
119
+ def run_benchmarks
120
+ Term.header("XMI Performance Benchmarks", color: Term::CYAN)
121
+
122
+ unless @env_shown
123
+ Term.env_info(RUBY_VERSION, RUBY_PLATFORM)
124
+ @env_shown = true
125
+ end
126
+
127
+ BENCHMARKS.each do |category, tests|
128
+ run_category(category, tests)
129
+ end
130
+
131
+ print_summary
132
+
133
+ @results
134
+ end
135
+
136
+ private
137
+
138
+ def run_category(category, tests)
139
+ config = CATEGORIES[category]
140
+ Term.category(
141
+ config[:name],
142
+ icon: config[:icon],
143
+ description: config[:description],
144
+ failure_means: config[:failure_means],
145
+ compare_against: config[:compare_against],
146
+ )
147
+
148
+ category_results = []
149
+
150
+ tests.each do |test|
151
+ # Redirect stdout during benchmark
152
+ original_stdout = $stdout
153
+ $stdout = StringIO.new
154
+
155
+ result = run_single_test(test[:method])
156
+ (result[:lower] + result[:upper]) / 2.0
157
+ category_results << { name: test[:name], result: result }
158
+
159
+ # Restore stdout
160
+ $stdout = original_stdout
161
+ end
162
+
163
+ # Print results
164
+ puts " #{'Benchmark'.ljust(40)} #{'IPS'.rjust(12)} #{'Deviation'.rjust(12)}"
165
+ puts " #{Term::DIM}#{Term::HL * 66}#{Term::CLEAR}"
166
+
167
+ category_results.each do |r|
168
+ ips = (r[:result][:lower] + r[:result][:upper]) / 2.0
169
+ deviation = calculate_deviation(r[:result])
170
+ label = "#{config[:name]}: #{r[:name]}"
171
+ @all_results << { label: label, ips: ips }
172
+ @results[label] = r[:result]
173
+
174
+ puts " #{r[:name].ljust(40)} #{format('%.2f',
175
+ ips).rjust(12)} #{format('%.1f%%',
176
+ deviation).rjust(12)}"
177
+ end
178
+
179
+ puts
180
+ end
181
+
182
+ def run_single_test(method)
183
+ fixture_path = FIXTURES[method]
184
+ raise "Unknown fixture: #{method}" unless fixture_path
185
+
186
+ # Try to resolve fixture path relative to REPO_ROOT
187
+ full_path = File.join(REPO_ROOT, fixture_path)
188
+ unless File.exist?(full_path)
189
+ # Fallback: try current directory
190
+ full_path = fixture_path
191
+ end
192
+
193
+ xml_content = File.read(full_path)
194
+
195
+ case method
196
+ when :xmi_parse_242_small, :xmi_parse_242_medium, :xmi_parse_242_large, :xmi_parse_251
197
+ measure_time { Xmi::Sparx::SparxRoot.parse_xml(xml_content) }
198
+ else
199
+ raise "Unknown benchmark: #{method}"
200
+ end
201
+ end
202
+
203
+ def measure(&)
204
+ job = Benchmark::IPS::Job.new
205
+ job.config(time: @run_time, warmup: @warmup)
206
+ job.report("test", &)
207
+ job.run
208
+
209
+ entry = job.full_report.entries.first
210
+ samples = entry.stats.samples
211
+
212
+ return { lower: 0, upper: 0 } if samples.empty?
213
+
214
+ mean = samples.sum.to_f / samples.size
215
+ variance = samples.sum { |x| (x - mean)**2 } / (samples.size - 1)
216
+ std_dev = Math.sqrt(variance)
217
+ error_margin = std_dev / mean
218
+ error_pct = error_margin.round(4)
219
+
220
+ { lower: mean.round(4) * (1 - error_pct),
221
+ upper: mean.round(4) * (1 + error_pct) }
222
+ end
223
+
224
+ def measure_time
225
+ times = []
226
+ iterations = 5
227
+
228
+ iterations.times do
229
+ start_t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
230
+ yield
231
+ finish_t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
232
+ times << (finish_t - start_t)
233
+ end
234
+
235
+ mean = times.sum / times.size
236
+ variance = times.sum { |t| (t - mean)**2 } / (times.size - 1)
237
+ std_dev = Math.sqrt(variance)
238
+
239
+ # Use conservative estimates for time-based measurement
240
+ lower_time = [mean - std_dev, mean * 0.5].max
241
+ lower_ips = (1.0 / (lower_time * 1.5)).round(4)
242
+ upper_ips = (1.0 / mean).round(4)
243
+
244
+ # For fast operations, estimate more conservatively
245
+ if mean < 0.001
246
+ upper_ips = (1.0 / mean).round(4)
247
+ lower_ips = (upper_ips * 0.8).round(4)
248
+ end
249
+
250
+ { lower: lower_ips, upper: upper_ips }
251
+ end
252
+
253
+ def calculate_deviation(metrics)
254
+ return 0 if metrics[:upper].zero?
255
+
256
+ ((metrics[:upper] - metrics[:lower]) / metrics[:upper] * 100).round(1)
257
+ end
258
+
259
+ def print_summary
260
+ puts
261
+ Term.sep(width: 78)
262
+ puts
263
+ puts " #{Term::BOLD}#{Term::MAGENTA}SUMMARY#{Term::CLEAR}"
264
+ puts
265
+
266
+ @all_results.each do |r|
267
+ puts " #{r[:label].ljust(60)} #{format('%.2f', r[:ips]).rjust(10)} IPS"
268
+ end
269
+
270
+ puts
271
+ puts " #{Term::DIM}#{@all_results.length} benchmarks completed#{Term::CLEAR}"
272
+ puts
273
+ end
274
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "performance_comparator"
4
+ require_relative "benchmark_runner"
5
+
6
+ desc "Run performance benchmarks"
7
+ namespace :performance do
8
+ desc "Compare performance of current branch against base branch (default: main)"
9
+ task :compare do
10
+ PerformanceComparator.new.run
11
+ end
12
+
13
+ desc "Run benchmarks on current branch only (for development)"
14
+ task :run do
15
+ runner = BenchmarkRunner.new(run_time: 5)
16
+ runner.run_benchmarks
17
+ end
18
+
19
+ desc "Quick benchmark run (faster, less accurate)"
20
+ task :quick do
21
+ runner = BenchmarkRunner.new(run_time: 2, warmup: 1)
22
+ runner.run_benchmarks
23
+ end
24
+
25
+ desc "Run benchmarks and output as JSON"
26
+ task :json do
27
+ require "json"
28
+ runner = BenchmarkRunner.new(run_time: 5)
29
+
30
+ # Suppress pretty output, just get results
31
+ results = runner.send(:run_benchmarks)
32
+
33
+ output = results.each_with_object({}) do |(label, metrics), h|
34
+ ips = (metrics[:lower] + metrics[:upper]) / 2.0
35
+ deviation = ((metrics[:upper] - metrics[:lower]) / metrics[:upper] * 100).round(1)
36
+ h[label] = {
37
+ ips: ips.round(2),
38
+ lower: metrics[:lower].round(2),
39
+ upper: metrics[:upper].round(2),
40
+ deviation: deviation,
41
+ }
42
+ end
43
+
44
+ puts JSON.pretty_generate(output)
45
+ end
46
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "performance_helpers"
4
+
5
+ class PerformanceComparator
6
+ REPO_ROOT = File.expand_path(File.join(__dir__, "..", ".."))
7
+ DEFAULT_RUN_TIME = 10
8
+ DEFAULT_THRESHOLD = 0.10 # 10% (more lenient for complex operations)
9
+ DEFAULT_BASE = "main"
10
+ TMP_PERF_DIR = File.join(REPO_ROOT, "tmp", "performance")
11
+ BENCH_SCRIPT = File.join(TMP_PERF_DIR, "benchmark_runner.rb")
12
+
13
+ def run
14
+ setup_environment
15
+ run_benchmarks_comparison
16
+ ensure
17
+ cleanup
18
+ end
19
+
20
+ private
21
+
22
+ def setup_environment
23
+ Dir.chdir(REPO_ROOT)
24
+ FileUtils.mkdir_p(TMP_PERF_DIR)
25
+ FileUtils.cp(File.join(REPO_ROOT, "lib", "tasks", "benchmark_runner.rb"),
26
+ BENCH_SCRIPT)
27
+
28
+ PerformanceHelpers.load_into_namespace(PerformanceHelpers::Current,
29
+ BENCH_SCRIPT)
30
+ PerformanceHelpers.clone_base_repo(DEFAULT_BASE, TMP_PERF_DIR, BENCH_SCRIPT)
31
+ end
32
+
33
+ def run_benchmarks_comparison
34
+ all_current = {}
35
+ all_base = {}
36
+
37
+ puts PerformanceHelpers::Term.header("Performance Comparison", color: PerformanceHelpers::CYAN)
38
+ puts
39
+ puts " #{PerformanceHelpers::DIM}Comparing#{PerformanceHelpers::CLEAR}:"
40
+ puts " #{PerformanceHelpers::CYAN} Current#{PerformanceHelpers::CLEAR}: #{PerformanceHelpers.current_branch}"
41
+ puts " #{PerformanceHelpers::CYAN} Base#{PerformanceHelpers::CLEAR}: #{DEFAULT_BASE}"
42
+ puts " #{PerformanceHelpers::CYAN} Threshold#{PerformanceHelpers::CLEAR}: #{(DEFAULT_THRESHOLD * 100).round(0)}% regression allowed"
43
+ puts
44
+
45
+ # Run all benchmarks
46
+ base_runner = PerformanceHelpers::Base::BenchmarkRunner.new(
47
+ run_time: DEFAULT_RUN_TIME,
48
+ )
49
+ current_runner = PerformanceHelpers::Current::BenchmarkRunner.new(
50
+ run_time: DEFAULT_RUN_TIME,
51
+ )
52
+
53
+ PerformanceHelpers.run_benchmarks(
54
+ base_runner,
55
+ current_runner,
56
+ DEFAULT_THRESHOLD,
57
+ all_base,
58
+ all_current,
59
+ )
60
+
61
+ summary = PerformanceHelpers.summary_report(
62
+ all_current,
63
+ all_base,
64
+ DEFAULT_BASE,
65
+ DEFAULT_RUN_TIME,
66
+ DEFAULT_THRESHOLD,
67
+ )
68
+
69
+ handle_results(summary)
70
+ end
71
+
72
+ def handle_results(summary)
73
+ puts
74
+ if summary[:regressions].any?
75
+ puts " #{PerformanceHelpers::RED}#{PerformanceHelpers::BOLD}❌ PERFORMANCE REGRESSIONS DETECTED#{PerformanceHelpers::CLEAR}"
76
+ puts " #{PerformanceHelpers::RED}#{summary[:regressions].length} benchmark(s) regressed beyond threshold#{PerformanceHelpers::CLEAR}"
77
+ puts
78
+ exit(1)
79
+ else
80
+ puts " #{PerformanceHelpers::GREEN}#{PerformanceHelpers::BOLD}✅ ALL BENCHMARKS PASSED#{PerformanceHelpers::CLEAR}"
81
+ puts
82
+ end
83
+ end
84
+
85
+ def cleanup
86
+ FileUtils.rm_rf(TMP_PERF_DIR)
87
+ end
88
+ end
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+ require "tmpdir"
6
+ require "fileutils"
7
+
8
+ module PerformanceHelpers
9
+ # ANSI color codes for terminal output
10
+ CLEAR = "\e[0m"
11
+ BOLD = "\e[1m"
12
+ DIM = "\e[2m"
13
+ CYAN = "\e[36m"
14
+ GREEN = "\e[32m"
15
+ YELLOW = "\e[33m"
16
+ RED = "\e[31m"
17
+ GRAY = "\e[90m"
18
+ MAGENTA = "\e[35m"
19
+
20
+ # Terminal formatting helpers
21
+ module Term
22
+ extend self
23
+
24
+ HL = "─"
25
+ VL = "│"
26
+ TL = "┌"
27
+ TR = "┐"
28
+ BL = "└"
29
+ BR = "┘"
30
+
31
+ def header(title, color: PerformanceHelpers::CYAN)
32
+ width = 78
33
+ line = HL * width
34
+ puts
35
+ puts "#{color}#{TL}#{line}#{TR}#{CLEAR}"
36
+ puts "#{color}#{VL}#{CLEAR} #{BOLD}#{color}#{title}#{CLEAR}#{' ' * (width - title.length - 4)}#{color}#{VL}#{CLEAR}"
37
+ puts "#{color}#{BL}#{line}#{BR}#{CLEAR}"
38
+ end
39
+
40
+ def sep(char: HL, width: 78)
41
+ puts "#{DIM}#{char * width}#{CLEAR}"
42
+ end
43
+ end
44
+
45
+ module Base
46
+ end
47
+
48
+ module Current
49
+ end
50
+
51
+ class << self
52
+ def load_into_namespace(module_obj, file_path)
53
+ content = File.read(file_path)
54
+ module_obj.module_eval(content, file_path)
55
+ end
56
+
57
+ def ruby_exec(cmd, env: {})
58
+ Open3.capture3(env, cmd)
59
+ end
60
+
61
+ def current_branch
62
+ stdout, = ruby_exec("git rev-parse --abbrev-ref HEAD")
63
+ stdout.strip
64
+ end
65
+
66
+ # Clone base branch into a temp dir and return its path
67
+ def clone_base_repo(base, performance_dir, script)
68
+ puts "#{DIM}Cloning base #{base}...#{CLEAR}"
69
+ safe_ref = base.gsub(/[^0-9A-Za-z._-]/, "-")
70
+ clone_dir = File.join(performance_dir, "base-#{safe_ref}")
71
+ FileUtils.rm_rf(clone_dir)
72
+
73
+ repo_url, = ruby_exec("git config --get remote.origin.url")
74
+ repo_url = repo_url.strip
75
+
76
+ stdout, stderr, status = ruby_exec("git clone --branch #{safe_ref} --single-branch #{repo_url} #{clone_dir}")
77
+ raise "git clone failed: #{stderr}\n#{stdout}" unless status.success?
78
+
79
+ Dir.chdir(clone_dir) do
80
+ stdout, stderr, status = ruby_exec("bundle install --quiet")
81
+ raise "bundle install failed: #{stderr}\n#{stdout}" unless status.success?
82
+
83
+ bench_copy_dir = File.join(clone_dir, "lib", "tasks")
84
+ FileUtils.mkdir_p(bench_copy_dir)
85
+ bench_copy = File.join(bench_copy_dir, "benchmark_runner.rb")
86
+ File.write(bench_copy, File.read(script))
87
+ load_into_namespace(Base, bench_copy)
88
+ end
89
+ end
90
+
91
+ def run_benchmarks(base_runner, current_runner, threshold, all_base,
92
+ all_current)
93
+ base_results = base_runner.run_benchmarks
94
+ curr_results = current_runner.run_benchmarks
95
+
96
+ all_base.merge!(base_results)
97
+ all_current.merge!(curr_results)
98
+
99
+ # Collect comparison results
100
+ comparison_rows = []
101
+
102
+ curr_results.each do |label, result|
103
+ base_result = base_results[label]
104
+ cmp = compare_metrics(label, result, base_result, threshold)
105
+ comparison_rows << cmp
106
+ end
107
+
108
+ print_comparison_table(comparison_rows, threshold)
109
+ end
110
+
111
+ def print_comparison_table(comparison_rows, threshold)
112
+ rows = comparison_rows.map do |cmp|
113
+ {
114
+ benchmark: cmp[:label],
115
+ base_ips: cmp[:base_ips]&.round(1),
116
+ curr_ips: cmp[:curr_ips]&.round(1),
117
+ change: cmp[:change] ? "#{(cmp[:change] * 100).round(1)}%" : "N/A",
118
+ status: if cmp[:base_ips].nil?
119
+ "NEW"
120
+ elsif cmp[:change] < -threshold
121
+ "REGRESSED"
122
+ else
123
+ "OK"
124
+ end,
125
+ }
126
+ end
127
+
128
+ return if rows.empty?
129
+
130
+ puts " #{'Benchmark'.ljust(40)} #{'Base IPS'.rjust(12)} #{'Curr IPS'.rjust(12)} #{'Change'.rjust(10)} #{'Status'.rjust(10)}"
131
+ puts " #{DIM}#{'─' * 86}#{CLEAR}"
132
+
133
+ rows.each do |row|
134
+ status_color = case row[:status]
135
+ when "REGRESSED" then RED
136
+ when "NEW" then YELLOW
137
+ else GREEN
138
+ end
139
+ row[:status] == "REGRESSED" ? RED : DIM
140
+
141
+ puts " #{row[:benchmark].ljust(40)} #{format('%-12.1f',
142
+ row[:base_ips] || 0)} #{format('%-12.1f',
143
+ row[:curr_ips] || 0)} #{format('%-10s', row[:change]).gsub('%',
144
+ '%%')} #{status_color}#{row[:status].rjust(10)}#{CLEAR}"
145
+ end
146
+
147
+ puts
148
+ end
149
+
150
+ def compare_metrics(label, curr, base, threshold)
151
+ unless base
152
+ return { label: label, base_ips: nil, curr_ips: nil, change: nil,
153
+ regressed: false }
154
+ end
155
+
156
+ base_ips = base.fetch(:lower)
157
+ curr_ips = curr.fetch(:upper)
158
+ change = (curr_ips - base_ips) / base_ips.to_f
159
+
160
+ {
161
+ label: label,
162
+ base_ips: base_ips,
163
+ curr_ips: curr_ips,
164
+ change: change,
165
+ regressed: change < -threshold,
166
+ }
167
+ end
168
+
169
+ def summary_report(current_results, base_results, base, run_time, threshold)
170
+ summary = {
171
+ run_time: run_time,
172
+ threshold: threshold,
173
+ branch: current_branch,
174
+ base: base,
175
+ regressions: [],
176
+ new_benchmarks: [],
177
+ }
178
+
179
+ current_results.each do |label, metrics|
180
+ base_result = base_results[label]
181
+ cmp = compare_metrics(label, metrics, base_result, threshold)
182
+
183
+ # Track new benchmarks that don't exist in base
184
+ if base_result.nil?
185
+ summary[:new_benchmarks] << label
186
+ next
187
+ end
188
+
189
+ next unless cmp[:regressed]
190
+
191
+ summary[:regressions] << {
192
+ label: label,
193
+ base_ips: cmp[:base_ips],
194
+ curr_ips: cmp[:curr_ips],
195
+ delta_fraction: cmp[:change],
196
+ }
197
+ end
198
+
199
+ log_regressions(summary[:regressions], threshold)
200
+ log_new_benchmarks(summary[:new_benchmarks])
201
+ summary
202
+ end
203
+
204
+ def log_new_benchmarks(new_benchmarks)
205
+ return if new_benchmarks.empty?
206
+
207
+ puts
208
+ puts "#{YELLOW}🆕 New benchmarks (not in base branch):#{CLEAR}"
209
+ new_benchmarks.each do |label|
210
+ puts " • #{label}"
211
+ end
212
+ end
213
+
214
+ def log_regressions(regressions, threshold)
215
+ return if regressions.empty?
216
+
217
+ puts
218
+ puts "#{RED}⚠️ Performance Regressions Detected#{CLEAR}"
219
+ puts "#{RED} (< -#{(threshold * 100).round(2)}% IPS)#{CLEAR}"
220
+ puts
221
+ regressions.each do |regression|
222
+ delta = regression[:delta_fraction]
223
+ base_ips = regression[:base_ips]
224
+ curr_ips = regression[:curr_ips]
225
+
226
+ delta_str = delta ? format("%+0.2f%%", delta * 100) : "N/A"
227
+ base_str = base_ips ? format("%.2f", base_ips) : "N/A"
228
+ curr_str = curr_ips ? format("%.2f", curr_ips) : "N/A"
229
+
230
+ puts " #{BOLD}#{regression[:label]}#{CLEAR}"
231
+ puts " #{GRAY}base: #{base_str} IPS#{CLEAR}"
232
+ puts " #{RED}curr: #{curr_str} IPS#{CLEAR}"
233
+ puts " #{RED}change: #{delta_str}#{CLEAR}"
234
+ puts
235
+ end
236
+ end
237
+ end
238
+ end
data/lib/xmi/parsing.rb CHANGED
@@ -100,7 +100,10 @@ module Xmi
100
100
  # Explicit version
101
101
  if options[:version]
102
102
  reg = VersionRegistry.register_for_version(options[:version])
103
- raise ArgumentError, "Unknown version: #{options[:version]}" unless reg
103
+ unless reg
104
+ raise ArgumentError,
105
+ "Unknown version: #{options[:version]}"
106
+ end
104
107
 
105
108
  return reg
106
109
  end
@@ -194,7 +194,7 @@ module Xmi
194
194
 
195
195
  class Connector < Lutaml::Model::Serializable
196
196
  attribute :name, :string
197
- attribute :idref, :string
197
+ attribute :idref, ::Xmi::Type::XmiIdRef
198
198
  attribute :source, Source
199
199
  attribute :target, Target
200
200
  attribute :model, Model
data/lib/xmi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Xmi
4
- VERSION = "0.5.0"
4
+ VERSION = "0.5.2"
5
5
  end
@@ -95,7 +95,10 @@ module Xmi
95
95
  all_versions = [versions[:xmi], versions[:uml], versions[:umldi],
96
96
  versions[:umldc]].compact.uniq
97
97
 
98
- extend_fallback_for_mixed_namespaces(primary_register, all_versions) if all_versions.length > 1
98
+ if all_versions.length > 1
99
+ extend_fallback_for_mixed_namespaces(primary_register,
100
+ all_versions)
101
+ end
99
102
 
100
103
  primary_register
101
104
  end
data/lib/xmi/versioned.rb CHANGED
@@ -46,7 +46,8 @@ module Xmi
46
46
  #
47
47
  # @return [Lutaml::Model::Register]
48
48
  def create_register
49
- reg = Lutaml::Model::Register.new(register_id, fallback: fallback_registers)
49
+ reg = Lutaml::Model::Register.new(register_id,
50
+ fallback: fallback_registers)
50
51
 
51
52
  # Register in GlobalRegister first
52
53
  Lutaml::Model::GlobalRegister.register(reg)
@@ -128,7 +129,9 @@ module Xmi
128
129
  #
129
130
  # @return [Class, nil]
130
131
  def uml_namespace
131
- namespace_classes.find { |ns| ns.uri.include?("/UML/") && !ns.uri.include?("UMLD") }
132
+ namespace_classes.find do |ns|
133
+ ns.uri.include?("/UML/") && !ns.uri.include?("UMLD")
134
+ end
132
135
  end
133
136
 
134
137
  # @api public
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: xmi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.2
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-03-22 00:00:00.000000000 Z
11
+ date: 2026-04-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model
@@ -56,11 +56,14 @@ files:
56
56
  - Gemfile
57
57
  - README.adoc
58
58
  - Rakefile
59
- - benchmark_parse.rb
60
59
  - bin/console
61
60
  - bin/setup
62
61
  - docs/migration.md
63
62
  - docs/versioning.md
63
+ - lib/tasks/benchmark_runner.rb
64
+ - lib/tasks/performance.rake
65
+ - lib/tasks/performance_comparator.rb
66
+ - lib/tasks/performance_helpers.rb
64
67
  - lib/xmi.rb
65
68
  - lib/xmi/add.rb
66
69
  - lib/xmi/custom_profile.rb
@@ -100,7 +103,6 @@ files:
100
103
  - lib/xmi/version.rb
101
104
  - lib/xmi/version_registry.rb
102
105
  - lib/xmi/versioned.rb
103
- - scripts-xmi-profile/profile_xmi_simple.rb
104
106
  - sig/xmi.rbs
105
107
  - xmi.gemspec
106
108
  homepage: https://github.com/lutaml/xmi
data/benchmark_parse.rb DELETED
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Benchmark script for XMI parsing performance.
4
- # Usage: bundle exec ruby benchmark_parse.rb
5
- #
6
- # Parses the full-242.xmi fixture (3.5 MB) multiple times and reports
7
- # average, min, and max parse times.
8
-
9
- require "bundler/setup"
10
- require "xmi"
11
-
12
- FIXTURE_PATH = File.join(__dir__, "spec", "fixtures", "full-242.xmi")
13
- WARMUP_RUNS = 2
14
- BENCH_RUNS = 5
15
-
16
- abort "Fixture not found: #{FIXTURE_PATH}" unless File.exist?(FIXTURE_PATH)
17
-
18
- xml_content = File.read(FIXTURE_PATH)
19
- file_size_mb = File.size(FIXTURE_PATH).to_f / (1024 * 1024)
20
-
21
- puts "XMI Parsing Benchmark"
22
- puts "=" * 50
23
- puts "File: #{FIXTURE_PATH}"
24
- puts "Size: #{file_size_mb.round(2)} MB"
25
- puts "Ruby: #{RUBY_VERSION} (#{RUBY_PLATFORM})"
26
- puts "Warmup runs: #{WARMUP_RUNS}"
27
- puts "Benchmark runs: #{BENCH_RUNS}"
28
- puts
29
-
30
- # Warmup
31
- puts "Warming up..."
32
- WARMUP_RUNS.times do |i|
33
- GC.start
34
- Xmi::Sparx::SparxRoot.parse_xml(xml_content)
35
- puts " Warmup #{i + 1}/#{WARMUP_RUNS} complete"
36
- end
37
-
38
- # Benchmark
39
- puts "Benchmarking..."
40
- times = BENCH_RUNS.times.map do |i|
41
- GC.start
42
- t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
43
- Xmi::Sparx::SparxRoot.parse_xml(xml_content)
44
- t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
45
- elapsed = t1 - t0
46
- puts " Run #{i + 1}/#{BENCH_RUNS}: #{elapsed.round(3)}s"
47
- elapsed
48
- end
49
-
50
- avg = times.sum / times.size
51
- min = times.min
52
- max = times.max
53
-
54
- puts
55
- puts "Results"
56
- puts "-" * 50
57
- puts "Average: #{avg.round(3)} s"
58
- puts "Min: #{min.round(3)} s"
59
- puts "Max: #{max.round(3)} s"
60
- puts "StdDev: #{Math.sqrt(times.sum { |t| (t - avg)**2 } / times.size).round(3)} s"
@@ -1,213 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Simple XMI Performance Profiling Script (no external dependencies)
5
- #
6
- # This script helps identify performance bottlenecks using only Ruby's
7
- # standard library. Run from the xmi repository.
8
- #
9
- # Usage:
10
- # XMI_SAMPLE_FILE=path/to/sample.xmi ruby scripts-xmi-profile/profile_xmi_simple.rb
11
-
12
- require "bundler/setup"
13
- require "benchmark"
14
- require "objspace"
15
-
16
- begin
17
- require "xmi"
18
- rescue LoadError
19
- puts "ERROR: xmi gem not found. Please run this script from the xmi repository."
20
- exit 1
21
- end
22
-
23
- SAMPLE_FILE = ENV.fetch("XMI_SAMPLE_FILE", nil)
24
-
25
- unless SAMPLE_FILE && File.exist?(SAMPLE_FILE)
26
- puts <<~MSG
27
- ERROR: No sample XMI file specified.
28
-
29
- Please set the XMI_SAMPLE_FILE environment variable:
30
- XMI_SAMPLE_FILE=path/to/sample.xmi ruby scripts-xmi-profile/profile_xmi_simple.rb
31
- MSG
32
- exit 1
33
- end
34
-
35
- puts "=" * 80
36
- puts "XMI Simple Performance Profile"
37
- puts "=" * 80
38
-
39
- xmi_content = File.read(SAMPLE_FILE)
40
- puts "File: #{SAMPLE_FILE} (#{File.size(SAMPLE_FILE)} bytes)"
41
- puts
42
-
43
- # Method call tracing
44
- puts "-" * 40
45
- puts "Method Call Tracing (top 30 by calls)"
46
- puts "-" * 40
47
-
48
- call_counts = Hash.new(0)
49
- Hash.new(0.0)
50
-
51
- trace = TracePoint.new(:call, :c_call) do |tp|
52
- # Only trace lutaml-model code
53
- next unless tp.path&.include?("lutaml")
54
-
55
- method_name = "#{tp.defined_class}##{tp.method_id}"
56
- call_counts[method_name] += 1
57
- end
58
-
59
- # Enable tracing and run
60
- GC.start
61
- trace.enable
62
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
63
-
64
- begin
65
- Xmi::Sparx::SparxRoot.parse_xml(xmi_content)
66
- rescue StandardError => e
67
- puts "Parse error (continuing with profile): #{e.message}"
68
- end
69
-
70
- finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
71
- trace.disable
72
-
73
- puts "Total parse time: #{(finish - start).round(3)}s"
74
- puts
75
-
76
- # Sort by call count
77
- sorted_by_calls = call_counts.sort_by { |_, count| -count }.first(30)
78
-
79
- puts "By call count:"
80
- sorted_by_calls.each do |method, count|
81
- # Truncate long method names
82
- display_method = method.length > 70 ? "...#{method[-67..]}" : method
83
- puts " #{count.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse.rjust(12)}: #{display_method}"
84
- end
85
- puts
86
-
87
- # Look for potential issues
88
- puts "-" * 40
89
- puts "Potential Issues"
90
- puts "-" * 40
91
-
92
- # Check for methods called excessively
93
- excessive_threshold = 10_000
94
- excessive = call_counts.select { |_, count| count > excessive_threshold }
95
- if excessive.any?
96
- puts "Methods called more than #{excessive_threshold} times:"
97
- excessive.sort_by { |_, c| -c }.each do |method, count|
98
- puts " #{count.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse}: #{method}"
99
- end
100
- else
101
- puts "No methods called more than #{excessive_threshold} times"
102
- end
103
- puts
104
-
105
- # Check for duplicate detection in mapping
106
- duplicate_checks = call_counts.select { |m, _| m.include?("eql?") || m.include?("==") }
107
- if duplicate_checks.any?
108
- puts "Duplicate detection calls:"
109
- duplicate_checks.sort_by { |_, c| -c }.each do |method, count|
110
- puts " #{count}: #{method}"
111
- end
112
- end
113
- puts
114
-
115
- # Memory analysis
116
- puts "-" * 40
117
- puts "Memory Analysis"
118
- puts "-" * 40
119
-
120
- GC.start
121
- before = ObjectSpace.count_objects
122
-
123
- begin
124
- Xmi::Sparx::SparxRoot.parse_xml(xmi_content)
125
- rescue StandardError => e
126
- puts "Parse error (continuing): #{e.message}"
127
- end
128
-
129
- GC.start
130
- after = ObjectSpace.count_objects
131
-
132
- puts "Object count changes:"
133
- %i[T_OBJECT T_ARRAY T_HASH T_STRING T_DATA T_SYMBOL].each do |type|
134
- diff = (after[type] || 0) - (before[type] || 0)
135
- puts " #{type}: #{diff >= 0 ? '+' : ''}#{diff}"
136
- end
137
- puts
138
-
139
- # Transformation registry analysis
140
- puts "-" * 40
141
- puts "Transformation Registry Analysis"
142
- puts "-" * 40
143
-
144
- if defined?(Lutaml::Model::TransformationRegistry)
145
- registry = Lutaml::Model::TransformationRegistry.instance
146
- begin
147
- count = begin
148
- registry.send(:transformations)&.size
149
- rescue StandardError
150
- "N/A"
151
- end
152
- puts "Registered transformations: #{count}"
153
- rescue StandardError => e
154
- puts "Could not access transformation count: #{e.message}"
155
- end
156
-
157
- # Try to get cache stats if available
158
- if registry.respond_to?(:cache_stats)
159
- puts "Cache stats: #{registry.cache_stats}"
160
- end
161
- end
162
- puts
163
-
164
- # Check for mapping accumulation
165
- puts "-" * 40
166
- puts "Mapping Accumulation Check"
167
- puts "-" * 40
168
-
169
- # Look at all loaded classes that include Lutaml::Model::Serialize
170
- lutaml_classes = ObjectSpace.each_object(Class).select do |klass|
171
-
172
- klass.include?(Lutaml::Model::Serialize)
173
- rescue StandardError
174
- false
175
-
176
- end
177
-
178
- puts "Lutaml::Model classes loaded: #{lutaml_classes.size}"
179
-
180
- # Check for classes with many mappings
181
- classes_with_many_mappings = lutaml_classes.select do |klass|
182
- mappings = begin
183
- klass.mappings_for(:xml)&.elements
184
- rescue StandardError
185
- []
186
- end
187
- mappings.size > 20
188
- end
189
-
190
- if classes_with_many_mappings.any?
191
- puts "Classes with >20 mappings:"
192
- classes_with_many_mappings.each do |klass|
193
- mappings = begin
194
- klass.mappings_for(:xml)&.elements
195
- rescue StandardError
196
- []
197
- end
198
- puts " #{klass}: #{mappings.size} mappings"
199
- end
200
- else
201
- puts "No classes with excessive mappings (>20)"
202
- end
203
- puts
204
-
205
- # Summary
206
- puts "=" * 80
207
- puts "Summary"
208
- puts "=" * 80
209
- puts "Total method calls traced: #{call_counts.values.sum}"
210
- puts "Unique methods called: #{call_counts.size}"
211
- puts
212
- puts "To share this profile with the lutaml-model team:"
213
- puts " XMI_SAMPLE_FILE=spec/fixtures/full-242.xmi ruby scripts-xmi-profile/profile_xmi_simple.rb > profile_output.txt 2>&1"