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.
- checksums.yaml +4 -4
- data/.github/workflows/benchmark.yml +13 -5
- data/.github/workflows/docker.yml +35 -9
- data/.github/workflows/rake.yml +15 -0
- data/Gemfile +2 -1
- data/README.adoc +267 -1129
- 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.benchmark → Dockerfile.ubuntu} +4 -3
- data/docker/README.md +2 -2
- data/exe/serialbench +1 -1
- data/lib/serialbench/benchmark_runner.rb +261 -423
- 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 +58 -601
- 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/yajl_serializer.rb +0 -2
- 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 +0 -2
- 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 +0 -2
- data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +5 -1
- data/lib/serialbench/serializers/yaml/syck_serializer.rb +59 -22
- data/lib/serialbench/serializers.rb +23 -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 +4 -1
- metadata +86 -16
- data/config/ci.yml +0 -22
- data/config/full.yml +0 -30
- data/docker/run-benchmarks.sh +0 -356
- 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,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
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
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)
|