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.
- checksums.yaml +4 -4
- data/.github/workflows/benchmark.yml +13 -5
- data/.github/workflows/docker.yml +35 -9
- data/.github/workflows/rake.yml +15 -0
- data/Gemfile +2 -1
- data/README.adoc +267 -1129
- data/Rakefile +0 -55
- data/config/benchmarks/full.yml +29 -0
- data/config/benchmarks/short.yml +26 -0
- data/config/environments/asdf-ruby-3.2.yml +8 -0
- data/config/environments/asdf-ruby-3.3.yml +8 -0
- data/config/environments/docker-ruby-3.0.yml +9 -0
- data/config/environments/docker-ruby-3.1.yml +9 -0
- data/config/environments/docker-ruby-3.2.yml +9 -0
- data/config/environments/docker-ruby-3.3.yml +9 -0
- data/config/environments/docker-ruby-3.4.yml +9 -0
- data/docker/Dockerfile.alpine +33 -0
- data/docker/{Dockerfile.benchmark → Dockerfile.ubuntu} +4 -3
- data/docker/README.md +2 -2
- data/exe/serialbench +1 -1
- data/lib/serialbench/benchmark_runner.rb +261 -423
- data/lib/serialbench/cli/base_cli.rb +51 -0
- data/lib/serialbench/cli/benchmark_cli.rb +380 -0
- data/lib/serialbench/cli/environment_cli.rb +181 -0
- data/lib/serialbench/cli/resultset_cli.rb +215 -0
- data/lib/serialbench/cli/ruby_build_cli.rb +238 -0
- data/lib/serialbench/cli.rb +58 -601
- data/lib/serialbench/config_manager.rb +140 -0
- data/lib/serialbench/models/benchmark_config.rb +63 -0
- data/lib/serialbench/models/benchmark_result.rb +45 -0
- data/lib/serialbench/models/environment_config.rb +71 -0
- data/lib/serialbench/models/platform.rb +59 -0
- data/lib/serialbench/models/result.rb +53 -0
- data/lib/serialbench/models/result_set.rb +71 -0
- data/lib/serialbench/models/result_store.rb +108 -0
- data/lib/serialbench/models.rb +54 -0
- data/lib/serialbench/ruby_build_manager.rb +153 -0
- data/lib/serialbench/runners/asdf_runner.rb +296 -0
- data/lib/serialbench/runners/base.rb +32 -0
- data/lib/serialbench/runners/docker_runner.rb +142 -0
- data/lib/serialbench/serializers/base_serializer.rb +8 -16
- data/lib/serialbench/serializers/json/base_json_serializer.rb +4 -4
- data/lib/serialbench/serializers/json/json_serializer.rb +0 -2
- data/lib/serialbench/serializers/json/oj_serializer.rb +0 -2
- data/lib/serialbench/serializers/json/yajl_serializer.rb +0 -2
- data/lib/serialbench/serializers/toml/base_toml_serializer.rb +5 -3
- data/lib/serialbench/serializers/toml/toml_rb_serializer.rb +0 -2
- data/lib/serialbench/serializers/toml/tomlib_serializer.rb +0 -2
- data/lib/serialbench/serializers/toml/tomlrb_serializer.rb +56 -0
- data/lib/serialbench/serializers/xml/base_xml_serializer.rb +4 -9
- data/lib/serialbench/serializers/xml/libxml_serializer.rb +0 -2
- data/lib/serialbench/serializers/xml/nokogiri_serializer.rb +0 -2
- data/lib/serialbench/serializers/xml/oga_serializer.rb +0 -2
- data/lib/serialbench/serializers/xml/ox_serializer.rb +0 -2
- data/lib/serialbench/serializers/xml/rexml_serializer.rb +0 -2
- data/lib/serialbench/serializers/yaml/base_yaml_serializer.rb +5 -1
- data/lib/serialbench/serializers/yaml/syck_serializer.rb +59 -22
- data/lib/serialbench/serializers.rb +23 -6
- data/lib/serialbench/site_generator.rb +105 -0
- data/lib/serialbench/templates/assets/css/benchmark_report.css +535 -0
- data/lib/serialbench/templates/assets/css/format_based.css +526 -0
- data/lib/serialbench/templates/assets/css/themes.css +588 -0
- data/lib/serialbench/templates/assets/js/chart_helpers.js +381 -0
- data/lib/serialbench/templates/assets/js/dashboard.js +796 -0
- data/lib/serialbench/templates/assets/js/navigation.js +142 -0
- data/lib/serialbench/templates/base.liquid +49 -0
- data/lib/serialbench/templates/format_based.liquid +279 -0
- data/lib/serialbench/templates/partials/chart_section.liquid +4 -0
- data/lib/serialbench/version.rb +1 -1
- data/lib/serialbench.rb +2 -31
- data/serialbench.gemspec +4 -1
- metadata +86 -16
- data/config/ci.yml +0 -22
- data/config/full.yml +0 -30
- data/docker/run-benchmarks.sh +0 -356
- data/lib/serialbench/chart_generator.rb +0 -821
- data/lib/serialbench/result_formatter.rb +0 -182
- data/lib/serialbench/result_merger.rb +0 -1201
- data/lib/serialbench/serializers/xml/base_parser.rb +0 -69
- data/lib/serialbench/serializers/xml/libxml_parser.rb +0 -98
- data/lib/serialbench/serializers/xml/nokogiri_parser.rb +0 -111
- data/lib/serialbench/serializers/xml/oga_parser.rb +0 -85
- data/lib/serialbench/serializers/xml/ox_parser.rb +0 -64
- 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
|
+
}
|