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