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.
- checksums.yaml +4 -4
- data/.github/workflows/benchmark.yml +273 -220
- data/.github/workflows/rake.yml +26 -0
- data/.github/workflows/windows-debug.yml +171 -0
- data/.gitignore +32 -0
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +274 -0
- data/Gemfile +14 -1
- data/README.adoc +292 -1118
- 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/data/schemas/result.yml +29 -0
- data/docker/Dockerfile.alpine +33 -0
- data/docker/{Dockerfile.benchmark → Dockerfile.ubuntu} +4 -3
- data/docker/README.md +2 -2
- data/docs/PLATFORM_VALIDATION_FIX.md +79 -0
- data/docs/SYCK_YAML_FIX.md +91 -0
- data/docs/WEBSITE_COMPLETION_PLAN.md +440 -0
- data/docs/WINDOWS_LIBXML_FIX.md +136 -0
- data/docs/WINDOWS_SETUP.md +122 -0
- 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 +453 -0
- data/lib/serialbench/cli/environment_cli.rb +181 -0
- data/lib/serialbench/cli/resultset_cli.rb +261 -0
- data/lib/serialbench/cli/ruby_build_cli.rb +225 -0
- data/lib/serialbench/cli/validate_cli.rb +88 -0
- data/lib/serialbench/cli.rb +61 -600
- data/lib/serialbench/config_manager.rb +129 -0
- data/lib/serialbench/models/benchmark_config.rb +75 -0
- data/lib/serialbench/models/benchmark_result.rb +81 -0
- data/lib/serialbench/models/environment_config.rb +72 -0
- data/lib/serialbench/models/platform.rb +111 -0
- data/lib/serialbench/models/result.rb +80 -0
- data/lib/serialbench/models/result_set.rb +79 -0
- data/lib/serialbench/models/result_store.rb +108 -0
- data/lib/serialbench/models.rb +54 -0
- data/lib/serialbench/ruby_build_manager.rb +149 -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 +140 -0
- data/lib/serialbench/runners/local_runner.rb +71 -0
- data/lib/serialbench/serializers/base_serializer.rb +9 -17
- data/lib/serialbench/serializers/json/base_json_serializer.rb +4 -4
- data/lib/serialbench/serializers/json/json_serializer.rb +0 -2
- data/lib/serialbench/serializers/json/oj_serializer.rb +0 -2
- data/lib/serialbench/serializers/json/rapidjson_serializer.rb +1 -1
- data/lib/serialbench/serializers/json/yajl_serializer.rb +0 -2
- data/lib/serialbench/serializers/toml/base_toml_serializer.rb +5 -5
- data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +1 -3
- data/lib/serialbench/serializers/toml/tomlib_serializer.rb +1 -3
- 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 +4 -10
- data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +2 -4
- data/lib/serialbench/serializers/xml/oga_serializer.rb +4 -10
- data/lib/serialbench/serializers/xml/ox_serializer.rb +2 -4
- data/lib/serialbench/serializers/xml/rexml_serializer.rb +3 -5
- data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +5 -1
- data/lib/serialbench/serializers/yaml/psych_serializer.rb +1 -1
- data/lib/serialbench/serializers/yaml/syck_serializer.rb +60 -23
- data/lib/serialbench/serializers.rb +23 -6
- data/lib/serialbench/site_generator.rb +283 -0
- data/lib/serialbench/templates/assets/css/benchmark_report.css +535 -0
- data/lib/serialbench/templates/assets/css/format_based.css +474 -0
- data/lib/serialbench/templates/assets/css/themes.css +589 -0
- data/lib/serialbench/templates/assets/js/chart_helpers.js +411 -0
- data/lib/serialbench/templates/assets/js/dashboard.js +795 -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 +507 -0
- data/lib/serialbench/templates/partials/chart_section.liquid +4 -0
- data/lib/serialbench/version.rb +1 -1
- data/lib/serialbench/yaml_validator.rb +36 -0
- data/lib/serialbench.rb +2 -31
- data/serialbench.gemspec +15 -3
- metadata +106 -25
- data/.github/workflows/ci.yml +0 -74
- data/.github/workflows/docker.yml +0 -246
- 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,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require_relative 'result'
|
|
5
|
+
|
|
6
|
+
module Serialbench
|
|
7
|
+
module Models
|
|
8
|
+
class ResultStore
|
|
9
|
+
DEFAULT_BASE_PATH = 'results'
|
|
10
|
+
RUNS_PATH = 'runs'
|
|
11
|
+
SETS_PATH = 'sets'
|
|
12
|
+
|
|
13
|
+
attr_reader :base_path
|
|
14
|
+
|
|
15
|
+
def initialize(base_path = DEFAULT_BASE_PATH)
|
|
16
|
+
@base_path = base_path
|
|
17
|
+
ensure_results_directory
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.default
|
|
21
|
+
@default ||= new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Run management
|
|
25
|
+
def runs_path
|
|
26
|
+
File.join(@base_path, RUNS_PATH)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def find_runs(tags: nil, limit: nil)
|
|
30
|
+
runs = Result.find_all(runs_path)
|
|
31
|
+
|
|
32
|
+
runs = runs.select { |run| (Array(tags) - run.tags).empty? } if tags
|
|
33
|
+
|
|
34
|
+
limit ? runs.first(limit) : runs
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Run set management
|
|
38
|
+
def sets_path
|
|
39
|
+
File.join(@base_path, SETS_PATH)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def find_resultsets(tags: nil, limit: nil)
|
|
43
|
+
resultsets = ResultSet.find_all(sets_path)
|
|
44
|
+
|
|
45
|
+
resultsets = resultsets.select { |resultset| (Array(tags) - resultset.tags).empty? } if tags
|
|
46
|
+
|
|
47
|
+
limit ? resultsets.first(limit) : resultsets
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Convenience methods
|
|
51
|
+
def create_resultset(name, run_platform_strings, metadata: {})
|
|
52
|
+
run_paths = run_platform_strings.map { |ps| File.join(runs_path, ps) }
|
|
53
|
+
resultset = ResultSet.create(name, run_paths, metadata: metadata)
|
|
54
|
+
save_resultset(resultset)
|
|
55
|
+
resultset
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Validation
|
|
59
|
+
def validate_structure
|
|
60
|
+
errors = []
|
|
61
|
+
|
|
62
|
+
# Check base structure
|
|
63
|
+
errors << "Base path does not exist: #{@base_path}" unless Dir.exist?(@base_path)
|
|
64
|
+
errors << "Runs directory does not exist: #{runs_path}" unless Dir.exist?(runs_path)
|
|
65
|
+
errors << "Sets directory does not exist: #{sets_path}" unless Dir.exist?(sets_path)
|
|
66
|
+
|
|
67
|
+
# Validate individual runs
|
|
68
|
+
if Dir.exist?(runs_path)
|
|
69
|
+
Dir.glob(File.join(runs_path, '*')).each do |run_path|
|
|
70
|
+
next unless Dir.exist?(run_path)
|
|
71
|
+
|
|
72
|
+
begin
|
|
73
|
+
run = Result.load(run_path)
|
|
74
|
+
run.validate!
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
errors << "Invalid result at #{run_path}: #{e.message}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Validate result sets
|
|
82
|
+
if Dir.exist?(sets_path)
|
|
83
|
+
Dir.glob(File.join(sets_path, '*')).each do |set_path|
|
|
84
|
+
next unless Dir.exist?(set_path)
|
|
85
|
+
|
|
86
|
+
begin
|
|
87
|
+
resultset = ResultSet.load(set_path)
|
|
88
|
+
resultset.validate!
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
errors << "Invalid result set at #{set_path}: #{e.message}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
errors
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def valid?
|
|
99
|
+
validate_structure.empty?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def ensure_results_directory
|
|
103
|
+
FileUtils.mkdir_p(runs_path) unless Dir.exist?(runs_path)
|
|
104
|
+
FileUtils.mkdir_p(sets_path) unless Dir.exist?(sets_path)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'models/benchmark_result'
|
|
4
|
+
require_relative 'models/platform'
|
|
5
|
+
require_relative 'models/result'
|
|
6
|
+
require_relative 'models/result_store'
|
|
7
|
+
require_relative 'models/benchmark_config'
|
|
8
|
+
require_relative 'models/environment_config'
|
|
9
|
+
require_relative 'models/result_set'
|
|
10
|
+
|
|
11
|
+
module Serialbench
|
|
12
|
+
module Models
|
|
13
|
+
# Factory method to create appropriate model based on data structure
|
|
14
|
+
def self.from_file(file_path)
|
|
15
|
+
case File.extname(file_path).downcase
|
|
16
|
+
when '.yaml', '.yml'
|
|
17
|
+
data = YAML.load_file(file_path)
|
|
18
|
+
when '.json'
|
|
19
|
+
data = JSON.parse(File.read(file_path))
|
|
20
|
+
else
|
|
21
|
+
raise ArgumentError, "Unsupported file format: #{File.extname(file_path)}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
from_data(data)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.from_data(data)
|
|
28
|
+
BenchmarkResult.new(data)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Convert any benchmark result to YAML format
|
|
32
|
+
def self.to_yaml_file(result, file_path)
|
|
33
|
+
result.to_yaml_file(file_path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Convert any benchmark result to JSON format (for HTML templates)
|
|
37
|
+
def self.to_json_file(result, file_path)
|
|
38
|
+
result.to_json_file(file_path)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Convenience methods for the new OO architecture
|
|
42
|
+
def self.result_store
|
|
43
|
+
ResultStore.default
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.create_run(platform_string, benchmark_data, metadata: {})
|
|
47
|
+
Result.create(platform_string, benchmark_data, metadata: metadata)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.create_resultset(name, run_paths_or_objects, metadata: {})
|
|
51
|
+
ResultSet.create(name, run_paths_or_objects, metadata: metadata)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'octokit'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
module Serialbench
|
|
8
|
+
# Manages Ruby-Build definitions from the official ruby-build repository
|
|
9
|
+
class RubyBuildManager
|
|
10
|
+
GITHUB_API_URL = 'https://api.github.com/repos/rbenv/ruby-build/contents/share/ruby-build'
|
|
11
|
+
CACHE_DIR = File.expand_path('~/.serialbench')
|
|
12
|
+
CACHE_FILE = File.join(CACHE_DIR, 'ruby-build-definitions.yaml')
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def update_definitions
|
|
16
|
+
puts '🔄 Fetching Ruby-Build definitions from GitHub...'
|
|
17
|
+
|
|
18
|
+
definitions = fetch_definitions_from_github
|
|
19
|
+
save_definitions_to_cache(definitions)
|
|
20
|
+
|
|
21
|
+
puts "✅ Updated #{definitions.length} Ruby-Build definitions"
|
|
22
|
+
puts "📁 Cache location: #{CACHE_FILE}"
|
|
23
|
+
|
|
24
|
+
definitions
|
|
25
|
+
rescue StandardError => e
|
|
26
|
+
raise "Failed to update Ruby-Build definitions: #{e.message}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def list_definitions
|
|
30
|
+
load_definitions_from_cache
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def show_definition(tag)
|
|
34
|
+
definitions = load_definitions_from_cache
|
|
35
|
+
|
|
36
|
+
raise "Ruby-Build definition '#{tag}' not found. Available definitions: #{definitions.length}" unless definitions.include?(tag)
|
|
37
|
+
|
|
38
|
+
{
|
|
39
|
+
tag: tag,
|
|
40
|
+
available: true,
|
|
41
|
+
source: 'ruby-build',
|
|
42
|
+
cache_file: CACHE_FILE
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate_tag(tag)
|
|
47
|
+
puts "🔍 Validating Ruby-Build tag: #{tag} against #{CACHE_FILE}"
|
|
48
|
+
return false if tag.nil? || tag.strip.empty?
|
|
49
|
+
|
|
50
|
+
definitions = load_definitions_from_cache
|
|
51
|
+
definitions.include?(tag)
|
|
52
|
+
rescue StandardError
|
|
53
|
+
false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def suggest_current_ruby_tag
|
|
57
|
+
ruby_version = RUBY_VERSION
|
|
58
|
+
|
|
59
|
+
# Try exact match first
|
|
60
|
+
return ruby_version if validate_tag(ruby_version)
|
|
61
|
+
|
|
62
|
+
# Try common variations
|
|
63
|
+
variations = [
|
|
64
|
+
ruby_version,
|
|
65
|
+
"#{ruby_version}.0",
|
|
66
|
+
"#{ruby_version.split('.')[0..1].join('.')}.0"
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
variations.each do |variation|
|
|
70
|
+
return variation if validate_tag(variation)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Return the Ruby version even if not found, user can adjust
|
|
74
|
+
ruby_version
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def cache_exists?
|
|
78
|
+
File.exist?(CACHE_FILE)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def cache_age
|
|
82
|
+
return nil unless cache_exists?
|
|
83
|
+
|
|
84
|
+
Time.now - File.mtime(CACHE_FILE)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def ensure_cache_exists!
|
|
88
|
+
return if cache_exists?
|
|
89
|
+
|
|
90
|
+
raise <<~ERROR
|
|
91
|
+
Ruby-Build definitions cache not found.
|
|
92
|
+
|
|
93
|
+
Update the cache first:
|
|
94
|
+
serialbench ruby-build update
|
|
95
|
+
ERROR
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def fetch_definitions_from_github
|
|
101
|
+
# Initialize client with optional authentication
|
|
102
|
+
# Falls back to unauthenticated if GITHUB_TOKEN not set
|
|
103
|
+
client_options = {}
|
|
104
|
+
client_options[:access_token] = ENV['GITHUB_TOKEN'] if ENV['GITHUB_TOKEN']
|
|
105
|
+
|
|
106
|
+
client = Octokit::Client.new(client_options)
|
|
107
|
+
|
|
108
|
+
contents = client.contents('rbenv/ruby-build', path: 'share/ruby-build')
|
|
109
|
+
|
|
110
|
+
# Extract definition names from the file list
|
|
111
|
+
definitions = contents
|
|
112
|
+
.select { |item| item[:type] == 'file' && item[:name] != 'Makefile' }
|
|
113
|
+
.map { |item| item[:name] }
|
|
114
|
+
.sort
|
|
115
|
+
|
|
116
|
+
raise 'No Ruby-Build definitions found in GitHub response' if definitions.empty?
|
|
117
|
+
|
|
118
|
+
definitions
|
|
119
|
+
rescue Octokit::Error => e
|
|
120
|
+
warn "Warning: Error fetching Ruby-Build definitions from GitHub: #{e.message}"
|
|
121
|
+
warn "Note: Set GITHUB_TOKEN environment variable to avoid rate limits"
|
|
122
|
+
[]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def save_definitions_to_cache(definitions)
|
|
126
|
+
FileUtils.mkdir_p(CACHE_DIR)
|
|
127
|
+
|
|
128
|
+
cache_data = {
|
|
129
|
+
'updated_at' => Time.now.utc.iso8601,
|
|
130
|
+
'source' => GITHUB_API_URL,
|
|
131
|
+
'count' => definitions.length,
|
|
132
|
+
'definitions' => definitions
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
File.write(CACHE_FILE, cache_data.to_yaml)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def load_definitions_from_cache
|
|
139
|
+
return [] unless cache_exists?
|
|
140
|
+
|
|
141
|
+
cache_data = YAML.load_file(CACHE_FILE)
|
|
142
|
+
cache_data['definitions'] || []
|
|
143
|
+
rescue StandardError => e
|
|
144
|
+
warn "Warning: Failed to load Ruby-Build definitions from cache: #{e.message}"
|
|
145
|
+
[]
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
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
|