xmi 0.6.1 → 0.7.0
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 +6 -7
- data/CHANGELOG.md +61 -0
- data/CLAUDE.md +2 -2
- data/lib/tasks/performance.rake +6 -8
- data/lib/xmi/performance/comparator.rb +81 -0
- data/lib/xmi/performance/helpers.rb +190 -0
- data/lib/xmi/performance/runner.rb +203 -0
- data/lib/xmi/performance/term.rb +58 -0
- data/lib/xmi/performance.rb +27 -0
- data/lib/xmi/sparx/index.rb +1 -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_attribute.rb +3 -2
- data/lib/xmi/uml/owned_comment.rb +3 -2
- data/lib/xmi/uml/owned_element.rb +2 -2
- data/lib/xmi/uml/owned_end.rb +3 -2
- data/lib/xmi/uml/owned_parameter.rb +20 -2
- data/lib/xmi/uml/packaged_element.rb +21 -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 +7 -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
|
@@ -1,274 +0,0 @@
|
|
|
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::Root.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
|
|
@@ -1,88 +0,0 @@
|
|
|
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
|
|
@@ -1,238 +0,0 @@
|
|
|
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
|