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 +4 -4
- data/.rubocop_todo.yml +129 -13
- 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/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: d91bb8a1a4fa881e676c2ad0b0d62898c499fb608baadb435326384d473de46b
|
|
4
|
+
data.tar.gz: 552a2485053742144bf72ad4830f65b026996ce1c2976c742b9ea5fb4c3741f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
144
|
+
Max: 33
|
|
62
145
|
|
|
63
|
-
# Offense count:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
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/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.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-
|
|
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"
|