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.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/benchmark.yml +181 -30
  3. data/.github/workflows/ci.yml +3 -3
  4. data/.github/workflows/docker.yml +272 -0
  5. data/.github/workflows/rake.yml +15 -0
  6. data/.github/workflows/release.yml +25 -0
  7. data/Gemfile +6 -30
  8. data/README.adoc +381 -415
  9. data/Rakefile +0 -55
  10. data/config/benchmarks/full.yml +29 -0
  11. data/config/benchmarks/short.yml +26 -0
  12. data/config/environments/asdf-ruby-3.2.yml +8 -0
  13. data/config/environments/asdf-ruby-3.3.yml +8 -0
  14. data/config/environments/docker-ruby-3.0.yml +9 -0
  15. data/config/environments/docker-ruby-3.1.yml +9 -0
  16. data/config/environments/docker-ruby-3.2.yml +9 -0
  17. data/config/environments/docker-ruby-3.3.yml +9 -0
  18. data/config/environments/docker-ruby-3.4.yml +9 -0
  19. data/docker/Dockerfile.alpine +33 -0
  20. data/docker/Dockerfile.ubuntu +32 -0
  21. data/docker/README.md +214 -0
  22. data/exe/serialbench +1 -1
  23. data/lib/serialbench/benchmark_runner.rb +270 -350
  24. data/lib/serialbench/cli/base_cli.rb +51 -0
  25. data/lib/serialbench/cli/benchmark_cli.rb +380 -0
  26. data/lib/serialbench/cli/environment_cli.rb +181 -0
  27. data/lib/serialbench/cli/resultset_cli.rb +215 -0
  28. data/lib/serialbench/cli/ruby_build_cli.rb +238 -0
  29. data/lib/serialbench/cli.rb +59 -410
  30. data/lib/serialbench/config_manager.rb +140 -0
  31. data/lib/serialbench/models/benchmark_config.rb +63 -0
  32. data/lib/serialbench/models/benchmark_result.rb +45 -0
  33. data/lib/serialbench/models/environment_config.rb +71 -0
  34. data/lib/serialbench/models/platform.rb +59 -0
  35. data/lib/serialbench/models/result.rb +53 -0
  36. data/lib/serialbench/models/result_set.rb +71 -0
  37. data/lib/serialbench/models/result_store.rb +108 -0
  38. data/lib/serialbench/models.rb +54 -0
  39. data/lib/serialbench/ruby_build_manager.rb +153 -0
  40. data/lib/serialbench/runners/asdf_runner.rb +296 -0
  41. data/lib/serialbench/runners/base.rb +32 -0
  42. data/lib/serialbench/runners/docker_runner.rb +142 -0
  43. data/lib/serialbench/serializers/base_serializer.rb +8 -16
  44. data/lib/serialbench/serializers/json/base_json_serializer.rb +4 -4
  45. data/lib/serialbench/serializers/json/json_serializer.rb +0 -2
  46. data/lib/serialbench/serializers/json/oj_serializer.rb +0 -2
  47. data/lib/serialbench/serializers/json/rapidjson_serializer.rb +50 -0
  48. data/lib/serialbench/serializers/json/yajl_serializer.rb +6 -4
  49. data/lib/serialbench/serializers/toml/base_toml_serializer.rb +5 -3
  50. data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +0 -2
  51. data/lib/serialbench/serializers/toml/tomlib_serializer.rb +0 -2
  52. data/lib/serialbench/serializers/toml/tomlrb_serializer.rb +56 -0
  53. data/lib/serialbench/serializers/xml/base_xml_serializer.rb +4 -9
  54. data/lib/serialbench/serializers/xml/libxml_serializer.rb +0 -2
  55. data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +21 -5
  56. data/lib/serialbench/serializers/xml/oga_serializer.rb +0 -2
  57. data/lib/serialbench/serializers/xml/ox_serializer.rb +0 -2
  58. data/lib/serialbench/serializers/xml/rexml_serializer.rb +32 -4
  59. data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +59 -0
  60. data/lib/serialbench/serializers/yaml/psych_serializer.rb +54 -0
  61. data/lib/serialbench/serializers/yaml/syck_serializer.rb +102 -0
  62. data/lib/serialbench/serializers.rb +34 -6
  63. data/lib/serialbench/site_generator.rb +105 -0
  64. data/lib/serialbench/templates/assets/css/benchmark_report.css +535 -0
  65. data/lib/serialbench/templates/assets/css/format_based.css +526 -0
  66. data/lib/serialbench/templates/assets/css/themes.css +588 -0
  67. data/lib/serialbench/templates/assets/js/chart_helpers.js +381 -0
  68. data/lib/serialbench/templates/assets/js/dashboard.js +796 -0
  69. data/lib/serialbench/templates/assets/js/navigation.js +142 -0
  70. data/lib/serialbench/templates/base.liquid +49 -0
  71. data/lib/serialbench/templates/format_based.liquid +279 -0
  72. data/lib/serialbench/templates/partials/chart_section.liquid +4 -0
  73. data/lib/serialbench/version.rb +1 -1
  74. data/lib/serialbench.rb +2 -31
  75. data/serialbench.gemspec +28 -17
  76. metadata +192 -55
  77. data/lib/serialbench/chart_generator.rb +0 -821
  78. data/lib/serialbench/result_formatter.rb +0 -182
  79. data/lib/serialbench/result_merger.rb +0 -1201
  80. data/lib/serialbench/serializers/xml/base_parser.rb +0 -69
  81. data/lib/serialbench/serializers/xml/libxml_parser.rb +0 -98
  82. data/lib/serialbench/serializers/xml/nokogiri_parser.rb +0 -111
  83. data/lib/serialbench/serializers/xml/oga_parser.rb +0 -85
  84. data/lib/serialbench/serializers/xml/ox_parser.rb +0 -64
  85. 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