serialbench 0.1.2 → 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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/benchmark.yml +273 -228
  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 +13 -1
  9. data/README.adoc +36 -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 +72 -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 +56 -4
  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 -1
  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/libxml_serializer.rb +4 -8
  40. data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +2 -2
  41. data/lib/serialbench/serializers/xml/oga_serializer.rb +4 -8
  42. data/lib/serialbench/serializers/xml/ox_serializer.rb +2 -2
  43. data/lib/serialbench/serializers/xml/rexml_serializer.rb +3 -3
  44. data/lib/serialbench/serializers/yaml/psych_serializer.rb +1 -1
  45. data/lib/serialbench/serializers/yaml/syck_serializer.rb +1 -1
  46. data/lib/serialbench/serializers.rb +2 -2
  47. data/lib/serialbench/site_generator.rb +180 -2
  48. data/lib/serialbench/templates/assets/css/format_based.css +1 -53
  49. data/lib/serialbench/templates/assets/css/themes.css +5 -4
  50. data/lib/serialbench/templates/assets/js/chart_helpers.js +44 -14
  51. data/lib/serialbench/templates/assets/js/dashboard.js +14 -15
  52. data/lib/serialbench/templates/format_based.liquid +480 -252
  53. data/lib/serialbench/version.rb +1 -1
  54. data/lib/serialbench/yaml_validator.rb +36 -0
  55. data/serialbench.gemspec +11 -2
  56. metadata +34 -23
  57. data/.github/workflows/ci.yml +0 -74
  58. data/.github/workflows/docker.yml +0 -272
@@ -27,7 +27,7 @@ module Serialbench
27
27
  RapidJSON.parse(json_string)
28
28
  end
29
29
 
30
- def generate(object, options = {})
30
+ def generate(object, _options = {})
31
31
  require 'rapidjson'
32
32
  RapidJSON.dump(object)
33
33
  end
@@ -59,8 +59,6 @@ module Serialbench
59
59
  raise NotImplementedError, 'Subclasses must implement #library_require_name'
60
60
  end
61
61
 
62
- public
63
-
64
62
  # Check if the TOML library is available
65
63
  def available?
66
64
  return @available if defined?(@available)
@@ -27,7 +27,7 @@ module Serialbench
27
27
  TomlRB.parse(toml_string)
28
28
  end
29
29
 
30
- def generate(object, options = {})
30
+ def generate(object, _options = {})
31
31
  require 'toml-rb'
32
32
  TomlRB.dump(object)
33
33
  end
@@ -24,7 +24,7 @@ module Serialbench
24
24
  # TOML doesn't typically support streaming parsing
25
25
  # Parse the entire document and yield it
26
26
  result = parse(toml_string)
27
- block&.call(result) if block
27
+ block&.call(result)
28
28
  1 # Return 1 document processed
29
29
  end
30
30
 
@@ -58,26 +58,22 @@ module Serialbench
58
58
  def build_xml_from_data(data, name = 'root')
59
59
  require 'libxml'
60
60
 
61
+ element = LibXML::XML::Node.new(name.to_s)
61
62
  case data
62
63
  when Hash
63
- element = LibXML::XML::Node.new(name.to_s)
64
64
  data.each do |key, value|
65
65
  child = build_xml_from_data(value, key.to_s)
66
66
  element << child
67
67
  end
68
- element
69
68
  when Array
70
- element = LibXML::XML::Node.new(name.to_s)
71
69
  data.each_with_index do |item, index|
72
70
  child = build_xml_from_data(item, "item_#{index}")
73
71
  element << child
74
72
  end
75
- element
76
73
  else
77
- element = LibXML::XML::Node.new(name.to_s)
78
74
  element.content = data.to_s
79
- element
80
75
  end
76
+ element
81
77
  end
82
78
 
83
79
  # SAX handler for streaming
@@ -100,10 +96,10 @@ module Serialbench
100
96
  @element_stack.push(@current_element)
101
97
  end
102
98
 
103
- def on_end_element(element)
99
+ def on_end_element(_element)
104
100
  element_data = @element_stack.pop
105
101
  if @element_stack.empty?
106
- @block&.call(element_data) if @block
102
+ @block&.call(element_data)
107
103
  else
108
104
  @element_stack.last[:children] << element_data
109
105
  end
@@ -109,10 +109,10 @@ module Serialbench
109
109
  @element_stack.push(@current_element)
110
110
  end
111
111
 
112
- def end_element(name)
112
+ def end_element(_name)
113
113
  element = @element_stack.pop
114
114
  if @element_stack.empty?
115
- @block&.call(element) if @block
115
+ @block&.call(element)
116
116
  else
117
117
  @element_stack.last[:children] << element
118
118
  end
@@ -58,27 +58,23 @@ module Serialbench
58
58
  def build_xml_from_data(data, name = 'root')
59
59
  require 'oga'
60
60
 
61
+ element = Oga::XML::Element.new(name: name)
61
62
  case data
62
63
  when Hash
63
- element = Oga::XML::Element.new(name: name)
64
64
  data.each do |key, value|
65
65
  child = build_xml_from_data(value, key.to_s)
66
66
  element.children << child
67
67
  end
68
- element
69
68
  when Array
70
- element = Oga::XML::Element.new(name: name)
71
69
  data.each_with_index do |item, index|
72
70
  child = build_xml_from_data(item, "item_#{index}")
73
71
  element.children << child
74
72
  end
75
- element
76
73
  else
77
- element = Oga::XML::Element.new(name: name)
78
74
  text_node = Oga::XML::Text.new(text: data.to_s)
79
75
  element.children << text_node
80
- element
81
76
  end
77
+ element
82
78
  end
83
79
 
84
80
  # SAX handler for streaming
@@ -108,10 +104,10 @@ module Serialbench
108
104
  @element_stack.last[:text] += text if @element_stack.any?
109
105
  end
110
106
 
111
- def after_element(namespace, name)
107
+ def after_element(_namespace, _name)
112
108
  element = @element_stack.pop
113
109
  if @element_stack.empty?
114
- @block&.call(element) if @block
110
+ @block&.call(element)
115
111
  else
116
112
  @element_stack.last[:children] << element
117
113
  end
@@ -61,10 +61,10 @@ module Serialbench
61
61
  @element_stack.push(@current_element)
62
62
  end
63
63
 
64
- def end_element(name)
64
+ def end_element(_name)
65
65
  element = @element_stack.pop
66
66
  if @element_stack.empty?
67
- @block&.call(element) if @block
67
+ @block&.call(element)
68
68
  else
69
69
  @element_stack.last[:children] << element
70
70
  end
@@ -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
@@ -31,7 +31,7 @@ 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
@@ -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
@@ -80,11 +80,11 @@ module Serialbench
80
80
  end
81
81
 
82
82
  def self.available_for_format(format)
83
- for_format(format).select { |serializer_singleton| serializer_singleton.available? }
83
+ for_format(format).select(&:available?)
84
84
  end
85
85
 
86
86
  def self.available
87
- all.select { |serializer_singleton| serializer_singleton.available? }
87
+ all.select(&:available?)
88
88
  end
89
89
  end
90
90
  end
@@ -32,20 +32,29 @@ module Serialbench
32
32
 
33
33
  def generate_site
34
34
  target_name = @result ? @result.environment_config.name : @resultset.name
35
- data = @result ? @result.to_json : @resultset.to_json
36
35
 
37
36
  puts "🏗️ Generating HTML site for #{@result ? 'run' : 'resultset'}: #{target_name}"
38
37
  puts "Output: #{@output_path}"
39
38
 
39
+ # Transform data for dashboard.js compatibility
40
+ data = if @result
41
+ transform_result_for_dashboard(@result)
42
+ else
43
+ transform_resultset_for_dashboard(@resultset)
44
+ end
45
+
40
46
  prepare_output_directory
41
47
  render_site(
42
48
  {
43
- 'data' => data,
49
+ 'data' => JSON.generate(data),
44
50
  'kind' => @result ? 'run' : 'resultset'
45
51
  },
46
52
  'format_based.liquid'
47
53
  )
48
54
 
55
+ # Export raw data files for download
56
+ export_raw_data
57
+
49
58
  puts "✅ Site generated successfully at: #{@output_path}"
50
59
  @output_path
51
60
  end
@@ -101,5 +110,174 @@ module Serialbench
101
110
 
102
111
  FileUtils.cp_r(assets_source, assets_dest)
103
112
  end
113
+
114
+ # Transform a single Result into dashboard-compatible format
115
+ # Dashboard expects: { combined_results: {...}, environments: {...}, metadata: {...} }
116
+ def transform_result_for_dashboard(result)
117
+ env_key = "env-#{result.platform.ruby_version}"
118
+
119
+ # Build combined_results structure
120
+ combined_results = build_combined_results(result, env_key)
121
+
122
+ # Build environments structure
123
+ environments = {
124
+ env_key => {
125
+ 'ruby_version' => result.platform.ruby_version,
126
+ 'ruby_platform' => result.platform.ruby_platform || "#{result.platform.os}-#{result.platform.arch}",
127
+ 'os' => result.platform.os,
128
+ 'arch' => result.platform.arch,
129
+ 'source_file' => result.metadata.environment_config_path,
130
+ 'timestamp' => result.metadata.created_at
131
+ }
132
+ }
133
+
134
+ {
135
+ 'combined_results' => combined_results,
136
+ 'environments' => environments,
137
+ 'metadata' => {
138
+ 'generated_at' => Time.now.iso8601
139
+ }
140
+ }
141
+ end
142
+
143
+ # Transform a ResultSet (collection of Results) into dashboard-compatible format
144
+ # Combines all results into a single dashboard structure
145
+ def transform_resultset_for_dashboard(resultset)
146
+ combined_results = {}
147
+ environments = {}
148
+
149
+ resultset.results.each do |result|
150
+ # Create unique env key for this result
151
+ env_key = "#{result.platform.os}-#{result.platform.arch}-ruby-#{result.platform.ruby_version}"
152
+
153
+ # Merge this result's data into combined_results
154
+ result_combined = build_combined_results(result, env_key)
155
+ merge_combined_results!(combined_results, result_combined)
156
+
157
+ # Add environment info
158
+ environments[env_key] = {
159
+ 'ruby_version' => result.platform.ruby_version,
160
+ 'ruby_platform' => result.platform.ruby_platform || "#{result.platform.os}-#{result.platform.arch}",
161
+ 'os' => result.platform.os,
162
+ 'arch' => result.platform.arch,
163
+ 'source_file' => result.metadata.environment_config_path,
164
+ 'timestamp' => result.metadata.created_at
165
+ }
166
+ end
167
+
168
+ {
169
+ 'combined_results' => combined_results,
170
+ 'environments' => environments,
171
+ 'metadata' => {
172
+ 'resultset_name' => resultset.name,
173
+ 'resultset_description' => resultset.description,
174
+ 'total_runs' => resultset.results.size,
175
+ 'generated_at' => Time.now.iso8601
176
+ }
177
+ }
178
+ end
179
+
180
+ # Deep merge results from multiple runs
181
+ def merge_combined_results!(target, source)
182
+ source.each do |operation, sizes|
183
+ target[operation] ||= {}
184
+ sizes.each do |size, formats|
185
+ target[operation][size] ||= {}
186
+ formats.each do |format, serializers|
187
+ target[operation][size][format] ||= {}
188
+ serializers.each do |serializer, envs|
189
+ target[operation][size][format][serializer] ||= {}
190
+ target[operation][size][format][serializer].merge!(envs)
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ def build_combined_results(result, env_key)
198
+ combined = {}
199
+
200
+ %w[parsing generation streaming].each do |operation|
201
+ combined[operation] = {}
202
+
203
+ operation_results = result.benchmark_result.send(operation)
204
+ next if operation_results.nil? || operation_results.empty?
205
+
206
+ operation_results.each do |perf|
207
+ size = perf.data_size
208
+ format = perf.format
209
+ serializer = perf.adapter
210
+
211
+ combined[operation][size] ||= {}
212
+ combined[operation][size][format] ||= {}
213
+ combined[operation][size][format][serializer] ||= {}
214
+ combined[operation][size][format][serializer][env_key] = {
215
+ 'iterations_per_second' => perf.iterations_per_second,
216
+ 'time_per_iteration' => perf.time_per_iteration
217
+ }
218
+ end
219
+ end
220
+
221
+ # Handle memory separately
222
+ if result.benchmark_result.memory && !result.benchmark_result.memory.empty?
223
+ combined['memory'] = {}
224
+
225
+ result.benchmark_result.memory.each do |mem|
226
+ size = mem.data_size
227
+ format = mem.format
228
+ serializer = mem.adapter
229
+
230
+ combined['memory'][size] ||= {}
231
+ combined['memory'][size][format] ||= {}
232
+ combined['memory'][size][format][serializer] ||= {}
233
+ combined['memory'][size][format][serializer][env_key] = {
234
+ 'allocated_memory' => mem.allocated_memory,
235
+ 'retained_memory' => mem.retained_memory
236
+ }
237
+ end
238
+ end
239
+
240
+ combined
241
+ end
242
+
243
+ # Export raw data files for download
244
+ def export_raw_data
245
+ data_dir = File.join(@output_path, 'data')
246
+ FileUtils.mkdir_p(data_dir)
247
+
248
+ if @result
249
+ # Export single result
250
+ export_single_result(@result, data_dir)
251
+ elsif @resultset
252
+ # Export all results in resultset plus the complete resultset
253
+ export_resultset(@resultset, data_dir)
254
+ end
255
+
256
+ puts "📦 Raw data files exported to: #{data_dir}"
257
+ end
258
+
259
+ def export_single_result(result, data_dir)
260
+ # Create filename based on platform info
261
+ env_key = "#{result.platform.os}-#{result.platform.arch}-ruby-#{result.platform.ruby_version}"
262
+ filename = "#{env_key}.yaml"
263
+ filepath = File.join(data_dir, filename)
264
+
265
+ # Write result as YAML
266
+ File.write(filepath, result.to_yaml)
267
+ puts " 📄 Exported: #{filename}"
268
+ end
269
+
270
+ def export_resultset(resultset, data_dir)
271
+ # Export each individual result
272
+ resultset.results.each do |result|
273
+ export_single_result(result, data_dir)
274
+ end
275
+
276
+ # Export complete resultset
277
+ resultset_filename = 'resultset.yaml'
278
+ resultset_filepath = File.join(data_dir, resultset_filename)
279
+ File.write(resultset_filepath, resultset.to_yaml)
280
+ puts " 📄 Exported: #{resultset_filename} (complete dataset)"
281
+ end
104
282
  end
105
283
  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