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,795 @@
1
+ /**
2
+ * Modern Dashboard for SerialBench Format-Based Reports
3
+ * Features: Tag-based filtering, theme management, responsive charts
4
+ */
5
+
6
+ class SerialBenchDashboard {
7
+ constructor() {
8
+ // Handle the new nested data structure
9
+ const rawData = window.benchmarkData || {};
10
+ this.data = {
11
+ combined_results: rawData.combined_results || {},
12
+ environments: rawData.environments || {},
13
+ metadata: rawData.metadata || {}
14
+ };
15
+ this.charts = new Map();
16
+ this.filters = {
17
+ platforms: new Set(),
18
+ rubyTypes: new Set(['ruby']), // Default to ruby, will add jruby later
19
+ rubyVersions: new Set(),
20
+ format: 'xml'
21
+ };
22
+
23
+ this.theme = this.getStoredTheme() || this.getSystemTheme();
24
+ this.isInitialized = false;
25
+
26
+ this.init();
27
+ }
28
+
29
+ async init() {
30
+ try {
31
+ console.log('🚀 Initializing SerialBench Dashboard...');
32
+
33
+ // Apply theme immediately
34
+ this.applyTheme(this.theme);
35
+
36
+ // Initialize components
37
+ this.setupThemeToggle();
38
+ this.initializeFilters();
39
+ this.setupEventListeners();
40
+ this.createCharts();
41
+ this.updateSummary();
42
+ this.updateEnvironmentInfo();
43
+
44
+ // Set initial filter states
45
+ this.setDefaultFilters();
46
+ this.applyFilters();
47
+
48
+ this.isInitialized = true;
49
+ console.log('✅ Dashboard initialized successfully');
50
+
51
+ } catch (error) {
52
+ console.error('❌ Dashboard initialization failed:', error);
53
+ this.showError('Failed to initialize dashboard');
54
+ }
55
+ }
56
+
57
+ // Theme Management
58
+ getSystemTheme() {
59
+ return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
60
+ }
61
+
62
+ getStoredTheme() {
63
+ return localStorage.getItem('serialbench-theme');
64
+ }
65
+
66
+ applyTheme(theme) {
67
+ document.documentElement.setAttribute('data-theme', theme);
68
+ this.theme = theme;
69
+ localStorage.setItem('serialbench-theme', theme);
70
+
71
+ // Update theme toggle icon
72
+ const themeToggle = document.querySelector('.theme-toggle');
73
+ if (themeToggle) {
74
+ const icon = themeToggle.querySelector('svg');
75
+ if (icon) {
76
+ icon.innerHTML = theme === 'light'
77
+ ? '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>' // moon
78
+ : '<circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>'; // sun
79
+ }
80
+ }
81
+ }
82
+
83
+ setupThemeToggle() {
84
+ const themeToggle = document.querySelector('.theme-toggle');
85
+ if (themeToggle) {
86
+ themeToggle.addEventListener('click', () => {
87
+ const newTheme = this.theme === 'dark' ? 'light' : 'dark';
88
+ this.applyTheme(newTheme);
89
+
90
+ // Update charts with new theme
91
+ if (this.isInitialized) {
92
+ this.updateChartsTheme();
93
+ }
94
+ });
95
+ }
96
+ }
97
+
98
+ // Filter Management
99
+ initializeFilters() {
100
+ this.populateFilterOptions();
101
+ this.setupFilterEventListeners();
102
+ }
103
+
104
+ populateFilterOptions() {
105
+ // Extract available platforms from data
106
+ const platforms = new Set();
107
+ const rubyVersions = new Set();
108
+
109
+ if (this.data.environments) {
110
+ Object.values(this.data.environments).forEach(env => {
111
+ // Use os-arch combination instead of ruby_platform
112
+ const platformKey = `${env.os}-${env.arch}`;
113
+ platforms.add(platformKey);
114
+ rubyVersions.add(env.ruby_version);
115
+ });
116
+ }
117
+
118
+ // Populate platform filter
119
+ const platformSelect = document.getElementById('platform-filter');
120
+ if (platformSelect) {
121
+ platformSelect.innerHTML = '<option value="">All Platforms</option>';
122
+ Array.from(platforms).sort().forEach(platform => {
123
+ const option = document.createElement('option');
124
+ option.value = platform;
125
+ option.textContent = platform;
126
+ platformSelect.appendChild(option);
127
+ });
128
+ }
129
+
130
+ // Populate Ruby version filter
131
+ const versionSelect = document.getElementById('ruby-version-filter');
132
+ if (versionSelect) {
133
+ versionSelect.innerHTML = '<option value="">All Versions</option>';
134
+ Array.from(rubyVersions).sort().forEach(version => {
135
+ const option = document.createElement('option');
136
+ option.value = version;
137
+ option.textContent = `Ruby ${version}`;
138
+ versionSelect.appendChild(option);
139
+ });
140
+ }
141
+
142
+ // Ruby type filter (for future JRuby support)
143
+ const typeSelect = document.getElementById('ruby-type-filter');
144
+ if (typeSelect) {
145
+ typeSelect.innerHTML = `
146
+ <option value="">All Types</option>
147
+ <option value="ruby" selected>Ruby</option>
148
+ <option value="jruby" disabled>JRuby (Coming Soon)</option>
149
+ `;
150
+ }
151
+ }
152
+
153
+
154
+ setupFilterEventListeners() {
155
+ // Platform filter
156
+ const platformSelect = document.getElementById('platform-filter');
157
+ if (platformSelect) {
158
+ platformSelect.addEventListener('change', (e) => {
159
+ if (e.target.value) {
160
+ this.filters.platforms = new Set([e.target.value]);
161
+ } else {
162
+ this.filters.platforms.clear();
163
+ }
164
+ this.applyFilters();
165
+ });
166
+ }
167
+
168
+ // Ruby version filter
169
+ const versionSelect = document.getElementById('ruby-version-filter');
170
+ if (versionSelect) {
171
+ versionSelect.addEventListener('change', (e) => {
172
+ if (e.target.value) {
173
+ this.filters.rubyVersions = new Set([e.target.value]);
174
+ } else {
175
+ this.filters.rubyVersions.clear();
176
+ }
177
+ this.applyFilters();
178
+ });
179
+ }
180
+
181
+ // Ruby type filter
182
+ const typeSelect = document.getElementById('ruby-type-filter');
183
+ if (typeSelect) {
184
+ typeSelect.addEventListener('change', (e) => {
185
+ if (e.target.value) {
186
+ this.filters.rubyTypes = new Set([e.target.value]);
187
+ } else {
188
+ this.filters.rubyTypes = new Set(['ruby']); // Default to ruby
189
+ }
190
+ this.applyFilters();
191
+ });
192
+ }
193
+
194
+ // Format tabs
195
+ document.querySelectorAll('.format-tab').forEach(tab => {
196
+ tab.addEventListener('click', (e) => {
197
+ e.preventDefault();
198
+ const format = e.target.dataset.format;
199
+ this.setActiveFormat(format);
200
+ this.applyFilters();
201
+ });
202
+ });
203
+ }
204
+
205
+ setDefaultFilters() {
206
+ // Set all platforms and versions by default
207
+ if (this.data.environments) {
208
+ Object.values(this.data.environments).forEach(env => {
209
+ // Use os-arch combination instead of ruby_platform
210
+ const platformKey = `${env.os}-${env.arch}`;
211
+ this.filters.platforms.add(platformKey);
212
+ this.filters.rubyVersions.add(env.ruby_version);
213
+ });
214
+ }
215
+
216
+ console.log('🔧 Default filters set:', {
217
+ platforms: Array.from(this.filters.platforms),
218
+ rubyVersions: Array.from(this.filters.rubyVersions),
219
+ environmentCount: Object.keys(this.data.environments || {}).length
220
+ });
221
+ }
222
+
223
+ setActiveFormat(format) {
224
+ this.filters.format = format;
225
+
226
+ // Update active tab
227
+ document.querySelectorAll('.format-tab').forEach(tab => {
228
+ tab.classList.remove('active');
229
+ });
230
+
231
+ const activeTab = document.querySelector(`[data-format="${format}"]`);
232
+ if (activeTab) {
233
+ activeTab.classList.add('active');
234
+ }
235
+ }
236
+
237
+ applyFilters() {
238
+ if (!this.isInitialized) return;
239
+
240
+ console.log('🔍 Applying filters:', this.filters);
241
+
242
+ // Update charts with filtered data
243
+ this.updateCharts();
244
+ this.updateSummary();
245
+ this.updateEnvironmentInfo();
246
+
247
+ // Update URL to reflect current state
248
+ this.updateURL();
249
+ }
250
+
251
+ // Chart Management
252
+ createCharts() {
253
+ const operations = ['parsing', 'generation', 'memory', 'streaming'];
254
+
255
+ operations.forEach(operation => {
256
+ this.createChart(operation);
257
+ });
258
+ }
259
+
260
+ createChart(operation) {
261
+ const canvas = document.getElementById(`chart-${operation}`);
262
+ if (!canvas) {
263
+ console.warn(`Canvas not found for operation: ${operation}`);
264
+ return;
265
+ }
266
+
267
+ // Clear any existing error message and show canvas
268
+ this.clearChartError(canvas);
269
+
270
+ const ctx = canvas.getContext('2d');
271
+ const data = this.getFilteredChartData(operation);
272
+
273
+ if (!data || data.datasets.length === 0) {
274
+ this.showChartError(canvas, `No data available for ${operation}`);
275
+ return;
276
+ }
277
+
278
+ try {
279
+ const chart = new Chart(ctx, {
280
+ type: 'bar',
281
+ data: data,
282
+ options: this.getChartOptions(operation)
283
+ });
284
+
285
+ this.charts.set(operation, chart);
286
+ console.log(`📊 Created chart for ${operation}`);
287
+
288
+ } catch (error) {
289
+ console.error(`Failed to create chart for ${operation}:`, error);
290
+ this.showChartError(canvas, `Failed to load ${operation} chart`);
291
+ }
292
+ }
293
+
294
+ getFilteredChartData(operation) {
295
+ const format = this.filters.format;
296
+
297
+ if (!this.data.combined_results || !this.data.combined_results[operation]) {
298
+ return { labels: [], datasets: [] };
299
+ }
300
+
301
+ const operationData = this.data.combined_results[operation];
302
+
303
+ // Combine data from all sizes for this operation and format
304
+ const combinedData = {};
305
+
306
+ ['small', 'medium', 'large'].forEach(size => {
307
+ if (operationData[size] && operationData[size][format]) {
308
+ const sizeData = operationData[size][format];
309
+
310
+ Object.keys(sizeData).forEach(serializer => {
311
+ if (!combinedData[serializer]) {
312
+ combinedData[serializer] = {};
313
+ }
314
+
315
+ Object.keys(sizeData[serializer]).forEach(envKey => {
316
+ const envData = sizeData[serializer][envKey];
317
+ const env = this.data.environments[envKey];
318
+
319
+ if (this.shouldIncludeEnvironment(env)) {
320
+ const label = `${env.ruby_version} (${size})`;
321
+ combinedData[serializer][label] = envData;
322
+ }
323
+ });
324
+ });
325
+ }
326
+ });
327
+
328
+ return this.formatChartData(combinedData, operation);
329
+ }
330
+
331
+ shouldIncludeEnvironment(env) {
332
+ if (!env) return false;
333
+
334
+ // Use os-arch combination for platform matching
335
+ const platformKey = `${env.os}-${env.arch}`;
336
+ const platformMatch = this.filters.platforms.size === 0 ||
337
+ this.filters.platforms.has(platformKey);
338
+ const versionMatch = this.filters.rubyVersions.size === 0 ||
339
+ this.filters.rubyVersions.has(env.ruby_version);
340
+
341
+ return platformMatch && versionMatch;
342
+ }
343
+
344
+ formatChartData(data, operation) {
345
+ const serializers = Object.keys(data);
346
+ if (serializers.length === 0) {
347
+ return { labels: [], datasets: [] };
348
+ }
349
+
350
+ // Get all unique labels
351
+ const allLabels = new Set();
352
+ serializers.forEach(serializer => {
353
+ Object.keys(data[serializer]).forEach(label => allLabels.add(label));
354
+ });
355
+
356
+ const labels = Array.from(allLabels).sort();
357
+
358
+ // Create datasets
359
+ const datasets = serializers.map((serializer, index) => {
360
+ const serializerData = data[serializer];
361
+
362
+ const values = labels.map(label => {
363
+ const envData = serializerData[label];
364
+ if (!envData) return 0;
365
+
366
+ if (operation === 'memory') {
367
+ return envData.allocated_memory ? envData.allocated_memory / 1024 / 1024 : 0;
368
+ } else {
369
+ return envData.iterations_per_second || 0;
370
+ }
371
+ });
372
+
373
+ return {
374
+ label: this.formatSerializerName(serializer),
375
+ data: values,
376
+ backgroundColor: this.getSerializerColor(serializer, 0.8),
377
+ borderColor: this.getSerializerColor(serializer, 1),
378
+ borderWidth: 2,
379
+ borderRadius: 4,
380
+ borderSkipped: false,
381
+ };
382
+ });
383
+
384
+ return { labels, datasets };
385
+ }
386
+
387
+ formatSerializerName(serializer) {
388
+ const nameMap = {
389
+ 'rexml': 'REXML',
390
+ 'ox': 'Ox',
391
+ 'nokogiri': 'Nokogiri',
392
+ 'json': 'JSON',
393
+ 'oj': 'Oj',
394
+ 'rapidjson': 'RapidJSON',
395
+ 'yajl': 'YAJL',
396
+ 'psych': 'Psych',
397
+ 'syck': 'Syck',
398
+ 'toml-rb': 'TOML-RB',
399
+ 'tomlib': 'Tomlib'
400
+ };
401
+ return nameMap[serializer] || serializer.toUpperCase();
402
+ }
403
+
404
+ getSerializerColor(serializer, alpha = 1) {
405
+ const colors = {
406
+ 'rexml': `rgba(239, 68, 68, ${alpha})`, // red
407
+ 'ox': `rgba(249, 115, 22, ${alpha})`, // orange
408
+ 'nokogiri': `rgba(34, 197, 94, ${alpha})`, // green
409
+ 'json': `rgba(59, 130, 246, ${alpha})`, // blue
410
+ 'oj': `rgba(147, 51, 234, ${alpha})`, // purple
411
+ 'rapidjson': `rgba(236, 72, 153, ${alpha})`, // pink
412
+ 'yajl': `rgba(14, 165, 233, ${alpha})`, // sky
413
+ 'psych': `rgba(16, 185, 129, ${alpha})`, // emerald
414
+ 'syck': `rgba(245, 158, 11, ${alpha})`, // amber
415
+ 'toml-rb': `rgba(168, 85, 247, ${alpha})`, // violet
416
+ 'tomlib': `rgba(6, 182, 212, ${alpha})` // cyan
417
+ };
418
+
419
+ return colors[serializer] || `rgba(107, 114, 128, ${alpha})`;
420
+ }
421
+
422
+ getChartOptions(operation) {
423
+ const isDark = this.theme === 'dark';
424
+ const textColor = isDark ? '#CBD5E1' : '#334155';
425
+ const gridColor = isDark ? '#475569' : '#E2E8F0';
426
+
427
+ return {
428
+ responsive: true,
429
+ maintainAspectRatio: false,
430
+ interaction: {
431
+ intersect: false,
432
+ mode: 'index'
433
+ },
434
+ plugins: {
435
+ title: {
436
+ display: true,
437
+ text: this.getChartTitle(operation),
438
+ color: textColor,
439
+ font: {
440
+ size: 16,
441
+ weight: 'bold'
442
+ },
443
+ padding: 20
444
+ },
445
+ legend: {
446
+ position: 'top',
447
+ labels: {
448
+ color: textColor,
449
+ usePointStyle: true,
450
+ padding: 15
451
+ }
452
+ },
453
+ tooltip: {
454
+ backgroundColor: isDark ? '#1E293B' : '#FFFFFF',
455
+ titleColor: textColor,
456
+ bodyColor: textColor,
457
+ borderColor: isDark ? '#475569' : '#E2E8F0',
458
+ borderWidth: 1,
459
+ cornerRadius: 8,
460
+ displayColors: true,
461
+ callbacks: {
462
+ label: (context) => {
463
+ const value = context.parsed.y;
464
+ const unit = operation === 'memory' ? 'MB' : 'ops/sec';
465
+ return `${context.dataset.label}: ${value.toLocaleString()} ${unit}`;
466
+ }
467
+ }
468
+ }
469
+ },
470
+ scales: {
471
+ x: {
472
+ grid: {
473
+ color: gridColor,
474
+ drawBorder: false
475
+ },
476
+ ticks: {
477
+ color: textColor,
478
+ maxRotation: 45
479
+ }
480
+ },
481
+ y: {
482
+ beginAtZero: true,
483
+ grid: {
484
+ color: gridColor,
485
+ drawBorder: false
486
+ },
487
+ ticks: {
488
+ color: textColor,
489
+ callback: function(value) {
490
+ if (operation === 'memory') {
491
+ return value.toLocaleString() + ' MB';
492
+ } else {
493
+ return value.toLocaleString() + ' ops/sec';
494
+ }
495
+ }
496
+ },
497
+ title: {
498
+ display: true,
499
+ text: operation === 'memory' ? 'Memory Usage (MB)' : 'Operations per Second',
500
+ color: textColor,
501
+ font: {
502
+ weight: 'bold'
503
+ }
504
+ }
505
+ }
506
+ },
507
+ animation: {
508
+ duration: 750,
509
+ easing: 'easeInOutQuart'
510
+ }
511
+ };
512
+ }
513
+
514
+ getChartTitle(operation) {
515
+ const format = this.filters.format.toUpperCase();
516
+ const titles = {
517
+ 'parsing': `${format} Parsing Performance`,
518
+ 'generation': `${format} Generation Performance`,
519
+ 'memory': `${format} Memory Usage`,
520
+ 'streaming': `${format} Streaming Performance`
521
+ };
522
+ return titles[operation] || `${format} ${operation}`;
523
+ }
524
+
525
+ updateCharts() {
526
+ const operations = ['parsing', 'generation', 'memory', 'streaming'];
527
+
528
+ operations.forEach(operation => {
529
+ try {
530
+ console.log(`🔄 Updating chart for ${operation}...`);
531
+ const chart = this.charts.get(operation);
532
+ const newData = this.getFilteredChartData(operation);
533
+ console.log(`📊 Data for ${operation}:`, newData.datasets.length, 'datasets');
534
+
535
+ if (newData.datasets.length === 0) {
536
+ console.log(`❌ No data for ${operation}, destroying chart`);
537
+ // Destroy existing chart and show error
538
+ if (chart) {
539
+ chart.destroy();
540
+ this.charts.delete(operation);
541
+ }
542
+ const canvas = document.getElementById(`chart-${operation}`);
543
+ if (canvas) {
544
+ this.showChartError(canvas, `No data available for ${operation}`);
545
+ }
546
+ return;
547
+ }
548
+
549
+ // Check if chart exists and canvas is valid
550
+ if (!chart || !chart.canvas || !chart.canvas.getContext) {
551
+ console.log(`🔧 Recreating chart for ${operation}`);
552
+ // Recreate chart if it was destroyed or canvas was replaced
553
+ this.createChart(operation);
554
+ return;
555
+ }
556
+
557
+ console.log(`✅ Updating existing chart for ${operation}`);
558
+ chart.data = newData;
559
+ chart.options = this.getChartOptions(operation);
560
+ chart.update('active');
561
+
562
+ } catch (error) {
563
+ console.error(`❌ Error updating chart for ${operation}:`, error);
564
+ // Try to recreate the chart
565
+ try {
566
+ this.createChart(operation);
567
+ } catch (recreateError) {
568
+ console.error(`❌ Failed to recreate chart for ${operation}:`, recreateError);
569
+ }
570
+ }
571
+ });
572
+ }
573
+
574
+ updateChartsTheme() {
575
+ this.charts.forEach((chart, operation) => {
576
+ chart.options = this.getChartOptions(operation);
577
+ chart.update('none');
578
+ });
579
+ }
580
+
581
+ clearChartError(canvas) {
582
+ const container = canvas.parentElement;
583
+
584
+ // Remove any existing error message
585
+ const existingError = container.querySelector('.chart-error');
586
+ if (existingError) {
587
+ existingError.remove();
588
+ }
589
+
590
+ // Show the canvas
591
+ canvas.style.display = 'block';
592
+ }
593
+
594
+ showChartError(canvas, message) {
595
+ const container = canvas.parentElement;
596
+ const canvasId = canvas.id;
597
+
598
+ // Clear any existing error message
599
+ const existingError = container.querySelector('.chart-error');
600
+ if (existingError) {
601
+ existingError.remove();
602
+ }
603
+
604
+ // Hide the canvas and show error message
605
+ canvas.style.display = 'none';
606
+
607
+ const errorDiv = document.createElement('div');
608
+ errorDiv.className = 'chart-error';
609
+ errorDiv.innerHTML = `
610
+ <svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
611
+ <circle cx="12" cy="12" r="10"></circle>
612
+ <line x1="15" y1="9" x2="9" y2="15"></line>
613
+ <line x1="9" y1="9" x2="15" y2="15"></line>
614
+ </svg>
615
+ <span>${message}</span>
616
+ `;
617
+
618
+ container.appendChild(errorDiv);
619
+ }
620
+
621
+ // Summary and Environment Updates
622
+ updateSummary() {
623
+ // This would analyze the current filtered data and update performance summaries
624
+ console.log('📈 Updating performance summary...');
625
+ }
626
+
627
+ updateEnvironmentInfo() {
628
+ const container = document.getElementById('environment-info');
629
+ if (!container || !this.data.environments) return;
630
+
631
+ // Show all environments, but apply platform and version filters only (not format)
632
+ const filteredEnvs = Object.entries(this.data.environments)
633
+ .filter(([key, env]) => {
634
+ // Use os-arch combination for platform matching
635
+ const platformKey = `${env.os}-${env.arch}`;
636
+ const platformMatch = this.filters.platforms.size === 0 ||
637
+ this.filters.platforms.has(platformKey);
638
+ const versionMatch = this.filters.rubyVersions.size === 0 ||
639
+ this.filters.rubyVersions.has(env.ruby_version);
640
+ return platformMatch && versionMatch;
641
+ });
642
+
643
+ if (filteredEnvs.length === 0) {
644
+ container.innerHTML = '<p class="text-muted">No environments match current filters</p>';
645
+ return;
646
+ }
647
+
648
+ container.innerHTML = filteredEnvs.map(([key, env]) => `
649
+ <div class="environment-card fade-in-up">
650
+ <h3 class="environment-card-title">
651
+ Ruby ${env.ruby_version} on ${env.os}-${env.arch}
652
+ </h3>
653
+ <p><strong>Source:</strong> ${env.source_file ? env.source_file.split('/').pop() : 'Unknown'}</p>
654
+ <p><strong>Timestamp:</strong> ${new Date(env.timestamp).toLocaleString()}</p>
655
+ ${this.generateSerializerVersions(env.environment)}
656
+ </div>
657
+ `).join('');
658
+ }
659
+
660
+ generateSerializerVersions(environment) {
661
+ if (!environment || !environment.serializer_versions) {
662
+ return '';
663
+ }
664
+
665
+ const versions = Object.entries(environment.serializer_versions)
666
+ .map(([name, version]) => `<li><strong>${name}:</strong> ${version}</li>`)
667
+ .join('');
668
+
669
+ return `
670
+ <div class="serializer-versions">
671
+ <h4>Serializer Versions:</h4>
672
+ <ul style="margin: 0.5rem 0; padding-left: 1.5rem; color: var(--text-muted);">${versions}</ul>
673
+ </div>
674
+ `;
675
+ }
676
+
677
+ // Event Listeners
678
+ setupEventListeners() {
679
+ // Handle window resize
680
+ window.addEventListener('resize', _.debounce(() => {
681
+ this.charts.forEach(chart => chart.resize());
682
+ }, 250));
683
+
684
+ // Handle system theme changes
685
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
686
+ if (!localStorage.getItem('serialbench-theme')) {
687
+ this.applyTheme(e.matches ? 'dark' : 'light');
688
+ this.updateChartsTheme();
689
+ }
690
+ });
691
+ }
692
+
693
+ // URL Management
694
+ updateURL() {
695
+ const params = new URLSearchParams();
696
+
697
+ if (this.filters.platforms.size > 0) {
698
+ params.set('platforms', Array.from(this.filters.platforms).join(','));
699
+ }
700
+ if (this.filters.rubyVersions.size > 0) {
701
+ params.set('versions', Array.from(this.filters.rubyVersions).join(','));
702
+ }
703
+ if (this.filters.format !== 'xml') {
704
+ params.set('format', this.filters.format);
705
+ }
706
+
707
+ const newURL = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}`;
708
+ window.history.replaceState({}, '', newURL);
709
+ }
710
+
711
+ loadFromURL() {
712
+ const params = new URLSearchParams(window.location.search);
713
+
714
+ if (params.has('platforms')) {
715
+ this.filters.platforms = new Set(params.get('platforms').split(','));
716
+ }
717
+ if (params.has('versions')) {
718
+ this.filters.rubyVersions = new Set(params.get('versions').split(','));
719
+ }
720
+ if (params.has('format')) {
721
+ this.filters.format = params.get('format');
722
+ }
723
+ }
724
+
725
+ // Utility Methods
726
+ showError(message) {
727
+ console.error('Dashboard Error:', message);
728
+ // Could show a toast notification here
729
+ }
730
+
731
+ // Public API
732
+ getFilterState() {
733
+ return { ...this.filters };
734
+ }
735
+
736
+ setFilters(newFilters) {
737
+ Object.assign(this.filters, newFilters);
738
+ this.applyFilters();
739
+ }
740
+
741
+ exportData() {
742
+ const filteredData = this.getFilteredData();
743
+ const blob = new Blob([JSON.stringify(filteredData, null, 2)], {
744
+ type: 'application/json'
745
+ });
746
+ const url = URL.createObjectURL(blob);
747
+ const a = document.createElement('a');
748
+ a.href = url;
749
+ a.download = `serialbench-${this.filters.format}-${Date.now()}.json`;
750
+ a.click();
751
+ URL.revokeObjectURL(url);
752
+ }
753
+
754
+ getFilteredData() {
755
+ // Return filtered dataset for export
756
+ return {
757
+ filters: this.filters,
758
+ timestamp: new Date().toISOString(),
759
+ data: this.data
760
+ };
761
+ }
762
+ }
763
+
764
+ // Utility function for debouncing (simple implementation)
765
+ const _ = {
766
+ debounce: (func, wait) => {
767
+ let timeout;
768
+ return function executedFunction(...args) {
769
+ const later = () => {
770
+ clearTimeout(timeout);
771
+ func(...args);
772
+ };
773
+ clearTimeout(timeout);
774
+ timeout = setTimeout(later, wait);
775
+ };
776
+ }
777
+ };
778
+
779
+ // Initialize dashboard when DOM is ready
780
+ document.addEventListener('DOMContentLoaded', () => {
781
+ console.log('🎯 DOM loaded, checking for benchmark data...');
782
+
783
+ if (!window.benchmarkData) {
784
+ console.error('❌ No benchmark data found');
785
+ return;
786
+ }
787
+
788
+ console.log('📊 Benchmark data found, initializing dashboard...');
789
+ window.serialBenchDashboard = new SerialBenchDashboard();
790
+ });
791
+
792
+ // Export for external use
793
+ if (typeof module !== 'undefined' && module.exports) {
794
+ module.exports = SerialBenchDashboard;
795
+ }