xmi 0.6.1 → 0.6.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 +7 -7
- data/lib/tasks/performance.rake +6 -8
- data/lib/xmi/performance/comparator.rb +81 -0
- data/lib/xmi/performance/helpers.rb +226 -0
- data/lib/xmi/performance/runner.rb +262 -0
- data/lib/xmi/performance.rb +15 -0
- data/lib/xmi/uml/association_class.rb +1 -1
- data/lib/xmi/uml/component.rb +1 -1
- data/lib/xmi/uml/dependency.rb +1 -1
- data/lib/xmi/uml/extension.rb +1 -1
- data/lib/xmi/uml/owned_comment.rb +3 -2
- data/lib/xmi/uml/owned_element.rb +2 -2
- data/lib/xmi/uml/packaged_element.rb +15 -9
- data/lib/xmi/uml/signal.rb +1 -1
- data/lib/xmi/uml/stereotype.rb +1 -1
- data/lib/xmi/uml/uml_class.rb +1 -1
- data/lib/xmi/uml/usage.rb +1 -1
- data/lib/xmi/uml/value_specification.rb +10 -4
- data/lib/xmi/version.rb +1 -1
- data/lib/xmi.rb +1 -0
- metadata +6 -5
- data/lib/tasks/benchmark_runner.rb +0 -274
- data/lib/tasks/performance_comparator.rb +0 -88
- data/lib/tasks/performance_helpers.rb +0 -238
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 594be55008babe25af18895181e47ff22127d597bc56cc19c2c0b2717682e821
|
|
4
|
+
data.tar.gz: aea140572fcae2dd36e4d8b5fce6e3d8fefffe1a30fc4ce74a6ff18fd5f8f409
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 12c4a3dc4575038dfde21225351477d37177cda25004cc517690b0bdbfa86aa4014ad5d2204ce10e5d0718d5d9ab4fed82cea3cefa9610c56a6bc3accd0c4142
|
|
7
|
+
data.tar.gz: b12d8eafaf2e5dcbf306769b809df0f5b80b8ca161aa4fda49e17cdeea13af5008d39b92e5f3066a73c276fe7d9a0fa3a87546fe53179cd1eeeee4f51ecbdb26
|
data/.rubocop_todo.yml
CHANGED
|
@@ -113,9 +113,9 @@ Lint/ConstantDefinitionInBlock:
|
|
|
113
113
|
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max.
|
|
114
114
|
Metrics/AbcSize:
|
|
115
115
|
Exclude:
|
|
116
|
-
- 'lib/
|
|
117
|
-
- 'lib/
|
|
118
|
-
- 'lib/
|
|
116
|
+
- 'lib/xmi/performance/runner.rb'
|
|
117
|
+
- 'lib/xmi/performance/comparator.rb'
|
|
118
|
+
- 'lib/xmi/performance/helpers.rb'
|
|
119
119
|
- 'lib/xmi/ea_root/code_generation.rb'
|
|
120
120
|
- 'lib/xmi/sparx/index.rb'
|
|
121
121
|
- 'lib/xmi/version_registry.rb'
|
|
@@ -131,7 +131,7 @@ Metrics/BlockLength:
|
|
|
131
131
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
132
132
|
Metrics/CyclomaticComplexity:
|
|
133
133
|
Exclude:
|
|
134
|
-
- 'lib/
|
|
134
|
+
- 'lib/xmi/performance/helpers.rb'
|
|
135
135
|
- 'lib/xmi/ea_root/code_generation.rb'
|
|
136
136
|
- 'lib/xmi/sparx/index.rb'
|
|
137
137
|
|
|
@@ -144,7 +144,7 @@ Metrics/MethodLength:
|
|
|
144
144
|
# Configuration parameters: AllowedMethods, AllowedPatterns, Max.
|
|
145
145
|
Metrics/PerceivedComplexity:
|
|
146
146
|
Exclude:
|
|
147
|
-
- 'lib/
|
|
147
|
+
- 'lib/xmi/performance/helpers.rb'
|
|
148
148
|
- 'lib/xmi/ea_root/code_generation.rb'
|
|
149
149
|
- 'lib/xmi/sparx/index.rb'
|
|
150
150
|
- 'lib/xmi/version_registry.rb'
|
|
@@ -155,7 +155,7 @@ Metrics/PerceivedComplexity:
|
|
|
155
155
|
# AllowedIdentifiers: TLS1_1, TLS1_2, capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64
|
|
156
156
|
Naming/VariableNumber:
|
|
157
157
|
Exclude:
|
|
158
|
-
- 'lib/
|
|
158
|
+
- 'lib/xmi/performance/runner.rb'
|
|
159
159
|
- 'lib/xmi/v20110701.rb'
|
|
160
160
|
- 'lib/xmi/v20131001.rb'
|
|
161
161
|
- 'lib/xmi/v20161101.rb'
|
|
@@ -247,6 +247,6 @@ Style/BlockDelimiters:
|
|
|
247
247
|
# Configuration parameters: AllowedClasses.
|
|
248
248
|
Style/OneClassPerFile:
|
|
249
249
|
Exclude:
|
|
250
|
-
- 'lib/
|
|
250
|
+
- 'lib/xmi/performance/runner.rb'
|
|
251
251
|
- 'lib/xmi.rb'
|
|
252
252
|
- 'lib/xmi/namespace/dynamic.rb'
|
data/lib/tasks/performance.rake
CHANGED
|
@@ -1,34 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require_relative "benchmark_runner"
|
|
3
|
+
require "xmi"
|
|
5
4
|
|
|
6
5
|
desc "Run performance benchmarks"
|
|
7
6
|
namespace :performance do
|
|
8
7
|
desc "Compare performance of current branch against base branch (default: main)"
|
|
9
8
|
task :compare do
|
|
10
|
-
|
|
9
|
+
Xmi::Performance::Comparator.new.run
|
|
11
10
|
end
|
|
12
11
|
|
|
13
12
|
desc "Run benchmarks on current branch only (for development)"
|
|
14
13
|
task :run do
|
|
15
|
-
runner =
|
|
14
|
+
runner = Xmi::Performance::Runner.new(run_time: 5)
|
|
16
15
|
runner.run_benchmarks
|
|
17
16
|
end
|
|
18
17
|
|
|
19
18
|
desc "Quick benchmark run (faster, less accurate)"
|
|
20
19
|
task :quick do
|
|
21
|
-
runner =
|
|
20
|
+
runner = Xmi::Performance::Runner.new(run_time: 2, warmup: 1)
|
|
22
21
|
runner.run_benchmarks
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
desc "Run benchmarks and output as JSON"
|
|
26
25
|
task :json do
|
|
27
26
|
require "json"
|
|
28
|
-
runner =
|
|
27
|
+
runner = Xmi::Performance::Runner.new(run_time: 5)
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
results = runner.send(:run_benchmarks)
|
|
29
|
+
results = runner.run_benchmarks
|
|
32
30
|
|
|
33
31
|
output = results.each_with_object({}) do |(label, metrics), h|
|
|
34
32
|
ips = (metrics[:lower] + metrics[:upper]) / 2.0
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Xmi
|
|
6
|
+
module Performance
|
|
7
|
+
# Orchestrates the performance comparison flow: loads the Runner
|
|
8
|
+
# into Base and Current namespaces, runs both, and reports
|
|
9
|
+
# regressions.
|
|
10
|
+
class Comparator
|
|
11
|
+
REPO_ROOT = File.expand_path(File.join(__dir__, "..", "..", ".."))
|
|
12
|
+
DEFAULT_RUN_TIME = 10
|
|
13
|
+
DEFAULT_THRESHOLD = 0.10 # 10% (more lenient for complex operations)
|
|
14
|
+
DEFAULT_BASE = "main"
|
|
15
|
+
|
|
16
|
+
def run
|
|
17
|
+
setup_environment
|
|
18
|
+
run_benchmarks_comparison
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def setup_environment
|
|
24
|
+
Dir.chdir(REPO_ROOT)
|
|
25
|
+
Helpers.load_runner_into(Helpers::Current)
|
|
26
|
+
Helpers.load_runner_into(Helpers::Base)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def run_benchmarks_comparison
|
|
30
|
+
all_current = {}
|
|
31
|
+
all_base = {}
|
|
32
|
+
|
|
33
|
+
puts Helpers::Term.header("Performance Comparison", color: Helpers::CYAN)
|
|
34
|
+
puts
|
|
35
|
+
puts " #{Helpers::DIM}Comparing#{Helpers::CLEAR}:"
|
|
36
|
+
puts " #{Helpers::CYAN} Current#{Helpers::CLEAR}: #{Helpers.current_branch}"
|
|
37
|
+
puts " #{Helpers::CYAN} Base#{Helpers::CLEAR}: #{DEFAULT_BASE}"
|
|
38
|
+
puts " #{Helpers::CYAN} Threshold#{Helpers::CLEAR}: #{(DEFAULT_THRESHOLD * 100).round(0)}% regression allowed"
|
|
39
|
+
puts
|
|
40
|
+
|
|
41
|
+
base_runner = Helpers::Base::Runner.new(
|
|
42
|
+
run_time: DEFAULT_RUN_TIME,
|
|
43
|
+
)
|
|
44
|
+
current_runner = Helpers::Current::Runner.new(
|
|
45
|
+
run_time: DEFAULT_RUN_TIME,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
Helpers.run_benchmarks(
|
|
49
|
+
base_runner,
|
|
50
|
+
current_runner,
|
|
51
|
+
DEFAULT_THRESHOLD,
|
|
52
|
+
all_base,
|
|
53
|
+
all_current,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
summary = Helpers.summary_report(
|
|
57
|
+
all_current,
|
|
58
|
+
all_base,
|
|
59
|
+
DEFAULT_BASE,
|
|
60
|
+
DEFAULT_RUN_TIME,
|
|
61
|
+
DEFAULT_THRESHOLD,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
handle_results(summary)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def handle_results(summary)
|
|
68
|
+
puts
|
|
69
|
+
if summary[:regressions].any?
|
|
70
|
+
puts " #{Helpers::RED}#{Helpers::BOLD}❌ PERFORMANCE REGRESSIONS DETECTED#{Helpers::CLEAR}"
|
|
71
|
+
puts " #{Helpers::RED}#{summary[:regressions].length} benchmark(s) regressed beyond threshold#{Helpers::CLEAR}"
|
|
72
|
+
puts
|
|
73
|
+
exit(1)
|
|
74
|
+
else
|
|
75
|
+
puts " #{Helpers::GREEN}#{Helpers::BOLD}✅ ALL BENCHMARKS PASSED#{Helpers::CLEAR}"
|
|
76
|
+
puts
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
|
|
8
|
+
module Xmi
|
|
9
|
+
module Performance
|
|
10
|
+
# Shared helpers for the performance benchmark rake tasks.
|
|
11
|
+
# Provides terminal formatting, git/process helpers, and the
|
|
12
|
+
# dual-namespace Runner loader used by the comparison flow.
|
|
13
|
+
module Helpers
|
|
14
|
+
# ANSI color codes for terminal output
|
|
15
|
+
CLEAR = "\e[0m"
|
|
16
|
+
BOLD = "\e[1m"
|
|
17
|
+
DIM = "\e[2m"
|
|
18
|
+
CYAN = "\e[36m"
|
|
19
|
+
GREEN = "\e[32m"
|
|
20
|
+
YELLOW = "\e[33m"
|
|
21
|
+
RED = "\e[31m"
|
|
22
|
+
GRAY = "\e[90m"
|
|
23
|
+
MAGENTA = "\e[35m"
|
|
24
|
+
|
|
25
|
+
# Terminal formatting helpers
|
|
26
|
+
module Term
|
|
27
|
+
extend self
|
|
28
|
+
|
|
29
|
+
HL = "─"
|
|
30
|
+
VL = "│"
|
|
31
|
+
TL = "┌"
|
|
32
|
+
TR = "┐"
|
|
33
|
+
BL = "└"
|
|
34
|
+
BR = "┘"
|
|
35
|
+
|
|
36
|
+
def header(title, color: Helpers::CYAN)
|
|
37
|
+
width = 78
|
|
38
|
+
line = HL * width
|
|
39
|
+
puts
|
|
40
|
+
puts "#{color}#{TL}#{line}#{TR}#{CLEAR}"
|
|
41
|
+
puts "#{color}#{VL}#{CLEAR} #{BOLD}#{color}#{title}#{CLEAR}#{' ' * (width - title.length - 4)}#{color}#{VL}#{CLEAR}"
|
|
42
|
+
puts "#{color}#{BL}#{line}#{BR}#{CLEAR}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def sep(char: HL, width: 78)
|
|
46
|
+
puts "#{DIM}#{char * width}#{CLEAR}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Dual-namespace containers. The Runner class is cloned into
|
|
51
|
+
# each so that Base and Current hold independent class objects
|
|
52
|
+
# (preserving the shape of the comparison API even though both
|
|
53
|
+
# currently resolve to the same benchmark code).
|
|
54
|
+
module Base
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
module Current
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class << self
|
|
61
|
+
# Clone Xmi::Performance::Runner into the given namespace as
|
|
62
|
+
# `::Runner`. Replaces the old module_eval-of-a-top-level-file
|
|
63
|
+
# mechanism, which required require_relative and a top-level
|
|
64
|
+
# class definition incompatible with autoload.
|
|
65
|
+
def load_runner_into(namespace)
|
|
66
|
+
Xmi::Performance::Runner # force autoload
|
|
67
|
+
namespace.const_set(:Runner, Xmi::Performance::Runner.clone)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ruby_exec(cmd, env: {})
|
|
71
|
+
Open3.capture3(env, cmd)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def current_branch
|
|
75
|
+
stdout, = ruby_exec("git rev-parse --abbrev-ref HEAD")
|
|
76
|
+
stdout.strip
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def run_benchmarks(base_runner, current_runner, threshold, all_base,
|
|
80
|
+
all_current)
|
|
81
|
+
base_results = base_runner.run_benchmarks
|
|
82
|
+
curr_results = current_runner.run_benchmarks
|
|
83
|
+
|
|
84
|
+
all_base.merge!(base_results)
|
|
85
|
+
all_current.merge!(curr_results)
|
|
86
|
+
|
|
87
|
+
comparison_rows = []
|
|
88
|
+
|
|
89
|
+
curr_results.each do |label, result|
|
|
90
|
+
base_result = base_results[label]
|
|
91
|
+
cmp = compare_metrics(label, result, base_result, threshold)
|
|
92
|
+
comparison_rows << cmp
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
print_comparison_table(comparison_rows, threshold)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def print_comparison_table(comparison_rows, threshold)
|
|
99
|
+
rows = comparison_rows.map do |cmp|
|
|
100
|
+
{
|
|
101
|
+
benchmark: cmp[:label],
|
|
102
|
+
base_ips: cmp[:base_ips]&.round(1),
|
|
103
|
+
curr_ips: cmp[:curr_ips]&.round(1),
|
|
104
|
+
change: cmp[:change] ? "#{(cmp[:change] * 100).round(1)}%" : "N/A",
|
|
105
|
+
status: if cmp[:base_ips].nil?
|
|
106
|
+
"NEW"
|
|
107
|
+
elsif cmp[:change] < -threshold
|
|
108
|
+
"REGRESSED"
|
|
109
|
+
else
|
|
110
|
+
"OK"
|
|
111
|
+
end,
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
return if rows.empty?
|
|
116
|
+
|
|
117
|
+
puts " #{'Benchmark'.ljust(40)} #{'Base IPS'.rjust(12)} #{'Curr IPS'.rjust(12)} #{'Change'.rjust(10)} #{'Status'.rjust(10)}"
|
|
118
|
+
puts " #{DIM}#{'─' * 86}#{CLEAR}"
|
|
119
|
+
|
|
120
|
+
rows.each do |row|
|
|
121
|
+
status_color = case row[:status]
|
|
122
|
+
when "REGRESSED" then RED
|
|
123
|
+
when "NEW" then YELLOW
|
|
124
|
+
else GREEN
|
|
125
|
+
end
|
|
126
|
+
row[:status] == "REGRESSED" ? RED : DIM
|
|
127
|
+
|
|
128
|
+
puts " #{row[:benchmark].ljust(40)} #{format('%-12.1f',
|
|
129
|
+
row[:base_ips] || 0)} #{format('%-12.1f',
|
|
130
|
+
row[:curr_ips] || 0)} #{format('%-10s', row[:change]).gsub('%',
|
|
131
|
+
'%%')} #{status_color}#{row[:status].rjust(10)}#{CLEAR}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
puts
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def compare_metrics(label, curr, base, threshold)
|
|
138
|
+
unless base
|
|
139
|
+
return { label: label, base_ips: nil, curr_ips: nil, change: nil,
|
|
140
|
+
regressed: false }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
base_ips = base.fetch(:lower)
|
|
144
|
+
curr_ips = curr.fetch(:upper)
|
|
145
|
+
change = (curr_ips - base_ips) / base_ips.to_f
|
|
146
|
+
|
|
147
|
+
{
|
|
148
|
+
label: label,
|
|
149
|
+
base_ips: base_ips,
|
|
150
|
+
curr_ips: curr_ips,
|
|
151
|
+
change: change,
|
|
152
|
+
regressed: change < -threshold,
|
|
153
|
+
}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def summary_report(current_results, base_results, base, run_time, threshold)
|
|
157
|
+
summary = {
|
|
158
|
+
run_time: run_time,
|
|
159
|
+
threshold: threshold,
|
|
160
|
+
branch: current_branch,
|
|
161
|
+
base: base,
|
|
162
|
+
regressions: [],
|
|
163
|
+
new_benchmarks: [],
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
current_results.each do |label, metrics|
|
|
167
|
+
base_result = base_results[label]
|
|
168
|
+
cmp = compare_metrics(label, metrics, base_result, threshold)
|
|
169
|
+
|
|
170
|
+
if base_result.nil?
|
|
171
|
+
summary[:new_benchmarks] << label
|
|
172
|
+
next
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
next unless cmp[:regressed]
|
|
176
|
+
|
|
177
|
+
summary[:regressions] << {
|
|
178
|
+
label: label,
|
|
179
|
+
base_ips: cmp[:base_ips],
|
|
180
|
+
curr_ips: cmp[:curr_ips],
|
|
181
|
+
delta_fraction: cmp[:change],
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
log_regressions(summary[:regressions], threshold)
|
|
186
|
+
log_new_benchmarks(summary[:new_benchmarks])
|
|
187
|
+
summary
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def log_new_benchmarks(new_benchmarks)
|
|
191
|
+
return if new_benchmarks.empty?
|
|
192
|
+
|
|
193
|
+
puts
|
|
194
|
+
puts "#{YELLOW}🆕 New benchmarks (not in base branch):#{CLEAR}"
|
|
195
|
+
new_benchmarks.each do |label|
|
|
196
|
+
puts " • #{label}"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def log_regressions(regressions, threshold)
|
|
201
|
+
return if regressions.empty?
|
|
202
|
+
|
|
203
|
+
puts
|
|
204
|
+
puts "#{RED}⚠️ Performance Regressions Detected#{CLEAR}"
|
|
205
|
+
puts "#{RED} (< -#{(threshold * 100).round(2)}% IPS)#{CLEAR}"
|
|
206
|
+
puts
|
|
207
|
+
regressions.each do |regression|
|
|
208
|
+
delta = regression[:delta_fraction]
|
|
209
|
+
base_ips = regression[:base_ips]
|
|
210
|
+
curr_ips = regression[:curr_ips]
|
|
211
|
+
|
|
212
|
+
delta_str = delta ? format("%+0.2f%%", delta * 100) : "N/A"
|
|
213
|
+
base_str = base_ips ? format("%.2f", base_ips) : "N/A"
|
|
214
|
+
curr_str = curr_ips ? format("%.2f", curr_ips) : "N/A"
|
|
215
|
+
|
|
216
|
+
puts " #{BOLD}#{regression[:label]}#{CLEAR}"
|
|
217
|
+
puts " #{GRAY}base: #{base_str} IPS#{CLEAR}"
|
|
218
|
+
puts " #{RED}curr: #{curr_str} IPS#{CLEAR}"
|
|
219
|
+
puts " #{RED}change: #{delta_str}#{CLEAR}"
|
|
220
|
+
puts
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "benchmark/ips"
|
|
4
|
+
require "stringio"
|
|
5
|
+
|
|
6
|
+
module Xmi
|
|
7
|
+
module Performance
|
|
8
|
+
# Pretty terminal formatting for benchmark output.
|
|
9
|
+
module Term
|
|
10
|
+
CLEAR = "\e[0m"
|
|
11
|
+
BOLD = "\e[1m"
|
|
12
|
+
DIM = "\e[2m"
|
|
13
|
+
RED = "\e[31m"
|
|
14
|
+
GREEN = "\e[32m"
|
|
15
|
+
YELLOW = "\e[33m"
|
|
16
|
+
CYAN = "\e[36m"
|
|
17
|
+
MAGENTA = "\e[35m"
|
|
18
|
+
|
|
19
|
+
HL = "─"
|
|
20
|
+
VL = "│"
|
|
21
|
+
TL = "┌"
|
|
22
|
+
TR = "┐"
|
|
23
|
+
BL = "└"
|
|
24
|
+
BR = "┘"
|
|
25
|
+
|
|
26
|
+
def self.header(title, color: CYAN)
|
|
27
|
+
width = 78
|
|
28
|
+
line = HL * width
|
|
29
|
+
puts
|
|
30
|
+
puts "#{color}#{TL}#{line}#{TR}#{CLEAR}"
|
|
31
|
+
puts "#{color}#{VL}#{CLEAR} #{BOLD}#{color}#{title}#{CLEAR}#{' ' * (width - title.length - 4)}#{color}#{VL}#{CLEAR}"
|
|
32
|
+
puts "#{color}#{BL}#{line}#{BR}#{CLEAR}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.sep(char: HL, width: 78)
|
|
36
|
+
puts "#{DIM}#{char * width}#{CLEAR}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.env_info(ruby_version, platform)
|
|
40
|
+
puts
|
|
41
|
+
puts " #{DIM}Environment:#{CLEAR}"
|
|
42
|
+
puts " #{VL} Ruby #{ruby_version} on #{platform}#{' ' * (60 - ruby_version.length - platform.length)}#{VL}"
|
|
43
|
+
puts " #{DIM}#{BL}#{HL * 76}#{BR}#{CLEAR}"
|
|
44
|
+
puts
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.category(title, icon:, description:, failure_means:,
|
|
48
|
+
compare_against: nil)
|
|
49
|
+
puts
|
|
50
|
+
puts "#{CYAN}#{VL}#{CLEAR} #{BOLD}#{MAGENTA}#{icon} #{title}#{CLEAR}"
|
|
51
|
+
puts
|
|
52
|
+
puts " #{DIM}#{description}#{CLEAR}"
|
|
53
|
+
puts
|
|
54
|
+
|
|
55
|
+
if compare_against
|
|
56
|
+
puts " #{CYAN}Comparing against:#{CLEAR} #{compare_against}"
|
|
57
|
+
puts
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
puts " #{YELLOW}⚠️ Failure means:#{CLEAR} #{failure_means}"
|
|
61
|
+
puts
|
|
62
|
+
sep(width: 76)
|
|
63
|
+
puts
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Runs XMI parsing benchmarks against fixture files and reports
|
|
68
|
+
# iterations-per-second with error margins.
|
|
69
|
+
class Runner
|
|
70
|
+
REPO_ROOT = File.expand_path(File.join(__dir__, "..", "..", ".."))
|
|
71
|
+
|
|
72
|
+
DEFAULT_RUN_TIME = 5
|
|
73
|
+
DEFAULT_WARMUP = 2
|
|
74
|
+
|
|
75
|
+
CATEGORIES = {
|
|
76
|
+
xmi_parsing: {
|
|
77
|
+
name: "XMI Parsing",
|
|
78
|
+
icon: "📄",
|
|
79
|
+
description: "XMI parsing performance tests. Measures how quickly we can convert XMI files into Ruby objects.",
|
|
80
|
+
failure_means: "Slow XMI parsing impacts all downstream operations. A regression here means users will experience delays when processing XMI documents.",
|
|
81
|
+
compare_against: "Previous branch (main).",
|
|
82
|
+
},
|
|
83
|
+
}.freeze
|
|
84
|
+
|
|
85
|
+
BENCHMARKS = {
|
|
86
|
+
xmi_parsing: [
|
|
87
|
+
{ name: "XMI 2.4.2 (small)", method: :xmi_parse_242_small,
|
|
88
|
+
desc: "XMI 2.4.2 ~100KB file" },
|
|
89
|
+
{ name: "XMI 2.4.2 (medium)", method: :xmi_parse_242_medium,
|
|
90
|
+
desc: "XMI 2.4.2 ~500KB file with extensions" },
|
|
91
|
+
{ name: "XMI 2.4.2 (large)", method: :xmi_parse_242_large,
|
|
92
|
+
desc: "XMI 2.4.2 ~3.5MB file" },
|
|
93
|
+
{ name: "XMI 2.5.1", method: :xmi_parse_251,
|
|
94
|
+
desc: "XMI 2.5.1 ~100KB file" },
|
|
95
|
+
],
|
|
96
|
+
}.freeze
|
|
97
|
+
|
|
98
|
+
FIXTURES = {
|
|
99
|
+
xmi_parse_242_small: "spec/fixtures/xmi-v2-4-2-default.xmi",
|
|
100
|
+
xmi_parse_242_medium: "spec/fixtures/xmi-v2-4-2-default-with-citygml.xmi",
|
|
101
|
+
xmi_parse_242_large: "spec/fixtures/full-242.xmi",
|
|
102
|
+
xmi_parse_251: "spec/fixtures/ea-xmi-2.5.1.xmi",
|
|
103
|
+
}.freeze
|
|
104
|
+
|
|
105
|
+
def initialize(run_time: nil, warmup: nil, benchmark: nil)
|
|
106
|
+
@run_time = run_time || DEFAULT_RUN_TIME
|
|
107
|
+
@warmup = warmup || DEFAULT_WARMUP
|
|
108
|
+
@benchmark = benchmark
|
|
109
|
+
@results = {}
|
|
110
|
+
@env_shown = false
|
|
111
|
+
@all_results = []
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def run_benchmarks
|
|
115
|
+
Term.header("XMI Performance Benchmarks", color: Term::CYAN)
|
|
116
|
+
|
|
117
|
+
unless @env_shown
|
|
118
|
+
Term.env_info(RUBY_VERSION, RUBY_PLATFORM)
|
|
119
|
+
@env_shown = true
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
BENCHMARKS.each do |category, tests|
|
|
123
|
+
run_category(category, tests)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
print_summary
|
|
127
|
+
|
|
128
|
+
@results
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def run_category(category, tests)
|
|
134
|
+
config = CATEGORIES[category]
|
|
135
|
+
Term.category(
|
|
136
|
+
config[:name],
|
|
137
|
+
icon: config[:icon],
|
|
138
|
+
description: config[:description],
|
|
139
|
+
failure_means: config[:failure_means],
|
|
140
|
+
compare_against: config[:compare_against],
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
category_results = []
|
|
144
|
+
|
|
145
|
+
tests.each do |test|
|
|
146
|
+
original_stdout = $stdout
|
|
147
|
+
$stdout = StringIO.new
|
|
148
|
+
|
|
149
|
+
result = run_single_test(test[:method])
|
|
150
|
+
(result[:lower] + result[:upper]) / 2.0
|
|
151
|
+
category_results << { name: test[:name], result: result }
|
|
152
|
+
|
|
153
|
+
$stdout = original_stdout
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
puts " #{'Benchmark'.ljust(40)} #{'IPS'.rjust(12)} #{'Deviation'.rjust(12)}"
|
|
157
|
+
puts " #{Term::DIM}#{Term::HL * 66}#{Term::CLEAR}"
|
|
158
|
+
|
|
159
|
+
category_results.each do |r|
|
|
160
|
+
ips = (r[:result][:lower] + r[:result][:upper]) / 2.0
|
|
161
|
+
deviation = calculate_deviation(r[:result])
|
|
162
|
+
label = "#{config[:name]}: #{r[:name]}"
|
|
163
|
+
@all_results << { label: label, ips: ips }
|
|
164
|
+
@results[label] = r[:result]
|
|
165
|
+
|
|
166
|
+
puts " #{r[:name].ljust(40)} #{format('%.2f',
|
|
167
|
+
ips).rjust(12)} #{format('%.1f%%',
|
|
168
|
+
deviation).rjust(12)}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
puts
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def run_single_test(method)
|
|
175
|
+
fixture_path = FIXTURES[method]
|
|
176
|
+
raise "Unknown fixture: #{method}" unless fixture_path
|
|
177
|
+
|
|
178
|
+
full_path = File.join(REPO_ROOT, fixture_path)
|
|
179
|
+
full_path = fixture_path unless File.exist?(full_path)
|
|
180
|
+
|
|
181
|
+
xml_content = File.read(full_path)
|
|
182
|
+
|
|
183
|
+
case method
|
|
184
|
+
when :xmi_parse_242_small, :xmi_parse_242_medium, :xmi_parse_242_large, :xmi_parse_251
|
|
185
|
+
measure_time { Xmi::Sparx::Root.parse_xml(xml_content) }
|
|
186
|
+
else
|
|
187
|
+
raise "Unknown benchmark: #{method}"
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def measure(&)
|
|
192
|
+
job = Benchmark::IPS::Job.new
|
|
193
|
+
job.config(time: @run_time, warmup: @warmup)
|
|
194
|
+
job.report("test", &)
|
|
195
|
+
job.run
|
|
196
|
+
|
|
197
|
+
entry = job.full_report.entries.first
|
|
198
|
+
samples = entry.stats.samples
|
|
199
|
+
|
|
200
|
+
return { lower: 0, upper: 0 } if samples.empty?
|
|
201
|
+
|
|
202
|
+
mean = samples.sum.to_f / samples.size
|
|
203
|
+
variance = samples.sum { |x| (x - mean)**2 } / (samples.size - 1)
|
|
204
|
+
std_dev = Math.sqrt(variance)
|
|
205
|
+
error_margin = std_dev / mean
|
|
206
|
+
error_pct = error_margin.round(4)
|
|
207
|
+
|
|
208
|
+
{ lower: mean.round(4) * (1 - error_pct),
|
|
209
|
+
upper: mean.round(4) * (1 + error_pct) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def measure_time
|
|
213
|
+
times = []
|
|
214
|
+
iterations = 5
|
|
215
|
+
|
|
216
|
+
iterations.times do
|
|
217
|
+
start_t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
218
|
+
yield
|
|
219
|
+
finish_t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
220
|
+
times << (finish_t - start_t)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
mean = times.sum / times.size
|
|
224
|
+
variance = times.sum { |t| (t - mean)**2 } / (times.size - 1)
|
|
225
|
+
std_dev = Math.sqrt(variance)
|
|
226
|
+
|
|
227
|
+
lower_time = [mean - std_dev, mean * 0.5].max
|
|
228
|
+
lower_ips = (1.0 / (lower_time * 1.5)).round(4)
|
|
229
|
+
upper_ips = (1.0 / mean).round(4)
|
|
230
|
+
|
|
231
|
+
if mean < 0.001
|
|
232
|
+
upper_ips = (1.0 / mean).round(4)
|
|
233
|
+
lower_ips = (upper_ips * 0.8).round(4)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
{ lower: lower_ips, upper: upper_ips }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def calculate_deviation(metrics)
|
|
240
|
+
return 0 if metrics[:upper].zero?
|
|
241
|
+
|
|
242
|
+
((metrics[:upper] - metrics[:lower]) / metrics[:upper] * 100).round(1)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def print_summary
|
|
246
|
+
puts
|
|
247
|
+
Term.sep(width: 78)
|
|
248
|
+
puts
|
|
249
|
+
puts " #{Term::BOLD}#{Term::MAGENTA}SUMMARY#{Term::CLEAR}"
|
|
250
|
+
puts
|
|
251
|
+
|
|
252
|
+
@all_results.each do |r|
|
|
253
|
+
puts " #{r[:label].ljust(60)} #{format('%.2f', r[:ips]).rjust(10)} IPS"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
puts
|
|
257
|
+
puts " #{Term::DIM}#{@all_results.length} benchmarks completed#{Term::CLEAR}"
|
|
258
|
+
puts
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Xmi
|
|
4
|
+
# Benchmarking and performance comparison tooling.
|
|
5
|
+
#
|
|
6
|
+
# Loaded on demand via autoload — never required at gem runtime.
|
|
7
|
+
# The rake tasks in lib/tasks/performance.rake load this module
|
|
8
|
+
# through `require "xmi"` and reference Xmi::Performance::*
|
|
9
|
+
# constants.
|
|
10
|
+
module Performance
|
|
11
|
+
autoload :Helpers, "xmi/performance/helpers"
|
|
12
|
+
autoload :Comparator, "xmi/performance/comparator"
|
|
13
|
+
autoload :Runner, "xmi/performance/runner"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -5,7 +5,7 @@ module Xmi
|
|
|
5
5
|
# UML `<packagedElement xmi:type="uml:AssociationClass">`. An
|
|
6
6
|
# association that is also a class (UML 2.5 §11.4).
|
|
7
7
|
#
|
|
8
|
-
# Phase A
|
|
8
|
+
# Phase A: subclass is a type tag on
|
|
9
9
|
# PackagedElement. The union-bag attribute set is inherited
|
|
10
10
|
# unchanged so existing consumers keep working. Phase B
|
|
11
11
|
# (narrowing attrs to the subclass) is future work.
|