serialbench 0.1.0 → 0.1.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/.github/workflows/benchmark.yml +181 -30
- data/.github/workflows/ci.yml +3 -3
- data/.github/workflows/docker.yml +272 -0
- data/.github/workflows/rake.yml +15 -0
- data/.github/workflows/release.yml +25 -0
- data/Gemfile +6 -30
- data/README.adoc +381 -415
- data/Rakefile +0 -55
- data/config/benchmarks/full.yml +29 -0
- data/config/benchmarks/short.yml +26 -0
- data/config/environments/asdf-ruby-3.2.yml +8 -0
- data/config/environments/asdf-ruby-3.3.yml +8 -0
- data/config/environments/docker-ruby-3.0.yml +9 -0
- data/config/environments/docker-ruby-3.1.yml +9 -0
- data/config/environments/docker-ruby-3.2.yml +9 -0
- data/config/environments/docker-ruby-3.3.yml +9 -0
- data/config/environments/docker-ruby-3.4.yml +9 -0
- data/docker/Dockerfile.alpine +33 -0
- data/docker/Dockerfile.ubuntu +32 -0
- data/docker/README.md +214 -0
- data/exe/serialbench +1 -1
- data/lib/serialbench/benchmark_runner.rb +270 -350
- data/lib/serialbench/cli/base_cli.rb +51 -0
- data/lib/serialbench/cli/benchmark_cli.rb +380 -0
- data/lib/serialbench/cli/environment_cli.rb +181 -0
- data/lib/serialbench/cli/resultset_cli.rb +215 -0
- data/lib/serialbench/cli/ruby_build_cli.rb +238 -0
- data/lib/serialbench/cli.rb +59 -410
- data/lib/serialbench/config_manager.rb +140 -0
- data/lib/serialbench/models/benchmark_config.rb +63 -0
- data/lib/serialbench/models/benchmark_result.rb +45 -0
- data/lib/serialbench/models/environment_config.rb +71 -0
- data/lib/serialbench/models/platform.rb +59 -0
- data/lib/serialbench/models/result.rb +53 -0
- data/lib/serialbench/models/result_set.rb +71 -0
- data/lib/serialbench/models/result_store.rb +108 -0
- data/lib/serialbench/models.rb +54 -0
- data/lib/serialbench/ruby_build_manager.rb +153 -0
- data/lib/serialbench/runners/asdf_runner.rb +296 -0
- data/lib/serialbench/runners/base.rb +32 -0
- data/lib/serialbench/runners/docker_runner.rb +142 -0
- data/lib/serialbench/serializers/base_serializer.rb +8 -16
- data/lib/serialbench/serializers/json/base_json_serializer.rb +4 -4
- data/lib/serialbench/serializers/json/json_serializer.rb +0 -2
- data/lib/serialbench/serializers/json/oj_serializer.rb +0 -2
- data/lib/serialbench/serializers/json/rapidjson_serializer.rb +50 -0
- data/lib/serialbench/serializers/json/yajl_serializer.rb +6 -4
- data/lib/serialbench/serializers/toml/base_toml_serializer.rb +5 -3
- data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +0 -2
- data/lib/serialbench/serializers/toml/tomlib_serializer.rb +0 -2
- data/lib/serialbench/serializers/toml/tomlrb_serializer.rb +56 -0
- data/lib/serialbench/serializers/xml/base_xml_serializer.rb +4 -9
- data/lib/serialbench/serializers/xml/libxml_serializer.rb +0 -2
- data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +21 -5
- data/lib/serialbench/serializers/xml/oga_serializer.rb +0 -2
- data/lib/serialbench/serializers/xml/ox_serializer.rb +0 -2
- data/lib/serialbench/serializers/xml/rexml_serializer.rb +32 -4
- data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +59 -0
- data/lib/serialbench/serializers/yaml/psych_serializer.rb +54 -0
- data/lib/serialbench/serializers/yaml/syck_serializer.rb +102 -0
- data/lib/serialbench/serializers.rb +34 -6
- data/lib/serialbench/site_generator.rb +105 -0
- data/lib/serialbench/templates/assets/css/benchmark_report.css +535 -0
- data/lib/serialbench/templates/assets/css/format_based.css +526 -0
- data/lib/serialbench/templates/assets/css/themes.css +588 -0
- data/lib/serialbench/templates/assets/js/chart_helpers.js +381 -0
- data/lib/serialbench/templates/assets/js/dashboard.js +796 -0
- data/lib/serialbench/templates/assets/js/navigation.js +142 -0
- data/lib/serialbench/templates/base.liquid +49 -0
- data/lib/serialbench/templates/format_based.liquid +279 -0
- data/lib/serialbench/templates/partials/chart_section.liquid +4 -0
- data/lib/serialbench/version.rb +1 -1
- data/lib/serialbench.rb +2 -31
- data/serialbench.gemspec +28 -17
- metadata +192 -55
- data/lib/serialbench/chart_generator.rb +0 -821
- data/lib/serialbench/result_formatter.rb +0 -182
- data/lib/serialbench/result_merger.rb +0 -1201
- data/lib/serialbench/serializers/xml/base_parser.rb +0 -69
- data/lib/serialbench/serializers/xml/libxml_parser.rb +0 -98
- data/lib/serialbench/serializers/xml/nokogiri_parser.rb +0 -111
- data/lib/serialbench/serializers/xml/oga_parser.rb +0 -85
- data/lib/serialbench/serializers/xml/ox_parser.rb +0 -64
- data/lib/serialbench/serializers/xml/rexml_parser.rb +0 -129
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'json'
|
5
|
+
require 'yaml'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
module Serialbench
|
9
|
+
module Cli
|
10
|
+
# Base class for CLI commands with shared functionality
|
11
|
+
class BaseCli < Thor
|
12
|
+
include Thor::Actions
|
13
|
+
|
14
|
+
def self.exit_on_failure?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def load_configuration(config_path)
|
21
|
+
unless File.exist?(config_path)
|
22
|
+
say "Configuration file not found: #{config_path}", :red
|
23
|
+
exit 1
|
24
|
+
end
|
25
|
+
|
26
|
+
begin
|
27
|
+
config = YAML.load_file(config_path)
|
28
|
+
say "Loaded configuration from: #{config_path}", :cyan
|
29
|
+
config
|
30
|
+
rescue StandardError => e
|
31
|
+
say "Error loading configuration: #{e.message}", :red
|
32
|
+
exit 1
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate_name(name_with_path)
|
37
|
+
return if name_with_path.nil? || name_with_path.empty?
|
38
|
+
|
39
|
+
name = File.basename(name_with_path)
|
40
|
+
return if name.match?(/\A[a-zA-Z0-9_-]+\z/)
|
41
|
+
|
42
|
+
say "Invalid name '#{name}'. Names can only contain letters, numbers, hyphens, and underscores.", :red
|
43
|
+
exit 1
|
44
|
+
end
|
45
|
+
|
46
|
+
def generate_timestamp
|
47
|
+
Time.now.utc.strftime('%Y%m%d_%H%M%S')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,380 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ostruct'
|
4
|
+
require_relative 'base_cli'
|
5
|
+
require_relative '../benchmark_runner'
|
6
|
+
require_relative '../models/result'
|
7
|
+
require_relative '../models/benchmark_config'
|
8
|
+
require_relative '../models/environment_config'
|
9
|
+
require_relative '../models/platform'
|
10
|
+
|
11
|
+
module Serialbench
|
12
|
+
module Cli
|
13
|
+
# CLI for managing individual benchmark runs
|
14
|
+
class BenchmarkCli < BaseCli
|
15
|
+
desc 'create [NAME]', 'Generate a run configuration file'
|
16
|
+
long_desc <<~DESC
|
17
|
+
Generate a configuration file for a benchmark run.
|
18
|
+
|
19
|
+
NAME is optional - if not provided, a timestamped name will be generated.
|
20
|
+
|
21
|
+
Examples:
|
22
|
+
serialbench benchmark create # Creates config with timestamp
|
23
|
+
serialbench benchmark create my-benchmark # Creates config/runs/my-benchmark.yml
|
24
|
+
DESC
|
25
|
+
option :formats, type: :array, default: %w[xml json yaml toml],
|
26
|
+
desc: 'Formats to benchmark (xml, json, yaml, toml)'
|
27
|
+
option :warmup, type: :numeric, default: 3,
|
28
|
+
desc: 'Number of warmup iterations'
|
29
|
+
option :data_sizes, type: :array, default: %w[small medium large],
|
30
|
+
desc: 'Data sizes to test (small, medium, large)'
|
31
|
+
def create(name = nil)
|
32
|
+
raise 'Run name cannot be empty' if name&.strip&.empty?
|
33
|
+
|
34
|
+
validate_name(name)
|
35
|
+
|
36
|
+
config_dir = 'config/runs'
|
37
|
+
FileUtils.mkdir_p(config_dir)
|
38
|
+
|
39
|
+
benchmark_config_path = File.join(config_dir, "#{name}.yml")
|
40
|
+
|
41
|
+
if File.exist?(benchmark_config_path)
|
42
|
+
say "Configuration file already exists: #{benchmark_config_path}", :yellow
|
43
|
+
return unless yes?('Overwrite existing file? (y/n)')
|
44
|
+
end
|
45
|
+
|
46
|
+
benchmark_config = Models::BenchmarkConfig.new(
|
47
|
+
formats: options[:formats],
|
48
|
+
warmup: options[:warmup],
|
49
|
+
data_sizes: options[:data_sizes]
|
50
|
+
)
|
51
|
+
|
52
|
+
benchmark_config.to_file(benchmark_config_path)
|
53
|
+
|
54
|
+
say "✅ Generated run configuration: #{benchmark_config_path}", :green
|
55
|
+
say 'Edit the file to customize benchmark settings', :cyan
|
56
|
+
end
|
57
|
+
|
58
|
+
desc 'execute ENVIRONMENT_CONFIG_PATH BENCHMARK_CONFIG_PATH', 'Execute a benchmark run'
|
59
|
+
long_desc <<~DESC
|
60
|
+
Execute a benchmark run using the specified environment and configuration file.
|
61
|
+
|
62
|
+
ENVIRONMENT must be the name of an existing environment (from environments/ directory).
|
63
|
+
BENCHMARK_CONFIG_PATH is the path to the benchmark configuration file.
|
64
|
+
|
65
|
+
Results will be saved to results/runs/ with platform-specific naming based on the environment.
|
66
|
+
|
67
|
+
Examples:
|
68
|
+
serialbench benchmark execute environments/local-dev.yml config/short.yml
|
69
|
+
serialbench benchmark execute environments/docker-alpine.yml config/full.yml
|
70
|
+
DESC
|
71
|
+
def execute(environment_config_path, benchmark_config_path)
|
72
|
+
environment = load_environment_config(environment_config_path)
|
73
|
+
config = load_benchmark_config(benchmark_config_path)
|
74
|
+
|
75
|
+
say '🚀 Executing benchmark run', :green
|
76
|
+
say "Environment: #{environment.name} (#{environment.kind})", :cyan
|
77
|
+
say "Configuration: #{benchmark_config_path}", :cyan
|
78
|
+
|
79
|
+
begin
|
80
|
+
# Execute benchmark based on environment type
|
81
|
+
case environment.kind
|
82
|
+
when 'local'
|
83
|
+
execute_local_benchmark(environment, config, benchmark_config_path)
|
84
|
+
when 'docker'
|
85
|
+
execute_docker_benchmark(environment, config, benchmark_config_path)
|
86
|
+
when 'asdf'
|
87
|
+
execute_asdf_benchmark(environment, config, benchmark_config_path)
|
88
|
+
else
|
89
|
+
raise "Unsupported environment type: #{environment.kind}"
|
90
|
+
end
|
91
|
+
rescue StandardError => e
|
92
|
+
say "❌ Error executing benchmark run: #{e.message}", :red
|
93
|
+
exit 1
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
desc '_docker_execute ENVIRONMENT_CONFIG_PATH BENCHMARK_CONFIG_PATH', '(Private) Execute a benchmark run'
|
98
|
+
long_desc <<~DESC
|
99
|
+
For docker used internally by the CLI.
|
100
|
+
|
101
|
+
Examples:
|
102
|
+
serialbench benchmark _docker_execute /app/environment.yml /app/benchmark_config.yml
|
103
|
+
serialbench benchmark _docker_execute /app/environment.yml /app/benchmark_config.yml
|
104
|
+
DESC
|
105
|
+
option :result_dir, type: :string, default: 'results/runs',
|
106
|
+
desc: 'Directory to save benchmark results'
|
107
|
+
def _docker_execute(environment_config_path, benchmark_config_path)
|
108
|
+
environment_config = load_environment_config(environment_config_path)
|
109
|
+
benchmark_config = load_benchmark_config(benchmark_config_path)
|
110
|
+
|
111
|
+
say '🚀 Executing benchmark run', :green
|
112
|
+
say "Environment: #{environment_config_path} (#{environment_config.kind})", :cyan
|
113
|
+
say "Configuration: #{benchmark_config_path}", :cyan
|
114
|
+
|
115
|
+
runner = Serialbench::BenchmarkRunner.new(
|
116
|
+
environment_config: environment_config,
|
117
|
+
benchmark_config: benchmark_config
|
118
|
+
)
|
119
|
+
|
120
|
+
# Run benchmarks
|
121
|
+
results = runner.run_all_benchmarks
|
122
|
+
|
123
|
+
platform = Serialbench::Models::Platform.current_local
|
124
|
+
|
125
|
+
metadata = Models::RunMetadata.new(
|
126
|
+
benchmark_config_path: benchmark_config_path,
|
127
|
+
environment_config_path: environment_config_path,
|
128
|
+
tags: [
|
129
|
+
'docker',
|
130
|
+
platform.os,
|
131
|
+
platform.arch,
|
132
|
+
"ruby-#{environment_config.ruby_build_tag}"
|
133
|
+
]
|
134
|
+
)
|
135
|
+
# Create results directory
|
136
|
+
result_dir = options[:result_dir]
|
137
|
+
FileUtils.mkdir_p(result_dir)
|
138
|
+
|
139
|
+
# Save results to single YAML file with platform and metadata merged in
|
140
|
+
results_model = Models::Result.new(
|
141
|
+
platform: platform,
|
142
|
+
metadata: metadata,
|
143
|
+
environment_config: environment_config,
|
144
|
+
benchmark_config: benchmark_config,
|
145
|
+
benchmark_result: results
|
146
|
+
)
|
147
|
+
|
148
|
+
# Restore YAML to use Psych for output, otherwise lutaml-model's to_yaml
|
149
|
+
# will have no output
|
150
|
+
Object.const_set(:YAML, Psych)
|
151
|
+
|
152
|
+
results_file = File.join(result_dir, 'results.yaml')
|
153
|
+
results_model.to_file(results_file)
|
154
|
+
|
155
|
+
say '✅ Local benchmark completed successfully!', :green
|
156
|
+
say 'Results saved.', :cyan
|
157
|
+
rescue StandardError => e
|
158
|
+
say "❌ Local benchmark failed: #{e.message}", :red
|
159
|
+
say "Details: #{e.backtrace.first(3).join("\n")}", :white if options[:verbose]
|
160
|
+
raise e
|
161
|
+
end
|
162
|
+
|
163
|
+
desc 'build-site RUN_PATH [OUTPUT_DIR]', 'Generate HTML site for a run'
|
164
|
+
long_desc <<~DESC
|
165
|
+
Generate an HTML site for a specific benchmark run.
|
166
|
+
|
167
|
+
RUN_PATH should be the path to a run directory in results/runs/
|
168
|
+
OUTPUT_DIR defaults to _site/
|
169
|
+
|
170
|
+
Examples:
|
171
|
+
serialbench benchmark build-site results/runs/my-run-local-macos-arm64-ruby-3.3.8
|
172
|
+
serialbench benchmark build-site results/runs/performance-test-docker-alpine-arm64-ruby-3.3
|
173
|
+
DESC
|
174
|
+
option :output_dir, type: :string, default: '_site', desc: 'Output directory for generated site'
|
175
|
+
def build_site(result_path)
|
176
|
+
unless Dir.exist?(result_path)
|
177
|
+
say "Result directory not found: #{result_path}", :red
|
178
|
+
say "Please create a result first using 'serialbench benchmark create'", :white
|
179
|
+
exit 1
|
180
|
+
end
|
181
|
+
|
182
|
+
result = Serialbench::Models::Result.load(result_path)
|
183
|
+
|
184
|
+
if result.nil?
|
185
|
+
say "Result '#{result_path}' not found", :yellow
|
186
|
+
say "Use 'serialbench benchmark add-result' to add a result first", :white
|
187
|
+
return
|
188
|
+
end
|
189
|
+
|
190
|
+
say "🏗️ Generating HTML site for result: #{result_path}", :green
|
191
|
+
|
192
|
+
# Use the unified site generator for results
|
193
|
+
Serialbench::SiteGenerator.generate_for_result(result, options[:output_dir])
|
194
|
+
|
195
|
+
say '✅ HTML site generated successfully!', :green
|
196
|
+
say "Site location: #{options[:output_dir]}", :cyan
|
197
|
+
say "Open: #{File.join(options[:output_dir], 'index.html')}", :white
|
198
|
+
rescue StandardError => e
|
199
|
+
say "Error generating site: #{e.message}", :red
|
200
|
+
say "Details: #{e.backtrace.first(3).join("\n")}", :red if options[:verbose]
|
201
|
+
exit 1
|
202
|
+
end
|
203
|
+
|
204
|
+
desc 'list', 'List all available runs'
|
205
|
+
long_desc <<~DESC
|
206
|
+
List all benchmark runs in the results/runs/ directory.
|
207
|
+
|
208
|
+
Shows benchmark run names, platforms, and timestamps.
|
209
|
+
DESC
|
210
|
+
option :tags, type: :array, desc: 'Filter by tags (e.g., docker, ruby-3.3)'
|
211
|
+
def list
|
212
|
+
store = Serialbench::Models::ResultStore.default
|
213
|
+
runs = if options[:tags]
|
214
|
+
store.find_runs(tags: options[:tags])
|
215
|
+
else
|
216
|
+
store.find_runs
|
217
|
+
end
|
218
|
+
|
219
|
+
if runs.empty?
|
220
|
+
say 'No runs found', :yellow
|
221
|
+
return
|
222
|
+
end
|
223
|
+
|
224
|
+
say 'Available Runs:', :green
|
225
|
+
say '=' * 50, :green
|
226
|
+
|
227
|
+
runs.each do |run|
|
228
|
+
say '📊 Run:', :cyan
|
229
|
+
say " Created: #{run.metadata.created_at}", :white
|
230
|
+
say " Platform: #{run.platform.platform_string} (os: #{run.platform.os}, arch: #{run.platform.arch})",
|
231
|
+
:white
|
232
|
+
say " Environment config: #{run.metadata.environment_config_path}", :white
|
233
|
+
say " Benchmark config: #{run.metadata.benchmark_config_path}", :white
|
234
|
+
say " Environment: #{run.environment_config.name} (#{run.environment_config.kind})", :white
|
235
|
+
say " Tags: [#{run.metadata.tags.join(', ')}]", :white
|
236
|
+
say ''
|
237
|
+
end
|
238
|
+
rescue StandardError => e
|
239
|
+
say "Error listing runs: #{e.message}", :red
|
240
|
+
exit 1
|
241
|
+
end
|
242
|
+
|
243
|
+
private
|
244
|
+
|
245
|
+
def show_execute_usage_and_exit
|
246
|
+
say '❌ Error: Environment and config file arguments are required.', :red
|
247
|
+
say ''
|
248
|
+
say 'Usage:', :white
|
249
|
+
say ' serialbench benchmark execute ENVIRONMENT_CONFIG_PATH BENCHMARK_CONFIG_PATH', :cyan
|
250
|
+
say ''
|
251
|
+
say 'Arguments:', :white
|
252
|
+
say ' ENVIRONMENT_CONFIG_PATH Path to environment configuration file', :white
|
253
|
+
say ' BENCHMARK_CONFIG_PATH Path to benchmark configuration file', :white
|
254
|
+
say ''
|
255
|
+
say 'Examples:', :white
|
256
|
+
say ' serialbench benchmark execute config/environments/local-dev.yml config/short.yml', :cyan
|
257
|
+
say ' serialbench benchmark execute config/environments/docker-alpine.yml config/full.yml', :cyan
|
258
|
+
exit 1
|
259
|
+
end
|
260
|
+
|
261
|
+
def load_environment_config(environment_config_path)
|
262
|
+
unless File.exist?(environment_config_path)
|
263
|
+
say "❌ Environment not found: #{environment_config_path}", :red
|
264
|
+
exit 1
|
265
|
+
end
|
266
|
+
|
267
|
+
Models::EnvironmentConfig.from_file(environment_config_path)
|
268
|
+
rescue StandardError => e
|
269
|
+
say "❌ Failed to load environment: #{e.message}", :red
|
270
|
+
say "Environment file: #{environment_config_path}", :white
|
271
|
+
exit 1
|
272
|
+
end
|
273
|
+
|
274
|
+
def load_benchmark_config(benchmark_config_path)
|
275
|
+
unless File.exist?(benchmark_config_path)
|
276
|
+
say "❌ Benchmark config not found: #{benchmark_config_path}", :red
|
277
|
+
exit 1
|
278
|
+
end
|
279
|
+
|
280
|
+
Models::BenchmarkConfig.from_file(benchmark_config_path)
|
281
|
+
rescue StandardError => e
|
282
|
+
say "❌ Failed to load benchmark config: #{e.message}", :red
|
283
|
+
say "Benchmark config file: #{benchmark_config_path}", :white
|
284
|
+
exit 1
|
285
|
+
end
|
286
|
+
|
287
|
+
# def execute_local_benchmark(environment, config, benchmark_config_path)
|
288
|
+
# say '🏠 Executing local benchmark', :green
|
289
|
+
|
290
|
+
# # Create benchmark runner with config
|
291
|
+
# runner_options = {
|
292
|
+
# formats: (config['formats'] || %w[xml json yaml toml]).map(&:to_sym),
|
293
|
+
# iterations: config['iterations'] || 10,
|
294
|
+
# warmup: config['warmup'] || 3,
|
295
|
+
# config: config
|
296
|
+
# }
|
297
|
+
|
298
|
+
# runner = Serialbench::BenchmarkRunner.new(**runner_options)
|
299
|
+
|
300
|
+
# # Run benchmarks
|
301
|
+
# say "Running benchmarks with #{runner_options[:iterations]} iterations...", :white
|
302
|
+
# results = runner.run_all_benchmarks
|
303
|
+
|
304
|
+
# # Create platform-specific directory name using environment's ruby_build_tag
|
305
|
+
# require_relative '../models/platform'
|
306
|
+
# platform = Serialbench::Models::Platform.current_local
|
307
|
+
# platform_string = "local-#{platform.os}-#{platform.arch}-ruby-#{environment['ruby_build_tag']}"
|
308
|
+
|
309
|
+
# # Create results directory
|
310
|
+
# result_dir = "results/runs/#{environment['name']}"
|
311
|
+
# FileUtils.mkdir_p(result_dir)
|
312
|
+
|
313
|
+
# # Save results to single YAML file with platform and metadata merged in
|
314
|
+
# results_file = File.join(result_dir, 'results.yaml')
|
315
|
+
# full_results = {
|
316
|
+
# 'platform' => {
|
317
|
+
# 'platform_string' => platform_string,
|
318
|
+
# 'os' => platform.os,
|
319
|
+
# 'arch' => platform.arch
|
320
|
+
# },
|
321
|
+
# 'metadata' => {
|
322
|
+
# 'environment_name' => environment['name'],
|
323
|
+
# 'benchmark_config' => benchmark_config_path,
|
324
|
+
# 'created_at' => Time.now.iso8601,
|
325
|
+
# 'tags' => ['local', platform.os, platform.arch, "ruby-#{environment['ruby_build_tag']}"]
|
326
|
+
# },
|
327
|
+
# 'environment' => {
|
328
|
+
# 'name' => environment['name'],
|
329
|
+
# 'type' => environment.kind,
|
330
|
+
# 'ruby_build_tag' => environment['ruby_build_tag'],
|
331
|
+
# 'created_at' => Time.now.iso8601
|
332
|
+
# },
|
333
|
+
# 'config' => {
|
334
|
+
# 'benchmark_config' => benchmark_config_path,
|
335
|
+
# 'formats' => config['formats'],
|
336
|
+
# 'iterations' => config['iterations'],
|
337
|
+
# 'data_sizes' => config['data_sizes']
|
338
|
+
# },
|
339
|
+
# 'results' => results
|
340
|
+
# }
|
341
|
+
|
342
|
+
# File.write(results_file, full_results.to_yaml)
|
343
|
+
|
344
|
+
# say '✅ Local benchmark completed successfully!', :green
|
345
|
+
# say "Results saved to: #{result_dir}", :cyan
|
346
|
+
# say "Generate site: serialbench benchmark build-site #{result_dir}", :white
|
347
|
+
# rescue StandardError => e
|
348
|
+
# say "❌ Local benchmark failed: #{e.message}", :red
|
349
|
+
# say "Details: #{e.backtrace.first(3).join("\n")}", :white if options[:verbose]
|
350
|
+
# raise e
|
351
|
+
# end
|
352
|
+
|
353
|
+
def execute_asdf_benchmark(environment, config, benchmark_config_path)
|
354
|
+
say '🔧 Executing ASDF benchmark', :green
|
355
|
+
|
356
|
+
# Use the ASDF runner to execute the benchmark
|
357
|
+
require_relative '../asdf_runner'
|
358
|
+
|
359
|
+
# Create a config object that AsdfRunner expects
|
360
|
+
asdf_config = environment.merge({
|
361
|
+
'benchmark_config' => benchmark_config_path
|
362
|
+
})
|
363
|
+
|
364
|
+
runner = Serialbench::AsdfRunner.new(asdf_config)
|
365
|
+
|
366
|
+
say "Installing Ruby #{environment['ruby_build_tag']} via ASDF...", :white
|
367
|
+
runner.prepare
|
368
|
+
runner.benchmark
|
369
|
+
|
370
|
+
say '✅ ASDF benchmark completed successfully!', :green
|
371
|
+
say "Results saved to: results/runs/#{environment['name']}", :cyan
|
372
|
+
say "Generate site: serialbench benchmark build-site results/runs/#{environment['name']}", :white
|
373
|
+
rescue StandardError => e
|
374
|
+
say "❌ ASDF benchmark failed: #{e.message}", :red
|
375
|
+
say "Details: #{e.backtrace.first(3).join("\n")}", :white if options[:verbose]
|
376
|
+
raise e
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'yaml'
|
5
|
+
require 'fileutils'
|
6
|
+
require_relative '../models/environment_config'
|
7
|
+
require_relative '../models/benchmark_config'
|
8
|
+
|
9
|
+
module Serialbench
|
10
|
+
module Cli
|
11
|
+
# Environment management CLI
|
12
|
+
class EnvironmentCli < BaseCli
|
13
|
+
desc 'new NAME KIND RUBY_BUILD_TAG', 'Create a new environment configuration'
|
14
|
+
long_desc <<~DESC
|
15
|
+
Create a new environment configuration file.
|
16
|
+
|
17
|
+
NAME: Unique name for the environment
|
18
|
+
KIND: Environment kind (docker, asdf, local)
|
19
|
+
RUBY_BUILD_TAG: Ruby version/tag to use (from ruby-build definitions)
|
20
|
+
|
21
|
+
Examples:
|
22
|
+
serialbench environment new docker-ruby33 docker 3.3.2 --docker_image ruby:3.3-alpine --dockerfile Dockerfile --dir config/environments
|
23
|
+
serialbench environment new asdf-ruby32 asdf 3.2.4
|
24
|
+
serialbench environment new local-dev local 3.1.0
|
25
|
+
|
26
|
+
This creates a configuration file at environments/NAME.yml
|
27
|
+
DESC
|
28
|
+
option :dir, type: :string, default: 'config/environments', desc: 'Directory to create environment config in'
|
29
|
+
option :docker_image, type: :string, default: 'ruby:3.3-alpine',
|
30
|
+
desc: 'Default Docker image for docker environments'
|
31
|
+
option :dockerfile, type: :string, default: 'Dockerfile', desc: 'Default Dockerfile for docker environments'
|
32
|
+
def new(name, kind, ruby_build_tag)
|
33
|
+
validate_environment_name!(name)
|
34
|
+
|
35
|
+
config_path = File.join(options[:dir], "#{name}.yml")
|
36
|
+
|
37
|
+
if File.exist?(config_path)
|
38
|
+
say "Environment '#{name}' already exists at #{config_path}", :yellow
|
39
|
+
return unless yes?('Overwrite existing environment? (y/n)')
|
40
|
+
end
|
41
|
+
|
42
|
+
FileUtils.mkdir_p('config/environments')
|
43
|
+
|
44
|
+
kind_config = case kind
|
45
|
+
when 'docker'
|
46
|
+
raise unless options[:docker_image] && options[:dockerfile]
|
47
|
+
|
48
|
+
Models::EnvironmentConfig.new(
|
49
|
+
name: name,
|
50
|
+
type: kind,
|
51
|
+
ruby_build_tag: ruby_build_tag,
|
52
|
+
docker: Models::DockerEnvConfig.new(
|
53
|
+
image: options[:docker_image],
|
54
|
+
dockerfile: options[:dockerfile]
|
55
|
+
),
|
56
|
+
description: "Docker environment for Ruby #{ruby_build_tag} benchmarks"
|
57
|
+
)
|
58
|
+
when 'asdf'
|
59
|
+
Models::EnvironmentConfig.new(
|
60
|
+
name: name,
|
61
|
+
type: kind,
|
62
|
+
ruby_build_tag: ruby_build_tag,
|
63
|
+
asdf: Models::AsdfEnvConfig.new(auto_install: true),
|
64
|
+
description: "ASDF environment for Ruby #{ruby_build_tag} benchmarks"
|
65
|
+
)
|
66
|
+
end
|
67
|
+
File.write(config_path, kind_config.to_yaml)
|
68
|
+
|
69
|
+
say "✅ Created environment template: #{config_path}", :green
|
70
|
+
|
71
|
+
# Show ruby-build tag suggestion for local environments
|
72
|
+
show_ruby_build_suggestion if kind == 'local'
|
73
|
+
|
74
|
+
say ''
|
75
|
+
say 'Next steps:', :white
|
76
|
+
say "1. Edit #{config_path} to confirm/change configuration", :cyan
|
77
|
+
say "2. Validate: serialbench environment validate #{config_path}", :cyan
|
78
|
+
say "3. Run benchmark: serialbench benchmark execute #{config_path} config/short.yml", :cyan
|
79
|
+
end
|
80
|
+
|
81
|
+
desc 'execute ENVIRONMENT_CONFIG BENCHMARK_CONFIG RESULT_PATH', 'Execute benchmark in environment'
|
82
|
+
long_desc <<~DESC
|
83
|
+
Execute a benchmark run in the specified environment.
|
84
|
+
|
85
|
+
ENVIRONMENT_CONFIG: Path to environment configuration file
|
86
|
+
BENCHMARK_CONFIG: Path to benchmark configuration file
|
87
|
+
RESULT_PATH: Path to create result output at
|
88
|
+
|
89
|
+
Examples:
|
90
|
+
serialbench environment execute config/environments/docker-ruby33.yml config/xml-only.yml results/runs/xml-perf-test
|
91
|
+
serialbench environment execute environments/custom.yml config/full.yml results/runs/my-test
|
92
|
+
DESC
|
93
|
+
def execute(environment_path, benchmark_config_path, result_dir)
|
94
|
+
benchmark_config = load_benchmark_config(benchmark_config_path)
|
95
|
+
environment_config = load_environment_config(environment_path)
|
96
|
+
|
97
|
+
say "🚀 Running benchmark '#{benchmark_config_path}' in environment '#{environment_config.name}'...", :green
|
98
|
+
|
99
|
+
FileUtils.mkdir_p(result_dir)
|
100
|
+
say "Results will be saved to: #{result_dir}", :cyan
|
101
|
+
|
102
|
+
runner = create_environment_runner(environment_config, environment_path)
|
103
|
+
runner.run_benchmark(benchmark_config, benchmark_config_path, result_dir)
|
104
|
+
|
105
|
+
say '✅ Benchmark completed successfully!', :green
|
106
|
+
say "Results saved to: #{result_dir}", :cyan
|
107
|
+
rescue StandardError => e
|
108
|
+
say "❌ Benchmark failed: #{e.message}", :red
|
109
|
+
exit 1
|
110
|
+
end
|
111
|
+
|
112
|
+
desc 'prepare ENVIRONMENT_CONFIG', 'Prepare environment for benchmarking'
|
113
|
+
long_desc <<~DESC
|
114
|
+
Prepare the specified environment for benchmark execution.
|
115
|
+
This installs dependencies, sets up runtime environments, etc.
|
116
|
+
|
117
|
+
ENVIRONMENT_CONFIG: Path to environment configuration file
|
118
|
+
|
119
|
+
Examples:
|
120
|
+
serialbench environment prepare config/environments/docker-ruby33.yml
|
121
|
+
serialbench environment prepare environments/custom.yml
|
122
|
+
DESC
|
123
|
+
def prepare(environment_config_path)
|
124
|
+
environment_config = load_environment_config(environment_config_path)
|
125
|
+
|
126
|
+
say "🔧 Preparing environment '#{environment_config.name}'...", :green
|
127
|
+
|
128
|
+
runner = create_environment_runner(environment_config, environment_config_path)
|
129
|
+
runner.prepare
|
130
|
+
|
131
|
+
say '✅ Environment prepared successfully!', :green
|
132
|
+
rescue StandardError => e
|
133
|
+
say "❌ Environment preparation failed: #{e.message}", :red
|
134
|
+
say e.backtrace.join("\n"), :red
|
135
|
+
exit 1
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def load_benchmark_config(benchmark_config_path)
|
141
|
+
unless File.exist?(benchmark_config_path)
|
142
|
+
say "❌ Benchmark configuration file not found: #{benchmark_config_path}", :red
|
143
|
+
exit 1
|
144
|
+
end
|
145
|
+
|
146
|
+
Models::BenchmarkConfig.from_yaml(IO.read(benchmark_config_path))
|
147
|
+
rescue StandardError => e
|
148
|
+
say "❌ Failed to load benchmark config: #{e.message}", :red
|
149
|
+
exit 1
|
150
|
+
end
|
151
|
+
|
152
|
+
def load_environment_config(environment_config_path)
|
153
|
+
unless File.exist?(environment_config_path)
|
154
|
+
say "❌ Environment not found: #{environment_config_path}", :red
|
155
|
+
exit 1
|
156
|
+
end
|
157
|
+
|
158
|
+
Models::EnvironmentConfig.from_yaml(IO.read(environment_config_path))
|
159
|
+
rescue StandardError => e
|
160
|
+
say "❌ Failed to load environment config: #{e.message}", :red
|
161
|
+
exit 1
|
162
|
+
end
|
163
|
+
|
164
|
+
def create_environment_runner(environment_config, environment_config_path)
|
165
|
+
case environment_config.kind
|
166
|
+
when 'docker'
|
167
|
+
require_relative '../runners/docker_runner'
|
168
|
+
Runners::DockerRunner.new(environment_config, environment_config_path)
|
169
|
+
when 'asdf'
|
170
|
+
require_relative '../runners/asdf_runner'
|
171
|
+
Runners::AsdfRunner.new(environment_config, environment_config_path)
|
172
|
+
# when 'local'
|
173
|
+
# require_relative '../runners/local_runner'
|
174
|
+
# Runners::LocalRunner.new(environment_config, environment_config_path)
|
175
|
+
else
|
176
|
+
raise "Unknown environment type: #{environment_config.kind}"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|