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,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'ostruct'
|
|
5
|
+
require_relative 'schema_validator'
|
|
6
|
+
|
|
7
|
+
module Serialbench
|
|
8
|
+
# Manages configuration loading and validation for Serialbench
|
|
9
|
+
class ConfigManager
|
|
10
|
+
class ConfigurationError < StandardError; end
|
|
11
|
+
|
|
12
|
+
SCHEMA_PATH = File.join(__dir__, '../../docs/serialbench_config_schema.yaml')
|
|
13
|
+
|
|
14
|
+
# Load and validate configuration from file
|
|
15
|
+
def self.load_and_validate(config_path)
|
|
16
|
+
new.load_and_validate(config_path)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@validator = SchemaValidator.new
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Load configuration file and validate against schema
|
|
24
|
+
def load_and_validate(config_path)
|
|
25
|
+
raise ConfigurationError, "Configuration file not found: #{config_path}" unless File.exist?(config_path)
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
config_data = YAML.load_file(config_path)
|
|
29
|
+
rescue Psych::SyntaxError => e
|
|
30
|
+
raise ConfigurationError, "Invalid YAML syntax in #{config_path}: #{e.message}"
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
raise ConfigurationError, "Error reading configuration file #{config_path}: #{e.message}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
validate_config(config_data, config_path)
|
|
36
|
+
normalize_config(config_data)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Validate configuration against schema
|
|
42
|
+
def validate_config(config_data, _config_path)
|
|
43
|
+
# For now, perform basic validation since we don't have a specific config schema validator
|
|
44
|
+
validate_basic_config(config_data)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Load the configuration schema
|
|
48
|
+
def load_schema
|
|
49
|
+
raise ConfigurationError, "Configuration schema not found: #{SCHEMA_PATH}" unless File.exist?(SCHEMA_PATH)
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
YAML.load_file(SCHEMA_PATH)
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
raise ConfigurationError, "Error loading configuration schema: #{e.message}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Normalize and convert configuration to structured object
|
|
59
|
+
def normalize_config(config_data)
|
|
60
|
+
config = OpenStruct.new(config_data)
|
|
61
|
+
|
|
62
|
+
# Apply defaults
|
|
63
|
+
config.output_dir ||= 'benchmark-results'
|
|
64
|
+
config.benchmark_config ||= 'config/full.yml'
|
|
65
|
+
config.auto_install = true if config.auto_install.nil?
|
|
66
|
+
|
|
67
|
+
# Validate runtime-specific requirements
|
|
68
|
+
case config.runtime
|
|
69
|
+
when 'docker'
|
|
70
|
+
validate_docker_config(config)
|
|
71
|
+
when 'asdf'
|
|
72
|
+
validate_asdf_config(config)
|
|
73
|
+
else
|
|
74
|
+
raise ConfigurationError, "Unknown runtime: #{config.runtime}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
config
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Validate Docker-specific configuration
|
|
81
|
+
def validate_docker_config(config)
|
|
82
|
+
raise ConfigurationError, "Docker runtime requires 'image_variants' to be specified" unless config.image_variants && !config.image_variants.empty?
|
|
83
|
+
|
|
84
|
+
invalid_variants = config.image_variants - %w[slim alpine]
|
|
85
|
+
return if invalid_variants.empty?
|
|
86
|
+
|
|
87
|
+
raise ConfigurationError, "Invalid image variants: #{invalid_variants.join(', ')}. Valid variants: slim, alpine"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Validate ASDF-specific configuration
|
|
91
|
+
def validate_asdf_config(config)
|
|
92
|
+
# Check if ASDF is available
|
|
93
|
+
raise ConfigurationError, 'ASDF is not installed or not in PATH. Please install ASDF to use asdf runtime.' unless command_available?('asdf')
|
|
94
|
+
|
|
95
|
+
# Validate Ruby version format for ASDF (should include patch version)
|
|
96
|
+
config.ruby_versions.each do |version|
|
|
97
|
+
raise ConfigurationError, "ASDF runtime requires full version numbers (e.g., '3.2.8'), got: #{version}" unless version.match?(/^\d+\.\d+\.\d+$/)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Basic configuration validation
|
|
102
|
+
def validate_basic_config(config_data)
|
|
103
|
+
# Check required fields
|
|
104
|
+
required_fields = %w[runtime ruby_versions output_dir benchmark_config]
|
|
105
|
+
required_fields.each do |field|
|
|
106
|
+
raise ConfigurationError, "Missing required field: #{field}" unless config_data.key?(field)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Validate runtime
|
|
110
|
+
valid_runtimes = %w[docker asdf]
|
|
111
|
+
unless valid_runtimes.include?(config_data['runtime'])
|
|
112
|
+
raise ConfigurationError,
|
|
113
|
+
"Invalid runtime: #{config_data['runtime']}. Valid runtimes: #{valid_runtimes.join(', ')}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Validate ruby_versions is an array
|
|
117
|
+
raise ConfigurationError, 'ruby_versions must be an array' unless config_data['ruby_versions'].is_a?(Array)
|
|
118
|
+
|
|
119
|
+
return unless config_data['ruby_versions'].empty?
|
|
120
|
+
|
|
121
|
+
raise ConfigurationError, 'ruby_versions cannot be empty'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check if a command is available in PATH
|
|
125
|
+
def command_available?(command)
|
|
126
|
+
system("which #{command} > /dev/null 2>&1")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lutaml/model'
|
|
4
|
+
|
|
5
|
+
module Serialbench
|
|
6
|
+
module Models
|
|
7
|
+
# Configuration for comprehensive benchmarks - Full testing with all data sizes
|
|
8
|
+
# Used by Docker script for complete performance analysis
|
|
9
|
+
|
|
10
|
+
# data_sizes:
|
|
11
|
+
# - small
|
|
12
|
+
# - medium
|
|
13
|
+
# - large
|
|
14
|
+
|
|
15
|
+
# formats:
|
|
16
|
+
# - xml
|
|
17
|
+
# - json
|
|
18
|
+
# - yaml
|
|
19
|
+
# - toml
|
|
20
|
+
|
|
21
|
+
# iterations:
|
|
22
|
+
# small: 20
|
|
23
|
+
# medium: 5
|
|
24
|
+
# large: 2
|
|
25
|
+
|
|
26
|
+
# # Enable memory profiling for comprehensive analysis
|
|
27
|
+
# memory_profiling: true
|
|
28
|
+
|
|
29
|
+
# # Standard warmup iterations
|
|
30
|
+
# warmup_iterations: 3
|
|
31
|
+
|
|
32
|
+
# # Enable streaming benchmarks where supported
|
|
33
|
+
# streaming_benchmarks: true
|
|
34
|
+
|
|
35
|
+
class BenchmarkIteration < Lutaml::Model::Serializable
|
|
36
|
+
attribute :small, :integer, default: -> { 20 }
|
|
37
|
+
attribute :medium, :integer, default: -> { 5 }
|
|
38
|
+
attribute :large, :integer, default: -> { 2 }
|
|
39
|
+
|
|
40
|
+
key_value do
|
|
41
|
+
map 'small', to: :small
|
|
42
|
+
map 'medium', to: :medium
|
|
43
|
+
map 'large', to: :large
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class BenchmarkConfig < Lutaml::Model::Serializable
|
|
48
|
+
attribute :name, :string
|
|
49
|
+
attribute :description, :string
|
|
50
|
+
attribute :data_sizes, :string, collection: true, values: %w[small medium large]
|
|
51
|
+
attribute :formats, :string, collection: true, values: %w[xml json yaml toml]
|
|
52
|
+
attribute :iterations, BenchmarkIteration
|
|
53
|
+
attribute :warmup, :integer, default: -> { 1 }
|
|
54
|
+
attribute :operations, :string, collection: true, values: %w[parse generate memory streaming]
|
|
55
|
+
|
|
56
|
+
key_value do
|
|
57
|
+
map 'name', to: :name
|
|
58
|
+
map 'description', to: :description
|
|
59
|
+
map 'data_sizes', to: :data_sizes
|
|
60
|
+
map 'formats', to: :formats
|
|
61
|
+
map 'iterations', to: :iterations
|
|
62
|
+
map 'warmup', to: :warmup
|
|
63
|
+
map 'operations', to: :operations
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_file(file_path)
|
|
67
|
+
File.write(file_path, to_yaml)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.from_file(file_path)
|
|
71
|
+
from_yaml(IO.read(file_path))
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lutaml/model'
|
|
4
|
+
|
|
5
|
+
module Serialbench
|
|
6
|
+
module Models
|
|
7
|
+
class SerializerInformation < Lutaml::Model::Serializable
|
|
8
|
+
attribute :format, :string, values: %w[xml json yaml toml]
|
|
9
|
+
attribute :name, :string
|
|
10
|
+
attribute :version, :string
|
|
11
|
+
|
|
12
|
+
key_value do
|
|
13
|
+
map 'format', to: :format
|
|
14
|
+
map 'name', to: :name
|
|
15
|
+
map 'version', to: :version
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class AdapterPerformance < Lutaml::Model::Serializable
|
|
20
|
+
attribute :adapter, :string
|
|
21
|
+
attribute :format, :string, values: %w[xml json yaml toml]
|
|
22
|
+
attribute :data_size, :string, values: %w[small medium large]
|
|
23
|
+
|
|
24
|
+
key_value do
|
|
25
|
+
map 'adapter', to: :adapter
|
|
26
|
+
map 'format', to: :format
|
|
27
|
+
map 'data_size', to: :data_size
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class IterationPerformance < AdapterPerformance
|
|
32
|
+
attribute :time_per_iterations, :float
|
|
33
|
+
attribute :time_per_iteration, :float
|
|
34
|
+
attribute :iterations_per_second, :float
|
|
35
|
+
attribute :iterations_count, :integer
|
|
36
|
+
|
|
37
|
+
key_value do
|
|
38
|
+
map 'adapter', to: :adapter
|
|
39
|
+
map 'format', to: :format
|
|
40
|
+
map 'data_size', to: :data_size
|
|
41
|
+
map 'time_per_iterations', to: :time_per_iterations
|
|
42
|
+
map 'time_per_iteration', to: :time_per_iteration
|
|
43
|
+
map 'iterations_per_second', to: :iterations_per_second
|
|
44
|
+
map 'iterations_count', to: :iterations_count
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class MemoryPerformance < AdapterPerformance
|
|
49
|
+
attribute :total_allocated, :integer
|
|
50
|
+
attribute :total_retained, :integer
|
|
51
|
+
attribute :allocated_memory, :integer
|
|
52
|
+
attribute :retained_memory, :integer
|
|
53
|
+
|
|
54
|
+
key_value do
|
|
55
|
+
map 'adapter', to: :adapter
|
|
56
|
+
map 'format', to: :format
|
|
57
|
+
map 'data_size', to: :data_size
|
|
58
|
+
map 'total_allocated', to: :total_allocated
|
|
59
|
+
map 'total_retained', to: :total_retained
|
|
60
|
+
map 'allocated_memory', to: :allocated_memory
|
|
61
|
+
map 'retained_memory', to: :retained_memory
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class BenchmarkResult < Lutaml::Model::Serializable
|
|
66
|
+
attribute :serializers, SerializerInformation, collection: true
|
|
67
|
+
attribute :parsing, IterationPerformance, collection: true
|
|
68
|
+
attribute :generation, IterationPerformance, collection: true
|
|
69
|
+
attribute :memory, MemoryPerformance, collection: true
|
|
70
|
+
attribute :streaming, IterationPerformance, collection: true
|
|
71
|
+
|
|
72
|
+
key_value do
|
|
73
|
+
map 'serializers', to: :serializers
|
|
74
|
+
map 'parsing', to: :parsing
|
|
75
|
+
map 'generation', to: :generation
|
|
76
|
+
map 'memory', to: :memory
|
|
77
|
+
map 'streaming', to: :streaming
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lutaml/model'
|
|
4
|
+
require_relative '../ruby_build_manager'
|
|
5
|
+
|
|
6
|
+
module Serialbench
|
|
7
|
+
module Models
|
|
8
|
+
# ---
|
|
9
|
+
# name: docker-ruby-3.2
|
|
10
|
+
# kind: docker
|
|
11
|
+
# created_at: '2025-06-13T15:18:43+08:00'
|
|
12
|
+
# ruby_build_tag: "3.2.4"
|
|
13
|
+
# description: Docker environment for Ruby 3.2 benchmarks
|
|
14
|
+
# docker:
|
|
15
|
+
# image: 'ruby:3.2-slim'
|
|
16
|
+
# dockerfile: '../../docker/Dockerfile.ubuntu'
|
|
17
|
+
|
|
18
|
+
class DockerEnvConfig < Lutaml::Model::Serializable
|
|
19
|
+
attribute :image, :string
|
|
20
|
+
attribute :dockerfile, :string
|
|
21
|
+
|
|
22
|
+
key_value do
|
|
23
|
+
map 'image', to: :image
|
|
24
|
+
map 'dockerfile', to: :dockerfile
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# ---
|
|
29
|
+
# name: ruby-324-asdf
|
|
30
|
+
# kind: asdf
|
|
31
|
+
# created_at: '2025-06-12T22:54:43+08:00'
|
|
32
|
+
# ruby_build_tag: 3.2.4
|
|
33
|
+
# description: ASDF environment
|
|
34
|
+
# asdf:
|
|
35
|
+
# auto_install: true
|
|
36
|
+
class AsdfEnvConfig < Lutaml::Model::Serializable
|
|
37
|
+
attribute :auto_install, :boolean, default: -> { true }
|
|
38
|
+
|
|
39
|
+
key_value do
|
|
40
|
+
map 'auto_install', to: :auto_install
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class EnvironmentConfig < Lutaml::Model::Serializable
|
|
45
|
+
attribute :name, :string
|
|
46
|
+
attribute :kind, :string
|
|
47
|
+
attribute :created_at, :string, default: -> { Time.now.utc.iso8601 }
|
|
48
|
+
attribute :ruby_build_tag, :string
|
|
49
|
+
attribute :description, :string
|
|
50
|
+
attribute :docker, DockerEnvConfig
|
|
51
|
+
attribute :asdf, AsdfEnvConfig
|
|
52
|
+
|
|
53
|
+
key_value do
|
|
54
|
+
map 'name', to: :name
|
|
55
|
+
map 'description', to: :description
|
|
56
|
+
map 'kind', to: :kind
|
|
57
|
+
map 'created_at', to: :created_at
|
|
58
|
+
map 'ruby_build_tag', to: :ruby_build_tag
|
|
59
|
+
map 'docker', to: :docker
|
|
60
|
+
map 'asdf', to: :asdf
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def to_file(file_path)
|
|
64
|
+
File.write(file_path, to_yaml)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.from_file(file_path)
|
|
68
|
+
from_yaml(IO.read(file_path))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lutaml/model'
|
|
4
|
+
require_relative '../ruby_build_manager'
|
|
5
|
+
|
|
6
|
+
module Serialbench
|
|
7
|
+
module Models
|
|
8
|
+
# platform:
|
|
9
|
+
# platform_string: docker-ruby-3.0
|
|
10
|
+
# kind: docker
|
|
11
|
+
# os: linux
|
|
12
|
+
# arch: arm64
|
|
13
|
+
# ruby_build_tag: 3.0.7
|
|
14
|
+
|
|
15
|
+
class Platform < Lutaml::Model::Serializable
|
|
16
|
+
attribute :platform_string, :string
|
|
17
|
+
attribute :kind, :string, default: -> { 'local' }
|
|
18
|
+
attribute :os, :string, default: -> { detect_os }
|
|
19
|
+
attribute :arch, :string, default: -> { detect_arch }
|
|
20
|
+
attribute :ruby_version, :string, default: -> { RUBY_VERSION }
|
|
21
|
+
attribute :ruby_platform, :string, default: -> { RUBY_PLATFORM }
|
|
22
|
+
attribute :ruby_build_tag, :string
|
|
23
|
+
|
|
24
|
+
key_value do
|
|
25
|
+
map 'platform_string', to: :platform_string
|
|
26
|
+
map 'kind', to: :kind
|
|
27
|
+
map 'os', to: :os
|
|
28
|
+
map 'arch', to: :arch
|
|
29
|
+
map 'ruby_version', to: :ruby_version
|
|
30
|
+
map 'ruby_platform', to: :ruby_platform
|
|
31
|
+
map 'ruby_build_tag', to: :ruby_build_tag
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.current_local(ruby_version: nil)
|
|
35
|
+
version = ruby_version || RUBY_VERSION
|
|
36
|
+
|
|
37
|
+
# Check for GitHub Actions runner platform
|
|
38
|
+
github_platform = ENV['GITHUB_RUNNER_PLATFORM']
|
|
39
|
+
if github_platform
|
|
40
|
+
os, arch = parse_github_platform(github_platform)
|
|
41
|
+
platform_string = "#{github_platform}-ruby-#{version}"
|
|
42
|
+
else
|
|
43
|
+
os = detect_os
|
|
44
|
+
arch = detect_arch
|
|
45
|
+
platform_string = "local-#{version}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
new(
|
|
49
|
+
platform_string: platform_string,
|
|
50
|
+
kind: 'local',
|
|
51
|
+
os: os,
|
|
52
|
+
arch: arch,
|
|
53
|
+
ruby_version: version,
|
|
54
|
+
ruby_build_tag: version
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.detect_os
|
|
59
|
+
case RbConfig::CONFIG['host_os']
|
|
60
|
+
when /darwin/i
|
|
61
|
+
'macos'
|
|
62
|
+
when /linux/i
|
|
63
|
+
'linux'
|
|
64
|
+
when /mswin|mingw|cygwin/i
|
|
65
|
+
'windows'
|
|
66
|
+
else
|
|
67
|
+
'unknown'
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.detect_arch
|
|
72
|
+
case RbConfig::CONFIG['host_cpu']
|
|
73
|
+
when /x86_64|amd64/i
|
|
74
|
+
'x86_64'
|
|
75
|
+
when /aarch64|arm64/i
|
|
76
|
+
'arm64'
|
|
77
|
+
when /arm/i
|
|
78
|
+
'arm'
|
|
79
|
+
else
|
|
80
|
+
'unknown'
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.parse_github_platform(platform_name)
|
|
85
|
+
# Parse GitHub Actions runner platform names
|
|
86
|
+
# Examples: ubuntu-24.04, ubuntu-24.04-arm, macos-13, macos-15-intel,
|
|
87
|
+
# macos-14, macos-15, macos-26, windows-2022, windows-2025, windows-11-arm
|
|
88
|
+
|
|
89
|
+
case platform_name
|
|
90
|
+
when /^ubuntu.*-arm$/
|
|
91
|
+
['linux', 'arm64']
|
|
92
|
+
when /^ubuntu/
|
|
93
|
+
['linux', 'x86_64']
|
|
94
|
+
when /^macos-.*-intel$/
|
|
95
|
+
['macos', 'x86_64']
|
|
96
|
+
when /^macos-(13)$/
|
|
97
|
+
['macos', 'x86_64']
|
|
98
|
+
when /^macos-(14|15|26)$/
|
|
99
|
+
['macos', 'arm64']
|
|
100
|
+
when /^windows.*-arm$/
|
|
101
|
+
['windows', 'arm64']
|
|
102
|
+
when /^windows/
|
|
103
|
+
['windows', 'x86_64']
|
|
104
|
+
else
|
|
105
|
+
# Fallback to automatic detection
|
|
106
|
+
[detect_os, detect_arch]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require_relative 'platform'
|
|
6
|
+
require_relative 'benchmark_result'
|
|
7
|
+
require_relative 'benchmark_config'
|
|
8
|
+
require_relative 'environment_config'
|
|
9
|
+
|
|
10
|
+
module Serialbench
|
|
11
|
+
module Models
|
|
12
|
+
class RunMetadata < Lutaml::Model::Serializable
|
|
13
|
+
attribute :created_at, :string, default: -> { Time.now.utc.iso8601 }
|
|
14
|
+
attribute :benchmark_config_path, :string
|
|
15
|
+
attribute :environment_config_path, :string
|
|
16
|
+
attribute :tags, :string, collection: true
|
|
17
|
+
|
|
18
|
+
key_value do
|
|
19
|
+
map 'created_at', to: :created_at
|
|
20
|
+
map 'benchmark_config_path', to: :benchmark_config_path
|
|
21
|
+
map 'environment_config_path', to: :environment_config_path
|
|
22
|
+
map 'tags', to: :tags
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Result < Lutaml::Model::Serializable
|
|
27
|
+
attribute :platform, Platform
|
|
28
|
+
attribute :metadata, RunMetadata
|
|
29
|
+
attribute :environment_config, EnvironmentConfig
|
|
30
|
+
attribute :benchmark_config, BenchmarkConfig
|
|
31
|
+
attribute :benchmark_result, BenchmarkResult
|
|
32
|
+
|
|
33
|
+
key_value do
|
|
34
|
+
map 'platform', to: :platform
|
|
35
|
+
map 'metadata', to: :metadata
|
|
36
|
+
map 'environment_config', to: :environment_config
|
|
37
|
+
map 'benchmark_config', to: :benchmark_config
|
|
38
|
+
map 'benchmark_result', to: :benchmark_result
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.load(path)
|
|
42
|
+
raise ArgumentError, "Path does not exist: #{path}" unless Dir.exist?(path)
|
|
43
|
+
|
|
44
|
+
# Load benchmark data
|
|
45
|
+
data_file = File.join(path, 'results.yaml')
|
|
46
|
+
|
|
47
|
+
raise ArgumentError, "No results data found in #{path}" unless File.exist?(data_file)
|
|
48
|
+
|
|
49
|
+
yaml_content = IO.read(data_file)
|
|
50
|
+
|
|
51
|
+
# Debug: Check if yaml_content is empty or too small
|
|
52
|
+
if yaml_content.nil? || yaml_content.strip.empty?
|
|
53
|
+
raise ArgumentError, "Results file at #{data_file} is empty"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if yaml_content.bytesize < 200
|
|
57
|
+
warn "WARNING: Results file at #{data_file} is suspiciously small (#{yaml_content.bytesize} bytes)"
|
|
58
|
+
warn "Content preview: #{yaml_content[0..100]}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
from_yaml(yaml_content)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.find_all(base_path = 'results/runs')
|
|
65
|
+
return [] unless Dir.exist?(base_path)
|
|
66
|
+
|
|
67
|
+
Dir.glob(File.join(base_path, '*')).select { |path| Dir.exist?(path) }.map do |path|
|
|
68
|
+
load(path)
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
warn "Failed to load run result from #{path}: #{e.message}"
|
|
71
|
+
nil
|
|
72
|
+
end.compact
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def to_file(path)
|
|
76
|
+
File.write(path, to_yaml)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'time'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
module Serialbench
|
|
8
|
+
module Models
|
|
9
|
+
# ResultSet model for managing collections of benchmark results
|
|
10
|
+
class ResultSet < Lutaml::Model::Serializable
|
|
11
|
+
attribute :name, :string
|
|
12
|
+
attribute :description, :string
|
|
13
|
+
attribute :created_at, :string, default: -> { Time.now.utc.iso8601 }
|
|
14
|
+
attribute :updated_at, :string, default: -> { Time.now.utc.iso8601 }
|
|
15
|
+
attribute :results, Result, collection: true, initialize_empty: true
|
|
16
|
+
|
|
17
|
+
def self.load(path)
|
|
18
|
+
raise ArgumentError, "Path does not exist: #{path}" unless Dir.exist?(path)
|
|
19
|
+
|
|
20
|
+
# Load benchmark data
|
|
21
|
+
data_file = File.join(path, 'resultset.yaml')
|
|
22
|
+
|
|
23
|
+
raise ArgumentError, "No results data found in #{path}" unless File.exist?(data_file)
|
|
24
|
+
|
|
25
|
+
from_yaml(IO.read(data_file))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_file(path)
|
|
29
|
+
File.write(path, to_yaml)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def save(dir)
|
|
33
|
+
FileUtils.mkdir_p(dir)
|
|
34
|
+
to_file(File.join(dir, 'resultset.yaml'))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def add_result(result_path)
|
|
38
|
+
# Assume result_path is the directory containing benchmark results and is named
|
|
39
|
+
# accordingly
|
|
40
|
+
result_name = File.basename(result_path)
|
|
41
|
+
raise ArgumentError, 'Result name cannot be empty' if result_name.empty?
|
|
42
|
+
# Validate that the result path is a directory
|
|
43
|
+
raise ArgumentError, 'Result path must be a directory' unless File.directory?(result_path)
|
|
44
|
+
|
|
45
|
+
result_file_path = File.join(result_path, 'results.yaml')
|
|
46
|
+
raise ArgumentError, "No results data found in #{result_path}" unless File.exist?(result_file_path)
|
|
47
|
+
|
|
48
|
+
result = Result.load(result_path)
|
|
49
|
+
|
|
50
|
+
# Validate that the result has required fields
|
|
51
|
+
raise ArgumentError, "Result from #{result_path} is missing platform information" if result.platform.nil?
|
|
52
|
+
raise ArgumentError, "Result from #{result_path} is missing environment_config" if result.environment_config.nil?
|
|
53
|
+
raise ArgumentError, "Result from #{result_path} is missing benchmark_config" if result.benchmark_config.nil?
|
|
54
|
+
|
|
55
|
+
# Check if result already exists:
|
|
56
|
+
# If environment_config.created_at is identical;
|
|
57
|
+
# If platform.platform_string is identical;
|
|
58
|
+
# If benchmark_config.benchmark_name is identical;
|
|
59
|
+
|
|
60
|
+
duplicates = results.select do |r|
|
|
61
|
+
# Skip results with nil platform (defensive check)
|
|
62
|
+
next if r.platform.nil? || result.platform.nil?
|
|
63
|
+
|
|
64
|
+
r.platform.platform_string == result.platform.platform_string &&
|
|
65
|
+
r.environment_config.created_at == result.environment_config.created_at &&
|
|
66
|
+
r.benchmark_config.benchmark_name == result.benchmark_config.benchmark_name
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
raise ArgumentError, 'Result is already present in this resultset' if duplicates.any?
|
|
70
|
+
|
|
71
|
+
# Add the result to the resultset
|
|
72
|
+
results << result
|
|
73
|
+
self.updated_at = Time.now.utc.iso8601
|
|
74
|
+
|
|
75
|
+
result
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|