serialbench 0.1.2 → 0.2.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/benchmark.yml +294 -229
  3. data/.github/workflows/rake.yml +11 -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 +15 -1
  9. data/README.adoc +41 -0
  10. data/data/schemas/result.yml +29 -0
  11. data/docs/PLATFORM_VALIDATION_FIX.md +79 -0
  12. data/docs/SYCK_YAML_FIX.md +91 -0
  13. data/docs/WEBSITE_COMPLETION_PLAN.md +440 -0
  14. data/docs/WINDOWS_LIBXML_FIX.md +136 -0
  15. data/docs/WINDOWS_SETUP.md +122 -0
  16. data/lib/serialbench/benchmark_runner.rb +3 -3
  17. data/lib/serialbench/cli/benchmark_cli.rb +74 -1
  18. data/lib/serialbench/cli/environment_cli.rb +3 -3
  19. data/lib/serialbench/cli/resultset_cli.rb +112 -26
  20. data/lib/serialbench/cli/ruby_build_cli.rb +75 -88
  21. data/lib/serialbench/cli/validate_cli.rb +88 -0
  22. data/lib/serialbench/cli.rb +6 -2
  23. data/lib/serialbench/config_manager.rb +15 -26
  24. data/lib/serialbench/models/benchmark_config.rb +12 -0
  25. data/lib/serialbench/models/benchmark_result.rb +39 -3
  26. data/lib/serialbench/models/environment_config.rb +3 -2
  27. data/lib/serialbench/models/platform.rb +61 -6
  28. data/lib/serialbench/models/result.rb +28 -1
  29. data/lib/serialbench/models/result_set.rb +8 -0
  30. data/lib/serialbench/ruby_build_manager.rb +19 -23
  31. data/lib/serialbench/runners/asdf_runner.rb +1 -1
  32. data/lib/serialbench/runners/docker_runner.rb +2 -4
  33. data/lib/serialbench/runners/local_runner.rb +71 -0
  34. data/lib/serialbench/serializers/base_serializer.rb +1 -1
  35. data/lib/serialbench/serializers/json/rapidjson_serializer.rb +1 -5
  36. data/lib/serialbench/serializers/toml/base_toml_serializer.rb +0 -2
  37. data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +1 -1
  38. data/lib/serialbench/serializers/toml/tomlib_serializer.rb +1 -1
  39. data/lib/serialbench/serializers/xml/base_xml_serializer.rb +6 -1
  40. data/lib/serialbench/serializers/xml/leptris_serializer.rb +167 -0
  41. data/lib/serialbench/serializers/xml/libxml_serializer.rb +13 -8
  42. data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +11 -2
  43. data/lib/serialbench/serializers/xml/oga_serializer.rb +8 -8
  44. data/lib/serialbench/serializers/xml/ox_serializer.rb +2 -2
  45. data/lib/serialbench/serializers/xml/rexml_serializer.rb +3 -3
  46. data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +1 -3
  47. data/lib/serialbench/serializers/yaml/psych_serializer.rb +1 -5
  48. data/lib/serialbench/serializers/yaml/syck_serializer.rb +1 -1
  49. data/lib/serialbench/serializers.rb +5 -3
  50. data/lib/serialbench/site_generator.rb +234 -2
  51. data/lib/serialbench/templates/assets/css/format_based.css +1 -53
  52. data/lib/serialbench/templates/assets/css/themes.css +5 -4
  53. data/lib/serialbench/templates/assets/js/chart_helpers.js +44 -14
  54. data/lib/serialbench/templates/assets/js/dashboard.js +14 -15
  55. data/lib/serialbench/templates/format_based.liquid +480 -252
  56. data/lib/serialbench/version.rb +1 -1
  57. data/lib/serialbench/yaml_validator.rb +36 -0
  58. data/serialbench.gemspec +11 -2
  59. data/site/.gitignore +5 -0
  60. data/site/README.md +32 -0
  61. data/site/astro.config.mjs +12 -0
  62. data/site/package-lock.json +6160 -0
  63. data/site/package.json +22 -0
  64. data/site/public/favicon.svg +4 -0
  65. data/site/scripts/gen-sample-data.mjs +154 -0
  66. data/site/src/components/AvailabilityMatrix.astro +61 -0
  67. data/site/src/components/CalibratedLeaderboard.vue +134 -0
  68. data/site/src/components/CitationBox.vue +50 -0
  69. data/site/src/components/DashboardConsole.vue +193 -0
  70. data/site/src/components/FeaturesTable.astro +97 -0
  71. data/site/src/components/MemoryPanel.vue +44 -0
  72. data/site/src/components/OpsChart.vue +135 -0
  73. data/site/src/config.ts +18 -0
  74. data/site/src/layouts/Layout.astro +85 -0
  75. data/site/src/lib/channels.ts +24 -0
  76. data/site/src/lib/dashboard.ts +269 -0
  77. data/site/src/pages/index.astro +44 -0
  78. data/site/src/pages/library/[slug].astro +128 -0
  79. data/site/src/pages/methodology.astro +55 -0
  80. data/site/src/styles/global.css +103 -0
  81. data/site/tsconfig.json +5 -0
  82. metadata +58 -23
  83. data/.github/workflows/ci.yml +0 -74
  84. data/.github/workflows/docker.yml +0 -272
@@ -41,7 +41,7 @@ module Serialbench
41
41
 
42
42
  indent = options.fetch(:indent, 0)
43
43
  output = String.new
44
- if indent > 0
44
+ if indent.positive?
45
45
  document.write(output, indent)
46
46
  else
47
47
  document.write(output)
@@ -120,7 +120,7 @@ module Serialbench
120
120
  @result = nil
121
121
  end
122
122
 
123
- def start_element(uri, localname, qname, attributes)
123
+ def start_element(_uri, _localname, qname, attributes)
124
124
  @elements_processed += 1
125
125
  @block&.call(:start_element, { name: qname, attributes: attributes })
126
126
  end
@@ -132,7 +132,7 @@ module Serialbench
132
132
  @block&.call(:text, text)
133
133
  end
134
134
 
135
- def end_element(uri, localname, qname)
135
+ def end_element(_uri, _localname, qname)
136
136
  @block&.call(:end_element, { name: qname })
137
137
  end
138
138
  end
@@ -16,9 +16,7 @@ module Serialbench
16
16
  end
17
17
 
18
18
  def features
19
- features = %w[parsing generation]
20
- features << 'streaming' if supports_streaming?
21
- features
19
+ { streaming: supports_streaming? }
22
20
  end
23
21
 
24
22
  # Default YAML generation options
@@ -31,15 +31,11 @@ module Serialbench
31
31
  end
32
32
  end
33
33
 
34
- def generate(object, options = {})
34
+ def generate(object, _options = {})
35
35
  require 'psych'
36
36
  Psych.dump(object)
37
37
  end
38
38
 
39
- def features
40
- %w[parsing generation built-in]
41
- end
42
-
43
39
  private
44
40
 
45
41
  def require_library(library_name)
@@ -53,7 +53,7 @@ module Serialbench
53
53
  end
54
54
  end
55
55
 
56
- def generate(object, options = {})
56
+ def generate(object, _options = {})
57
57
  return nil unless available?
58
58
 
59
59
  begin
@@ -10,6 +10,7 @@ require_relative 'serializers/xml/ox_serializer'
10
10
  require_relative 'serializers/xml/nokogiri_serializer'
11
11
  require_relative 'serializers/xml/oga_serializer'
12
12
  require_relative 'serializers/xml/libxml_serializer'
13
+ require_relative 'serializers/xml/leptris_serializer'
13
14
 
14
15
  # JSON Serializers
15
16
  require_relative 'serializers/json/base_json_serializer'
@@ -38,7 +39,8 @@ module Serialbench
38
39
  Xml::OxSerializer,
39
40
  Xml::NokogiriSerializer,
40
41
  Xml::OgaSerializer,
41
- Xml::LibxmlSerializer
42
+ Xml::LibxmlSerializer,
43
+ Xml::LeptrisSerializer
42
44
  ],
43
45
  json: [
44
46
  Json::JsonSerializer,
@@ -80,11 +82,11 @@ module Serialbench
80
82
  end
81
83
 
82
84
  def self.available_for_format(format)
83
- for_format(format).select { |serializer_singleton| serializer_singleton.available? }
85
+ for_format(format).select(&:available?)
84
86
  end
85
87
 
86
88
  def self.available
87
- all.select { |serializer_singleton| serializer_singleton.available? }
89
+ all.select(&:available?)
88
90
  end
89
91
  end
90
92
  end
@@ -30,22 +30,73 @@ module Serialbench
30
30
  generator.generate_site
31
31
  end
32
32
 
33
+ def self.resultset_payload(resultset)
34
+ new(output_path: '/dev/null', resultset: resultset).resultset_payload
35
+ end
36
+
37
+ def self.export_raw_data(resultset, data_dir)
38
+ new(output_path: '/dev/null', resultset: resultset).export_raw_data_to(data_dir)
39
+ end
40
+
41
+ def resultset_payload
42
+ transform_resultset_for_dashboard(@resultset)
43
+ .merge('libraries' => libraries_information(@resultset))
44
+ end
45
+
46
+ # Static capability matrix (adapter declarations) plus the versions
47
+ # actually measured in this resultset.
48
+ def libraries_information(resultset)
49
+ versions = {}
50
+ resultset.results.each do |result|
51
+ serializers = result.benchmark_result.serializers || []
52
+ serializers.each { |si| versions[si.name] ||= si.version }
53
+ end
54
+
55
+ Serialbench::Serializers.all.filter_map do |adapter|
56
+ next if adapter.name == 'syck'
57
+
58
+ features = adapter.features
59
+ next if features.is_a?(Array)
60
+
61
+ {
62
+ 'name' => adapter.name,
63
+ 'format' => adapter.format.to_s,
64
+ 'version' => versions[adapter.name] || 'unknown',
65
+ 'features' => features.transform_values { |v| v == true }
66
+ }
67
+ end
68
+ end
69
+
70
+ def export_raw_data_to(data_dir)
71
+ FileUtils.mkdir_p(data_dir)
72
+ export_resultset(@resultset, data_dir)
73
+ end
74
+
33
75
  def generate_site
34
76
  target_name = @result ? @result.environment_config.name : @resultset.name
35
- data = @result ? @result.to_json : @resultset.to_json
36
77
 
37
78
  puts "🏗️ Generating HTML site for #{@result ? 'run' : 'resultset'}: #{target_name}"
38
79
  puts "Output: #{@output_path}"
39
80
 
81
+ # Transform data for dashboard.js compatibility
82
+ data = if @result
83
+ transform_result_for_dashboard(@result)
84
+ else
85
+ transform_resultset_for_dashboard(@resultset)
86
+ end
87
+
40
88
  prepare_output_directory
41
89
  render_site(
42
90
  {
43
- 'data' => data,
91
+ 'data' => JSON.generate(data),
44
92
  'kind' => @result ? 'run' : 'resultset'
45
93
  },
46
94
  'format_based.liquid'
47
95
  )
48
96
 
97
+ # Export raw data files for download
98
+ export_raw_data
99
+
49
100
  puts "✅ Site generated successfully at: #{@output_path}"
50
101
  @output_path
51
102
  end
@@ -101,5 +152,186 @@ module Serialbench
101
152
 
102
153
  FileUtils.cp_r(assets_source, assets_dest)
103
154
  end
155
+
156
+ # Transform a single Result into dashboard-compatible format
157
+ # Dashboard expects: { combined_results: {...}, environments: {...}, metadata: {...} }
158
+ def transform_result_for_dashboard(result)
159
+ env_key = "env-#{result.platform.ruby_version}"
160
+
161
+ # Build combined_results structure
162
+ combined_results = build_combined_results(result, env_key)
163
+
164
+ # Build environments structure
165
+ environments = {
166
+ env_key => {
167
+ 'ruby_version' => result.platform.ruby_version,
168
+ 'ruby_platform' => result.platform.ruby_platform || "#{result.platform.os}-#{result.platform.arch}",
169
+ 'os' => result.platform.os,
170
+ 'arch' => result.platform.arch,
171
+ 'source_file' => result.metadata.environment_config_path,
172
+ 'timestamp' => result.metadata.created_at
173
+ }
174
+ }
175
+
176
+ {
177
+ 'combined_results' => combined_results,
178
+ 'environments' => environments,
179
+ 'metadata' => {
180
+ 'generated_at' => Time.now.iso8601
181
+ }
182
+ }
183
+ end
184
+
185
+ # Environment identity for the site: use the CI platform_string
186
+ # ("macos-26-ruby-3.4") when it is runner-shaped, so each runner label is
187
+ # its own environment; fall back to os-arch for other sources (docker,
188
+ # local runs, older resultsets).
189
+ def env_identity(result)
190
+ platform_string = result.platform.platform_string
191
+ ruby = result.platform.ruby_version
192
+ return platform_string if platform_string&.end_with?("-ruby-#{ruby}")
193
+
194
+ "#{result.platform.os}-#{result.platform.arch}-ruby-#{ruby}"
195
+ end
196
+
197
+ # Transform a ResultSet (collection of Results) into dashboard-compatible format
198
+ # Combines all results into a single dashboard structure
199
+ def transform_resultset_for_dashboard(resultset)
200
+ combined_results = {}
201
+ environments = {}
202
+
203
+ resultset.results.each do |result|
204
+ # Create unique env key for this result
205
+ env_key = env_identity(result)
206
+
207
+ # Merge this result's data into combined_results
208
+ result_combined = build_combined_results(result, env_key)
209
+ merge_combined_results!(combined_results, result_combined)
210
+
211
+ # Add environment info
212
+ environments[env_key] = {
213
+ 'ruby_version' => result.platform.ruby_version,
214
+ 'ruby_platform' => result.platform.ruby_platform || "#{result.platform.os}-#{result.platform.arch}",
215
+ 'os' => result.platform.os,
216
+ 'arch' => result.platform.arch,
217
+ 'source_file' => result.metadata.environment_config_path,
218
+ 'timestamp' => result.metadata.created_at
219
+ }
220
+ end
221
+
222
+ {
223
+ 'combined_results' => combined_results,
224
+ 'environments' => environments,
225
+ 'metadata' => {
226
+ 'resultset_name' => resultset.name,
227
+ 'resultset_description' => resultset.description,
228
+ 'total_runs' => resultset.results.size,
229
+ 'generated_at' => Time.now.iso8601
230
+ }
231
+ }
232
+ end
233
+
234
+ # Deep merge results from multiple runs
235
+ def merge_combined_results!(target, source)
236
+ source.each do |operation, sizes|
237
+ target[operation] ||= {}
238
+ sizes.each do |size, formats|
239
+ target[operation][size] ||= {}
240
+ formats.each do |format, serializers|
241
+ target[operation][size][format] ||= {}
242
+ serializers.each do |serializer, envs|
243
+ target[operation][size][format][serializer] ||= {}
244
+ target[operation][size][format][serializer].merge!(envs)
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
250
+
251
+ def build_combined_results(result, env_key)
252
+ combined = {}
253
+
254
+ %w[parsing generation streaming].each do |operation|
255
+ combined[operation] = {}
256
+
257
+ operation_results = result.benchmark_result.send(operation)
258
+ next if operation_results.nil? || operation_results.empty?
259
+
260
+ operation_results.each do |perf|
261
+ size = perf.data_size
262
+ format = perf.format
263
+ serializer = perf.adapter
264
+
265
+ combined[operation][size] ||= {}
266
+ combined[operation][size][format] ||= {}
267
+ combined[operation][size][format][serializer] ||= {}
268
+ combined[operation][size][format][serializer][env_key] = {
269
+ 'iterations_per_second' => perf.iterations_per_second,
270
+ 'time_per_iteration' => perf.time_per_iteration
271
+ }
272
+ end
273
+ end
274
+
275
+ # Handle memory separately
276
+ if result.benchmark_result.memory && !result.benchmark_result.memory.empty?
277
+ combined['memory'] = {}
278
+
279
+ result.benchmark_result.memory.each do |mem|
280
+ size = mem.data_size
281
+ format = mem.format
282
+ serializer = mem.adapter
283
+
284
+ combined['memory'][size] ||= {}
285
+ combined['memory'][size][format] ||= {}
286
+ combined['memory'][size][format][serializer] ||= {}
287
+ combined['memory'][size][format][serializer][env_key] = {
288
+ 'allocated_memory' => mem.allocated_memory,
289
+ 'retained_memory' => mem.retained_memory
290
+ }
291
+ end
292
+ end
293
+
294
+ combined
295
+ end
296
+
297
+ # Export raw data files for download
298
+ def export_raw_data
299
+ data_dir = File.join(@output_path, 'data')
300
+ FileUtils.mkdir_p(data_dir)
301
+
302
+ if @result
303
+ # Export single result
304
+ export_single_result(@result, data_dir)
305
+ elsif @resultset
306
+ # Export all results in resultset plus the complete resultset
307
+ export_resultset(@resultset, data_dir)
308
+ end
309
+
310
+ puts "📦 Raw data files exported to: #{data_dir}"
311
+ end
312
+
313
+ def export_single_result(result, data_dir)
314
+ # Create filename based on platform info
315
+ env_key = env_identity(result)
316
+ filename = "#{env_key}.yaml"
317
+ filepath = File.join(data_dir, filename)
318
+
319
+ # Write result as YAML
320
+ File.write(filepath, result.to_yaml)
321
+ puts " 📄 Exported: #{filename}"
322
+ end
323
+
324
+ def export_resultset(resultset, data_dir)
325
+ # Export each individual result
326
+ resultset.results.each do |result|
327
+ export_single_result(result, data_dir)
328
+ end
329
+
330
+ # Export complete resultset
331
+ resultset_filename = 'resultset.yaml'
332
+ resultset_filepath = File.join(data_dir, resultset_filename)
333
+ File.write(resultset_filepath, resultset.to_yaml)
334
+ puts " 📄 Exported: #{resultset_filename} (complete dataset)"
335
+ end
104
336
  end
105
337
  end
@@ -13,59 +13,7 @@
13
13
  flex-wrap: wrap;
14
14
  }
15
15
 
16
- .format-tab {
17
- padding: 12px 24px;
18
- border: none;
19
- background: var(--card-bg);
20
- color: var(--text-color);
21
- border-radius: 25px;
22
- cursor: pointer;
23
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
24
- font-weight: 600;
25
- font-size: 0.95em;
26
- box-shadow: var(--shadow-soft);
27
- border: 2px solid transparent;
28
- position: relative;
29
- overflow: hidden;
30
- }
31
-
32
- .format-tab::before {
33
- content: '';
34
- position: absolute;
35
- top: 0;
36
- left: -100%;
37
- width: 100%;
38
- height: 100%;
39
- background: var(--gradient-accent);
40
- transition: left 0.3s ease;
41
- z-index: 0;
42
- }
43
-
44
- .format-tab:hover {
45
- transform: translateY(-3px);
46
- box-shadow: var(--shadow-medium);
47
- border-color: var(--secondary-color);
48
- }
49
-
50
- .format-tab:hover::before {
51
- left: 0;
52
- }
53
-
54
- .format-tab > * {
55
- position: relative;
56
- z-index: 1;
57
- }
58
-
59
- .format-tab.active {
60
- background: var(--gradient-primary);
61
- color: white;
62
- transform: translateY(-2px);
63
- box-shadow: var(--shadow-medium);
64
- }
65
-
66
- .format-tab.active::before {
67
- display: none;
68
- }
16
+ /* Format tab styles are defined in themes.css */
69
17
 
70
18
  /* Format Content */
71
19
  .format-content {
@@ -259,7 +259,7 @@ body {
259
259
 
260
260
  .format-tab {
261
261
  padding: var(--space-sm) var(--space-lg);
262
- background: transparent;
262
+ background: var(--bg-card);
263
263
  border: 1px solid var(--border-primary);
264
264
  border-radius: var(--radius-md);
265
265
  color: var(--text-secondary);
@@ -271,14 +271,15 @@ body {
271
271
  letter-spacing: 0.05em;
272
272
  }
273
273
 
274
- .format-tab:hover {
275
- background: var(--bg-hover);
274
+ .format-tab:hover:not(.active) {
275
+ background: var(--bg-tertiary);
276
276
  color: var(--text-primary);
277
+ border-color: var(--border-secondary);
277
278
  transform: translateY(-1px);
278
279
  }
279
280
 
280
281
  .format-tab.active {
281
- background: var(--gradient-primary);
282
+ background: var(--accent-primary);
282
283
  border-color: var(--accent-primary);
283
284
  color: white;
284
285
  box-shadow: var(--shadow-md);
@@ -172,9 +172,15 @@ function createPerformanceChart(canvasId, title, data, metric, environments) {
172
172
  console.log(`🌍 Environments:`, environments);
173
173
  console.log(`📊 Raw data:`, data);
174
174
 
175
+ // Get theme-aware colors
176
+ const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark';
177
+ const textColor = getComputedStyle(document.documentElement).getPropertyValue('--text-primary').trim() || (isDarkMode ? '#F8FAFC' : '#0F172A');
178
+ const gridColor = getComputedStyle(document.documentElement).getPropertyValue('--border-primary').trim() || (isDarkMode ? '#475569' : '#E2E8F0');
179
+
180
+ // High contrast color palette
175
181
  const colors = [
176
- '#6366f1', '#8b5cf6', '#06b6d4', '#10b981', '#f59e0b',
177
- '#ef4444', '#ec4899', '#84cc16', '#f97316', '#6b7280'
182
+ '#F97316', '#3B82F6', '#10B981', '#EF4444', '#8B5CF6',
183
+ '#F59E0B', '#EC4899', '#06B6D4', '#84CC16', '#6366F1'
178
184
  ];
179
185
 
180
186
  const datasets = serializers.map((serializer, index) => {
@@ -224,14 +230,15 @@ function createPerformanceChart(canvasId, title, data, metric, environments) {
224
230
  display: true,
225
231
  text: title,
226
232
  font: { size: 16, weight: 'bold' },
227
- color: '#374151'
233
+ color: textColor
228
234
  },
229
235
  legend: {
230
236
  position: 'top',
231
237
  labels: {
232
238
  usePointStyle: true,
233
239
  padding: 20,
234
- font: { size: 12 }
240
+ font: { size: 12 },
241
+ color: textColor
235
242
  }
236
243
  }
237
244
  },
@@ -241,17 +248,25 @@ function createPerformanceChart(canvasId, title, data, metric, environments) {
241
248
  title: {
242
249
  display: true,
243
250
  text: 'Iterations per Second',
244
- font: { size: 12, weight: 'bold' }
251
+ font: { size: 12, weight: 'bold' },
252
+ color: textColor
253
+ },
254
+ ticks: {
255
+ color: textColor
245
256
  },
246
257
  grid: {
247
- color: '#f3f4f6'
258
+ color: gridColor
248
259
  }
249
260
  },
250
261
  x: {
251
262
  title: {
252
263
  display: true,
253
264
  text: 'Ruby Versions',
254
- font: { size: 12, weight: 'bold' }
265
+ font: { size: 12, weight: 'bold' },
266
+ color: textColor
267
+ },
268
+ ticks: {
269
+ color: textColor
255
270
  },
256
271
  grid: {
257
272
  display: false
@@ -294,9 +309,15 @@ function createMemoryChart(canvasId, title, data, environments) {
294
309
  return;
295
310
  }
296
311
 
312
+ // Get theme-aware colors
313
+ const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark';
314
+ const textColor = getComputedStyle(document.documentElement).getPropertyValue('--text-primary').trim() || (isDarkMode ? '#F8FAFC' : '#0F172A');
315
+ const gridColor = getComputedStyle(document.documentElement).getPropertyValue('--border-primary').trim() || (isDarkMode ? '#475569' : '#E2E8F0');
316
+
317
+ // High contrast color palette
297
318
  const colors = [
298
- '#6366f1', '#8b5cf6', '#06b6d4', '#10b981', '#f59e0b',
299
- '#ef4444', '#ec4899', '#84cc16', '#f97316', '#6b7280'
319
+ '#F97316', '#3B82F6', '#10B981', '#EF4444', '#8B5CF6',
320
+ '#F59E0B', '#EC4899', '#06B6D4', '#84CC16', '#6366F1'
300
321
  ];
301
322
 
302
323
  const datasets = serializers.map((serializer, index) => {
@@ -338,14 +359,15 @@ function createMemoryChart(canvasId, title, data, environments) {
338
359
  display: true,
339
360
  text: title,
340
361
  font: { size: 16, weight: 'bold' },
341
- color: '#374151'
362
+ color: textColor
342
363
  },
343
364
  legend: {
344
365
  position: 'top',
345
366
  labels: {
346
367
  usePointStyle: true,
347
368
  padding: 20,
348
- font: { size: 12 }
369
+ font: { size: 12 },
370
+ color: textColor
349
371
  }
350
372
  }
351
373
  },
@@ -355,17 +377,25 @@ function createMemoryChart(canvasId, title, data, environments) {
355
377
  title: {
356
378
  display: true,
357
379
  text: 'Memory Usage (MB)',
358
- font: { size: 12, weight: 'bold' }
380
+ font: { size: 12, weight: 'bold' },
381
+ color: textColor
382
+ },
383
+ ticks: {
384
+ color: textColor
359
385
  },
360
386
  grid: {
361
- color: '#f3f4f6'
387
+ color: gridColor
362
388
  }
363
389
  },
364
390
  x: {
365
391
  title: {
366
392
  display: true,
367
393
  text: 'Ruby Versions',
368
- font: { size: 12, weight: 'bold' }
394
+ font: { size: 12, weight: 'bold' },
395
+ color: textColor
396
+ },
397
+ ticks: {
398
+ color: textColor
369
399
  },
370
400
  grid: {
371
401
  display: false
@@ -108,7 +108,9 @@ class SerialBenchDashboard {
108
108
 
109
109
  if (this.data.environments) {
110
110
  Object.values(this.data.environments).forEach(env => {
111
- platforms.add(env.ruby_platform);
111
+ // Use os-arch combination instead of ruby_platform
112
+ const platformKey = `${env.os}-${env.arch}`;
113
+ platforms.add(platformKey);
112
114
  rubyVersions.add(env.ruby_version);
113
115
  });
114
116
  }
@@ -120,7 +122,7 @@ class SerialBenchDashboard {
120
122
  Array.from(platforms).sort().forEach(platform => {
121
123
  const option = document.createElement('option');
122
124
  option.value = platform;
123
- option.textContent = this.formatPlatformName(platform);
125
+ option.textContent = platform;
124
126
  platformSelect.appendChild(option);
125
127
  });
126
128
  }
@@ -148,15 +150,6 @@ class SerialBenchDashboard {
148
150
  }
149
151
  }
150
152
 
151
- formatPlatformName(platform) {
152
- const platformMap = {
153
- 'aarch64-linux': 'ARM64 Linux',
154
- 'x86_64-linux': 'x64 Linux',
155
- 'x86_64-darwin': 'x64 macOS',
156
- 'aarch64-darwin': 'ARM64 macOS'
157
- };
158
- return platformMap[platform] || platform;
159
- }
160
153
 
161
154
  setupFilterEventListeners() {
162
155
  // Platform filter
@@ -213,7 +206,9 @@ class SerialBenchDashboard {
213
206
  // Set all platforms and versions by default
214
207
  if (this.data.environments) {
215
208
  Object.values(this.data.environments).forEach(env => {
216
- this.filters.platforms.add(env.ruby_platform);
209
+ // Use os-arch combination instead of ruby_platform
210
+ const platformKey = `${env.os}-${env.arch}`;
211
+ this.filters.platforms.add(platformKey);
217
212
  this.filters.rubyVersions.add(env.ruby_version);
218
213
  });
219
214
  }
@@ -336,8 +331,10 @@ class SerialBenchDashboard {
336
331
  shouldIncludeEnvironment(env) {
337
332
  if (!env) return false;
338
333
 
334
+ // Use os-arch combination for platform matching
335
+ const platformKey = `${env.os}-${env.arch}`;
339
336
  const platformMatch = this.filters.platforms.size === 0 ||
340
- this.filters.platforms.has(env.ruby_platform);
337
+ this.filters.platforms.has(platformKey);
341
338
  const versionMatch = this.filters.rubyVersions.size === 0 ||
342
339
  this.filters.rubyVersions.has(env.ruby_version);
343
340
 
@@ -634,8 +631,10 @@ class SerialBenchDashboard {
634
631
  // Show all environments, but apply platform and version filters only (not format)
635
632
  const filteredEnvs = Object.entries(this.data.environments)
636
633
  .filter(([key, env]) => {
634
+ // Use os-arch combination for platform matching
635
+ const platformKey = `${env.os}-${env.arch}`;
637
636
  const platformMatch = this.filters.platforms.size === 0 ||
638
- this.filters.platforms.has(env.ruby_platform);
637
+ this.filters.platforms.has(platformKey);
639
638
  const versionMatch = this.filters.rubyVersions.size === 0 ||
640
639
  this.filters.rubyVersions.has(env.ruby_version);
641
640
  return platformMatch && versionMatch;
@@ -649,7 +648,7 @@ class SerialBenchDashboard {
649
648
  container.innerHTML = filteredEnvs.map(([key, env]) => `
650
649
  <div class="environment-card fade-in-up">
651
650
  <h3 class="environment-card-title">
652
- Ruby ${env.ruby_version} on ${this.formatPlatformName(env.ruby_platform)}
651
+ Ruby ${env.ruby_version} on ${env.os}-${env.arch}
653
652
  </h3>
654
653
  <p><strong>Source:</strong> ${env.source_file ? env.source_file.split('/').pop() : 'Unknown'}</p>
655
654
  <p><strong>Timestamp:</strong> ${new Date(env.timestamp).toLocaleString()}</p>