serialbench 0.1.1 → 0.1.2

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