serialbench 0.1.1 → 0.1.3

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 (100) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/benchmark.yml +273 -220
  3. data/.github/workflows/rake.yml +26 -0
  4. data/.github/workflows/windows-debug.yml +171 -0
  5. data/.gitignore +32 -0
  6. data/.rubocop.yml +1 -0
  7. data/.rubocop_todo.yml +274 -0
  8. data/Gemfile +14 -1
  9. data/README.adoc +292 -1118
  10. data/Rakefile +0 -55
  11. data/config/benchmarks/full.yml +29 -0
  12. data/config/benchmarks/short.yml +26 -0
  13. data/config/environments/asdf-ruby-3.2.yml +8 -0
  14. data/config/environments/asdf-ruby-3.3.yml +8 -0
  15. data/config/environments/docker-ruby-3.0.yml +9 -0
  16. data/config/environments/docker-ruby-3.1.yml +9 -0
  17. data/config/environments/docker-ruby-3.2.yml +9 -0
  18. data/config/environments/docker-ruby-3.3.yml +9 -0
  19. data/config/environments/docker-ruby-3.4.yml +9 -0
  20. data/data/schemas/result.yml +29 -0
  21. data/docker/Dockerfile.alpine +33 -0
  22. data/docker/{Dockerfile.benchmark → Dockerfile.ubuntu} +4 -3
  23. data/docker/README.md +2 -2
  24. data/docs/PLATFORM_VALIDATION_FIX.md +79 -0
  25. data/docs/SYCK_YAML_FIX.md +91 -0
  26. data/docs/WEBSITE_COMPLETION_PLAN.md +440 -0
  27. data/docs/WINDOWS_LIBXML_FIX.md +136 -0
  28. data/docs/WINDOWS_SETUP.md +122 -0
  29. data/exe/serialbench +1 -1
  30. data/lib/serialbench/benchmark_runner.rb +261 -423
  31. data/lib/serialbench/cli/base_cli.rb +51 -0
  32. data/lib/serialbench/cli/benchmark_cli.rb +453 -0
  33. data/lib/serialbench/cli/environment_cli.rb +181 -0
  34. data/lib/serialbench/cli/resultset_cli.rb +261 -0
  35. data/lib/serialbench/cli/ruby_build_cli.rb +225 -0
  36. data/lib/serialbench/cli/validate_cli.rb +88 -0
  37. data/lib/serialbench/cli.rb +61 -600
  38. data/lib/serialbench/config_manager.rb +129 -0
  39. data/lib/serialbench/models/benchmark_config.rb +75 -0
  40. data/lib/serialbench/models/benchmark_result.rb +81 -0
  41. data/lib/serialbench/models/environment_config.rb +72 -0
  42. data/lib/serialbench/models/platform.rb +111 -0
  43. data/lib/serialbench/models/result.rb +80 -0
  44. data/lib/serialbench/models/result_set.rb +79 -0
  45. data/lib/serialbench/models/result_store.rb +108 -0
  46. data/lib/serialbench/models.rb +54 -0
  47. data/lib/serialbench/ruby_build_manager.rb +149 -0
  48. data/lib/serialbench/runners/asdf_runner.rb +296 -0
  49. data/lib/serialbench/runners/base.rb +32 -0
  50. data/lib/serialbench/runners/docker_runner.rb +140 -0
  51. data/lib/serialbench/runners/local_runner.rb +71 -0
  52. data/lib/serialbench/serializers/base_serializer.rb +9 -17
  53. data/lib/serialbench/serializers/json/base_json_serializer.rb +4 -4
  54. data/lib/serialbench/serializers/json/json_serializer.rb +0 -2
  55. data/lib/serialbench/serializers/json/oj_serializer.rb +0 -2
  56. data/lib/serialbench/serializers/json/rapidjson_serializer.rb +1 -1
  57. data/lib/serialbench/serializers/json/yajl_serializer.rb +0 -2
  58. data/lib/serialbench/serializers/toml/base_toml_serializer.rb +5 -5
  59. data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +1 -3
  60. data/lib/serialbench/serializers/toml/tomlib_serializer.rb +1 -3
  61. data/lib/serialbench/serializers/toml/tomlrb_serializer.rb +56 -0
  62. data/lib/serialbench/serializers/xml/base_xml_serializer.rb +4 -9
  63. data/lib/serialbench/serializers/xml/libxml_serializer.rb +4 -10
  64. data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +2 -4
  65. data/lib/serialbench/serializers/xml/oga_serializer.rb +4 -10
  66. data/lib/serialbench/serializers/xml/ox_serializer.rb +2 -4
  67. data/lib/serialbench/serializers/xml/rexml_serializer.rb +3 -5
  68. data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +5 -1
  69. data/lib/serialbench/serializers/yaml/psych_serializer.rb +1 -1
  70. data/lib/serialbench/serializers/yaml/syck_serializer.rb +60 -23
  71. data/lib/serialbench/serializers.rb +23 -6
  72. data/lib/serialbench/site_generator.rb +283 -0
  73. data/lib/serialbench/templates/assets/css/benchmark_report.css +535 -0
  74. data/lib/serialbench/templates/assets/css/format_based.css +474 -0
  75. data/lib/serialbench/templates/assets/css/themes.css +589 -0
  76. data/lib/serialbench/templates/assets/js/chart_helpers.js +411 -0
  77. data/lib/serialbench/templates/assets/js/dashboard.js +795 -0
  78. data/lib/serialbench/templates/assets/js/navigation.js +142 -0
  79. data/lib/serialbench/templates/base.liquid +49 -0
  80. data/lib/serialbench/templates/format_based.liquid +507 -0
  81. data/lib/serialbench/templates/partials/chart_section.liquid +4 -0
  82. data/lib/serialbench/version.rb +1 -1
  83. data/lib/serialbench/yaml_validator.rb +36 -0
  84. data/lib/serialbench.rb +2 -31
  85. data/serialbench.gemspec +15 -3
  86. metadata +106 -25
  87. data/.github/workflows/ci.yml +0 -74
  88. data/.github/workflows/docker.yml +0 -246
  89. data/config/ci.yml +0 -22
  90. data/config/full.yml +0 -30
  91. data/docker/run-benchmarks.sh +0 -356
  92. data/lib/serialbench/chart_generator.rb +0 -821
  93. data/lib/serialbench/result_formatter.rb +0 -182
  94. data/lib/serialbench/result_merger.rb +0 -1201
  95. data/lib/serialbench/serializers/xml/base_parser.rb +0 -69
  96. data/lib/serialbench/serializers/xml/libxml_parser.rb +0 -98
  97. data/lib/serialbench/serializers/xml/nokogiri_parser.rb +0 -111
  98. data/lib/serialbench/serializers/xml/oga_parser.rb +0 -85
  99. data/lib/serialbench/serializers/xml/ox_parser.rb +0 -64
  100. 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,453 @@
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 execute_local_benchmark(environment, config, benchmark_config_path)
246
+ say '🏠 Executing local benchmark', :green
247
+
248
+ runner = Serialbench::BenchmarkRunner.new(
249
+ environment_config: environment,
250
+ benchmark_config: config
251
+ )
252
+
253
+ # Run benchmarks
254
+ results = runner.run_all_benchmarks
255
+
256
+ platform = Serialbench::Models::Platform.current_local
257
+
258
+ metadata = Models::RunMetadata.new(
259
+ benchmark_config_path: benchmark_config_path,
260
+ environment_config_path: "config/environments/#{environment.name}.yml",
261
+ tags: [
262
+ 'local',
263
+ platform.os,
264
+ platform.arch,
265
+ "ruby-#{environment.ruby_build_tag}"
266
+ ]
267
+ )
268
+
269
+ # Create results directory
270
+ result_dir = "results/runs/#{environment.name}-results"
271
+ FileUtils.mkdir_p(result_dir)
272
+
273
+ # Save results to single YAML file with platform and metadata merged in
274
+ results_model = Models::Result.new(
275
+ platform: platform,
276
+ metadata: metadata,
277
+ environment_config: environment,
278
+ benchmark_config: config,
279
+ benchmark_result: results
280
+ )
281
+
282
+ results_file = File.join(result_dir, 'results.yaml')
283
+ results_model.to_file(results_file)
284
+
285
+ say '✅ Local benchmark completed successfully!', :green
286
+ say "Results saved to: #{result_dir}", :cyan
287
+ say "Generate site: serialbench benchmark build-site #{result_dir}", :white
288
+ rescue StandardError => e
289
+ say "❌ Local benchmark failed: #{e.message}", :red
290
+ say "Details: #{e.backtrace.first(3).join("\n")}", :white if options[:verbose]
291
+ raise e
292
+ end
293
+
294
+ def execute_docker_benchmark(environment, config, benchmark_config_path)
295
+ say '🐳 Executing Docker benchmark', :green
296
+
297
+ require_relative '../runners/docker_runner'
298
+
299
+ environment_config_path = "config/environments/#{environment.name}.yml"
300
+ runner = Runners::DockerRunner.new(environment, environment_config_path)
301
+
302
+ # Create results directory
303
+ result_dir = "results/runs/#{environment.name}-results"
304
+ FileUtils.mkdir_p(result_dir)
305
+
306
+ # Run benchmark
307
+ runner.run_benchmark(config, benchmark_config_path, result_dir)
308
+
309
+ say '✅ Docker benchmark completed successfully!', :green
310
+ say "Results saved to: #{result_dir}", :cyan
311
+ say "Generate site: serialbench benchmark build-site #{result_dir}", :white
312
+ rescue StandardError => e
313
+ say "❌ Docker benchmark failed: #{e.message}", :red
314
+ say "Details: #{e.backtrace.first(3).join("\n")}", :white if options[:verbose]
315
+ raise e
316
+ end
317
+
318
+ def show_execute_usage_and_exit
319
+ say '❌ Error: Environment and config file arguments are required.', :red
320
+ say ''
321
+ say 'Usage:', :white
322
+ say ' serialbench benchmark execute ENVIRONMENT_CONFIG_PATH BENCHMARK_CONFIG_PATH', :cyan
323
+ say ''
324
+ say 'Arguments:', :white
325
+ say ' ENVIRONMENT_CONFIG_PATH Path to environment configuration file', :white
326
+ say ' BENCHMARK_CONFIG_PATH Path to benchmark configuration file', :white
327
+ say ''
328
+ say 'Examples:', :white
329
+ say ' serialbench benchmark execute config/environments/local-dev.yml config/short.yml', :cyan
330
+ say ' serialbench benchmark execute config/environments/docker-alpine.yml config/full.yml', :cyan
331
+ exit 1
332
+ end
333
+
334
+ def load_environment_config(environment_config_path)
335
+ unless File.exist?(environment_config_path)
336
+ say "❌ Environment not found: #{environment_config_path}", :red
337
+ exit 1
338
+ end
339
+
340
+ Models::EnvironmentConfig.from_file(environment_config_path)
341
+ rescue StandardError => e
342
+ say "❌ Failed to load environment: #{e.message}", :red
343
+ say "Environment file: #{environment_config_path}", :white
344
+ exit 1
345
+ end
346
+
347
+ def load_benchmark_config(benchmark_config_path)
348
+ unless File.exist?(benchmark_config_path)
349
+ say "❌ Benchmark config not found: #{benchmark_config_path}", :red
350
+ exit 1
351
+ end
352
+
353
+ Models::BenchmarkConfig.from_file(benchmark_config_path)
354
+ rescue StandardError => e
355
+ say "❌ Failed to load benchmark config: #{e.message}", :red
356
+ say "Benchmark config file: #{benchmark_config_path}", :white
357
+ exit 1
358
+ end
359
+
360
+ # def execute_local_benchmark(environment, config, benchmark_config_path)
361
+ # say '🏠 Executing local benchmark', :green
362
+
363
+ # # Create benchmark runner with config
364
+ # runner_options = {
365
+ # formats: (config['formats'] || %w[xml json yaml toml]).map(&:to_sym),
366
+ # iterations: config['iterations'] || 10,
367
+ # warmup: config['warmup'] || 3,
368
+ # config: config
369
+ # }
370
+
371
+ # runner = Serialbench::BenchmarkRunner.new(**runner_options)
372
+
373
+ # # Run benchmarks
374
+ # say "Running benchmarks with #{runner_options[:iterations]} iterations...", :white
375
+ # results = runner.run_all_benchmarks
376
+
377
+ # # Create platform-specific directory name using environment's ruby_build_tag
378
+ # require_relative '../models/platform'
379
+ # platform = Serialbench::Models::Platform.current_local
380
+ # platform_string = "local-#{platform.os}-#{platform.arch}-ruby-#{environment['ruby_build_tag']}"
381
+
382
+ # # Create results directory
383
+ # result_dir = "results/runs/#{environment['name']}"
384
+ # FileUtils.mkdir_p(result_dir)
385
+
386
+ # # Save results to single YAML file with platform and metadata merged in
387
+ # results_file = File.join(result_dir, 'results.yaml')
388
+ # full_results = {
389
+ # 'platform' => {
390
+ # 'platform_string' => platform_string,
391
+ # 'os' => platform.os,
392
+ # 'arch' => platform.arch
393
+ # },
394
+ # 'metadata' => {
395
+ # 'environment_name' => environment['name'],
396
+ # 'benchmark_config' => benchmark_config_path,
397
+ # 'created_at' => Time.now.iso8601,
398
+ # 'tags' => ['local', platform.os, platform.arch, "ruby-#{environment['ruby_build_tag']}"]
399
+ # },
400
+ # 'environment' => {
401
+ # 'name' => environment['name'],
402
+ # 'type' => environment.kind,
403
+ # 'ruby_build_tag' => environment['ruby_build_tag'],
404
+ # 'created_at' => Time.now.iso8601
405
+ # },
406
+ # 'config' => {
407
+ # 'benchmark_config' => benchmark_config_path,
408
+ # 'formats' => config['formats'],
409
+ # 'iterations' => config['iterations'],
410
+ # 'data_sizes' => config['data_sizes']
411
+ # },
412
+ # 'results' => results
413
+ # }
414
+
415
+ # File.write(results_file, full_results.to_yaml)
416
+
417
+ # say '✅ Local benchmark completed successfully!', :green
418
+ # say "Results saved to: #{result_dir}", :cyan
419
+ # say "Generate site: serialbench benchmark build-site #{result_dir}", :white
420
+ # rescue StandardError => e
421
+ # say "❌ Local benchmark failed: #{e.message}", :red
422
+ # say "Details: #{e.backtrace.first(3).join("\n")}", :white if options[:verbose]
423
+ # raise e
424
+ # end
425
+
426
+ def execute_asdf_benchmark(environment, _config, benchmark_config_path)
427
+ say '🔧 Executing ASDF benchmark', :green
428
+
429
+ # Use the ASDF runner to execute the benchmark
430
+ require_relative '../asdf_runner'
431
+
432
+ # Create a config object that AsdfRunner expects
433
+ asdf_config = environment.merge({
434
+ 'benchmark_config' => benchmark_config_path
435
+ })
436
+
437
+ runner = Serialbench::AsdfRunner.new(asdf_config)
438
+
439
+ say "Installing Ruby #{environment['ruby_build_tag']} via ASDF...", :white
440
+ runner.prepare
441
+ runner.benchmark
442
+
443
+ say '✅ ASDF benchmark completed successfully!', :green
444
+ say "Results saved to: results/runs/#{environment['name']}", :cyan
445
+ say "Generate site: serialbench benchmark build-site results/runs/#{environment['name']}", :white
446
+ rescue StandardError => e
447
+ say "❌ ASDF benchmark failed: #{e.message}", :red
448
+ say "Details: #{e.backtrace.first(3).join("\n")}", :white if options[:verbose]
449
+ raise e
450
+ end
451
+ end
452
+ end
453
+ 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