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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/benchmark.yml +13 -5
  3. data/.github/workflows/docker.yml +35 -9
  4. data/.github/workflows/rake.yml +15 -0
  5. data/Gemfile +2 -1
  6. data/README.adoc +267 -1129
  7. data/Rakefile +0 -55
  8. data/config/benchmarks/full.yml +29 -0
  9. data/config/benchmarks/short.yml +26 -0
  10. data/config/environments/asdf-ruby-3.2.yml +8 -0
  11. data/config/environments/asdf-ruby-3.3.yml +8 -0
  12. data/config/environments/docker-ruby-3.0.yml +9 -0
  13. data/config/environments/docker-ruby-3.1.yml +9 -0
  14. data/config/environments/docker-ruby-3.2.yml +9 -0
  15. data/config/environments/docker-ruby-3.3.yml +9 -0
  16. data/config/environments/docker-ruby-3.4.yml +9 -0
  17. data/docker/Dockerfile.alpine +33 -0
  18. data/docker/{Dockerfile.benchmark → Dockerfile.ubuntu} +4 -3
  19. data/docker/README.md +2 -2
  20. data/exe/serialbench +1 -1
  21. data/lib/serialbench/benchmark_runner.rb +261 -423
  22. data/lib/serialbench/cli/base_cli.rb +51 -0
  23. data/lib/serialbench/cli/benchmark_cli.rb +380 -0
  24. data/lib/serialbench/cli/environment_cli.rb +181 -0
  25. data/lib/serialbench/cli/resultset_cli.rb +215 -0
  26. data/lib/serialbench/cli/ruby_build_cli.rb +238 -0
  27. data/lib/serialbench/cli.rb +58 -601
  28. data/lib/serialbench/config_manager.rb +140 -0
  29. data/lib/serialbench/models/benchmark_config.rb +63 -0
  30. data/lib/serialbench/models/benchmark_result.rb +45 -0
  31. data/lib/serialbench/models/environment_config.rb +71 -0
  32. data/lib/serialbench/models/platform.rb +59 -0
  33. data/lib/serialbench/models/result.rb +53 -0
  34. data/lib/serialbench/models/result_set.rb +71 -0
  35. data/lib/serialbench/models/result_store.rb +108 -0
  36. data/lib/serialbench/models.rb +54 -0
  37. data/lib/serialbench/ruby_build_manager.rb +153 -0
  38. data/lib/serialbench/runners/asdf_runner.rb +296 -0
  39. data/lib/serialbench/runners/base.rb +32 -0
  40. data/lib/serialbench/runners/docker_runner.rb +142 -0
  41. data/lib/serialbench/serializers/base_serializer.rb +8 -16
  42. data/lib/serialbench/serializers/json/base_json_serializer.rb +4 -4
  43. data/lib/serialbench/serializers/json/json_serializer.rb +0 -2
  44. data/lib/serialbench/serializers/json/oj_serializer.rb +0 -2
  45. data/lib/serialbench/serializers/json/yajl_serializer.rb +0 -2
  46. data/lib/serialbench/serializers/toml/base_toml_serializer.rb +5 -3
  47. data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +0 -2
  48. data/lib/serialbench/serializers/toml/tomlib_serializer.rb +0 -2
  49. data/lib/serialbench/serializers/toml/tomlrb_serializer.rb +56 -0
  50. data/lib/serialbench/serializers/xml/base_xml_serializer.rb +4 -9
  51. data/lib/serialbench/serializers/xml/libxml_serializer.rb +0 -2
  52. data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +0 -2
  53. data/lib/serialbench/serializers/xml/oga_serializer.rb +0 -2
  54. data/lib/serialbench/serializers/xml/ox_serializer.rb +0 -2
  55. data/lib/serialbench/serializers/xml/rexml_serializer.rb +0 -2
  56. data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +5 -1
  57. data/lib/serialbench/serializers/yaml/syck_serializer.rb +59 -22
  58. data/lib/serialbench/serializers.rb +23 -6
  59. data/lib/serialbench/site_generator.rb +105 -0
  60. data/lib/serialbench/templates/assets/css/benchmark_report.css +535 -0
  61. data/lib/serialbench/templates/assets/css/format_based.css +526 -0
  62. data/lib/serialbench/templates/assets/css/themes.css +588 -0
  63. data/lib/serialbench/templates/assets/js/chart_helpers.js +381 -0
  64. data/lib/serialbench/templates/assets/js/dashboard.js +796 -0
  65. data/lib/serialbench/templates/assets/js/navigation.js +142 -0
  66. data/lib/serialbench/templates/base.liquid +49 -0
  67. data/lib/serialbench/templates/format_based.liquid +279 -0
  68. data/lib/serialbench/templates/partials/chart_section.liquid +4 -0
  69. data/lib/serialbench/version.rb +1 -1
  70. data/lib/serialbench.rb +2 -31
  71. data/serialbench.gemspec +4 -1
  72. metadata +86 -16
  73. data/config/ci.yml +0 -22
  74. data/config/full.yml +0 -30
  75. data/docker/run-benchmarks.sh +0 -356
  76. data/lib/serialbench/chart_generator.rb +0 -821
  77. data/lib/serialbench/result_formatter.rb +0 -182
  78. data/lib/serialbench/result_merger.rb +0 -1201
  79. data/lib/serialbench/serializers/xml/base_parser.rb +0 -69
  80. data/lib/serialbench/serializers/xml/libxml_parser.rb +0 -98
  81. data/lib/serialbench/serializers/xml/nokogiri_parser.rb +0 -111
  82. data/lib/serialbench/serializers/xml/oga_parser.rb +0 -85
  83. data/lib/serialbench/serializers/xml/ox_parser.rb +0 -64
  84. 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