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,140 @@
|
|
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
|
+
unless File.exist?(SCHEMA_PATH)
|
50
|
+
raise ConfigurationError, "Configuration schema not found: #{SCHEMA_PATH}"
|
51
|
+
end
|
52
|
+
|
53
|
+
begin
|
54
|
+
YAML.load_file(SCHEMA_PATH)
|
55
|
+
rescue StandardError => e
|
56
|
+
raise ConfigurationError, "Error loading configuration schema: #{e.message}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Normalize and convert configuration to structured object
|
61
|
+
def normalize_config(config_data)
|
62
|
+
config = OpenStruct.new(config_data)
|
63
|
+
|
64
|
+
# Apply defaults
|
65
|
+
config.output_dir ||= 'benchmark-results'
|
66
|
+
config.benchmark_config ||= 'config/full.yml'
|
67
|
+
config.auto_install = true if config.auto_install.nil?
|
68
|
+
|
69
|
+
# Validate runtime-specific requirements
|
70
|
+
case config.runtime
|
71
|
+
when 'docker'
|
72
|
+
validate_docker_config(config)
|
73
|
+
when 'asdf'
|
74
|
+
validate_asdf_config(config)
|
75
|
+
else
|
76
|
+
raise ConfigurationError, "Unknown runtime: #{config.runtime}"
|
77
|
+
end
|
78
|
+
|
79
|
+
config
|
80
|
+
end
|
81
|
+
|
82
|
+
# Validate Docker-specific configuration
|
83
|
+
def validate_docker_config(config)
|
84
|
+
unless config.image_variants && !config.image_variants.empty?
|
85
|
+
raise ConfigurationError, "Docker runtime requires 'image_variants' to be specified"
|
86
|
+
end
|
87
|
+
|
88
|
+
invalid_variants = config.image_variants - %w[slim alpine]
|
89
|
+
unless invalid_variants.empty?
|
90
|
+
raise ConfigurationError, "Invalid image variants: #{invalid_variants.join(', ')}. Valid variants: slim, alpine"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Validate ASDF-specific configuration
|
95
|
+
def validate_asdf_config(config)
|
96
|
+
# Check if ASDF is available
|
97
|
+
unless command_available?('asdf')
|
98
|
+
raise ConfigurationError, "ASDF is not installed or not in PATH. Please install ASDF to use asdf runtime."
|
99
|
+
end
|
100
|
+
|
101
|
+
# Validate Ruby version format for ASDF (should include patch version)
|
102
|
+
config.ruby_versions.each do |version|
|
103
|
+
unless version.match?(/^\d+\.\d+\.\d+$/)
|
104
|
+
raise ConfigurationError, "ASDF runtime requires full version numbers (e.g., '3.2.8'), got: #{version}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Basic configuration validation
|
110
|
+
def validate_basic_config(config_data)
|
111
|
+
# Check required fields
|
112
|
+
required_fields = %w[runtime ruby_versions output_dir benchmark_config]
|
113
|
+
required_fields.each do |field|
|
114
|
+
unless config_data.key?(field)
|
115
|
+
raise ConfigurationError, "Missing required field: #{field}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Validate runtime
|
120
|
+
valid_runtimes = %w[docker asdf]
|
121
|
+
unless valid_runtimes.include?(config_data['runtime'])
|
122
|
+
raise ConfigurationError, "Invalid runtime: #{config_data['runtime']}. Valid runtimes: #{valid_runtimes.join(', ')}"
|
123
|
+
end
|
124
|
+
|
125
|
+
# Validate ruby_versions is an array
|
126
|
+
unless config_data['ruby_versions'].is_a?(Array)
|
127
|
+
raise ConfigurationError, "ruby_versions must be an array"
|
128
|
+
end
|
129
|
+
|
130
|
+
if config_data['ruby_versions'].empty?
|
131
|
+
raise ConfigurationError, "ruby_versions cannot be empty"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Check if a command is available in PATH
|
136
|
+
def command_available?(command)
|
137
|
+
system("which #{command} > /dev/null 2>&1")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'lutaml/model'
|
2
|
+
|
3
|
+
module Serialbench
|
4
|
+
module Models
|
5
|
+
# Configuration for comprehensive benchmarks - Full testing with all data sizes
|
6
|
+
# Used by Docker script for complete performance analysis
|
7
|
+
|
8
|
+
# data_sizes:
|
9
|
+
# - small
|
10
|
+
# - medium
|
11
|
+
# - large
|
12
|
+
|
13
|
+
# formats:
|
14
|
+
# - xml
|
15
|
+
# - json
|
16
|
+
# - yaml
|
17
|
+
# - toml
|
18
|
+
|
19
|
+
# iterations:
|
20
|
+
# small: 20
|
21
|
+
# medium: 5
|
22
|
+
# large: 2
|
23
|
+
|
24
|
+
# # Enable memory profiling for comprehensive analysis
|
25
|
+
# memory_profiling: true
|
26
|
+
|
27
|
+
# # Standard warmup iterations
|
28
|
+
# warmup_iterations: 3
|
29
|
+
|
30
|
+
# # Enable streaming benchmarks where supported
|
31
|
+
# streaming_benchmarks: true
|
32
|
+
|
33
|
+
class BenchmarkIteration < Lutaml::Model::Serializable
|
34
|
+
attribute :small, :integer, default: -> { 20 }
|
35
|
+
attribute :medium, :integer, default: -> { 5 }
|
36
|
+
attribute :large, :integer, default: -> { 2 }
|
37
|
+
|
38
|
+
key_value do
|
39
|
+
map 'small', to: :small
|
40
|
+
map 'medium', to: :medium
|
41
|
+
map 'large', to: :large
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class BenchmarkConfig < Lutaml::Model::Serializable
|
46
|
+
attribute :name, :string
|
47
|
+
attribute :description, :string
|
48
|
+
attribute :data_sizes, :string, collection: true, values: %w[small medium large]
|
49
|
+
attribute :formats, :string, collection: true, values: %w[xml json yaml toml]
|
50
|
+
attribute :iterations, BenchmarkIteration
|
51
|
+
attribute :warmup, :integer, default: -> { 1 }
|
52
|
+
attribute :operations, :string, collection: true, values: %w[parse generate memory streaming]
|
53
|
+
|
54
|
+
def to_file(file_path)
|
55
|
+
File.write(file_path, to_yaml)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.from_file(file_path)
|
59
|
+
from_yaml(IO.read(file_path))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,45 @@
|
|
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
|
+
end
|
12
|
+
|
13
|
+
class SerializerInformationCollection < Lutaml::Model::Collection
|
14
|
+
instances :items, SerializerInformation
|
15
|
+
end
|
16
|
+
|
17
|
+
class AdapterPerformance < Lutaml::Model::Serializable
|
18
|
+
attribute :adapter, :string
|
19
|
+
attribute :format, :string, values: %w[xml json yaml toml]
|
20
|
+
attribute :data_size, :string, values: %w[small medium large]
|
21
|
+
end
|
22
|
+
|
23
|
+
class IterationPerformance < AdapterPerformance
|
24
|
+
attribute :time_per_iterations, :float
|
25
|
+
attribute :time_per_iteration, :float
|
26
|
+
attribute :iterations_per_second, :float
|
27
|
+
attribute :iterations_count, :integer
|
28
|
+
end
|
29
|
+
|
30
|
+
class MemoryPerformance < AdapterPerformance
|
31
|
+
attribute :total_allocated, :integer
|
32
|
+
attribute :total_retained, :integer
|
33
|
+
attribute :allocated_memory, :integer
|
34
|
+
attribute :retained_memory, :integer
|
35
|
+
end
|
36
|
+
|
37
|
+
class BenchmarkResult < Lutaml::Model::Serializable
|
38
|
+
attribute :serializers, SerializerInformation, collection: true
|
39
|
+
attribute :parsing, IterationPerformance, collection: true
|
40
|
+
attribute :generation, IterationPerformance, collection: true
|
41
|
+
attribute :memory, MemoryPerformance, collection: true
|
42
|
+
attribute :streaming, IterationPerformance, collection: true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'lutaml/model'
|
2
|
+
require_relative '../ruby_build_manager'
|
3
|
+
|
4
|
+
module Serialbench
|
5
|
+
module Models
|
6
|
+
# ---
|
7
|
+
# name: docker-ruby-3.2
|
8
|
+
# kind: docker
|
9
|
+
# created_at: '2025-06-13T15:18:43+08:00'
|
10
|
+
# ruby_build_tag: "3.2.4"
|
11
|
+
# description: Docker environment for Ruby 3.2 benchmarks
|
12
|
+
# docker:
|
13
|
+
# image: 'ruby:3.2-slim'
|
14
|
+
# dockerfile: '../../docker/Dockerfile.ubuntu'
|
15
|
+
|
16
|
+
class DockerEnvConfig < Lutaml::Model::Serializable
|
17
|
+
attribute :image, :string
|
18
|
+
attribute :dockerfile, :string
|
19
|
+
|
20
|
+
key_value do
|
21
|
+
map 'image', to: :image
|
22
|
+
map 'dockerfile', to: :dockerfile
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# ---
|
27
|
+
# name: ruby-324-asdf
|
28
|
+
# kind: asdf
|
29
|
+
# created_at: '2025-06-12T22:54:43+08:00'
|
30
|
+
# ruby_build_tag: 3.2.4
|
31
|
+
# description: ASDF environment
|
32
|
+
# asdf:
|
33
|
+
# auto_install: true
|
34
|
+
class AsdfEnvConfig < Lutaml::Model::Serializable
|
35
|
+
attribute :auto_install, :boolean, default: -> { true }
|
36
|
+
|
37
|
+
key_value do
|
38
|
+
map 'auto_install', to: :auto_install
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class EnvironmentConfig < Lutaml::Model::Serializable
|
43
|
+
attribute :name, :string
|
44
|
+
attribute :kind, :string
|
45
|
+
attribute :created_at, :string, default: -> { Time.now.utc.iso8601 }
|
46
|
+
attribute :ruby_build_tag, :string, values:
|
47
|
+
RubyBuildManager.list_definitions
|
48
|
+
attribute :description, :string
|
49
|
+
attribute :docker, DockerEnvConfig
|
50
|
+
attribute :asdf, AsdfEnvConfig
|
51
|
+
|
52
|
+
key_value do
|
53
|
+
map 'name', to: :name
|
54
|
+
map 'description', to: :description
|
55
|
+
map 'kind', to: :kind
|
56
|
+
map 'created_at', to: :created_at
|
57
|
+
map 'ruby_build_tag', to: :ruby_build_tag
|
58
|
+
map 'docker', to: :docker
|
59
|
+
map 'asdf', to: :asdf
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_file(file_path)
|
63
|
+
File.write(file_path, to_yaml)
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.from_file(file_path)
|
67
|
+
from_yaml(IO.read(file_path))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,59 @@
|
|
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, values: RubyBuildManager.list_definitions
|
23
|
+
|
24
|
+
def self.current_local
|
25
|
+
new(
|
26
|
+
platform_string: "local-#{RUBY_VERSION}",
|
27
|
+
kind: 'local',
|
28
|
+
ruby_build_tag: RUBY_VERSION
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.detect_os
|
33
|
+
case RbConfig::CONFIG['host_os']
|
34
|
+
when /darwin/i
|
35
|
+
'macos'
|
36
|
+
when /linux/i
|
37
|
+
'linux'
|
38
|
+
when /mswin|mingw|cygwin/i
|
39
|
+
'windows'
|
40
|
+
else
|
41
|
+
'unknown'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.detect_arch
|
46
|
+
case RbConfig::CONFIG['host_cpu']
|
47
|
+
when /x86_64|amd64/i
|
48
|
+
'x86_64'
|
49
|
+
when /aarch64|arm64/i
|
50
|
+
'arm64'
|
51
|
+
when /arm/i
|
52
|
+
'arm'
|
53
|
+
else
|
54
|
+
'unknown'
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,53 @@
|
|
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
|
+
end
|
18
|
+
|
19
|
+
class Result < Lutaml::Model::Serializable
|
20
|
+
attribute :platform, Platform
|
21
|
+
attribute :metadata, RunMetadata
|
22
|
+
attribute :environment_config, EnvironmentConfig
|
23
|
+
attribute :benchmark_config, BenchmarkConfig
|
24
|
+
attribute :benchmark_result, BenchmarkResult
|
25
|
+
|
26
|
+
def self.load(path)
|
27
|
+
raise ArgumentError, "Path does not exist: #{path}" unless Dir.exist?(path)
|
28
|
+
|
29
|
+
# Load benchmark data
|
30
|
+
data_file = File.join(path, 'results.yaml')
|
31
|
+
|
32
|
+
raise ArgumentError, "No results data found in #{path}" unless File.exist?(data_file)
|
33
|
+
|
34
|
+
from_yaml(IO.read(data_file))
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.find_all(base_path = 'results/runs')
|
38
|
+
return [] unless Dir.exist?(base_path)
|
39
|
+
|
40
|
+
Dir.glob(File.join(base_path, '*')).select { |path| Dir.exist?(path) }.map do |path|
|
41
|
+
load(path)
|
42
|
+
rescue StandardError => e
|
43
|
+
warn "Failed to load run result from #{path}: #{e.message}"
|
44
|
+
nil
|
45
|
+
end.compact
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_file(path)
|
49
|
+
File.write(path, to_yaml)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,71 @@
|
|
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
|
+
# Check if result already exists:
|
51
|
+
# If environment_config.created_at is identical;
|
52
|
+
# If platform.platform_string is identical;
|
53
|
+
# If benchmark_config.benchmark_name is identical;
|
54
|
+
|
55
|
+
duplicates = results.select do |r|
|
56
|
+
r.platform.platform_string == result.platform.platform_string &&
|
57
|
+
r.environment_config.created_at == result.environment_config.created_at &&
|
58
|
+
r.benchmark_config.benchmark_name == result.benchmark_config.benchmark_name
|
59
|
+
end
|
60
|
+
|
61
|
+
raise ArgumentError, 'Result is already present in this resultset' if duplicates.any?
|
62
|
+
|
63
|
+
# Add the result to the resultset
|
64
|
+
results << result
|
65
|
+
self.updated_at = Time.now.utc.iso8601
|
66
|
+
|
67
|
+
result
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -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
|