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
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'tempfile'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'open3'
|
|
6
|
+
require 'timeout'
|
|
7
|
+
|
|
8
|
+
module TheMechanic2
|
|
9
|
+
# Service for spawning Rails runner processes to execute benchmark code
|
|
10
|
+
# Each benchmark runs in a completely isolated Rails environment
|
|
11
|
+
class RailsRunnerService
|
|
12
|
+
class BenchmarkTimeout < StandardError; end
|
|
13
|
+
class BenchmarkError < StandardError; end
|
|
14
|
+
|
|
15
|
+
# Executes benchmark code in a separate Rails runner process
|
|
16
|
+
# @param code [String] The Ruby code to benchmark
|
|
17
|
+
# @param shared_setup [String] Optional setup code to run before benchmark
|
|
18
|
+
# @param timeout [Integer] Maximum execution time in seconds
|
|
19
|
+
# @return [Hash] Benchmark results with IPS, memory, and other metrics
|
|
20
|
+
def execute(code:, shared_setup: nil, timeout: 30)
|
|
21
|
+
script_file = create_script(code, shared_setup)
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
stdout, stderr, status = spawn_runner(script_file.path, timeout)
|
|
25
|
+
parse_output(stdout, stderr, status)
|
|
26
|
+
ensure
|
|
27
|
+
script_file.close
|
|
28
|
+
script_file.unlink
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Creates a temporary Ruby script file with the benchmark code
|
|
35
|
+
# @param code [String] The benchmark code
|
|
36
|
+
# @param shared_setup [String] Optional setup code
|
|
37
|
+
# @return [Tempfile] The temporary script file
|
|
38
|
+
def create_script(code, shared_setup)
|
|
39
|
+
script_file = Tempfile.new(['benchmark', '.rb'])
|
|
40
|
+
|
|
41
|
+
script_content = generate_script_content(code, shared_setup)
|
|
42
|
+
script_file.write(script_content)
|
|
43
|
+
script_file.flush
|
|
44
|
+
script_file.rewind
|
|
45
|
+
|
|
46
|
+
script_file
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Generates the complete script content for benchmarking
|
|
50
|
+
# @param code [String] The benchmark code
|
|
51
|
+
# @param shared_setup [String] Optional setup code
|
|
52
|
+
# @return [String] The complete Ruby script
|
|
53
|
+
def generate_script_content(code, shared_setup)
|
|
54
|
+
require 'base64'
|
|
55
|
+
|
|
56
|
+
# Encode the code and setup to avoid interpolation issues
|
|
57
|
+
encoded_code = Base64.strict_encode64(code)
|
|
58
|
+
encoded_setup = shared_setup ? Base64.strict_encode64(shared_setup) : nil
|
|
59
|
+
|
|
60
|
+
# Generate script with Base64 encoded code
|
|
61
|
+
<<~RUBY
|
|
62
|
+
require 'benchmark/ips'
|
|
63
|
+
require 'memory_profiler'
|
|
64
|
+
require 'json'
|
|
65
|
+
require 'base64'
|
|
66
|
+
require 'stringio'
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
# Decode the user code
|
|
70
|
+
user_code = Base64.strict_decode64('#{encoded_code}')
|
|
71
|
+
#{encoded_setup ? "shared_setup_code = Base64.strict_decode64('#{encoded_setup}')" : "shared_setup_code = nil"}
|
|
72
|
+
|
|
73
|
+
# Create a shared binding for eval context
|
|
74
|
+
shared_binding = binding
|
|
75
|
+
|
|
76
|
+
# Execute shared setup if provided
|
|
77
|
+
eval(shared_setup_code, shared_binding) if shared_setup_code
|
|
78
|
+
|
|
79
|
+
# Wrap code in a lambda that suppresses stdout
|
|
80
|
+
code_block = lambda do
|
|
81
|
+
original_stdout = $stdout
|
|
82
|
+
$stdout = File.open(File::NULL, 'w')
|
|
83
|
+
begin
|
|
84
|
+
eval(user_code, shared_binding)
|
|
85
|
+
ensure
|
|
86
|
+
$stdout.close unless $stdout == original_stdout
|
|
87
|
+
$stdout = original_stdout
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Capture Benchmark.ips results
|
|
92
|
+
ips_result = nil
|
|
93
|
+
stddev_result = nil
|
|
94
|
+
|
|
95
|
+
# Save the real stdout
|
|
96
|
+
real_stdout = $stdout
|
|
97
|
+
|
|
98
|
+
# Redirect stdout temporarily to capture benchmark output
|
|
99
|
+
$stdout = StringIO.new
|
|
100
|
+
|
|
101
|
+
Benchmark.ips do |x|
|
|
102
|
+
x.config(time: 5, warmup: 2)
|
|
103
|
+
|
|
104
|
+
x.report('benchmark') do
|
|
105
|
+
code_block.call
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Store the results
|
|
109
|
+
x.compare!
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Get the benchmark output
|
|
113
|
+
benchmark_output = $stdout.string
|
|
114
|
+
$stdout = real_stdout
|
|
115
|
+
|
|
116
|
+
# Parse IPS from benchmark output
|
|
117
|
+
# Format: "benchmark 42.072M (± 2.1%) i/s"
|
|
118
|
+
# Can be in format like "42.072M" or "123.456k" or just "1234.5"
|
|
119
|
+
if benchmark_output =~ /benchmark\\s+([\\d.]+)([MKk]?)\\s+\\(±\\s*([\\d.]+)%\\)\\s+i\\/s/
|
|
120
|
+
value = $1.to_f
|
|
121
|
+
unit = $2
|
|
122
|
+
stddev_percent = $3.to_f
|
|
123
|
+
|
|
124
|
+
# Convert to actual IPS based on unit
|
|
125
|
+
case unit
|
|
126
|
+
when 'M'
|
|
127
|
+
ips_result = value * 1_000_000
|
|
128
|
+
when 'K', 'k'
|
|
129
|
+
ips_result = value * 1_000
|
|
130
|
+
else
|
|
131
|
+
ips_result = value
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
stddev_result = ips_result * (stddev_percent / 100.0)
|
|
135
|
+
else
|
|
136
|
+
# Fallback if parsing fails
|
|
137
|
+
ips_result = 0.0
|
|
138
|
+
stddev_result = 0.0
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Measure memory usage (suppress output)
|
|
142
|
+
null_file = File.open(File::NULL, 'w')
|
|
143
|
+
$stdout = null_file
|
|
144
|
+
memory_report = MemoryProfiler.report do
|
|
145
|
+
100.times do
|
|
146
|
+
code_block.call
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
null_file.close
|
|
150
|
+
$stdout = real_stdout
|
|
151
|
+
|
|
152
|
+
# Calculate execution time for a single run
|
|
153
|
+
execution_start = Time.now
|
|
154
|
+
code_block.call
|
|
155
|
+
execution_time = Time.now - execution_start
|
|
156
|
+
|
|
157
|
+
# Serialize results as JSON
|
|
158
|
+
results = {
|
|
159
|
+
ips: ips_result.round(2),
|
|
160
|
+
stddev: stddev_result.round(2),
|
|
161
|
+
objects: memory_report.total_allocated,
|
|
162
|
+
memory_mb: (memory_report.total_allocated_memsize / 1024.0 / 1024.0).round(4),
|
|
163
|
+
execution_time: execution_time.round(6)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
puts JSON.generate(results)
|
|
167
|
+
|
|
168
|
+
rescue => e
|
|
169
|
+
# Output error as JSON
|
|
170
|
+
error_result = {
|
|
171
|
+
error: e.message,
|
|
172
|
+
backtrace: e.backtrace.first(10)
|
|
173
|
+
}
|
|
174
|
+
puts JSON.generate(error_result)
|
|
175
|
+
exit(1)
|
|
176
|
+
end
|
|
177
|
+
RUBY
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Spawns a Rails runner process with the script
|
|
181
|
+
# @param script_path [String] Path to the temporary script file
|
|
182
|
+
# @param timeout [Integer] Maximum execution time
|
|
183
|
+
# @return [Array] stdout, stderr, and status
|
|
184
|
+
def spawn_runner(script_path, timeout)
|
|
185
|
+
# For testing, use ruby directly. In production, this will be called from a real Rails app
|
|
186
|
+
# where rails runner will work properly
|
|
187
|
+
cmd = if ENV['RAILS_ENV'] == 'test'
|
|
188
|
+
"bundle exec ruby #{script_path}"
|
|
189
|
+
else
|
|
190
|
+
"bundle exec rails runner #{script_path}"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
Timeout.timeout(timeout) do
|
|
194
|
+
Open3.capture3(
|
|
195
|
+
cmd,
|
|
196
|
+
chdir: Rails.root.to_s
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
rescue Timeout::Error
|
|
200
|
+
raise BenchmarkTimeout, "Execution exceeded #{timeout} seconds"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Parses the output from the Rails runner process
|
|
204
|
+
# @param stdout [String] Standard output from the process
|
|
205
|
+
# @param stderr [String] Standard error from the process
|
|
206
|
+
# @param status [Process::Status] Process exit status
|
|
207
|
+
# @return [Hash] Parsed benchmark results
|
|
208
|
+
def parse_output(stdout, stderr, status)
|
|
209
|
+
if status.success?
|
|
210
|
+
begin
|
|
211
|
+
# Extract the JSON line from stdout (last line should be JSON)
|
|
212
|
+
json_line = stdout.lines.last&.strip
|
|
213
|
+
raise BenchmarkError, "No JSON output found" if json_line.nil? || json_line.empty?
|
|
214
|
+
|
|
215
|
+
JSON.parse(json_line, symbolize_names: true)
|
|
216
|
+
rescue JSON::ParserError => e
|
|
217
|
+
raise BenchmarkError, "Failed to parse benchmark results: #{e.message}\nOutput: #{stdout}"
|
|
218
|
+
end
|
|
219
|
+
else
|
|
220
|
+
# Try to parse error from stdout
|
|
221
|
+
begin
|
|
222
|
+
json_line = stdout.lines.last&.strip
|
|
223
|
+
if json_line && !json_line.empty?
|
|
224
|
+
error_data = JSON.parse(json_line, symbolize_names: true)
|
|
225
|
+
raise BenchmarkError, "Benchmark failed: #{error_data[:error]}"
|
|
226
|
+
else
|
|
227
|
+
raise BenchmarkError, "Benchmark failed with exit code #{status.exitstatus}\nStderr: #{stderr}\nStdout: #{stdout}"
|
|
228
|
+
end
|
|
229
|
+
rescue JSON::ParserError
|
|
230
|
+
raise BenchmarkError, "Benchmark failed with exit code #{status.exitstatus}\nStderr: #{stderr}\nStdout: #{stdout}"
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TheMechanic2
|
|
4
|
+
# Service for validating Ruby code before execution
|
|
5
|
+
# Blocks dangerous operations like system calls, file I/O, network access, etc.
|
|
6
|
+
class SecurityService
|
|
7
|
+
# Forbidden patterns that should not be allowed in benchmark code
|
|
8
|
+
FORBIDDEN_PATTERNS = {
|
|
9
|
+
system_calls: [
|
|
10
|
+
/\bsystem\s*\(/,
|
|
11
|
+
/\bexec\s*\(/,
|
|
12
|
+
/\bspawn\s*\(/,
|
|
13
|
+
/`[^`]+`/, # backticks
|
|
14
|
+
/\%x\{/, # %x{} syntax
|
|
15
|
+
/\bfork\s*(\(|do|\{)/,
|
|
16
|
+
/Process\.spawn/,
|
|
17
|
+
/Process\.exec/,
|
|
18
|
+
/Kernel\.system/,
|
|
19
|
+
/Kernel\.exec/,
|
|
20
|
+
/Kernel\.spawn/
|
|
21
|
+
],
|
|
22
|
+
network_operations: [
|
|
23
|
+
/URI\.open/, # Check this first before generic open
|
|
24
|
+
/Net::HTTP/,
|
|
25
|
+
/Net::FTP/,
|
|
26
|
+
/Net::SMTP/,
|
|
27
|
+
/Net::POP3/,
|
|
28
|
+
/Net::IMAP/,
|
|
29
|
+
/Socket\./,
|
|
30
|
+
/TCPSocket/,
|
|
31
|
+
/UDPSocket/,
|
|
32
|
+
/UNIXSocket/,
|
|
33
|
+
/HTTParty/,
|
|
34
|
+
/Faraday/,
|
|
35
|
+
/RestClient/
|
|
36
|
+
],
|
|
37
|
+
file_operations: [
|
|
38
|
+
/File\.open/,
|
|
39
|
+
/File\.read/,
|
|
40
|
+
/File\.write/,
|
|
41
|
+
/File\.delete/,
|
|
42
|
+
/File\.unlink/,
|
|
43
|
+
/File\.rename/,
|
|
44
|
+
/File\.chmod/,
|
|
45
|
+
/File\.chown/,
|
|
46
|
+
/FileUtils\./,
|
|
47
|
+
/IO\.read/,
|
|
48
|
+
/IO\.write/,
|
|
49
|
+
/IO\.open/,
|
|
50
|
+
/\bopen\s*\(/ # Kernel#open
|
|
51
|
+
],
|
|
52
|
+
database_writes: [
|
|
53
|
+
/\.save[!\s(]/,
|
|
54
|
+
/\.save$/,
|
|
55
|
+
/\.update[!\s(]/,
|
|
56
|
+
/\.update$/,
|
|
57
|
+
/\.update_all/,
|
|
58
|
+
/\.update_attribute/,
|
|
59
|
+
/\.update_column/,
|
|
60
|
+
/\.destroy[!\s(]/,
|
|
61
|
+
/\.destroy$/,
|
|
62
|
+
/\.destroy_all/,
|
|
63
|
+
/\.delete[!\s(]/,
|
|
64
|
+
/\.delete$/,
|
|
65
|
+
/\.delete_all/,
|
|
66
|
+
/\.create[!\s(]/,
|
|
67
|
+
/\.create$/,
|
|
68
|
+
/\.insert/,
|
|
69
|
+
/\.upsert/,
|
|
70
|
+
/ActiveRecord::Base\.connection\.execute/,
|
|
71
|
+
/\.connection\.execute/
|
|
72
|
+
],
|
|
73
|
+
dangerous_evals: [
|
|
74
|
+
/\beval\s*\(/,
|
|
75
|
+
/instance_eval/,
|
|
76
|
+
/class_eval/,
|
|
77
|
+
/module_eval/,
|
|
78
|
+
/define_method/,
|
|
79
|
+
/send\s*\(/,
|
|
80
|
+
/__send__/,
|
|
81
|
+
/public_send/,
|
|
82
|
+
/method\s*\(/,
|
|
83
|
+
/const_get/,
|
|
84
|
+
/const_set/,
|
|
85
|
+
/remove_const/,
|
|
86
|
+
/class_variable_set/,
|
|
87
|
+
/instance_variable_set/
|
|
88
|
+
],
|
|
89
|
+
thread_operations: [
|
|
90
|
+
/Thread\.new/,
|
|
91
|
+
/Thread\.start/,
|
|
92
|
+
/Thread\.fork/
|
|
93
|
+
]
|
|
94
|
+
}.freeze
|
|
95
|
+
|
|
96
|
+
# Validates the given code for security issues
|
|
97
|
+
# @param code [String] The Ruby code to validate
|
|
98
|
+
# @return [Hash] Validation result with :valid and :errors keys
|
|
99
|
+
def self.validate(code)
|
|
100
|
+
return { valid: false, errors: ['Code cannot be empty'] } if code.nil? || code.strip.empty?
|
|
101
|
+
|
|
102
|
+
errors = []
|
|
103
|
+
|
|
104
|
+
FORBIDDEN_PATTERNS.each do |category, patterns|
|
|
105
|
+
patterns.each do |pattern|
|
|
106
|
+
if code.match?(pattern)
|
|
107
|
+
errors << format_error(category, pattern, code)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
valid: errors.empty?,
|
|
114
|
+
errors: errors
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# Formats an error message for a forbidden pattern match
|
|
121
|
+
def self.format_error(category, pattern, code)
|
|
122
|
+
matched_text = code.match(pattern)&.to_s || 'unknown'
|
|
123
|
+
category_name = category.to_s.tr('_', ' ').capitalize
|
|
124
|
+
|
|
125
|
+
"#{category_name} detected: '#{matched_text}' is not allowed for security reasons"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>The mechanic</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= yield :head %>
|
|
9
|
+
|
|
10
|
+
<%= stylesheet_link_tag "the_mechanic_2/application", media: "all" %>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
|
|
14
|
+
<%= yield %>
|
|
15
|
+
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>The Mechanic 2 - Ruby Code Benchmarking</title>
|
|
7
|
+
<meta name="csrf-param" content="authenticity_token" />
|
|
8
|
+
<meta name="csrf-token" content="<%= form_authenticity_token %>" />
|
|
9
|
+
<style>
|
|
10
|
+
<%= raw inline_css %>
|
|
11
|
+
</style>
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
<div id="app">
|
|
15
|
+
<!-- Header -->
|
|
16
|
+
<header class="header">
|
|
17
|
+
<div class="header-content">
|
|
18
|
+
<div>
|
|
19
|
+
<h1>🔧 The Mechanic 2</h1>
|
|
20
|
+
<p class="header-subtitle">Ruby Code Benchmarking Engine</p>
|
|
21
|
+
</div>
|
|
22
|
+
<button id="reset-btn" class="btn btn-secondary">Reset</button>
|
|
23
|
+
</div>
|
|
24
|
+
</header>
|
|
25
|
+
|
|
26
|
+
<!-- Main Content -->
|
|
27
|
+
<main class="main-content">
|
|
28
|
+
<!-- Message Container -->
|
|
29
|
+
<div id="message-container"></div>
|
|
30
|
+
|
|
31
|
+
<!-- Code Input Section -->
|
|
32
|
+
<section class="code-input-section">
|
|
33
|
+
<div class="card">
|
|
34
|
+
<h2 class="card-header">Code Input</h2>
|
|
35
|
+
|
|
36
|
+
<!-- Code A and B Grid -->
|
|
37
|
+
<div class="code-input-grid">
|
|
38
|
+
<div class="code-input-container">
|
|
39
|
+
<label class="code-input-label" for="code-a">
|
|
40
|
+
Code A
|
|
41
|
+
</label>
|
|
42
|
+
<textarea
|
|
43
|
+
id="code-a"
|
|
44
|
+
class="code-editor"
|
|
45
|
+
rows="10"
|
|
46
|
+
placeholder="# Enter first code snippet # Example: arr.sum"
|
|
47
|
+
></textarea>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div class="code-input-container">
|
|
51
|
+
<label class="code-input-label" for="code-b">
|
|
52
|
+
Code B
|
|
53
|
+
</label>
|
|
54
|
+
<textarea
|
|
55
|
+
id="code-b"
|
|
56
|
+
class="code-editor"
|
|
57
|
+
rows="10"
|
|
58
|
+
placeholder="# Enter second code snippet # Example: arr.reduce(:+)"
|
|
59
|
+
></textarea>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<!-- Shared Setup -->
|
|
64
|
+
<div class="shared-setup-container">
|
|
65
|
+
<label class="code-input-label" for="shared-setup">
|
|
66
|
+
Shared Setup (Optional)
|
|
67
|
+
</label>
|
|
68
|
+
<textarea
|
|
69
|
+
id="shared-setup"
|
|
70
|
+
class="code-editor"
|
|
71
|
+
rows="4"
|
|
72
|
+
placeholder="# Optional: Code that runs before both snippets # Example: arr = [1, 2, 3, 4, 5]"
|
|
73
|
+
></textarea>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<!-- Timeout Setting -->
|
|
77
|
+
<div style="margin-top: 1rem;">
|
|
78
|
+
<label class="code-input-label" for="timeout">
|
|
79
|
+
Timeout (seconds)
|
|
80
|
+
</label>
|
|
81
|
+
<input
|
|
82
|
+
type="number"
|
|
83
|
+
id="timeout"
|
|
84
|
+
value="30"
|
|
85
|
+
min="1"
|
|
86
|
+
max="300"
|
|
87
|
+
style="padding: 0.5rem; border: 1px solid #d1d5db; border-radius: 0.375rem; width: 100px;"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<!-- Action Buttons -->
|
|
92
|
+
<div class="btn-group">
|
|
93
|
+
<button id="validate-btn" class="btn btn-secondary">
|
|
94
|
+
Validate Code
|
|
95
|
+
</button>
|
|
96
|
+
<button id="run-btn" class="btn btn-primary">
|
|
97
|
+
Run Benchmark
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
102
|
+
|
|
103
|
+
<!-- Results Section -->
|
|
104
|
+
<section id="results-section" class="results-section hidden">
|
|
105
|
+
<h2 class="card-header">Results</h2>
|
|
106
|
+
|
|
107
|
+
<!-- Summary -->
|
|
108
|
+
<div class="summary-section">
|
|
109
|
+
<p id="summary-text" class="summary-text"></p>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- Results Grid -->
|
|
113
|
+
<div class="results-grid">
|
|
114
|
+
<!-- Code A Results -->
|
|
115
|
+
<div id="code-a-card" class="result-card">
|
|
116
|
+
<div class="result-card-header">
|
|
117
|
+
<h3 class="result-card-title">Code A</h3>
|
|
118
|
+
<span class="winner-badge hidden">Winner</span>
|
|
119
|
+
</div>
|
|
120
|
+
<ul class="metrics-list">
|
|
121
|
+
<li class="metric-item">
|
|
122
|
+
<span class="metric-label">IPS (iterations/sec)</span>
|
|
123
|
+
<span class="metric-value" data-metric="ips">-</span>
|
|
124
|
+
</li>
|
|
125
|
+
<li class="metric-item">
|
|
126
|
+
<span class="metric-label">Standard Deviation</span>
|
|
127
|
+
<span class="metric-value" data-metric="stddev">-</span>
|
|
128
|
+
</li>
|
|
129
|
+
<li class="metric-item">
|
|
130
|
+
<span class="metric-label">Objects Allocated</span>
|
|
131
|
+
<span class="metric-value" data-metric="objects">-</span>
|
|
132
|
+
</li>
|
|
133
|
+
<li class="metric-item">
|
|
134
|
+
<span class="metric-label">Memory (MB)</span>
|
|
135
|
+
<span class="metric-value" data-metric="memory">-</span>
|
|
136
|
+
</li>
|
|
137
|
+
<li class="metric-item">
|
|
138
|
+
<span class="metric-label">Execution Time</span>
|
|
139
|
+
<span class="metric-value" data-metric="time">-</span>
|
|
140
|
+
</li>
|
|
141
|
+
</ul>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<!-- Code B Results -->
|
|
145
|
+
<div id="code-b-card" class="result-card">
|
|
146
|
+
<div class="result-card-header">
|
|
147
|
+
<h3 class="result-card-title">Code B</h3>
|
|
148
|
+
<span class="winner-badge hidden">Winner</span>
|
|
149
|
+
</div>
|
|
150
|
+
<ul class="metrics-list">
|
|
151
|
+
<li class="metric-item">
|
|
152
|
+
<span class="metric-label">IPS (iterations/sec)</span>
|
|
153
|
+
<span class="metric-value" data-metric="ips">-</span>
|
|
154
|
+
</li>
|
|
155
|
+
<li class="metric-item">
|
|
156
|
+
<span class="metric-label">Standard Deviation</span>
|
|
157
|
+
<span class="metric-value" data-metric="stddev">-</span>
|
|
158
|
+
</li>
|
|
159
|
+
<li class="metric-item">
|
|
160
|
+
<span class="metric-label">Objects Allocated</span>
|
|
161
|
+
<span class="metric-value" data-metric="objects">-</span>
|
|
162
|
+
</li>
|
|
163
|
+
<li class="metric-item">
|
|
164
|
+
<span class="metric-label">Memory (MB)</span>
|
|
165
|
+
<span class="metric-value" data-metric="memory">-</span>
|
|
166
|
+
</li>
|
|
167
|
+
<li class="metric-item">
|
|
168
|
+
<span class="metric-label">Execution Time</span>
|
|
169
|
+
<span class="metric-value" data-metric="time">-</span>
|
|
170
|
+
</li>
|
|
171
|
+
</ul>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<!-- Runtime Logs & Detailed Analysis -->
|
|
176
|
+
<div id="runtime-logs-section" class="runtime-logs-section hidden">
|
|
177
|
+
<div class="logs-container">
|
|
178
|
+
<button id="logs-toggle" class="logs-header" aria-expanded="true">
|
|
179
|
+
<div class="logs-header-content">
|
|
180
|
+
<svg class="logs-chevron expanded" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
181
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
|
|
182
|
+
</svg>
|
|
183
|
+
<h3 class="logs-title">Runtime Logs & Detailed Analysis</h3>
|
|
184
|
+
</div>
|
|
185
|
+
<span class="logs-toggle-text">Click to collapse</span>
|
|
186
|
+
</button>
|
|
187
|
+
|
|
188
|
+
<div id="logs-content" class="logs-content">
|
|
189
|
+
<!-- Tab Navigation -->
|
|
190
|
+
<div class="logs-tabs">
|
|
191
|
+
<button class="log-tab active" data-tab="summary">
|
|
192
|
+
<span class="tab-icon">📊</span>
|
|
193
|
+
<span>Comparison Summary</span>
|
|
194
|
+
</button>
|
|
195
|
+
<button class="log-tab" data-tab="benchmark">
|
|
196
|
+
<span class="tab-icon">âš¡</span>
|
|
197
|
+
<span>Benchmark Output</span>
|
|
198
|
+
</button>
|
|
199
|
+
<button class="log-tab" data-tab="memory">
|
|
200
|
+
<span class="tab-icon">💾</span>
|
|
201
|
+
<span>Memory Report</span>
|
|
202
|
+
</button>
|
|
203
|
+
<button class="log-tab" data-tab="gc">
|
|
204
|
+
<span class="tab-icon">🔄</span>
|
|
205
|
+
<span>GC Statistics</span>
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<!-- Tab Panels -->
|
|
210
|
+
<div class="logs-panels">
|
|
211
|
+
<!-- Summary Panel -->
|
|
212
|
+
<div class="log-panel active" data-panel="summary">
|
|
213
|
+
<h4 class="panel-title">Comparison Summary</h4>
|
|
214
|
+
<div class="panel-content">
|
|
215
|
+
<pre id="summary-log" class="log-output">Run a benchmark to see the comparison summary...</pre>
|
|
216
|
+
</div>
|
|
217
|
+
<p class="panel-description">High-level comparison of both code snippets.</p>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<!-- Benchmark Panel -->
|
|
221
|
+
<div class="log-panel hidden" data-panel="benchmark">
|
|
222
|
+
<h4 class="panel-title">Benchmark Output</h4>
|
|
223
|
+
<div class="panel-content">
|
|
224
|
+
<pre id="benchmark-log" class="log-output">Benchmark.ips output will appear here...</pre>
|
|
225
|
+
</div>
|
|
226
|
+
<p class="panel-description">Raw output from Benchmark.ips execution.</p>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<!-- Memory Panel -->
|
|
230
|
+
<div class="log-panel hidden" data-panel="memory">
|
|
231
|
+
<h4 class="panel-title">Memory Report</h4>
|
|
232
|
+
<div class="panel-content">
|
|
233
|
+
<pre id="memory-log" class="log-output">Memory profiling data will appear here...</pre>
|
|
234
|
+
</div>
|
|
235
|
+
<p class="panel-description">Detailed memory allocation and usage statistics.</p>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<!-- GC Panel -->
|
|
239
|
+
<div class="log-panel hidden" data-panel="gc">
|
|
240
|
+
<h4 class="panel-title">Garbage Collection Statistics</h4>
|
|
241
|
+
<div class="panel-content">
|
|
242
|
+
<pre id="gc-log" class="log-output">GC statistics will be collected during benchmark execution...</pre>
|
|
243
|
+
</div>
|
|
244
|
+
<p class="panel-description">Ruby garbage collector statistics during benchmark execution.</p>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
</section>
|
|
251
|
+
</main>
|
|
252
|
+
|
|
253
|
+
<!-- Footer -->
|
|
254
|
+
<footer class="footer">
|
|
255
|
+
<p>
|
|
256
|
+
The Mechanic 2 - Ruby Code Benchmarking Engine |
|
|
257
|
+
<a href="https://github.com/yourusername/the_mechanic" target="_blank">GitHub</a>
|
|
258
|
+
</p>
|
|
259
|
+
</footer>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<script>
|
|
263
|
+
<%= raw inline_javascript %>
|
|
264
|
+
</script>
|
|
265
|
+
</body>
|
|
266
|
+
</html>
|
data/config/routes.rb
ADDED