xmi 0.3.21 → 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/.github/workflows/release.yml +13 -6
- data/.gitignore +2 -1
- data/.rubocop.yml +12 -13
- data/.rubocop_todo.yml +265 -12
- data/CHANGELOG.md +55 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +12 -0
- data/README.adoc +319 -6
- data/Rakefile +2 -0
- data/docs/migration.md +141 -0
- data/docs/versioning.md +255 -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/add.rb +14 -38
- data/lib/xmi/{the_custom_profile.rb → custom_profile.rb} +25 -25
- data/lib/xmi/delete.rb +14 -38
- data/lib/xmi/difference.rb +14 -38
- data/lib/xmi/documentation.rb +16 -101
- data/lib/xmi/ea_root.rb +114 -33
- data/lib/xmi/extension.rb +6 -6
- data/lib/xmi/namespace/dynamic.rb +28 -0
- data/lib/xmi/namespace/omg.rb +81 -0
- data/lib/xmi/namespace/sparx.rb +39 -0
- data/lib/xmi/namespace.rb +9 -0
- data/lib/xmi/namespace_detector.rb +138 -0
- data/lib/xmi/namespace_registry.rb +119 -0
- data/lib/xmi/parsing.rb +116 -0
- data/lib/xmi/replace.rb +14 -38
- data/lib/xmi/root.rb +49 -213
- data/lib/xmi/sparx/connector.rb +241 -0
- data/lib/xmi/sparx/custom_profile.rb +19 -0
- data/lib/xmi/sparx/diagram.rb +97 -0
- data/lib/xmi/sparx/ea_stub.rb +20 -0
- data/lib/xmi/{extensions/eauml.rb → sparx/ea_uml.rb} +3 -2
- data/lib/xmi/sparx/element.rb +453 -0
- data/lib/xmi/sparx/extension.rb +43 -0
- data/lib/xmi/{extensions → sparx}/gml.rb +9 -3
- data/lib/xmi/sparx/mappings/base_mapping.rb +182 -0
- data/lib/xmi/sparx/mappings.rb +10 -0
- data/lib/xmi/sparx/primitive_type.rb +18 -0
- data/lib/xmi/sparx/root.rb +60 -0
- data/lib/xmi/sparx/sys_ph_s.rb +18 -0
- data/lib/xmi/sparx.rb +17 -1376
- data/lib/xmi/type.rb +37 -0
- data/lib/xmi/uml.rb +191 -469
- data/lib/xmi/v20110701.rb +81 -0
- data/lib/xmi/v20131001.rb +68 -0
- data/lib/xmi/v20161101.rb +61 -0
- data/lib/xmi/version.rb +1 -1
- data/lib/xmi/version_registry.rb +167 -0
- data/lib/xmi/versioned.rb +145 -0
- data/lib/xmi.rb +83 -11
- data/xmi.gemspec +3 -9
- metadata +40 -77
data/docs/versioning.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# XMI Version Support
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
XMI files come in different versions with different namespace URIs. This gem handles version differences through namespace-bound registers that enable version-aware type resolution.
|
|
6
|
+
|
|
7
|
+
## Version-Specific Models
|
|
8
|
+
|
|
9
|
+
Some XMI elements differ between versions:
|
|
10
|
+
|
|
11
|
+
| Element | XMI 2.1 | XMI 2.5.1 | XMI 2.5.2 |
|
|
12
|
+
|---------|----------|------------|------------|
|
|
13
|
+
| Extension | v1 | v1 (reused) | v2 |
|
|
14
|
+
| Documentation | v1 | v2 | v2 (reused) |
|
|
15
|
+
| Model | v1 | v1 (reused) | v1 (reused) |
|
|
16
|
+
|
|
17
|
+
Where versions differ, the gem uses version-specific model classes through a fallback chain.
|
|
18
|
+
|
|
19
|
+
## Fallback Chain
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
xmi_20161101
|
|
23
|
+
↓ fallback
|
|
24
|
+
xmi_20131001
|
|
25
|
+
↓ fallback
|
|
26
|
+
xmi_20110701
|
|
27
|
+
↓ fallback
|
|
28
|
+
xmi_common
|
|
29
|
+
↓ fallback
|
|
30
|
+
default
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Types not found in a version register fall back to older versions.
|
|
34
|
+
|
|
35
|
+
## Using Version-Aware Parsing
|
|
36
|
+
|
|
37
|
+
### Automatic Version Detection
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
require 'xmi'
|
|
41
|
+
|
|
42
|
+
# Parse with automatic version detection
|
|
43
|
+
doc = Xmi.parse(xml_content)
|
|
44
|
+
|
|
45
|
+
# Get version information
|
|
46
|
+
info = Xmi::Parsing.detect_version(xml_content)
|
|
47
|
+
info[:xmi_version] # => "20131001"
|
|
48
|
+
info[:uml_version] # => "20131001"
|
|
49
|
+
info[:uris][:xmi] # => "http://www.omg.org/spec/XMI/20131001"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Explicit Version
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
# Parse with explicit version
|
|
56
|
+
doc = Xmi.parse_with_version(xml_content, "20131001")
|
|
57
|
+
|
|
58
|
+
# Check supported versions
|
|
59
|
+
Xmi::Parsing.supported_versions # => ["20110701", "20131001", "20161101"]
|
|
60
|
+
Xmi::Parsing.version_supported?("20131001") # => true
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Sparx EA Files
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
# For Enterprise Architect generated XMI files
|
|
67
|
+
doc = Xmi::Sparx::SparxRoot.parse_xml_with_versioning(xml_content)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Version Modules
|
|
71
|
+
|
|
72
|
+
The gem provides version-specific modules:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
# XMI 2.1 (20110701)
|
|
76
|
+
Xmi::V20110701.register # Register for this version
|
|
77
|
+
Xmi::V20110701::Extension # Version-specific model
|
|
78
|
+
Xmi::V20110701::Documentation # Version-specific model
|
|
79
|
+
|
|
80
|
+
# XMI 2.5.1 (20131001)
|
|
81
|
+
Xmi::V20131001.register
|
|
82
|
+
Xmi::V20131001::Documentation # Different from V20110701
|
|
83
|
+
|
|
84
|
+
# XMI 2.5.2 (20161101)
|
|
85
|
+
Xmi::V20161101.register
|
|
86
|
+
Xmi::V20161101::Extension # Different from previous versions
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Register Fallback Resolution
|
|
90
|
+
|
|
91
|
+
You can resolve types through the fallback chain:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
# Initialize versioning
|
|
95
|
+
Xmi.init_versioning!
|
|
96
|
+
|
|
97
|
+
# Get register for a version
|
|
98
|
+
register = Xmi::V20131001.register
|
|
99
|
+
|
|
100
|
+
# Resolve type with namespace awareness
|
|
101
|
+
klass = register.resolve_in_namespace(
|
|
102
|
+
:extension,
|
|
103
|
+
"http://www.omg.org/spec/XMI/20131001"
|
|
104
|
+
)
|
|
105
|
+
# Returns Xmi::V20110701::Extension (found via fallback)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Mixed namespace documents
|
|
109
|
+
|
|
110
|
+
XMI documents frequently use different namespace versions for XMI, UML, UMLDI, and
|
|
111
|
+
UMLDC. For example, an XMI 2.5.1 file may declare:
|
|
112
|
+
|
|
113
|
+
```xml
|
|
114
|
+
<xmi:XMI xmlns:xmi="http://www.omg.org/spec/XMI/20131001"
|
|
115
|
+
xmlns:uml="http://www.omg.org/spec/UML/20161101">
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The XMI namespace is version 20131001, but the UML namespace is 20161101.
|
|
119
|
+
This gem handles mixed namespace documents automatically through the
|
|
120
|
+
`Xmi::VersionRegistry.detect_register` method.
|
|
121
|
+
|
|
122
|
+
#### How mixed namespace detection works
|
|
123
|
+
|
|
124
|
+
`detect_register` performs these steps:
|
|
125
|
+
|
|
126
|
+
1. **Detect all namespace versions** from the document's namespace declarations:
|
|
127
|
+
- XMI namespace version (e.g., 20131001)
|
|
128
|
+
- UML namespace version (e.g., 20161101)
|
|
129
|
+
- UMLDI namespace version (if present)
|
|
130
|
+
- UMLDC namespace version (if present)
|
|
131
|
+
|
|
132
|
+
2. **Get the primary register** for the XMI namespace version (the primary register
|
|
133
|
+
determines the overall model tree structure).
|
|
134
|
+
|
|
135
|
+
3. **Extend the fallback chain** for additional namespace versions:
|
|
136
|
+
- Bind the additional namespace URIs to the primary register using proper
|
|
137
|
+
`Lutaml::Xml::Namespace` subclasses from the namespace registry.
|
|
138
|
+
- Add the additional register to the primary register's fallback chain, so
|
|
139
|
+
type resolution can find version-specific types.
|
|
140
|
+
|
|
141
|
+
4. **Prevent cycles**: If the additional register's fallback chain already
|
|
142
|
+
includes the primary register, no extension is made. This prevents infinite
|
|
143
|
+
loops in type resolution.
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
# Detect mixed namespaces and get configured register
|
|
147
|
+
register = Xmi::VersionRegistry.detect_register(xml_content)
|
|
148
|
+
# The returned register:
|
|
149
|
+
# - Is bound to all namespace URIs present in the document
|
|
150
|
+
# - Has an extended fallback chain for additional version registers
|
|
151
|
+
# - Resolves types correctly regardless of which namespace they appear in
|
|
152
|
+
|
|
153
|
+
doc = ModelClass.from_xml(xml_content, register: register)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### Fallback chain extension for mixed namespaces
|
|
157
|
+
|
|
158
|
+
Given a document with XMI=20131001 and UML=20161101, the register's fallback chain
|
|
159
|
+
is extended as follows:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
Primary: xmi_20131001
|
|
163
|
+
Handles: XMI 20131001, UML 20131001 namespaces
|
|
164
|
+
|
|
165
|
+
Extended fallback: xmi_20161101 (added because UML=20161101 was detected)
|
|
166
|
+
Handles: XMI 20161101, UML 20161101 namespaces
|
|
167
|
+
Own fallback: xmi_20131001 (already has XMI 20131001)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This allows the parser to resolve:
|
|
171
|
+
|
|
172
|
+
- Types specific to the UML 20161101 version (via the extended fallback)
|
|
173
|
+
- Types from the primary XMI 20131001 version (via the primary register)
|
|
174
|
+
|
|
175
|
+
### Version-specific namespace binding
|
|
176
|
+
|
|
177
|
+
Each version register is bound to its specific namespace URIs:
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
register = Xmi::V20131001.register
|
|
181
|
+
register.bound_namespace_uris
|
|
182
|
+
# => ["http://www.omg.org/spec/XMI/20131001",
|
|
183
|
+
# "http://www.omg.org/spec/UML/20131001",
|
|
184
|
+
# "http://www.omg.org/spec/UML/20131001/UMLDI",
|
|
185
|
+
# "http://www.omg.org/spec/UML/20131001/UMLDC"]
|
|
186
|
+
|
|
187
|
+
register.handles_namespace?("http://www.omg.org/spec/XMI/20131001")
|
|
188
|
+
# => true
|
|
189
|
+
|
|
190
|
+
register.handles_namespace?("http://www.omg.org/spec/XMI/20161101")
|
|
191
|
+
# => false (different version)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Programmatic Version Detection
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
require 'xmi'
|
|
198
|
+
|
|
199
|
+
# Detect version from XML content
|
|
200
|
+
xml = <<~XML
|
|
201
|
+
<?xml version="1.0"?>
|
|
202
|
+
<xmi:XMI xmlns:xmi="http://www.omg.org/spec/XMI/20110701"
|
|
203
|
+
xmlns:uml="http://www.omg.org/spec/UML/20110701">
|
|
204
|
+
<xmi:Documentation>...</xmi:Documentation>
|
|
205
|
+
</xmi:XMI>
|
|
206
|
+
XML
|
|
207
|
+
|
|
208
|
+
info = Xmi::Parsing.detect_version(xml)
|
|
209
|
+
# => { versions: { xmi: "20110701", uml: "20110701", ... },
|
|
210
|
+
# uris: { xmi: "http://...", uml: "http://...", ... },
|
|
211
|
+
# xmi_version: "20110701", uml_version: "20110701" }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Error Handling
|
|
215
|
+
|
|
216
|
+
Unknown versions are handled gracefully:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
# Unknown version raises ArgumentError when explicitly specified
|
|
220
|
+
Xmi.parse_with_version(xml, "19990101")
|
|
221
|
+
# => ArgumentError: Unknown version: 19990101
|
|
222
|
+
|
|
223
|
+
# Unknown version falls back to default parsing when auto-detected
|
|
224
|
+
doc = Xmi.parse(unknown_version_xml)
|
|
225
|
+
# Works, but may not resolve version-specific types correctly
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## API Reference
|
|
229
|
+
|
|
230
|
+
### Module Methods
|
|
231
|
+
|
|
232
|
+
| Method | Description |
|
|
233
|
+
|--------|-------------|
|
|
234
|
+
| `Xmi.parse(xml)` | Parse with auto-detection |
|
|
235
|
+
| `Xmi.parse_with_version(xml, version)` | Parse with explicit version |
|
|
236
|
+
| `Xmi.init_versioning!` | Initialize all version registers |
|
|
237
|
+
| `Xmi.versioning_initialized?` | Check initialization status |
|
|
238
|
+
|
|
239
|
+
### Parsing Module Methods
|
|
240
|
+
|
|
241
|
+
| Method | Description |
|
|
242
|
+
|--------|-------------|
|
|
243
|
+
| `Xmi::Parsing.parse(xml, **options)` | Parse with options |
|
|
244
|
+
| `Xmi::Parsing.parse_file(path, **options)` | Parse from file |
|
|
245
|
+
| `Xmi::Parsing.detect_version(xml)` | Detect version info |
|
|
246
|
+
| `Xmi::Parsing.supported_versions` | List supported versions |
|
|
247
|
+
| `Xmi::Parsing.version_supported?(version)` | Check if version supported |
|
|
248
|
+
|
|
249
|
+
### Version Registry Methods
|
|
250
|
+
|
|
251
|
+
| Method | Description |
|
|
252
|
+
|--------|-------------|
|
|
253
|
+
| `Xmi::VersionRegistry.register_for_version(version)` | Get register for version |
|
|
254
|
+
| `Xmi::VersionRegistry.register_for_namespace(uri)` | Get register for namespace |
|
|
255
|
+
| `Xmi::VersionRegistry.detect_register(xml)` | Detect and get register |
|
|
@@ -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
|