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,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