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.
@@ -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&#10;# 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&#10;# 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&#10;# 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
@@ -0,0 +1,7 @@
1
+ TheMechanic2::Engine.routes.draw do
2
+ root to: 'benchmarks#index'
3
+
4
+ post 'validate', to: 'benchmarks#validate'
5
+ post 'run', to: 'benchmarks#run'
6
+ post 'export', to: 'benchmarks#export'
7
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :the_mechanic do
3
+ # # Task goes here
4
+ # end