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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +254 -0
- data/Rakefile +5 -0
- data/app/assets/javascripts/the_mechanic_2/application.js +426 -0
- data/app/assets/stylesheets/the_mechanic_2/application.css +666 -0
- data/app/controllers/the_mechanic_2/application_controller.rb +6 -0
- data/app/controllers/the_mechanic_2/benchmarks_controller.rb +174 -0
- data/app/helpers/the_mechanic_2/application_helper.rb +4 -0
- data/app/jobs/the_mechanic_2/application_job.rb +4 -0
- data/app/mailers/the_mechanic_2/application_mailer.rb +6 -0
- data/app/models/the_mechanic_2/application_record.rb +5 -0
- data/app/models/the_mechanic_2/benchmark_request.rb +79 -0
- data/app/models/the_mechanic_2/benchmark_result.rb +136 -0
- data/app/services/the_mechanic_2/benchmark_service.rb +115 -0
- data/app/services/the_mechanic_2/rails_runner_service.rb +235 -0
- data/app/services/the_mechanic_2/security_service.rb +128 -0
- data/app/views/layouts/the_mechanic_2/application.html.erb +17 -0
- data/app/views/the_mechanic_2/benchmarks/index.html.erb +266 -0
- data/config/routes.rb +7 -0
- data/lib/tasks/the_mechanic_tasks.rake +4 -0
- data/lib/the_mechanic_2/configuration.rb +45 -0
- data/lib/the_mechanic_2/engine.rb +18 -0
- data/lib/the_mechanic_2/version.rb +3 -0
- data/lib/the_mechanic_2.rb +7 -0
- metadata +157 -0
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,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
|
+
})();
|