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 +4 -4
- data/.rubocop_todo.yml +36 -73
- data/Gemfile +2 -0
- data/Rakefile +2 -0
- data/lib/tasks/benchmark_runner.rb +274 -0
- data/lib/tasks/performance.rake +46 -0
- data/lib/tasks/performance_comparator.rb +88 -0
- data/lib/tasks/performance_helpers.rb +238 -0
- data/lib/xmi/parsing.rb +4 -1
- data/lib/xmi/sparx/connector.rb +1 -1
- data/lib/xmi/version.rb +1 -1
- data/lib/xmi/version_registry.rb +4 -1
- data/lib/xmi/versioned.rb +5 -2
- metadata +6 -4
- data/benchmark_parse.rb +0 -60
- data/scripts-xmi-profile/profile_xmi_simple.rb +0 -213
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ac4f03d86ac8abffeaecaf956bcd77ab9a3bb2ed6348a12f3ccb2bd587f2eec
|
|
4
|
+
data.tar.gz: 7f0051b7bbb867c4c7d82ad95fb435a4f5e5d9bb2b76e1297ded8953682489c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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-
|
|
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:
|
|
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:
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
|
|
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
|
-
- '
|
|
43
|
-
- 'spec/xmi/versioning_spec.rb'
|
|
26
|
+
- 'spec/performance/xmi_parsing_spec.rb'
|
|
44
27
|
|
|
45
|
-
# Offense count:
|
|
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:
|
|
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:
|
|
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:
|
|
54
|
+
Max: 33
|
|
62
55
|
|
|
63
|
-
# Offense count:
|
|
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:
|
|
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:
|
|
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:
|
|
104
|
+
Max: 33
|
|
122
105
|
|
|
123
|
-
# Offense count:
|
|
124
|
-
|
|
125
|
-
# Configuration parameters: CustomTransform, IgnoredWords, DisallowedExamples.
|
|
126
|
-
# DisallowedExamples: works
|
|
127
|
-
RSpec/ExampleWording:
|
|
106
|
+
# Offense count: 5
|
|
107
|
+
RSpec/LeakyConstantDeclaration:
|
|
128
108
|
Exclude:
|
|
129
|
-
- 'spec/
|
|
130
|
-
- 'spec/xmi/sparx/sparx_root_gml_spec.rb'
|
|
109
|
+
- 'spec/performance/xmi_parsing_spec.rb'
|
|
131
110
|
|
|
132
|
-
# Offense count:
|
|
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:
|
|
120
|
+
# Offense count: 8
|
|
142
121
|
# Configuration parameters: AllowedGroups.
|
|
143
122
|
RSpec/NestedGroups:
|
|
144
123
|
Max: 4
|
|
145
124
|
|
|
146
|
-
# Offense count:
|
|
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
data/Rakefile
CHANGED
|
@@ -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
|
-
|
|
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/sparx/connector.rb
CHANGED
|
@@ -194,7 +194,7 @@ module Xmi
|
|
|
194
194
|
|
|
195
195
|
class Connector < Lutaml::Model::Serializable
|
|
196
196
|
attribute :name, :string
|
|
197
|
-
attribute :idref,
|
|
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
data/lib/xmi/version_registry.rb
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
|
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.
|
|
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-
|
|
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"
|