serialbench 0.3.1 → 0.5.0

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/benchmark.yml +0 -24
  3. data/.github/workflows/rake.yml +15 -18
  4. data/Gemfile +1 -1
  5. data/config/benchmarks/full-json.yml +2 -2
  6. data/config/benchmarks/full-toml.yml +2 -2
  7. data/config/benchmarks/full-xml.yml +2 -2
  8. data/config/benchmarks/full-yaml.yml +2 -2
  9. data/config/benchmarks/full.yml +2 -2
  10. data/config/benchmarks/short.yml +2 -2
  11. data/lib/serialbench/benchmark_runner.rb +21 -296
  12. data/lib/serialbench/cli/benchmark_cli.rb +8 -81
  13. data/lib/serialbench/cli.rb +0 -5
  14. data/lib/serialbench/models/benchmark_config.rb +1 -1
  15. data/lib/serialbench/models/result_store.rb +0 -38
  16. data/lib/serialbench/models.rb +0 -5
  17. data/lib/serialbench/serializers/base_serializer.rb +0 -4
  18. data/lib/serialbench/serializers/json/base_json_serializer.rb +8 -20
  19. data/lib/serialbench/serializers/json/json_serializer.rb +0 -16
  20. data/lib/serialbench/serializers/json/oj_serializer.rb +2 -17
  21. data/lib/serialbench/serializers/json/yajl_serializer.rb +2 -2
  22. data/lib/serialbench/serializers/toml/base_toml_serializer.rb +8 -29
  23. data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +0 -16
  24. data/lib/serialbench/serializers/toml/tomlib_serializer.rb +0 -4
  25. data/lib/serialbench/serializers/toml/tomlrb_serializer.rb +2 -18
  26. data/lib/serialbench/serializers/xml/base_xml_serializer.rb +4 -16
  27. data/lib/serialbench/serializers/xml/leptris_serializer.rb +1 -5
  28. data/lib/serialbench/serializers/xml/libxml_serializer.rb +4 -9
  29. data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +3 -3
  30. data/lib/serialbench/serializers/xml/oga_serializer.rb +3 -5
  31. data/lib/serialbench/serializers/xml/ox_serializer.rb +2 -2
  32. data/lib/serialbench/serializers/xml/rexml_serializer.rb +5 -12
  33. data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +1 -9
  34. data/lib/serialbench/serializers/yaml/syck_serializer.rb +0 -8
  35. data/lib/serialbench/test_data.rb +315 -0
  36. data/lib/serialbench/version.rb +1 -1
  37. data/lib/serialbench.rb +2 -1
  38. data/serialbench.gemspec +1 -2
  39. metadata +6 -23
  40. data/.github/workflows/windows-debug.yml +0 -171
  41. data/lib/serialbench/cli/resultset_cli.rb +0 -301
  42. data/lib/serialbench/models/result_set.rb +0 -79
  43. data/lib/serialbench/site_generator.rb +0 -337
@@ -1,301 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'base_cli'
4
- require_relative '../models/result_set'
5
- require_relative '../site_generator'
6
-
7
- module Serialbench
8
- module Cli
9
- # CLI for managing benchmark resultsets (collections of runs)
10
- class ResultsetCli < BaseCli
11
- desc 'create NAME PATH', 'Create a new resultset'
12
- long_desc <<~DESC
13
- Create a new resultset (collection of benchmark runs).
14
-
15
- NAME is required and must be unique.
16
- PATH is the directory where the resultset will be created.
17
-
18
- Examples:
19
- serialbench resultset create performance-comparison results/sets/performance-comparison
20
- serialbench resultset create cross-platform-test results/sets/cross-platform-test
21
- DESC
22
- def create(resultset_name, resultset_path)
23
- Serialbench::Models::ResultStore.default
24
-
25
- # Check if resultset already exists
26
- definition_path = File.join(resultset_path, 'resultset.yaml')
27
-
28
- if File.exist?(definition_path)
29
- say "ResultSet at '#{resultset_path}' already exists", :yellow
30
- return unless yes?('Create anyway with timestamp suffix? (y/n)')
31
-
32
- resultset_path = "#{resultset_path}-#{generate_timestamp}"
33
- end
34
-
35
- # Create empty resultset using the new ResultSet model
36
- resultset = Serialbench::Models::ResultSet.new(
37
- name: resultset_name,
38
- description: "ResultSet for #{resultset_name} benchmarks",
39
- created_at: Time.now.utc.iso8601,
40
- updated_at: Time.now.utc.iso8601
41
- )
42
- resultset.save(resultset_path)
43
-
44
- say "✅ Created resultset: #{resultset_path}", :green
45
- say "Path: #{definition_path}", :cyan
46
- say "Use 'serialbench resultset add-result' to add benchmark runs", :white
47
- rescue StandardError => e
48
- say "Error creating resultset: #{e.message}", :red
49
- exit 1
50
- end
51
-
52
- desc 'add-result RESULTSET_PATH RESULT_PATH...', 'Add one or more runs to a resultset'
53
- long_desc <<~DESC
54
- Add one or more benchmark runs to a resultset.
55
-
56
- RESULTSET_PATH is the path to the resultset directory
57
- RESULT_PATH... accepts multiple result paths (supports shell expansion)
58
-
59
- Examples:
60
- # Add single result
61
- serialbench resultset add-result results/sets/weekly results/runs/my-run
62
-
63
- # Add multiple results explicitly
64
- serialbench resultset add-result results/sets/weekly results/runs/run1 results/runs/run2
65
-
66
- # Add multiple results with shell expansion
67
- serialbench resultset add-result results/sets/weekly artifacts/benchmark-results-*/
68
- serialbench resultset add-result results/sets/weekly results/runs/*
69
- DESC
70
- def add_result(resultset_path, *result_paths)
71
- if result_paths.empty?
72
- say '❌ Error: At least one result path must be provided', :red
73
- exit 1
74
- end
75
-
76
- resultset = Serialbench::Models::ResultSet.load(resultset_path)
77
-
78
- say "📦 Adding #{result_paths.size} result(s) to resultset", :cyan
79
- say "ResultSet: #{resultset_path}", :white
80
- say ''
81
-
82
- added_count = 0
83
- failed_count = 0
84
- skipped_count = 0
85
-
86
- result_paths.each_with_index do |result_path, index|
87
- say "#{index + 1}/#{result_paths.size} Processing: #{result_path}", :cyan
88
-
89
- # Find results.yaml in the path or subdirectories
90
- results_file = if File.exist?(File.join(result_path, 'results.yaml'))
91
- File.join(result_path, 'results.yaml')
92
- else
93
- Dir.glob(File.join(result_path, '**/results.yaml')).first
94
- end
95
-
96
- unless results_file
97
- say ' ⚠️ No results.yaml found - skipping', :yellow
98
- skipped_count += 1
99
- next
100
- end
101
-
102
- result_dir = File.dirname(results_file)
103
-
104
- begin
105
- resultset.add_result(result_dir)
106
- say ' ✅ Added successfully', :green
107
- added_count += 1
108
- rescue StandardError => e
109
- say " ❌ Failed: #{e.message}", :red
110
- failed_count += 1
111
- end
112
- say ''
113
- end
114
-
115
- resultset.save(resultset_path)
116
-
117
- say '=' * 60, :cyan
118
- say 'Summary:', :green
119
- say " Total processed: #{result_paths.size}", :white
120
- say " ✅ Successfully added: #{added_count}", :green
121
- say " ❌ Failed: #{failed_count}", :red if failed_count > 0
122
- say " ⚠️ Skipped: #{skipped_count}", :yellow if skipped_count > 0
123
- say " 📊 Total results in set: #{resultset.results.count}", :cyan
124
- say '=' * 60, :cyan
125
-
126
- exit 1 if failed_count > 0 && added_count == 0
127
- rescue StandardError => e
128
- say "❌ Error: #{e.message}", :red
129
- exit 1
130
- end
131
-
132
- desc 'remove-result RESULTSET_PATH RESULT_PATH', 'Remove a run from a resultset'
133
- long_desc <<~DESC
134
- Remove a benchmark run from a resultset.
135
-
136
- RESULTSET_PATH must be specified explicitly
137
- RESULT_PATH is the path to the run result directory
138
-
139
- Examples:
140
- serialbench resultset remove-result results/sets/performance-comparison results/runs/my-run-local-macos-arm64-ruby-3.3.8
141
- serialbench resultset remove-result results/sets/cross-platform-test results/runs/my-docker-run
142
- DESC
143
- def remove_result(resultset_path, _result_path)
144
- Serialbench::Models::ResultStore.default
145
-
146
- # Find the resultset
147
- resultset = Serialbench::Models::ResultSet.load(resultset_path)
148
- if resultset.nil?
149
- say "ResultSet '#{resultset_path}' not found", :red
150
- exit 1
151
- end
152
-
153
- # Remove run from resultset
154
- removed = resultset.remove_run(run_identifier)
155
- unless removed
156
- say "Run '#{run_identifier}' not found in resultset", :yellow
157
- say 'Available runs in resultset:', :white
158
- resultset.runs.each do |run_info|
159
- say " - #{run_info[:name]}", :white
160
- end
161
- return
162
- end
163
-
164
- resultset.save
165
-
166
- say '✅ Removed run from resultset', :green
167
- say "Run: #{run_identifier}", :cyan
168
- say "ResultSet: #{resultset_path}", :cyan
169
- say "Remaining runs in set: #{resultset.runs.length}", :white
170
- rescue StandardError => e
171
- say "Error removing run from resultset: #{e.message}", :red
172
- exit 1
173
- end
174
-
175
- desc 'build-site RESULTSET_PATH [OUTPUT_DIR]', 'Generate HTML site for a resultset'
176
- long_desc <<~DESC
177
- Generate an HTML site for a resultset (comparative analysis).
178
-
179
- RESULTSET_PATH must be specified explicitly
180
- OUTPUT_DIR defaults to _site/
181
-
182
- Examples:
183
- serialbench resultset build-site results/sets/performance-comparison
184
- serialbench resultset build-site results/sets/cross-platform-test output/
185
- DESC
186
- def build_site(resultset_path, output_dir = '_site')
187
- unless Dir.exist?(resultset_path)
188
- say "ResultSet directory not found: #{resultset_path}", :red
189
- say "Please create a resultset first using 'serialbench resultset create'", :white
190
- exit 1
191
- end
192
-
193
- resultset = Serialbench::Models::ResultSet.load(resultset_path)
194
-
195
- if resultset.results.empty?
196
- say "ResultSet '#{resultset_path}' contains no runs", :yellow
197
- say "Use 'serialbench resultset add-result' to add runs first", :white
198
- return
199
- end
200
-
201
- say "🏗️ Generating HTML site for resultset: #{resultset_path}", :green
202
- say "Runs in set: #{resultset.results.size}", :cyan
203
-
204
- # Use the unified site generator for resultsets
205
- Serialbench::SiteGenerator.generate_for_resultset(resultset, output_dir)
206
-
207
- say '✅ HTML site generated successfully!', :green
208
- say "Site location: #{output_dir}", :cyan
209
- say "Open: #{File.join(output_dir, 'index.html')}", :white
210
- rescue StandardError => e
211
- say "Error generating site: #{e.message}", :red
212
- say "Details: #{e.backtrace.first(3).join("\n")}", :red if options[:verbose]
213
- exit 1
214
- end
215
-
216
- desc 'export-data RESULTSET_PATH OUTPUT_JSON', 'Export a resultset as the site dashboard JSON payload'
217
- long_desc <<~DESC
218
- Write the aggregated dashboard payload (combined_results, environments,
219
- metadata) for a resultset as JSON. This is the data contract consumed
220
- by the Astro site in site/.
221
-
222
- Optionally also export the raw per-environment YAML files and the
223
- complete resultset YAML for download links.
224
-
225
- Examples:
226
- serialbench resultset export-data results/sets/weekly site/src/data/sample.json --raw-data-dir site/public/data
227
- DESC
228
- method_option 'raw-data-dir', type: :string,
229
- desc: 'Also write per-environment YAML and resultset.yaml to this directory'
230
- def export_data(resultset_path, output_json)
231
- resultset = Serialbench::Models::ResultSet.load(resultset_path)
232
-
233
- if resultset.results.empty?
234
- say "ResultSet '#{resultset_path}' contains no runs", :yellow
235
- say "Use 'serialbench resultset add-result' to add runs first", :white
236
- exit 1
237
- end
238
-
239
- payload = Serialbench::SiteGenerator.resultset_payload(resultset)
240
- output_dir = File.dirname(File.expand_path(output_json))
241
- FileUtils.mkdir_p(output_dir)
242
- File.write(output_json, JSON.pretty_generate(payload))
243
-
244
- say "✅ Dashboard payload exported: #{output_json}", :green
245
- say "Environments: #{payload['environments'].size}", :cyan
246
-
247
- if options['raw-data-dir']
248
- Serialbench::SiteGenerator.export_raw_data(resultset, options['raw-data-dir'])
249
- say "📦 Raw YAML exported to: #{options['raw-data-dir']}", :cyan
250
- end
251
- rescue StandardError => e
252
- say "Error exporting data: #{e.message}", :red
253
- exit 1
254
- end
255
-
256
- desc 'list', 'List all available resultsets'
257
- long_desc <<~DESC
258
- List all resultsets in the results/sets/ directory.
259
-
260
- Shows resultset names, number of runs, and timestamps.
261
- DESC
262
- def list
263
- ensure_results_directory
264
-
265
- begin
266
- store = Serialbench::Models::ResultStore.default
267
- resultsets = store.find_resultsets
268
-
269
- if resultsets.empty?
270
- say 'No resultsets found', :yellow
271
- say "Use 'serialbench resultset create' to create a resultset", :white
272
- return
273
- end
274
-
275
- say 'Available ResultSets:', :green
276
- say '=' * 50, :green
277
-
278
- resultsets.each do |resultset|
279
- say "📁 #{resultset.name}", :cyan
280
- say " Runs: #{resultset.runs.length}", :white
281
- say " Created: #{resultset.metadata[:timestamp]}", :white
282
- say " Path: #{resultset.directory}", :white
283
-
284
- if resultset.runs.any?
285
- say ' Contains:', :white
286
- resultset.runs.first(3).each do |run_info|
287
- say " - #{run_info[:name]}", :white
288
- end
289
- say " ... and #{resultset.runs.length - 3} more" if resultset.runs.length > 3
290
- end
291
-
292
- say ''
293
- end
294
- rescue StandardError => e
295
- say "Error listing resultsets: #{e.message}", :red
296
- exit 1
297
- end
298
- end
299
- end
300
- end
301
- end
@@ -1,79 +0,0 @@
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