serialbench 0.1.1 → 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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/benchmark.yml +13 -5
  3. data/.github/workflows/docker.yml +35 -9
  4. data/.github/workflows/rake.yml +15 -0
  5. data/Gemfile +2 -1
  6. data/README.adoc +267 -1129
  7. data/Rakefile +0 -55
  8. data/config/benchmarks/full.yml +29 -0
  9. data/config/benchmarks/short.yml +26 -0
  10. data/config/environments/asdf-ruby-3.2.yml +8 -0
  11. data/config/environments/asdf-ruby-3.3.yml +8 -0
  12. data/config/environments/docker-ruby-3.0.yml +9 -0
  13. data/config/environments/docker-ruby-3.1.yml +9 -0
  14. data/config/environments/docker-ruby-3.2.yml +9 -0
  15. data/config/environments/docker-ruby-3.3.yml +9 -0
  16. data/config/environments/docker-ruby-3.4.yml +9 -0
  17. data/docker/Dockerfile.alpine +33 -0
  18. data/docker/{Dockerfile.benchmark → Dockerfile.ubuntu} +4 -3
  19. data/docker/README.md +2 -2
  20. data/exe/serialbench +1 -1
  21. data/lib/serialbench/benchmark_runner.rb +261 -423
  22. data/lib/serialbench/cli/base_cli.rb +51 -0
  23. data/lib/serialbench/cli/benchmark_cli.rb +380 -0
  24. data/lib/serialbench/cli/environment_cli.rb +181 -0
  25. data/lib/serialbench/cli/resultset_cli.rb +215 -0
  26. data/lib/serialbench/cli/ruby_build_cli.rb +238 -0
  27. data/lib/serialbench/cli.rb +58 -601
  28. data/lib/serialbench/config_manager.rb +140 -0
  29. data/lib/serialbench/models/benchmark_config.rb +63 -0
  30. data/lib/serialbench/models/benchmark_result.rb +45 -0
  31. data/lib/serialbench/models/environment_config.rb +71 -0
  32. data/lib/serialbench/models/platform.rb +59 -0
  33. data/lib/serialbench/models/result.rb +53 -0
  34. data/lib/serialbench/models/result_set.rb +71 -0
  35. data/lib/serialbench/models/result_store.rb +108 -0
  36. data/lib/serialbench/models.rb +54 -0
  37. data/lib/serialbench/ruby_build_manager.rb +153 -0
  38. data/lib/serialbench/runners/asdf_runner.rb +296 -0
  39. data/lib/serialbench/runners/base.rb +32 -0
  40. data/lib/serialbench/runners/docker_runner.rb +142 -0
  41. data/lib/serialbench/serializers/base_serializer.rb +8 -16
  42. data/lib/serialbench/serializers/json/base_json_serializer.rb +4 -4
  43. data/lib/serialbench/serializers/json/json_serializer.rb +0 -2
  44. data/lib/serialbench/serializers/json/oj_serializer.rb +0 -2
  45. data/lib/serialbench/serializers/json/yajl_serializer.rb +0 -2
  46. data/lib/serialbench/serializers/toml/base_toml_serializer.rb +5 -3
  47. data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +0 -2
  48. data/lib/serialbench/serializers/toml/tomlib_serializer.rb +0 -2
  49. data/lib/serialbench/serializers/toml/tomlrb_serializer.rb +56 -0
  50. data/lib/serialbench/serializers/xml/base_xml_serializer.rb +4 -9
  51. data/lib/serialbench/serializers/xml/libxml_serializer.rb +0 -2
  52. data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +0 -2
  53. data/lib/serialbench/serializers/xml/oga_serializer.rb +0 -2
  54. data/lib/serialbench/serializers/xml/ox_serializer.rb +0 -2
  55. data/lib/serialbench/serializers/xml/rexml_serializer.rb +0 -2
  56. data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +5 -1
  57. data/lib/serialbench/serializers/yaml/syck_serializer.rb +59 -22
  58. data/lib/serialbench/serializers.rb +23 -6
  59. data/lib/serialbench/site_generator.rb +105 -0
  60. data/lib/serialbench/templates/assets/css/benchmark_report.css +535 -0
  61. data/lib/serialbench/templates/assets/css/format_based.css +526 -0
  62. data/lib/serialbench/templates/assets/css/themes.css +588 -0
  63. data/lib/serialbench/templates/assets/js/chart_helpers.js +381 -0
  64. data/lib/serialbench/templates/assets/js/dashboard.js +796 -0
  65. data/lib/serialbench/templates/assets/js/navigation.js +142 -0
  66. data/lib/serialbench/templates/base.liquid +49 -0
  67. data/lib/serialbench/templates/format_based.liquid +279 -0
  68. data/lib/serialbench/templates/partials/chart_section.liquid +4 -0
  69. data/lib/serialbench/version.rb +1 -1
  70. data/lib/serialbench.rb +2 -31
  71. data/serialbench.gemspec +4 -1
  72. metadata +86 -16
  73. data/config/ci.yml +0 -22
  74. data/config/full.yml +0 -30
  75. data/docker/run-benchmarks.sh +0 -356
  76. data/lib/serialbench/chart_generator.rb +0 -821
  77. data/lib/serialbench/result_formatter.rb +0 -182
  78. data/lib/serialbench/result_merger.rb +0 -1201
  79. data/lib/serialbench/serializers/xml/base_parser.rb +0 -69
  80. data/lib/serialbench/serializers/xml/libxml_parser.rb +0 -98
  81. data/lib/serialbench/serializers/xml/nokogiri_parser.rb +0 -111
  82. data/lib/serialbench/serializers/xml/oga_parser.rb +0 -85
  83. data/lib/serialbench/serializers/xml/ox_parser.rb +0 -64
  84. data/lib/serialbench/serializers/xml/rexml_parser.rb +0 -129
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'yaml'
6
+ require 'fileutils'
7
+
8
+ module Serialbench
9
+ # Manages Ruby-Build definitions from the official ruby-build repository
10
+ class RubyBuildManager
11
+ GITHUB_API_URL = 'https://api.github.com/repos/rbenv/ruby-build/contents/share/ruby-build'
12
+ CACHE_DIR = File.expand_path('~/.serialbench')
13
+ CACHE_FILE = File.join(CACHE_DIR, 'ruby-build-definitions.yaml')
14
+
15
+ class << self
16
+ def update_definitions
17
+ puts '🔄 Fetching Ruby-Build definitions from GitHub...'
18
+
19
+ definitions = fetch_definitions_from_github
20
+ save_definitions_to_cache(definitions)
21
+
22
+ puts "✅ Updated #{definitions.length} Ruby-Build definitions"
23
+ puts "📁 Cache location: #{CACHE_FILE}"
24
+
25
+ definitions
26
+ rescue StandardError => e
27
+ raise "Failed to update Ruby-Build definitions: #{e.message}"
28
+ end
29
+
30
+ def list_definitions
31
+ load_definitions_from_cache
32
+ end
33
+
34
+ def show_definition(tag)
35
+ definitions = load_definitions_from_cache
36
+
37
+ unless definitions.include?(tag)
38
+ raise "Ruby-Build definition '#{tag}' not found. Available definitions: #{definitions.length}"
39
+ end
40
+
41
+ {
42
+ tag: tag,
43
+ available: true,
44
+ source: 'ruby-build',
45
+ cache_file: CACHE_FILE
46
+ }
47
+ end
48
+
49
+ def validate_tag(tag)
50
+ puts "🔍 Validating Ruby-Build tag: #{tag} against #{CACHE_FILE}"
51
+ return false if tag.nil? || tag.strip.empty?
52
+
53
+ definitions = load_definitions_from_cache
54
+ definitions.include?(tag)
55
+ rescue StandardError
56
+ false
57
+ end
58
+
59
+ def suggest_current_ruby_tag
60
+ ruby_version = RUBY_VERSION
61
+
62
+ # Try exact match first
63
+ return ruby_version if validate_tag(ruby_version)
64
+
65
+ # Try common variations
66
+ variations = [
67
+ ruby_version,
68
+ "#{ruby_version}.0",
69
+ ruby_version.split('.')[0..1].join('.') + '.0'
70
+ ]
71
+
72
+ variations.each do |variation|
73
+ return variation if validate_tag(variation)
74
+ end
75
+
76
+ # Return the Ruby version even if not found, user can adjust
77
+ ruby_version
78
+ end
79
+
80
+ def cache_exists?
81
+ File.exist?(CACHE_FILE)
82
+ end
83
+
84
+ def cache_age
85
+ return nil unless cache_exists?
86
+
87
+ Time.now - File.mtime(CACHE_FILE)
88
+ end
89
+
90
+ def ensure_cache_exists!
91
+ return if cache_exists?
92
+
93
+ raise <<~ERROR
94
+ Ruby-Build definitions cache not found.
95
+
96
+ Update the cache first:
97
+ serialbench ruby-build update
98
+ ERROR
99
+ end
100
+
101
+ private
102
+
103
+ def fetch_definitions_from_github
104
+ uri = URI(GITHUB_API_URL)
105
+
106
+ http = Net::HTTP.new(uri.host, uri.port)
107
+ http.use_ssl = true
108
+
109
+ request = Net::HTTP::Get.new(uri)
110
+ request['Accept'] = 'application/vnd.github.v3+json'
111
+ request['User-Agent'] = 'Serialbench Ruby-Build Manager'
112
+
113
+ response = http.request(request)
114
+
115
+ raise "GitHub API request failed: #{response.code} #{response.message}" unless response.code == '200'
116
+
117
+ data = JSON.parse(response.body)
118
+
119
+ # Extract definition names from the file list
120
+ definitions = data
121
+ .select { |item| item['type'] == 'file' }
122
+ .map { |item| item['name'] }
123
+ .sort
124
+
125
+ raise 'No Ruby-Build definitions found in GitHub response' if definitions.empty?
126
+
127
+ definitions
128
+ end
129
+
130
+ def save_definitions_to_cache(definitions)
131
+ FileUtils.mkdir_p(CACHE_DIR)
132
+
133
+ cache_data = {
134
+ 'updated_at' => Time.now.utc.iso8601,
135
+ 'source' => GITHUB_API_URL,
136
+ 'count' => definitions.length,
137
+ 'definitions' => definitions
138
+ }
139
+
140
+ File.write(CACHE_FILE, cache_data.to_yaml)
141
+ end
142
+
143
+ def load_definitions_from_cache
144
+ ensure_cache_exists!
145
+
146
+ cache_data = YAML.load_file(CACHE_FILE)
147
+ cache_data['definitions'] || []
148
+ rescue StandardError => e
149
+ raise "Failed to load Ruby-Build definitions from cache: #{e.message}"
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'yaml'
6
+ require 'open3'
7
+ require 'stringio'
8
+ require_relative '../benchmark_runner'
9
+ require_relative 'base'
10
+
11
+ module Serialbench
12
+ # Handles ASDF-based benchmark execution
13
+ module Runners
14
+ class AsdfRunner < Base
15
+ class AsdfError < StandardError; end
16
+
17
+ def initialize(environment_config, environment_config_path)
18
+ super
19
+ validate_asdf_available
20
+ end
21
+
22
+ # Prepare Ruby versions via ASDF
23
+ def prepare
24
+ puts '💎 Preparing Ruby versions via ASDF...'
25
+
26
+ ruby_version = @environment_config.ruby_build_tag
27
+ installed_versions = get_installed_ruby_versions
28
+
29
+ unless installed_versions.include?(ruby_version)
30
+ if @environment_config.asdf&.auto_install
31
+ install_missing_versions([ruby_version])
32
+ else
33
+ raise AsdfError,
34
+ "Missing Ruby version: #{ruby_version}. Set auto_install: true to install automatically."
35
+ end
36
+ end
37
+
38
+ # Install gems for the Ruby version
39
+ install_gems_for_version(ruby_version)
40
+
41
+ puts '✅ Ruby version is prepared with gems installed'
42
+ end
43
+
44
+ # Run benchmark
45
+ def benchmark
46
+ puts '🚀 Running benchmarks...'
47
+
48
+ ruby_version = @environment_config.ruby_build_tag
49
+
50
+ raise AsdfError, 'Benchmark run failed' unless run_benchmark(ruby_version)
51
+
52
+ puts '✅ Completed 1 benchmark runs'
53
+ puts "📁 Individual results are available in: results/runs/#{@environment_name}"
54
+ puts '✅ ASDF benchmark completed successfully!'
55
+ puts "Results saved to: results/runs/#{@environment_name}"
56
+ puts "Generate site: serialbench benchmark build-site results/runs/#{@environment_name}"
57
+ end
58
+
59
+ private
60
+
61
+ # Validate ASDF is available
62
+ def validate_asdf_available
63
+ unless system('asdf --version > /dev/null 2>&1')
64
+ raise AsdfError,
65
+ 'ASDF is not installed or not available in PATH'
66
+ end
67
+
68
+ # Check if ruby plugin is installed
69
+ return if system('asdf plugin list | grep -q ruby')
70
+
71
+ raise AsdfError, 'ASDF ruby plugin is not installed. Run: asdf plugin add ruby'
72
+ end
73
+
74
+ # Get list of installed Ruby versions
75
+ def get_installed_ruby_versions
76
+ output = `asdf list ruby 2>/dev/null`.strip
77
+ return [] if output.empty?
78
+
79
+ output.split("\n").map(&:strip).reject(&:empty?).map do |line|
80
+ # Remove leading asterisk and whitespace
81
+ line.gsub(/^\*?\s*/, '')
82
+ end
83
+ end
84
+
85
+ # Install missing Ruby versions
86
+ def install_missing_versions(versions)
87
+ puts "📦 Installing missing Ruby versions: #{versions.join(', ')}"
88
+
89
+ versions.each do |version|
90
+ puts "🔨 Installing Ruby #{version}..."
91
+
92
+ # Create temporary log directory
93
+ Dir.mktmpdir('asdf_install_ruby') do |temp_log_dir|
94
+ # Create a temporary file for logging
95
+ install_log = File.join(temp_log_dir, "install-ruby-#{version}.log")
96
+
97
+ success = false
98
+ Dir.chdir(temp_log_dir) do
99
+ success = system("asdf install ruby #{version} > #{install_log} 2>&1")
100
+ end
101
+
102
+ if success
103
+ puts "✅ Installed Ruby #{version}"
104
+ else
105
+ puts "❌ Failed to install Ruby #{version} (see #{install_log})"
106
+ raise AsdfError, "Failed to install Ruby #{version}"
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ # Install gems for a specific Ruby version
113
+ def install_gems_for_version(ruby_version)
114
+ puts "🔧 Installing gems for Ruby #{ruby_version}..."
115
+
116
+ # Create temporary log directory
117
+ temp_log_dir = "results/asdf-#{ruby_version}"
118
+ FileUtils.mkdir_p(temp_log_dir)
119
+ gem_install_log = File.join(temp_log_dir, "gems-ruby-#{ruby_version}.log")
120
+
121
+ # Use ASDF to install bundler and the serialbench gem
122
+ puts " 📦 Installing bundler and serialbench for Ruby #{ruby_version}..."
123
+ env = { 'ASDF_RUBY_VERSION' => ruby_version }
124
+ cmd = ['asdf', 'exec', 'gem', 'install', 'bundler', 'serialbench', '--no-document']
125
+
126
+ success = system(env, *cmd, out: gem_install_log, err: gem_install_log)
127
+ unless success
128
+ puts "❌ Failed to install gems for Ruby #{ruby_version} (see #{gem_install_log})"
129
+ raise AsdfError, "Failed to install gems for Ruby #{ruby_version}"
130
+ end
131
+
132
+ # ASDF doesn't need reshash like rbenv - gems are immediately available
133
+ puts "✅ Gems installed for Ruby #{ruby_version}"
134
+ end
135
+
136
+ # Run benchmark for specific Ruby version
137
+ def run_benchmark(benchmark_config, benchmark_config_path, result_dir)
138
+ puts "🚀 Running benchmark for #{@name}..."
139
+
140
+ FileUtils.mkdir_p(result_dir)
141
+
142
+ prepare
143
+
144
+ ruby_version = @environment_config.ruby_build_tag
145
+ puts "🏃 Running benchmarks for Ruby #{ruby_version}..."
146
+ puts " 📁 Results will be saved to: #{result_dir}"
147
+
148
+ benchmark_log = File.join(result_dir, 'benchmark.log')
149
+
150
+ # Run benchmark directly using BenchmarkRunner instead of CLI
151
+ puts ' 🚀 Starting benchmark execution...'
152
+
153
+ # Set ASDF Ruby version for this process
154
+ ENV['ASDF_RUBY_VERSION'] = ruby_version
155
+
156
+ # Capture stdout/stderr for logging
157
+ log_output = StringIO.new
158
+
159
+ runner = Serialbench::BenchmarkRunner.new(
160
+ benchmark_config: benchmark_config,
161
+ environment_config: @environment_config
162
+ )
163
+
164
+ # Run benchmarks and capture output
165
+ puts " Running benchmarks with #{benchmark_config.iterations} iterations..."
166
+
167
+ # Redirect stdout to capture benchmark output
168
+ original_stdout = $stdout
169
+ $stdout = log_output
170
+
171
+ results = runner.run_all_benchmarks
172
+
173
+ # Restore stdout
174
+ $stdout = original_stdout
175
+
176
+ # Save benchmark results to results.yaml
177
+ results_file = File.join(result_dir, 'results.yaml')
178
+
179
+ # Create platform string
180
+ require_relative 'models/platform'
181
+ platform = Serialbench::Models::Platform.current_local
182
+ platform_string = "asdf-#{platform.os}-#{platform.arch}-ruby-#{ruby_version}"
183
+
184
+ # Create comprehensive results structure with platform and metadata merged in
185
+ full_results = {
186
+ 'platform' => {
187
+ 'platform_string' => platform_string,
188
+ 'type' => 'asdf',
189
+ 'os' => platform.os,
190
+ 'arch' => platform.arch
191
+ },
192
+ 'metadata' => {
193
+ 'environment_name' => @environment_name,
194
+ 'benchmark_config' => @benchmark_config,
195
+ 'created_at' => Time.now.iso8601,
196
+ 'tags' => ['asdf', platform.os, platform.arch, "ruby-#{ruby_version}"]
197
+ },
198
+ 'environment' => {
199
+ 'name' => @environment_name,
200
+ 'type' => 'asdf',
201
+ 'ruby_build_tag' => ruby_version,
202
+ 'created_at' => Time.now.iso8601
203
+ },
204
+ 'config' => {
205
+ 'benchmark_config' => @benchmark_config,
206
+ 'formats' => config['formats'],
207
+ 'iterations' => config['iterations'],
208
+ 'data_sizes' => config['data_sizes']
209
+ },
210
+ 'results' => results
211
+ }
212
+
213
+ File.write(results_file, full_results.to_yaml)
214
+
215
+ # Save execution log
216
+ File.write(benchmark_log, log_output.string)
217
+
218
+ puts "✅ Completed Ruby #{ruby_version}"
219
+ puts " Results saved to: #{results_file}"
220
+ puts " Log saved to: #{benchmark_log}"
221
+ true
222
+ rescue StandardError => e
223
+ puts "❌ Failed Ruby #{ruby_version}: #{e.message}"
224
+ File.write(benchmark_log, "Error: #{e.message}\n#{e.backtrace.join("\n")}")
225
+ false
226
+ ensure
227
+ # Clean up environment variable and restore stdout
228
+ ENV.delete('ASDF_RUBY_VERSION')
229
+ $stdout = original_stdout if defined?(original_stdout)
230
+ end
231
+
232
+ # Process and merge results
233
+ def process_results(successful_runs)
234
+ puts "📊 Processing #{successful_runs} successful results..."
235
+
236
+ # Find all result directories with valid results
237
+ # Look for directories that contain results/runs subdirectories
238
+ result_dirs = Dir.glob(File.join(@output_dir, 'asdf-*')).select do |dir|
239
+ results_runs_dir = File.join(dir, 'results', 'runs')
240
+ Dir.exist?(results_runs_dir) && !Dir.glob(File.join(results_runs_dir, '*')).empty?
241
+ end
242
+
243
+ if result_dirs.empty?
244
+ puts '⚠️ No results found for processing, but benchmarks completed successfully!'
245
+ puts "📁 Individual results are available in: #{@output_dir}"
246
+ return
247
+ end
248
+
249
+ puts '🎉 Results processed successfully!'
250
+ puts "📁 Results directory: #{@output_dir}"
251
+ puts '📊 Individual benchmark results available in:'
252
+ result_dirs.each do |dir|
253
+ puts " - #{dir}"
254
+ end
255
+ puts ''
256
+ puts '💡 To create a comparison report, use:'
257
+ puts ' serialbench resultset new multi-ruby-comparison'
258
+ result_dirs.each do |dir|
259
+ result_name = File.basename(dir)
260
+ puts " serialbench resultset add-result multi-ruby-comparison #{result_name}"
261
+ end
262
+ puts ' serialbench resultset build-site multi-ruby-comparison'
263
+ end
264
+
265
+ # Generate combined platform string for directory naming
266
+ # Format: asdf-{os}-{arch}-ruby-{version}
267
+ def generate_platform_string(runner_type, ruby_version)
268
+ # Get OS name
269
+ os = case RUBY_PLATFORM
270
+ when /darwin/
271
+ 'macos'
272
+ when /linux/
273
+ 'linux'
274
+ when /mswin|mingw|cygwin/
275
+ 'windows'
276
+ else
277
+ 'unknown'
278
+ end
279
+
280
+ # Get architecture (simplified)
281
+ arch = case RUBY_PLATFORM
282
+ when /x86_64|amd64/
283
+ 'x64'
284
+ when /arm64|aarch64/
285
+ 'arm64'
286
+ when /i386|i686/
287
+ 'x86'
288
+ else
289
+ 'unknown'
290
+ end
291
+
292
+ "#{runner_type}-#{os}-#{arch}-ruby-#{ruby_version}"
293
+ end
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'yaml'
6
+ require 'open3'
7
+ require 'stringio'
8
+
9
+ module Serialbench
10
+ # Handles ASDF-based benchmark execution
11
+ module Runners
12
+ class Base
13
+ def initialize(environment_config, environment_config_path)
14
+ @environment_config = environment_config
15
+ @environment_config_path = environment_config_path
16
+
17
+ raise 'environment_config is required' unless @environment_config
18
+ raise 'environment_config_path is required' unless @environment_config_path
19
+ raise 'environment_config_path must be a valid file' unless File.exist?(@environment_config_path)
20
+ end
21
+
22
+ def prepare
23
+ raise NotImplementedError, 'Subclasses must implement the prepare method'
24
+ end
25
+
26
+ # Run benchmark
27
+ def benchmark
28
+ raise NotImplementedError, 'Subclasses must implement the benchmark method'
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require_relative '../ruby_build_manager'
6
+ require_relative 'base'
7
+
8
+ module Serialbench
9
+ module Runners
10
+ # Handles Docker-based benchmark execution
11
+ class DockerRunner < Base
12
+ class DockerError < StandardError; end
13
+
14
+ def initialize(environment_config, environment_config_path)
15
+ super
16
+
17
+ @docker_image = @environment_config.docker&.image
18
+ raise 'docker.image is required' unless @docker_image
19
+
20
+ # Handle dockerfile path relative to environment file
21
+ dockerfile_path = @environment_config.docker&.dockerfile
22
+ raise 'docker.dockerfile path is required' unless dockerfile_path
23
+
24
+ @dockerfile = Pathname.new(environment_config_path).dirname.join(dockerfile_path).to_s
25
+ raise "path '#{@dockerfile}' specified in docker.dockerfile is not found" unless File.exist?(@dockerfile)
26
+
27
+ validate_docker_available
28
+ end
29
+
30
+ # Build Docker image for this environment
31
+ def prepare
32
+ puts "🐳 Preparing Docker image for #{@name}..."
33
+
34
+ puts " 🐍 Using Docker image: #{base_image_string}"
35
+ puts " 🔨 Building Docker image: #{docker_image_string}"
36
+
37
+ cmd = [
38
+ 'docker', 'build',
39
+ '--build-arg', "BASE_IMAGE=#{base_image_string}",
40
+ '-t', docker_image_string,
41
+ '-f', @dockerfile,
42
+ '.'
43
+ ]
44
+
45
+ # Show command being run
46
+ puts " 🔧 Running command: #{cmd.join(' ')}"
47
+ success = system(*cmd)
48
+
49
+ raise DockerError, "Failed to build Docker image '#{docker_image_string}'" unless success
50
+
51
+ puts "✅ Docker image prepared successfully for #{docker_image_string}"
52
+ end
53
+
54
+ def base_image_string
55
+ @environment_config.docker.image
56
+ end
57
+
58
+ def docker_image_string
59
+ "serialbench:#{@environment_config.ruby_build_tag}"
60
+ end
61
+
62
+ # Run benchmark for this environment
63
+ def run_benchmark(benchmark_config, benchmark_config_path, result_dir)
64
+ puts "🚀 Running benchmark at #{@environment_config_path}..."
65
+
66
+ FileUtils.mkdir_p(result_dir)
67
+
68
+ # Build image if not already built
69
+ prepare unless image_exists?
70
+
71
+ # Run the benchmark
72
+ run_benchmark_in_container(benchmark_config, benchmark_config_path, result_dir)
73
+
74
+ puts "✅ Benchmark completed for #{@environment_config_path}"
75
+ end
76
+
77
+ private
78
+
79
+ # Validate Docker is available
80
+ def validate_docker_available
81
+ unless system('docker --version > /dev/null 2>&1')
82
+ raise DockerError, 'Docker is not installed or not available in PATH'
83
+ end
84
+
85
+ return if system('docker info > /dev/null 2>&1')
86
+
87
+ raise DockerError, 'Docker daemon is not running'
88
+ end
89
+
90
+ # Check if Docker image exists
91
+ def image_exists?
92
+ image_name = "serialbench:#{@name}"
93
+ system("docker image inspect #{image_name} > /dev/null 2>&1")
94
+ end
95
+
96
+ # Run benchmark in container
97
+ def run_benchmark_in_container(benchmark_config, benchmark_config_path, result_dir)
98
+ puts '🏃 Running benchmark in Docker container...'
99
+ puts " 📁 Results will be saved to: #{result_dir}"
100
+ puts " 🐳 Using Docker image: #{docker_image_string}"
101
+
102
+ benchmark_log = File.join(result_dir, 'benchmark.log')
103
+
104
+ cmd = [
105
+ 'docker', 'run', '--rm',
106
+ '-v', "#{File.expand_path(result_dir)}:/app/results",
107
+ '-v', "#{File.expand_path(@environment_config_path)}:/app/environment.yml",
108
+ '-v', "#{File.expand_path(benchmark_config_path)}:/app/benchmark_config.yml",
109
+ # TODO: do not hard code this path in the docker image
110
+ '-v', "#{RubyBuildManager::CACHE_FILE}:/root/.serialbench/ruby-build-definitions.yaml",
111
+ docker_image_string,
112
+ 'benchmark',
113
+ '_docker_execute',
114
+ '--result_dir=/app/results',
115
+ '/app/environment.yml',
116
+ '/app/benchmark_config.yml'
117
+ ]
118
+
119
+ puts " 🔧 Running command: #{cmd.join(' ')}"
120
+ puts " 📝 Benchmark output will be logged to: #{benchmark_log}"
121
+ puts ' ⏳ This may take several minutes...'
122
+
123
+ success = system("#{cmd.join(' ')} > #{benchmark_log} 2>&1")
124
+ puts " ✅ Command executed with status: #{success ? 'success' : 'failure'}"
125
+ puts " 📊 Checking if results saved to: #{File.join(result_dir, 'results.yaml')}"
126
+
127
+ if success && File.exist?(File.join(result_dir, 'results.yaml'))
128
+ puts '✅ Benchmark completed successfully'
129
+ puts " 📊 Results saved to: #{File.join(result_dir, 'results.yaml')}"
130
+ else
131
+ puts "❌ Benchmark failed (see #{benchmark_log})"
132
+ puts " 🔍 Check log file for details: #{benchmark_log}"
133
+ if File.exist?(benchmark_log)
134
+ puts ' 📄 Last few lines of log:'
135
+ system("tail -10 #{benchmark_log} | sed 's/^/ /'")
136
+ end
137
+ raise DockerError, 'Benchmark execution failed'
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -1,26 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'singleton'
4
+
3
5
  module Serialbench
4
6
  module Serializers
5
7
  class BaseSerializer
8
+ include Singleton
9
+
6
10
  def initialize
7
11
  # Override in subclasses
8
12
  end
9
13
 
10
- def available?
11
- raise NotImplementedError, 'Subclasses must implement #available?'
12
- end
13
-
14
- def name
15
- raise NotImplementedError, 'Subclasses must implement #name'
16
- end
17
-
18
- def version
19
- raise NotImplementedError, 'Subclasses must implement #version'
20
- end
21
-
22
- def format
23
- raise NotImplementedError, 'Subclasses must implement #format'
14
+ %w[name version format library_require_name available?].each do |method_name|
15
+ define_method(method_name) do
16
+ self.class.send(method_name)
17
+ end
24
18
  end
25
19
 
26
20
  def parse(data)
@@ -44,8 +38,6 @@ module Serialbench
44
38
  false
45
39
  end
46
40
 
47
- protected
48
-
49
41
  def require_library(library_name)
50
42
  require library_name
51
43
  true
@@ -6,7 +6,7 @@ module Serialbench
6
6
  module Serializers
7
7
  module Json
8
8
  class BaseJsonSerializer < BaseSerializer
9
- def format
9
+ def self.format
10
10
  :json
11
11
  end
12
12
 
@@ -29,7 +29,9 @@ module Serialbench
29
29
  }
30
30
  end
31
31
 
32
- protected
32
+ def supports_generation?
33
+ true
34
+ end
33
35
 
34
36
  def supports_pretty_print?
35
37
  true
@@ -48,8 +50,6 @@ module Serialbench
48
50
  raise NotImplementedError, 'Subclasses must implement #library_require_name'
49
51
  end
50
52
 
51
- public
52
-
53
53
  # Check if the JSON library is available
54
54
  def available?
55
55
  return @available if defined?(@available)