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
data/lib/xmi/uml/component.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Xmi
|
|
|
9
9
|
# interface_realization — but signals a different deployment role
|
|
10
10
|
# to consumers.
|
|
11
11
|
#
|
|
12
|
-
# Phase A
|
|
12
|
+
# Phase A: subclass is a type tag on
|
|
13
13
|
# PackagedElement. The union-bag attribute set is inherited
|
|
14
14
|
# unchanged so existing consumers keep working.
|
|
15
15
|
class Component < PackagedElement
|
data/lib/xmi/uml/dependency.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Xmi
|
|
|
5
5
|
# UML `<packagedElement xmi:type="uml:Dependency">`. A relationship
|
|
6
6
|
# between a client and supplier packageable element.
|
|
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.
|
data/lib/xmi/uml/extension.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Xmi
|
|
|
6
6
|
# associates a stereotype with a metaclass to make the stereotype
|
|
7
7
|
# applicable to instances of that metaclass (UML 2.5 §22.3).
|
|
8
8
|
#
|
|
9
|
-
# Phase A
|
|
9
|
+
# Phase A: subclass is a type tag on
|
|
10
10
|
# PackagedElement. The union-bag attribute set is inherited
|
|
11
11
|
# unchanged so existing consumers keep working. Phase B
|
|
12
12
|
# (narrowing attrs to the subclass) is future work.
|
|
@@ -7,7 +7,7 @@ module Xmi
|
|
|
7
7
|
attribute :body_element, :string
|
|
8
8
|
attribute :body_attribute, :string
|
|
9
9
|
attribute :annotated_attribute, :string
|
|
10
|
-
attribute :annotated_element, AnnotatedElement
|
|
10
|
+
attribute :annotated_element, AnnotatedElement, collection: true
|
|
11
11
|
|
|
12
12
|
xml do
|
|
13
13
|
root "ownedComment"
|
|
@@ -15,7 +15,8 @@ module Xmi
|
|
|
15
15
|
map_attribute "body", to: :body_attribute
|
|
16
16
|
map_attribute "annotatedElement", to: :annotated_attribute
|
|
17
17
|
|
|
18
|
-
map_element "annotatedElement", to: :annotated_element
|
|
18
|
+
map_element "annotatedElement", to: :annotated_element,
|
|
19
|
+
value_map: VALUE_MAP
|
|
19
20
|
map_element "body", to: :body_element
|
|
20
21
|
end
|
|
21
22
|
end
|
|
@@ -12,7 +12,7 @@ module Xmi
|
|
|
12
12
|
attribute :bounds, Bounds, collection: true
|
|
13
13
|
attribute :source, :string
|
|
14
14
|
attribute :target, :string
|
|
15
|
-
attribute :waypoint, Waypoint
|
|
15
|
+
attribute :waypoint, Waypoint, collection: true
|
|
16
16
|
|
|
17
17
|
xml do
|
|
18
18
|
root "ownedElement"
|
|
@@ -23,7 +23,7 @@ module Xmi
|
|
|
23
23
|
map_element "bounds", to: :bounds, value_map: VALUE_MAP
|
|
24
24
|
map_element "source", to: :source
|
|
25
25
|
map_element "target", to: :target
|
|
26
|
-
map_element "waypoint", to: :waypoint
|
|
26
|
+
map_element "waypoint", to: :waypoint, value_map: VALUE_MAP
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -7,17 +7,23 @@ module Xmi
|
|
|
7
7
|
# packaged_element recursion on PackagedElement itself, and by
|
|
8
8
|
# UmlModel.packaged_element.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
# PackagedElement (no attrs narrowed). Phase B will narrow each
|
|
12
|
-
# subclass's attrs to its UML-2.5-conformant subset.
|
|
13
|
-
#
|
|
14
|
-
# Fallback contract: unknown or missing `xmi:type` resolves to
|
|
10
|
+
# Fallback contract: an unknown `xmi:type` resolves to
|
|
15
11
|
# `Xmi::Uml::PackagedElement` (the union-bag base) via the
|
|
16
|
-
# class_map's
|
|
12
|
+
# class_map's Hash default value. This avoids the lutaml-model
|
|
17
13
|
# `Object.const_get(nil)` TypeError when a Sparx XMI emits a
|
|
18
14
|
# type we haven't modelled yet, at the cost of treating the
|
|
19
|
-
# element as generic.
|
|
20
|
-
# graceful-fallback
|
|
15
|
+
# element as generic. polymorphic_map_contract_spec and
|
|
16
|
+
# polymorphic_robustness_spec lock in the graceful-fallback
|
|
17
|
+
# behavior.
|
|
18
|
+
#
|
|
19
|
+
# Two paths land at the base:
|
|
20
|
+
# 1. Missing discriminator — lutaml-model's
|
|
21
|
+
# `polymorphic_map_defined?` (lib/lutaml/model/attribute.rb)
|
|
22
|
+
# short-circuits and returns the declared attribute type
|
|
23
|
+
# (`PackagedElement`). The class_map default is not consulted.
|
|
24
|
+
# 2. Unknown discriminator — `polymorphic_map_defined?` is true,
|
|
25
|
+
# the class_map lookup hits a missing key, and the Hash default
|
|
26
|
+
# value supplies the fallback class name.
|
|
21
27
|
PACKAGED_ELEMENT_POLYMORPHIC_MAP = {
|
|
22
28
|
attribute: "xmi:type",
|
|
23
29
|
class_map: Hash.new("Xmi::Uml::PackagedElement").merge!(
|
|
@@ -37,7 +43,7 @@ module Xmi
|
|
|
37
43
|
"uml:Stereotype" => "Xmi::Uml::Stereotype",
|
|
38
44
|
"uml:Usage" => "Xmi::Uml::Usage",
|
|
39
45
|
"uml:Component" => "Xmi::Uml::Component",
|
|
40
|
-
),
|
|
46
|
+
).freeze,
|
|
41
47
|
}.freeze
|
|
42
48
|
|
|
43
49
|
class PackagedElement < Base
|
data/lib/xmi/uml/signal.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Xmi
|
|
|
6
6
|
# classifier that communicates the sending of a message instance
|
|
7
7
|
# (UML 2.5 §17.4).
|
|
8
8
|
#
|
|
9
|
-
# Phase A
|
|
9
|
+
# Phase A: subclass is a type tag on
|
|
10
10
|
# PackagedElement. The union-bag attribute set is inherited
|
|
11
11
|
# unchanged so existing consumers keep working. Phase B
|
|
12
12
|
# (narrowing attrs to the subclass) is future work.
|
data/lib/xmi/uml/stereotype.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Xmi
|
|
|
6
6
|
# is a kind of class defined to extend the UML metamodel
|
|
7
7
|
# (UML 2.5 §22.3).
|
|
8
8
|
#
|
|
9
|
-
# Phase A
|
|
9
|
+
# Phase A: subclass is a type tag on
|
|
10
10
|
# PackagedElement. The union-bag attribute set is inherited
|
|
11
11
|
# unchanged so existing consumers keep working. Phase B
|
|
12
12
|
# (narrowing attrs to the subclass) is future work.
|
data/lib/xmi/uml/uml_class.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Xmi
|
|
|
5
5
|
# UML `<packagedElement xmi:type="uml:Class">`. Classifier with
|
|
6
6
|
# attributes, operations, and structural relationships.
|
|
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.
|
data/lib/xmi/uml/usage.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Xmi
|
|
|
6
6
|
# subtype indicating that a client requires a supplier for its
|
|
7
7
|
# proper functioning (UML 2.5 §19.3).
|
|
8
8
|
#
|
|
9
|
-
# Phase A
|
|
9
|
+
# Phase A: subclass is a type tag on
|
|
10
10
|
# PackagedElement. The union-bag attribute set is inherited
|
|
11
11
|
# unchanged so existing consumers keep working. Phase B
|
|
12
12
|
# (narrowing attrs to the subclass) is future work.
|
|
@@ -32,10 +32,16 @@ module Xmi
|
|
|
32
32
|
# The literal class names are string form because lutaml-model's
|
|
33
33
|
# resolve_polymorphic_class calls Object.const_get on the value.
|
|
34
34
|
#
|
|
35
|
-
# Fallback contract: unknown
|
|
35
|
+
# Fallback contract: an unknown `xmi:type` resolves to
|
|
36
36
|
# `Xmi::Uml::ValueSpecification` (the abstract base) via the
|
|
37
|
-
# class_map's
|
|
38
|
-
# the graceful-fallback
|
|
37
|
+
# class_map's Hash default value. polymorphic_map_contract_spec
|
|
38
|
+
# and polymorphic_robustness_spec lock in the graceful-fallback
|
|
39
|
+
# behavior.
|
|
40
|
+
#
|
|
41
|
+
# Two paths land at the base (see PACKAGED_ELEMENT_POLYMORPHIC_MAP
|
|
42
|
+
# for the full rationale): missing discriminator short-circuits via
|
|
43
|
+
# lutaml-model's `polymorphic_map_defined?`; unknown discriminator
|
|
44
|
+
# hits the Hash default value.
|
|
39
45
|
VALUE_SPECIFICATION_POLYMORPHIC_MAP = {
|
|
40
46
|
attribute: "xmi:type",
|
|
41
47
|
class_map: Hash.new("Xmi::Uml::ValueSpecification").merge!(
|
|
@@ -45,7 +51,7 @@ module Xmi
|
|
|
45
51
|
"uml:LiteralBoolean" => "Xmi::Uml::LiteralBoolean",
|
|
46
52
|
"uml:LiteralUnlimitedNatural" => "Xmi::Uml::LiteralUnlimitedNatural",
|
|
47
53
|
"uml:LiteralNull" => "Xmi::Uml::LiteralNull",
|
|
48
|
-
),
|
|
54
|
+
).freeze,
|
|
49
55
|
}.freeze
|
|
50
56
|
end
|
|
51
57
|
end
|
data/lib/xmi/version.rb
CHANGED
data/lib/xmi.rb
CHANGED
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.6.
|
|
4
|
+
version: 0.6.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: lutaml-model
|
|
@@ -61,10 +61,7 @@ files:
|
|
|
61
61
|
- bin/setup
|
|
62
62
|
- docs/migration.md
|
|
63
63
|
- docs/versioning.md
|
|
64
|
-
- lib/tasks/benchmark_runner.rb
|
|
65
64
|
- lib/tasks/performance.rake
|
|
66
|
-
- lib/tasks/performance_comparator.rb
|
|
67
|
-
- lib/tasks/performance_helpers.rb
|
|
68
65
|
- lib/xmi.rb
|
|
69
66
|
- lib/xmi/add.rb
|
|
70
67
|
- lib/xmi/custom_profile.rb
|
|
@@ -97,6 +94,10 @@ files:
|
|
|
97
94
|
- lib/xmi/namespace_registry.rb
|
|
98
95
|
- lib/xmi/parser_pipeline.rb
|
|
99
96
|
- lib/xmi/parsing.rb
|
|
97
|
+
- lib/xmi/performance.rb
|
|
98
|
+
- lib/xmi/performance/comparator.rb
|
|
99
|
+
- lib/xmi/performance/helpers.rb
|
|
100
|
+
- lib/xmi/performance/runner.rb
|
|
100
101
|
- lib/xmi/replace.rb
|
|
101
102
|
- lib/xmi/root.rb
|
|
102
103
|
- lib/xmi/sparx.rb
|
|
@@ -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
|