xmi 0.5.0 → 0.5.1

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: d91bb8a1a4fa881e676c2ad0b0d62898c499fb608baadb435326384d473de46b
4
+ data.tar.gz: 552a2485053742144bf72ad4830f65b026996ce1c2976c742b9ea5fb4c3741f5
5
5
  SHA512:
6
- metadata.gz: 36141b75f57006f831f6c9e1081f73a737a08e79bd281f295de59a39ff495ef91fa0437899842158d49688b0b08f8b8a5801ade3c6a17f0dbbbce93b1cf4d07d
7
- data.tar.gz: 5bbd57fbae92032dbf081532f905974bf89f1faafa1c07a49d40d794050972a1d11ffeaff8b361fe3256f662a6e30a9ec855b14a372a9ef77ecf037d2817fd33
6
+ metadata.gz: 617e27289838fb3c263935833758d5750552911106008d6c7cccc998d596848889590e5cd4b1240c29936d79d0b34da6592d1de1968abd076ed16ebd1b2a77a2
7
+ data.tar.gz: 3b5a984b005fa87f8a5f4a9ce7949d8adb0f9fe596d3d78a4558b1a05e190294c60e17b6de7cd909dd4c805e4a44e693647d7849e13e3830d06e0df3e67c3e50
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-03-22 06:43:54 UTC using RuboCop version 1.85.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,15 +11,73 @@ Gemspec/RequiredRubyVersion:
11
11
  Exclude:
12
12
  - 'xmi.gemspec'
13
13
 
14
+ # Offense count: 12
15
+ # This cop supports safe autocorrection (--autocorrect).
16
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
17
+ # SupportedStyles: with_first_argument, with_fixed_indentation
18
+ Layout/ArgumentAlignment:
19
+ Exclude:
20
+ - 'lib/tasks/benchmark_runner.rb'
21
+ - 'lib/tasks/performance_helpers.rb'
22
+ - 'lib/xmi/parsing.rb'
23
+ - 'lib/xmi/version_registry.rb'
24
+ - 'lib/xmi/versioned.rb'
25
+ - 'scripts-xmi-profile/profile_xmi_simple.rb'
26
+ - 'spec/xmi/sparx/sparx_root_xmi_parsing_spec.rb'
27
+
14
28
  # Offense count: 8
15
29
  # This cop supports safe autocorrection (--autocorrect).
30
+ # Configuration parameters: EnforcedStyle, IndentationWidth.
31
+ # SupportedStyles: with_first_element, with_fixed_indentation
32
+ Layout/ArrayAlignment:
33
+ Exclude:
34
+ - 'spec/fixtures.rb'
35
+ - 'spec/xmi/sparx/shared_contexts.rb'
36
+
37
+ # Offense count: 5
38
+ # This cop supports safe autocorrection (--autocorrect).
39
+ # Configuration parameters: EnforcedStyleAlignWith.
40
+ # SupportedStylesAlignWith: either, start_of_block, start_of_line
41
+ Layout/BlockAlignment:
42
+ Exclude:
43
+ - 'lib/xmi/versioned.rb'
44
+ - 'spec/performance/xmi_parsing_spec.rb'
45
+ - 'spec/xmi/sparx/sparx_root_citygml_spec.rb'
46
+ - 'spec/xmi/sparx/sparx_root_xmi_parsing_spec.rb'
47
+
48
+ # Offense count: 7
49
+ # This cop supports safe autocorrection (--autocorrect).
50
+ Layout/BlockEndNewline:
51
+ Exclude:
52
+ - 'benchmark_parse.rb'
53
+ - 'lib/xmi/versioned.rb'
54
+ - 'scripts-xmi-profile/profile_xmi_simple.rb'
55
+ - 'spec/performance/xmi_parsing_spec.rb'
56
+ - 'spec/xmi/sparx/sparx_root_citygml_spec.rb'
57
+ - 'spec/xmi/sparx/sparx_root_xmi_parsing_spec.rb'
58
+
59
+ # Offense count: 2
60
+ # This cop supports safe autocorrection (--autocorrect).
61
+ # Configuration parameters: EnforcedStyle.
62
+ # SupportedStyles: empty_lines, no_empty_lines
63
+ Layout/EmptyLinesAroundBlockBody:
64
+ Exclude:
65
+ - 'scripts-xmi-profile/profile_xmi_simple.rb'
66
+
67
+ # Offense count: 16
68
+ # This cop supports safe autocorrection (--autocorrect).
16
69
  # Configuration parameters: Width, EnforcedStyleAlignWith, AllowedPatterns.
17
70
  # SupportedStylesAlignWith: start_of_line, relative_to_receiver
18
71
  Layout/IndentationWidth:
19
72
  Exclude:
73
+ - 'benchmark_parse.rb'
74
+ - 'lib/xmi/versioned.rb'
20
75
  - 'scripts-xmi-profile/profile_xmi_simple.rb'
76
+ - 'spec/performance/xmi_parsing_spec.rb'
77
+ - 'spec/xmi/sparx/sparx_root_citygml_spec.rb'
78
+ - 'spec/xmi/sparx/sparx_root_xmi_parsing_spec.rb'
21
79
 
22
- # Offense count: 67
80
+ # Offense count: 101
23
81
  # This cop supports safe autocorrection (--autocorrect).
24
82
  # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
25
83
  # URISchemes: http, https
@@ -34,47 +92,75 @@ Layout/MultilineMethodCallIndentation:
34
92
  Exclude:
35
93
  - 'spec/xmi/sparx/sparx_root_citygml_spec.rb'
36
94
 
37
- # Offense count: 3
95
+ # Offense count: 24
38
96
  # This cop supports safe autocorrection (--autocorrect).
39
97
  # Configuration parameters: AllowInHeredoc.
40
98
  Layout/TrailingWhitespace:
41
99
  Exclude:
100
+ - 'lib/tasks/benchmark_runner.rb'
101
+ - 'lib/tasks/performance_helpers.rb'
102
+ - 'lib/xmi/parsing.rb'
103
+ - 'lib/xmi/version_registry.rb'
104
+ - 'lib/xmi/versioned.rb'
42
105
  - 'scripts-xmi-profile/profile_xmi_simple.rb'
106
+ - 'spec/fixtures.rb'
107
+ - 'spec/xmi/sparx/shared_contexts.rb'
108
+ - 'spec/xmi/sparx/sparx_root_xmi_parsing_spec.rb'
43
109
  - 'spec/xmi/versioning_spec.rb'
44
110
 
45
- # Offense count: 2
111
+ # Offense count: 5
112
+ # Configuration parameters: AllowedMethods.
113
+ # AllowedMethods: enums
114
+ Lint/ConstantDefinitionInBlock:
115
+ Exclude:
116
+ - 'spec/performance/xmi_parsing_spec.rb'
117
+
118
+ # Offense count: 12
46
119
  # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
47
120
  Metrics/AbcSize:
48
121
  Exclude:
122
+ - 'lib/tasks/benchmark_runner.rb'
123
+ - 'lib/tasks/performance_comparator.rb'
124
+ - 'lib/tasks/performance_helpers.rb'
49
125
  - 'lib/xmi/ea_root.rb'
50
126
  - 'lib/xmi/version_registry.rb'
127
+ - 'spec/performance/xmi_parsing_spec.rb'
51
128
 
52
- # Offense count: 94
129
+ # Offense count: 95
53
130
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
54
131
  # AllowedMethods: refine
55
132
  Metrics/BlockLength:
56
133
  Max: 143
57
134
 
58
- # Offense count: 12
135
+ # Offense count: 1
136
+ # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
137
+ Metrics/CyclomaticComplexity:
138
+ Exclude:
139
+ - 'lib/tasks/performance_helpers.rb'
140
+
141
+ # Offense count: 26
59
142
  # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
60
143
  Metrics/MethodLength:
61
- Max: 25
144
+ Max: 33
62
145
 
63
- # Offense count: 1
146
+ # Offense count: 2
64
147
  # Configuration parameters: AllowedMethods, AllowedPatterns, Max.
65
148
  Metrics/PerceivedComplexity:
66
149
  Exclude:
150
+ - 'lib/tasks/performance_helpers.rb'
67
151
  - 'lib/xmi/version_registry.rb'
68
152
 
69
- # Offense count: 13
153
+ # Offense count: 19
70
154
  # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns.
71
155
  # SupportedStyles: snake_case, normalcase, non_integer
72
156
  # AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
73
157
  Naming/VariableNumber:
74
158
  Exclude:
159
+ - 'lib/tasks/benchmark_runner.rb'
75
160
  - 'lib/xmi/v20110701.rb'
76
161
  - 'lib/xmi/v20131001.rb'
77
162
  - 'lib/xmi/v20161101.rb'
163
+ - 'spec/performance/xmi_parsing_spec.rb'
78
164
  - 'spec/xmi/versioning_spec.rb'
79
165
 
80
166
  # Offense count: 1
@@ -99,10 +185,11 @@ RSpec/ContextWording:
99
185
  - 'spec/xmi/sparx/sparx_root_gml_spec.rb'
100
186
  - 'spec/xmi/sparx/sparx_root_mdg_spec.rb'
101
187
 
102
- # Offense count: 3
188
+ # Offense count: 4
103
189
  # Configuration parameters: IgnoredMetadata.
104
190
  RSpec/DescribeClass:
105
191
  Exclude:
192
+ - 'spec/performance/xmi_parsing_spec.rb'
106
193
  - 'spec/xmi/edge_cases_spec.rb'
107
194
  - 'spec/xmi/namespace_aliases_spec.rb'
108
195
  - 'spec/xmi/versioning_spec.rb'
@@ -118,7 +205,7 @@ RSpec/DescribedClass:
118
205
  # Offense count: 26
119
206
  # Configuration parameters: CountAsOne.
120
207
  RSpec/ExampleLength:
121
- Max: 31
208
+ Max: 33
122
209
 
123
210
  # Offense count: 2
124
211
  # This cop supports safe autocorrection (--autocorrect).
@@ -129,6 +216,11 @@ RSpec/ExampleWording:
129
216
  - 'spec/xmi/sparx/sparx_root_eauml_spec.rb'
130
217
  - 'spec/xmi/sparx/sparx_root_gml_spec.rb'
131
218
 
219
+ # Offense count: 5
220
+ RSpec/LeakyConstantDeclaration:
221
+ Exclude:
222
+ - 'spec/performance/xmi_parsing_spec.rb'
223
+
132
224
  # Offense count: 35
133
225
  RSpec/MultipleExpectations:
134
226
  Max: 8
@@ -138,11 +230,27 @@ RSpec/MultipleExpectations:
138
230
  RSpec/MultipleMemoizedHelpers:
139
231
  Max: 7
140
232
 
141
- # Offense count: 7
233
+ # Offense count: 8
142
234
  # Configuration parameters: AllowedGroups.
143
235
  RSpec/NestedGroups:
144
236
  Max: 4
145
237
 
238
+ # Offense count: 6
239
+ # This cop supports safe autocorrection (--autocorrect).
240
+ # Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods.
241
+ # SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces
242
+ # ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
243
+ # FunctionalMethods: let, let!, subject, watch
244
+ # AllowedMethods: lambda, proc, it
245
+ Style/BlockDelimiters:
246
+ Exclude:
247
+ - 'benchmark_parse.rb'
248
+ - 'lib/xmi/versioned.rb'
249
+ - 'scripts-xmi-profile/profile_xmi_simple.rb'
250
+ - 'spec/performance/xmi_parsing_spec.rb'
251
+ - 'spec/xmi/sparx/sparx_root_citygml_spec.rb'
252
+ - 'spec/xmi/sparx/sparx_root_xmi_parsing_spec.rb'
253
+
146
254
  # Offense count: 1
147
255
  # This cop supports safe autocorrection (--autocorrect).
148
256
  # Configuration parameters: EnforcedStyle.
@@ -151,10 +259,18 @@ Style/EmptyStringInsideInterpolation:
151
259
  Exclude:
152
260
  - 'scripts-xmi-profile/profile_xmi_simple.rb'
153
261
 
154
- # Offense count: 5
262
+ # Offense count: 2
263
+ # This cop supports safe autocorrection (--autocorrect).
264
+ Style/MultilineIfModifier:
265
+ Exclude:
266
+ - 'lib/xmi/parsing.rb'
267
+ - 'lib/xmi/version_registry.rb'
268
+
269
+ # Offense count: 6
155
270
  # Configuration parameters: AllowedClasses.
156
271
  Style/OneClassPerFile:
157
272
  Exclude:
273
+ - 'lib/tasks/benchmark_runner.rb'
158
274
  - 'lib/xmi.rb'
159
275
  - 'lib/xmi/namespace/dynamic.rb'
160
276
  - 'spec/xmi/sparx/shared_contexts.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
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.1"
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.1
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-12 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"