the_mechanic_2 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d9dcefb28ca750b5d08ffb9d386fe32e8d34a900c0ee1dffcf553b2ac6749709
4
+ data.tar.gz: 252eb9d7182588e6b1f26013ab1b59917b02c09e11726190fb6f426aa44e2f53
5
+ SHA512:
6
+ metadata.gz: 1e7a45220f066f260365325abd65e857ef8ea82cd3a6cf6c187c880c1b01fdd5d6904ab9629fda45d70fc2fdb5cc8007ae547f7b44f6a8bb5cf887f8176a6170
7
+ data.tar.gz: 64f74833b90beaffe0cafdb1f90e757ecc7b21353e842f6ad4b57dfa77587aae451702dec77ba463e0699f1c094c88cc94d5044ea1c8681a37ee25675528104d
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright mrpandapants
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,254 @@
1
+ # The Mechanic 2 🔧
2
+
3
+ A Rails Engine for benchmarking Ruby code with full access to your application's classes and dependencies.
4
+
5
+ ## Features
6
+
7
+ - **Rails Engine Integration**: Mount directly into any Rails application
8
+ - **Full Application Context**: Benchmark code with access to all your models, services, and gems
9
+ - **Side-by-Side Comparison**: Compare two code snippets with detailed performance metrics
10
+ - **Security First**: Built-in code validation prevents dangerous operations
11
+ - **Zero Configuration**: Just add the gem and mount the route
12
+ - **Self-Contained**: All assets (CSS & JavaScript) are inlined - no asset pipeline required
13
+ - **Export Results**: Download results as JSON or Markdown
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'the_mechanic_2', path: 'path/to/the_mechanic_2'
21
+ # or from git
22
+ gem 'the_mechanic_2', git: 'https://github.com/yourusername/the_mechanic_2'
23
+ ```
24
+
25
+ Then execute:
26
+
27
+ ```bash
28
+ bundle install
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ### 1. Mount the Engine
34
+
35
+ Add this to your `config/routes.rb`:
36
+
37
+ ```ruby
38
+ Rails.application.routes.draw do
39
+ mount TheMechanic2::Engine => '/ask_the_mechanic_2'
40
+
41
+ # Your other routes...
42
+ end
43
+ ```
44
+
45
+ ### 2. Start Your Rails Server
46
+
47
+ ```bash
48
+ rails server
49
+ ```
50
+
51
+ ### 3. Access The Mechanic
52
+
53
+ Navigate to: `http://localhost:3000/ask_the_mechanic_2`
54
+
55
+ That's it! No additional setup required.
56
+
57
+ ## Example Usage
58
+
59
+ ### Basic Comparison
60
+
61
+ **Shared Setup:**
62
+ ```ruby
63
+ arr = (1..1000).to_a
64
+ ```
65
+
66
+ **Code A:**
67
+ ```ruby
68
+ arr.sum
69
+ ```
70
+
71
+ **Code B:**
72
+ ```ruby
73
+ arr.reduce(:+)
74
+ ```
75
+
76
+ ### With ActiveRecord Models
77
+
78
+ **Shared Setup:**
79
+ ```ruby
80
+ users = User.limit(100)
81
+ ```
82
+
83
+ **Code A:**
84
+ ```ruby
85
+ users.map(&:full_name)
86
+ ```
87
+
88
+ **Code B:**
89
+ ```ruby
90
+ users.pluck(:first_name, :last_name).map { |f, l| "#{f} #{l}" }
91
+ ```
92
+
93
+ ## Configuration (Optional)
94
+
95
+ Create an initializer at `config/initializers/the_mechanic_2.rb`:
96
+
97
+ ```ruby
98
+ TheMechanic2.configure do |config|
99
+ # Set custom timeout (default: 30 seconds)
100
+ config.timeout = 60
101
+
102
+ # Enable authentication (default: false)
103
+ config.enable_authentication = true
104
+
105
+ # Set authentication callback
106
+ config.authentication_callback = ->(controller) {
107
+ controller.current_user&.admin?
108
+ }
109
+ end
110
+ ```
111
+
112
+ ### Configuration Options
113
+
114
+ - **timeout**: Maximum execution time per benchmark (1-300 seconds, default: 30)
115
+ - **enable_authentication**: Require authentication before allowing benchmarks (default: false)
116
+ - **authentication_callback**: Proc that receives the controller and returns true/false
117
+
118
+ ## Security
119
+
120
+ The Mechanic includes comprehensive security validation:
121
+
122
+ - ✅ **Process Isolation**: Each benchmark runs in a separate Rails runner process
123
+ - ✅ **Code Validation**: Blocks dangerous operations before execution
124
+ - ✅ **Timeout Protection**: Automatically terminates long-running code
125
+ - ✅ **Read-Only Database**: Write operations are blocked
126
+ - ✅ **No File System Access**: File operations are forbidden
127
+ - ✅ **No Network Access**: Network operations are blocked
128
+ - ✅ **No System Calls**: System-level operations are prevented
129
+
130
+ ### Forbidden Operations
131
+
132
+ The following operations are automatically blocked:
133
+
134
+ - System calls (`system`, `exec`, `spawn`, backticks)
135
+ - File operations (`File.open`, `File.read`, `File.write`)
136
+ - Network operations (`Net::HTTP`, `Socket`, `URI.open`)
137
+ - Database writes (`save`, `update`, `destroy`, `create`)
138
+ - Thread creation (`Thread.new`)
139
+ - Dangerous evals (`eval`, `instance_eval`, `class_eval`)
140
+
141
+ ## API Endpoints
142
+
143
+ The engine provides the following endpoints:
144
+
145
+ - `GET /ask_the_mechanic_2` - Main UI
146
+ - `POST /ask_the_mechanic_2/validate` - Validate code without executing
147
+ - `POST /ask_the_mechanic_2/run` - Execute benchmark
148
+ - `POST /ask_the_mechanic_2/export` - Export results (JSON or Markdown)
149
+
150
+ ## Platform Requirements
151
+
152
+ - **Ruby**: 2.7.0 or higher
153
+ - **Rails**: 6.0 or higher
154
+ - **Platform**: Linux, macOS, BSD, Unix, Windows
155
+
156
+ ## Performance Metrics
157
+
158
+ The Mechanic measures:
159
+
160
+ - **IPS**: Iterations per second
161
+ - **Standard Deviation**: Performance consistency
162
+ - **Objects Allocated**: Memory allocation count
163
+ - **Memory Usage**: Total memory in MB
164
+ - **Execution Time**: Single run duration
165
+
166
+ ## Development
167
+
168
+ ### Running Tests
169
+
170
+ ```bash
171
+ cd the_mechanic_2
172
+ bundle exec rspec
173
+ ```
174
+
175
+ ### Test Coverage
176
+
177
+ - 163 tests covering all services, models, and controllers
178
+ - Comprehensive security validation tests
179
+ - Integration tests with dummy Rails app
180
+
181
+ ## How It Works
182
+
183
+ 1. **Code Submission**: User submits two code snippets via the web interface
184
+ 2. **Security Validation**: Code is validated for dangerous operations
185
+ 3. **Process Spawning**: Each snippet runs in a separate `rails runner` process
186
+ 4. **Measurement**: Performance and memory metrics are collected
187
+ 5. **Comparison**: Results are compared and a winner is determined
188
+ 6. **Display**: Results are shown side-by-side with detailed metrics
189
+
190
+ ## Architecture
191
+
192
+ ```
193
+ TheMechanic2::Engine
194
+ ├── BenchmarksController # HTTP endpoints
195
+ ├── BenchmarkService # Orchestrates benchmarking
196
+ ├── RailsRunnerService # Spawns isolated processes
197
+ ├── SecurityService # Validates code safety
198
+ ├── BenchmarkRequest # Request validation
199
+ └── BenchmarkResult # Result formatting & export
200
+ ```
201
+
202
+ ## Troubleshooting
203
+
204
+ ### Benchmarks are slow
205
+
206
+ - Reduce the timeout value
207
+ - Simplify your code snippets
208
+ - Check if your Rails app has slow boot time
209
+
210
+ ### "Timeout exceeded" errors
211
+
212
+ - Increase the timeout in configuration
213
+ - Simplify your benchmark code
214
+ - Check for infinite loops
215
+
216
+ ### Can't access my models
217
+
218
+ - Ensure your Rails app is fully loaded
219
+ - Check that models are properly defined
220
+ - Verify the engine is mounted correctly
221
+
222
+ ### Authentication not working
223
+
224
+ - Verify `enable_authentication` is set to `true`
225
+ - Check your authentication callback logic
226
+ - Ensure the callback has access to the controller
227
+
228
+ ## Contributing
229
+
230
+ 1. Fork the repository
231
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
232
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
233
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
234
+ 5. Open a Pull Request
235
+
236
+ ## License
237
+
238
+ This project is licensed under the MIT License - see the LICENSE file for details.
239
+
240
+ ## Credits
241
+
242
+ Inspired by the original [The Mechanic](https://github.com/yourusername/the_mechanic_2) standalone application.
243
+
244
+ Built with:
245
+ - [benchmark-ips](https://github.com/evanphx/benchmark-ips) - Performance measurement
246
+ - [memory_profiler](https://github.com/SamSaffron/memory_profiler) - Memory tracking
247
+
248
+ ## Support
249
+
250
+ For issues and questions, please open an issue on GitHub.
251
+
252
+ ---
253
+
254
+ **Made with ❤️ for the Ruby community**
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require "bundler/setup"
2
+
3
+ load "rails/tasks/statistics.rake"
4
+
5
+ require "bundler/gem_tasks"
@@ -0,0 +1,426 @@
1
+ // The Mechanic 2 - Inline JavaScript
2
+ // Provides UI functionality without external dependencies
3
+
4
+ (function() {
5
+ 'use strict';
6
+
7
+ // Get the base path from the current URL
8
+ const basePath = window.location.pathname.replace(/\/$/, '');
9
+
10
+ // State management
11
+ const state = {
12
+ results: null,
13
+ logs: null,
14
+ isRunning: false,
15
+ logsExpanded: false,
16
+ activeLogTab: 'summary'
17
+ };
18
+
19
+ // Initialize the application
20
+ function init() {
21
+ setupEventListeners();
22
+ initializeEditors();
23
+ }
24
+
25
+ // Setup event listeners
26
+ function setupEventListeners() {
27
+ // Validate button
28
+ const validateBtn = document.getElementById('validate-btn');
29
+ if (validateBtn) {
30
+ validateBtn.addEventListener('click', handleValidate);
31
+ }
32
+
33
+ // Run button
34
+ const runBtn = document.getElementById('run-btn');
35
+ if (runBtn) {
36
+ runBtn.addEventListener('click', handleRun);
37
+ }
38
+
39
+ // Reset button
40
+ const resetBtn = document.getElementById('reset-btn');
41
+ if (resetBtn) {
42
+ resetBtn.addEventListener('click', handleReset);
43
+ }
44
+
45
+ // Logs toggle
46
+ const logsToggle = document.getElementById('logs-toggle');
47
+ if (logsToggle) {
48
+ logsToggle.addEventListener('click', toggleLogs);
49
+ }
50
+
51
+ // Log tabs
52
+ const logTabs = document.querySelectorAll('.log-tab');
53
+ logTabs.forEach(tab => {
54
+ tab.addEventListener('click', () => switchLogTab(tab.dataset.tab));
55
+ });
56
+ }
57
+
58
+ // Initialize simple text editors (fallback if Monaco is not available)
59
+ function initializeEditors() {
60
+ // For now, use simple textareas
61
+ // Monaco Editor can be added later if needed
62
+ const sharedSetup = document.getElementById('shared-setup');
63
+ const codeA = document.getElementById('code-a');
64
+ const codeB = document.getElementById('code-b');
65
+
66
+ if (sharedSetup) sharedSetup.style.fontFamily = 'Monaco, Menlo, monospace';
67
+ if (codeA) codeA.style.fontFamily = 'Monaco, Menlo, monospace';
68
+ if (codeB) codeB.style.fontFamily = 'Monaco, Menlo, monospace';
69
+ }
70
+
71
+ // Handle validate button click
72
+ async function handleValidate() {
73
+ clearMessages();
74
+
75
+ const sharedSetup = document.getElementById('shared-setup').value;
76
+ const codeA = document.getElementById('code-a').value;
77
+ const codeB = document.getElementById('code-b').value;
78
+
79
+ try {
80
+ const response = await fetch(`${basePath}/validate`, {
81
+ method: 'POST',
82
+ headers: {
83
+ 'Content-Type': 'application/json',
84
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
85
+ },
86
+ body: JSON.stringify({
87
+ shared_setup: sharedSetup,
88
+ code_a: codeA,
89
+ code_b: codeB
90
+ })
91
+ });
92
+
93
+ const data = await response.json();
94
+
95
+ if (data.valid) {
96
+ showSuccess('All code is valid and safe to run!');
97
+ } else {
98
+ showError('Validation failed', data.errors);
99
+ }
100
+ } catch (error) {
101
+ showError('Validation error', [error.message]);
102
+ }
103
+ }
104
+
105
+ // Handle run button click
106
+ async function handleRun() {
107
+ if (state.isRunning) return;
108
+
109
+ clearMessages();
110
+ hideResults();
111
+
112
+ const sharedSetup = document.getElementById('shared-setup').value;
113
+ const codeA = document.getElementById('code-a').value;
114
+ const codeB = document.getElementById('code-b').value;
115
+ const timeout = parseInt(document.getElementById('timeout').value) || 30;
116
+
117
+ if (!codeA.trim() || !codeB.trim()) {
118
+ showError('Missing code', ['Both Code A and Code B are required']);
119
+ return;
120
+ }
121
+
122
+ setRunning(true);
123
+
124
+ try {
125
+ const response = await fetch(`${basePath}/run`, {
126
+ method: 'POST',
127
+ headers: {
128
+ 'Content-Type': 'application/json',
129
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
130
+ },
131
+ body: JSON.stringify({
132
+ shared_setup: sharedSetup,
133
+ code_a: codeA,
134
+ code_b: codeB,
135
+ timeout: timeout
136
+ })
137
+ });
138
+
139
+ const data = await response.json();
140
+
141
+ if (response.ok) {
142
+ state.results = data;
143
+ displayResults(data);
144
+ showSuccess('Benchmark completed successfully!');
145
+ } else {
146
+ showError(data.error || 'Benchmark failed', data.errors || [data.message]);
147
+ }
148
+ } catch (error) {
149
+ showError('Benchmark error', [error.message]);
150
+ } finally {
151
+ setRunning(false);
152
+ }
153
+ }
154
+
155
+ // Handle reset button click
156
+ function handleReset() {
157
+ if (confirm('Are you sure you want to reset? This will clear all inputs and results.')) {
158
+ document.getElementById('shared-setup').value = '';
159
+ document.getElementById('code-a').value = '';
160
+ document.getElementById('code-b').value = '';
161
+ document.getElementById('timeout').value = '30';
162
+ clearMessages();
163
+ hideResults();
164
+ state.results = null;
165
+ state.logs = null;
166
+ }
167
+ }
168
+
169
+
170
+
171
+ // Display results
172
+ function displayResults(data) {
173
+ const resultsSection = document.getElementById('results-section');
174
+ if (!resultsSection) return;
175
+
176
+ resultsSection.classList.remove('hidden');
177
+
178
+ // Display Code A metrics
179
+ displayMetrics('code-a', data.code_a_metrics, data.winner === 'code_a');
180
+
181
+ // Display Code B metrics
182
+ displayMetrics('code-b', data.code_b_metrics, data.winner === 'code_b');
183
+
184
+ // Display summary
185
+ const summaryText = document.getElementById('summary-text');
186
+ if (summaryText) {
187
+ summaryText.textContent = data.summary;
188
+ }
189
+
190
+ // Show runtime logs section
191
+ const logsSection = document.getElementById('runtime-logs-section');
192
+ if (logsSection) {
193
+ logsSection.classList.remove('hidden');
194
+ }
195
+
196
+ // Update log panels
197
+ updateLogPanels(data);
198
+ }
199
+
200
+ // Display metrics for a code snippet
201
+ function displayMetrics(codeId, metrics, isWinner) {
202
+ const card = document.getElementById(`${codeId}-card`);
203
+ if (!card) return;
204
+
205
+ if (isWinner) {
206
+ card.classList.add('winner');
207
+ } else {
208
+ card.classList.remove('winner');
209
+ }
210
+
211
+ const winnerBadge = card.querySelector('.winner-badge');
212
+ if (winnerBadge) {
213
+ winnerBadge.classList.toggle('hidden', !isWinner);
214
+ }
215
+
216
+ // Update metrics
217
+ updateMetric(card, 'ips', formatNumber(metrics.ips));
218
+ updateMetric(card, 'stddev', formatNumber(metrics.stddev));
219
+ updateMetric(card, 'objects', formatNumber(metrics.objects));
220
+ updateMetric(card, 'memory', formatNumber(metrics.memory_mb) + ' MB');
221
+ updateMetric(card, 'time', formatNumber(metrics.execution_time) + ' sec');
222
+ }
223
+
224
+ // Update a single metric
225
+ function updateMetric(card, metricName, value) {
226
+ const element = card.querySelector(`[data-metric="${metricName}"]`);
227
+ if (element) {
228
+ element.textContent = value;
229
+ }
230
+ }
231
+
232
+ // Format number for display
233
+ function formatNumber(num) {
234
+ if (num === null || num === undefined) return '0';
235
+
236
+ if (typeof num === 'number') {
237
+ if (num < 0.01) {
238
+ return num.toFixed(6);
239
+ } else if (num < 1) {
240
+ return num.toFixed(4);
241
+ } else if (num < 100) {
242
+ return num.toFixed(2);
243
+ } else {
244
+ return Math.round(num).toLocaleString();
245
+ }
246
+ }
247
+
248
+ return num.toString();
249
+ }
250
+
251
+ // Update log panels with benchmark data
252
+ function updateLogPanels(data) {
253
+ // Summary log
254
+ const summaryLog = document.getElementById('summary-log');
255
+ if (summaryLog) {
256
+ summaryLog.textContent = `${data.summary}
257
+
258
+ Code A Performance:
259
+ IPS: ${formatNumber(data.code_a_metrics.ips)} iterations/sec
260
+ Std Dev: ±${formatNumber(data.code_a_metrics.stddev)}
261
+ Objects: ${formatNumber(data.code_a_metrics.objects)}
262
+ Memory: ${formatNumber(data.code_a_metrics.memory_mb)} MB
263
+ Time: ${formatNumber(data.code_a_metrics.execution_time)} sec
264
+
265
+ Code B Performance:
266
+ IPS: ${formatNumber(data.code_b_metrics.ips)} iterations/sec
267
+ Std Dev: ±${formatNumber(data.code_b_metrics.stddev)}
268
+ Objects: ${formatNumber(data.code_b_metrics.objects)}
269
+ Memory: ${formatNumber(data.code_b_metrics.memory_mb)} MB
270
+ Time: ${formatNumber(data.code_b_metrics.execution_time)} sec`;
271
+ }
272
+
273
+ // Benchmark log
274
+ const benchmarkLog = document.getElementById('benchmark-log');
275
+ if (benchmarkLog) {
276
+ benchmarkLog.textContent = `Benchmark.ips Results:
277
+
278
+ Code A: ${formatNumber(data.code_a_metrics.ips)} i/s
279
+ Code B: ${formatNumber(data.code_b_metrics.ips)} i/s
280
+
281
+ Winner: ${data.winner === 'code_a' ? 'Code A' : data.winner === 'code_b' ? 'Code B' : 'Tie'}
282
+ Performance Ratio: ${data.performance_ratio}×`;
283
+ }
284
+
285
+ // Memory log
286
+ const memoryLog = document.getElementById('memory-log');
287
+ if (memoryLog) {
288
+ memoryLog.textContent = `Memory Profiling Results:
289
+
290
+ Code A:
291
+ Total Objects Allocated: ${formatNumber(data.code_a_metrics.objects)}
292
+ Total Memory: ${formatNumber(data.code_a_metrics.memory_mb)} MB
293
+
294
+ Code B:
295
+ Total Objects Allocated: ${formatNumber(data.code_b_metrics.objects)}
296
+ Total Memory: ${formatNumber(data.code_b_metrics.memory_mb)} MB`;
297
+ }
298
+
299
+ // GC log
300
+ const gcLog = document.getElementById('gc-log');
301
+ if (gcLog) {
302
+ gcLog.textContent = `Garbage Collection Statistics:
303
+
304
+ Note: GC statistics collection is not yet implemented.
305
+ This will show Ruby GC stats during benchmark execution.`;
306
+ }
307
+ }
308
+
309
+ // Toggle logs visibility
310
+ function toggleLogs() {
311
+ state.logsExpanded = !state.logsExpanded;
312
+ const logsContent = document.getElementById('logs-content');
313
+ const logsToggle = document.getElementById('logs-toggle');
314
+ const chevron = logsToggle?.querySelector('.logs-chevron');
315
+ const toggleText = logsToggle?.querySelector('.logs-toggle-text');
316
+
317
+ if (logsContent) {
318
+ logsContent.classList.toggle('hidden');
319
+ }
320
+
321
+ if (chevron) {
322
+ chevron.classList.toggle('expanded');
323
+ }
324
+
325
+ if (toggleText) {
326
+ toggleText.textContent = state.logsExpanded ? 'Click to collapse' : 'Click to expand';
327
+ }
328
+
329
+ if (logsToggle) {
330
+ logsToggle.setAttribute('aria-expanded', state.logsExpanded);
331
+ }
332
+ }
333
+
334
+ // Switch log tab
335
+ function switchLogTab(tabName) {
336
+ state.activeLogTab = tabName;
337
+
338
+ // Update tab buttons
339
+ document.querySelectorAll('.log-tab').forEach(tab => {
340
+ tab.classList.toggle('active', tab.dataset.tab === tabName);
341
+ });
342
+
343
+ // Update panels
344
+ document.querySelectorAll('.log-panel').forEach(panel => {
345
+ panel.classList.toggle('hidden', panel.dataset.panel !== tabName);
346
+ });
347
+ }
348
+
349
+ // Set running state
350
+ function setRunning(running) {
351
+ state.isRunning = running;
352
+
353
+ const runBtn = document.getElementById('run-btn');
354
+ const validateBtn = document.getElementById('validate-btn');
355
+
356
+ if (runBtn) {
357
+ runBtn.disabled = running;
358
+ runBtn.innerHTML = running ?
359
+ '<span class="loading-spinner"></span> Running...' :
360
+ 'Run Benchmark';
361
+ }
362
+
363
+ if (validateBtn) {
364
+ validateBtn.disabled = running;
365
+ }
366
+ }
367
+
368
+ // Show success message
369
+ function showSuccess(message) {
370
+ const container = document.getElementById('message-container');
371
+ if (!container) return;
372
+
373
+ container.innerHTML = `
374
+ <div class="success-message">
375
+ ${message}
376
+ </div>
377
+ `;
378
+ }
379
+
380
+ // Show error message
381
+ function showError(title, errors) {
382
+ const container = document.getElementById('message-container');
383
+ if (!container) return;
384
+
385
+ const errorList = errors && errors.length > 0 ? `
386
+ <ul class="error-list">
387
+ ${errors.map(e => `<li>${e}</li>`).join('')}
388
+ </ul>
389
+ ` : '';
390
+
391
+ container.innerHTML = `
392
+ <div class="error-message">
393
+ <strong>${title}</strong>
394
+ ${errorList}
395
+ </div>
396
+ `;
397
+ }
398
+
399
+ // Clear messages
400
+ function clearMessages() {
401
+ const container = document.getElementById('message-container');
402
+ if (container) {
403
+ container.innerHTML = '';
404
+ }
405
+ }
406
+
407
+ // Hide results
408
+ function hideResults() {
409
+ const resultsSection = document.getElementById('results-section');
410
+ if (resultsSection) {
411
+ resultsSection.classList.add('hidden');
412
+ }
413
+
414
+ const logsSection = document.getElementById('runtime-logs-section');
415
+ if (logsSection) {
416
+ logsSection.classList.add('hidden');
417
+ }
418
+ }
419
+
420
+ // Initialize when DOM is ready
421
+ if (document.readyState === 'loading') {
422
+ document.addEventListener('DOMContentLoaded', init);
423
+ } else {
424
+ init();
425
+ }
426
+ })();