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,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TheMechanic2
4
+ # Main controller for benchmark operations
5
+ # Handles UI rendering and API endpoints
6
+ class BenchmarksController < ApplicationController
7
+ before_action :check_authentication, if: -> { TheMechanic2.configuration.enable_authentication }
8
+
9
+ # GET /ask_the_mechanic
10
+ # Renders the main benchmarking UI
11
+ def index
12
+ render template: 'the_mechanic_2/benchmarks/index'
13
+ end
14
+
15
+ # POST /ask_the_mechanic_2/validate
16
+ # Validates code without executing
17
+ def validate
18
+ code_a = params[:code_a]
19
+ code_b = params[:code_b]
20
+
21
+ errors = []
22
+
23
+ # Validate code_a
24
+ if code_a.present?
25
+ validation_a = SecurityService.validate(code_a)
26
+ errors.concat(validation_a[:errors].map { |e| "Code A: #{e}" }) unless validation_a[:valid]
27
+ else
28
+ errors << 'Code A: Code is required'
29
+ end
30
+
31
+ # Validate code_b
32
+ if code_b.present?
33
+ validation_b = SecurityService.validate(code_b)
34
+ errors.concat(validation_b[:errors].map { |e| "Code B: #{e}" }) unless validation_b[:valid]
35
+ else
36
+ errors << 'Code B: Code is required'
37
+ end
38
+
39
+ # Validate shared_setup if provided
40
+ if params[:shared_setup].present?
41
+ validation_setup = SecurityService.validate(params[:shared_setup])
42
+ errors.concat(validation_setup[:errors].map { |e| "Shared Setup: #{e}" }) unless validation_setup[:valid]
43
+ end
44
+
45
+ if errors.empty?
46
+ render json: { valid: true, message: 'All code is valid' }
47
+ else
48
+ render json: { valid: false, errors: errors }, status: :unprocessable_entity
49
+ end
50
+ end
51
+
52
+ # POST /ask_the_mechanic_2/run
53
+ # Executes benchmark comparison
54
+ def run
55
+ # Validate request
56
+ request = BenchmarkRequest.new(
57
+ shared_setup: params[:shared_setup],
58
+ code_a: params[:code_a],
59
+ code_b: params[:code_b],
60
+ timeout: params[:timeout]&.to_i
61
+ )
62
+
63
+ unless request.valid?
64
+ render json: { error: 'Invalid request', errors: request.all_errors }, status: :unprocessable_entity
65
+ return
66
+ end
67
+
68
+ # Validate code security
69
+ validation_result = validate_code_security(request)
70
+ unless validation_result[:valid]
71
+ render json: { error: 'Security validation failed', errors: validation_result[:errors] }, status: :unprocessable_entity
72
+ return
73
+ end
74
+
75
+ # Execute benchmark
76
+ begin
77
+ service = BenchmarkService.new
78
+ results = service.run(
79
+ shared_setup: request.shared_setup,
80
+ code_a: request.code_a,
81
+ code_b: request.code_b,
82
+ timeout: request.timeout
83
+ )
84
+
85
+ render json: results
86
+ rescue RailsRunnerService::BenchmarkTimeout => e
87
+ render json: { error: 'Benchmark timeout', message: e.message }, status: :request_timeout
88
+ rescue RailsRunnerService::BenchmarkError => e
89
+ render json: { error: 'Benchmark execution failed', message: e.message }, status: :internal_server_error
90
+ rescue StandardError => e
91
+ render json: { error: 'Unexpected error', message: e.message }, status: :internal_server_error
92
+ end
93
+ end
94
+
95
+ # POST /ask_the_mechanic_2/export
96
+ # Exports results in specified format
97
+ def export
98
+ results_data = params[:results]
99
+ format = params[:format] || 'json'
100
+
101
+ unless results_data
102
+ render json: { error: 'Results data is required' }, status: :unprocessable_entity
103
+ return
104
+ end
105
+
106
+ begin
107
+ result = BenchmarkResult.new(results_data.permit!.to_h.symbolize_keys)
108
+
109
+ case format
110
+ when 'json'
111
+ render json: result.to_json, content_type: 'application/json'
112
+ when 'markdown'
113
+ render plain: result.to_markdown, content_type: 'text/markdown'
114
+ else
115
+ render json: { error: 'Invalid format. Use json or markdown' }, status: :unprocessable_entity
116
+ end
117
+ rescue StandardError => e
118
+ render json: { error: 'Export failed', message: e.message }, status: :internal_server_error
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ # Checks authentication using configured callback
125
+ def check_authentication
126
+ callback = TheMechanic2.configuration.authentication_callback
127
+ return unless callback
128
+
129
+ unless callback.call(self)
130
+ render json: { error: 'Unauthorized' }, status: :unauthorized
131
+ end
132
+ end
133
+
134
+ # Validates code security for all code snippets
135
+ def validate_code_security(request)
136
+ errors = []
137
+
138
+ # Validate shared_setup if present
139
+ if request.shared_setup.present?
140
+ validation = SecurityService.validate(request.shared_setup)
141
+ errors.concat(validation[:errors].map { |e| "Shared Setup: #{e}" }) unless validation[:valid]
142
+ end
143
+
144
+ # Validate code_a
145
+ validation_a = SecurityService.validate(request.code_a)
146
+ errors.concat(validation_a[:errors].map { |e| "Code A: #{e}" }) unless validation_a[:valid]
147
+
148
+ # Validate code_b
149
+ validation_b = SecurityService.validate(request.code_b)
150
+ errors.concat(validation_b[:errors].map { |e| "Code B: #{e}" }) unless validation_b[:valid]
151
+
152
+ { valid: errors.empty?, errors: errors }
153
+ end
154
+
155
+ # Helper methods for asset inlining (to be implemented later)
156
+ helper_method :inline_css, :inline_javascript
157
+
158
+ def inline_css
159
+ @inline_css ||= read_asset_file('stylesheets/the_mechanic_2/application.css')
160
+ end
161
+
162
+ def inline_javascript
163
+ @inline_javascript ||= read_asset_file('javascripts/the_mechanic_2/application.js')
164
+ end
165
+
166
+ def read_asset_file(path)
167
+ file_path = TheMechanic2::Engine.root.join('app', 'assets', path)
168
+ File.exist?(file_path) ? File.read(file_path) : ''
169
+ rescue StandardError => e
170
+ Rails.logger.error("Failed to read asset file #{path}: #{e.message}")
171
+ ''
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,4 @@
1
+ module TheMechanic2
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module TheMechanic2
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module TheMechanic2
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module TheMechanic2
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TheMechanic2
4
+ # Model for validating benchmark request parameters
5
+ # Ensures all required fields are present and valid
6
+ class BenchmarkRequest
7
+ attr_reader :shared_setup, :code_a, :code_b, :timeout, :errors
8
+
9
+ # Minimum and maximum allowed timeout values
10
+ MIN_TIMEOUT = 1
11
+ MAX_TIMEOUT = 300
12
+
13
+ def initialize(params = {})
14
+ @shared_setup = params[:shared_setup]
15
+ @code_a = params[:code_a]
16
+ @code_b = params[:code_b]
17
+ @timeout = params[:timeout] || TheMechanic2.configuration.timeout
18
+ @errors = []
19
+ end
20
+
21
+ # Validates the request parameters
22
+ # @return [Boolean] true if valid, false otherwise
23
+ def valid?
24
+ @errors = []
25
+
26
+ validate_code_a
27
+ validate_code_b
28
+ validate_timeout
29
+
30
+ @errors.empty?
31
+ end
32
+
33
+ # Returns validation errors as a hash
34
+ # @return [Hash] errors grouped by field
35
+ def error_messages
36
+ {
37
+ code_a: @errors.select { |e| e.include?('Code A') },
38
+ code_b: @errors.select { |e| e.include?('Code B') },
39
+ timeout: @errors.select { |e| e.include?('Timeout') },
40
+ general: @errors.reject { |e| e.include?('Code A') || e.include?('Code B') || e.include?('Timeout') }
41
+ }
42
+ end
43
+
44
+ # Returns all errors as a flat array
45
+ # @return [Array<String>] all error messages
46
+ def all_errors
47
+ @errors
48
+ end
49
+
50
+ private
51
+
52
+ def validate_code_a
53
+ if @code_a.nil? || @code_a.strip.empty?
54
+ @errors << 'Code A is required and cannot be empty'
55
+ end
56
+ end
57
+
58
+ def validate_code_b
59
+ if @code_b.nil? || @code_b.strip.empty?
60
+ @errors << 'Code B is required and cannot be empty'
61
+ end
62
+ end
63
+
64
+ def validate_timeout
65
+ unless @timeout.is_a?(Numeric)
66
+ @errors << 'Timeout must be a number'
67
+ return
68
+ end
69
+
70
+ if @timeout < MIN_TIMEOUT
71
+ @errors << "Timeout must be at least #{MIN_TIMEOUT} second(s)"
72
+ end
73
+
74
+ if @timeout > MAX_TIMEOUT
75
+ @errors << "Timeout cannot exceed #{MAX_TIMEOUT} seconds"
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TheMechanic2
4
+ # Model for formatting and exporting benchmark results
5
+ # Provides JSON and Markdown export capabilities
6
+ class BenchmarkResult
7
+ attr_reader :code_a_metrics, :code_b_metrics, :winner, :performance_ratio, :summary
8
+
9
+ def initialize(data)
10
+ @code_a_metrics = data[:code_a_metrics]
11
+ @code_b_metrics = data[:code_b_metrics]
12
+ @winner = data[:winner]
13
+ @performance_ratio = data[:performance_ratio]
14
+ @summary = data[:summary]
15
+ end
16
+
17
+ # Exports results as JSON
18
+ # @return [String] JSON representation of results
19
+ def to_json(*_args)
20
+ {
21
+ code_a_metrics: @code_a_metrics,
22
+ code_b_metrics: @code_b_metrics,
23
+ winner: @winner,
24
+ performance_ratio: @performance_ratio,
25
+ summary: @summary,
26
+ timestamp: Time.now.iso8601
27
+ }.to_json
28
+ end
29
+
30
+ # Exports results as Markdown
31
+ # @return [String] Markdown formatted results
32
+ def to_markdown
33
+ <<~MARKDOWN
34
+ # Ruby Benchmark Results
35
+
36
+ **Winner:** #{winner_text}
37
+
38
+ **Summary:** #{@summary}
39
+
40
+ ## Performance Metrics
41
+
42
+ | Metric | Code A | Code B |
43
+ |--------|--------|--------|
44
+ | IPS (iterations/sec) | #{format_number(@code_a_metrics[:ips])} | #{format_number(@code_b_metrics[:ips])} |
45
+ | Standard Deviation | #{format_number(@code_a_metrics[:stddev])} | #{format_number(@code_b_metrics[:stddev])} |
46
+ | Objects Allocated | #{format_number(@code_a_metrics[:objects])} | #{format_number(@code_b_metrics[:objects])} |
47
+ | Memory (MB) | #{format_number(@code_a_metrics[:memory_mb])} | #{format_number(@code_b_metrics[:memory_mb])} |
48
+ | Execution Time (sec) | #{format_number(@code_a_metrics[:execution_time])} | #{format_number(@code_b_metrics[:execution_time])} |
49
+
50
+ ## Analysis
51
+
52
+ #{analysis_text}
53
+
54
+ ---
55
+
56
+ *Generated at #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}*
57
+ MARKDOWN
58
+ end
59
+
60
+ # Returns a hash representation of the results
61
+ # @return [Hash] results as a hash
62
+ def to_h
63
+ {
64
+ code_a_metrics: @code_a_metrics,
65
+ code_b_metrics: @code_b_metrics,
66
+ winner: @winner,
67
+ performance_ratio: @performance_ratio,
68
+ summary: @summary
69
+ }
70
+ end
71
+
72
+ private
73
+
74
+ def winner_text
75
+ case @winner
76
+ when 'code_a'
77
+ "Code A (#{@performance_ratio}× faster)"
78
+ when 'code_b'
79
+ "Code B (#{@performance_ratio}× faster)"
80
+ when 'tie'
81
+ 'Tie (similar performance)'
82
+ else
83
+ 'Unknown'
84
+ end
85
+ end
86
+
87
+ def analysis_text
88
+ lines = []
89
+
90
+ # Performance analysis
91
+ if @winner == 'tie'
92
+ lines << "Both code snippets have similar performance characteristics."
93
+ else
94
+ faster = @winner == 'code_a' ? 'Code A' : 'Code B'
95
+ slower = @winner == 'code_a' ? 'Code B' : 'Code A'
96
+ lines << "#{faster} is significantly faster than #{slower} by a factor of #{@performance_ratio}×."
97
+ end
98
+
99
+ # Memory analysis
100
+ memory_diff = (@code_a_metrics[:memory_mb] - @code_b_metrics[:memory_mb]).abs
101
+ if memory_diff > 0.01 # More than 0.01 MB difference
102
+ less_memory = @code_a_metrics[:memory_mb] < @code_b_metrics[:memory_mb] ? 'Code A' : 'Code B'
103
+ lines << "#{less_memory} uses less memory."
104
+ else
105
+ lines << "Both snippets have similar memory usage."
106
+ end
107
+
108
+ # Object allocation analysis
109
+ obj_diff = (@code_a_metrics[:objects] - @code_b_metrics[:objects]).abs
110
+ if obj_diff > 10 # More than 10 objects difference
111
+ fewer_objects = @code_a_metrics[:objects] < @code_b_metrics[:objects] ? 'Code A' : 'Code B'
112
+ lines << "#{fewer_objects} allocates fewer objects."
113
+ end
114
+
115
+ lines.join("\n\n")
116
+ end
117
+
118
+ def format_number(num)
119
+ return '0' if num.nil?
120
+
121
+ if num.is_a?(Float)
122
+ if num < 0.01
123
+ format('%.6f', num)
124
+ elsif num < 1
125
+ format('%.4f', num)
126
+ elsif num < 100
127
+ format('%.2f', num)
128
+ else
129
+ num.round.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
130
+ end
131
+ else
132
+ num.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TheMechanic2
4
+ # Service for orchestrating benchmark execution
5
+ # Coordinates between RailsRunnerService and result formatting
6
+ class BenchmarkService
7
+ # Executes a benchmark comparison between two code snippets
8
+ # @param shared_setup [String] Optional setup code to run before both snippets
9
+ # @param code_a [String] First code snippet to benchmark
10
+ # @param code_b [String] Second code snippet to benchmark
11
+ # @param timeout [Integer] Maximum execution time per benchmark in seconds
12
+ # @return [Hash] Formatted benchmark results with winner and metrics
13
+ def run(shared_setup:, code_a:, code_b:, timeout: 30)
14
+ runner = RailsRunnerService.new
15
+
16
+ # Execute code_a
17
+ result_a = runner.execute(
18
+ code: code_a,
19
+ shared_setup: shared_setup,
20
+ timeout: timeout
21
+ )
22
+
23
+ # Execute code_b
24
+ result_b = runner.execute(
25
+ code: code_b,
26
+ shared_setup: shared_setup,
27
+ timeout: timeout
28
+ )
29
+
30
+ # Format and return results
31
+ format_results(result_a, result_b)
32
+ end
33
+
34
+ private
35
+
36
+ # Formats the benchmark results and determines the winner
37
+ # @param result_a [Hash] Results from code_a
38
+ # @param result_b [Hash] Results from code_b
39
+ # @return [Hash] Formatted results with winner and comparison
40
+ def format_results(result_a, result_b)
41
+ # Determine winner based on IPS (higher is better)
42
+ winner = determine_winner(result_a[:ips], result_b[:ips])
43
+
44
+ # Calculate performance ratio
45
+ ratio = calculate_ratio(result_a[:ips], result_b[:ips], winner)
46
+
47
+ {
48
+ code_a_metrics: {
49
+ ips: result_a[:ips],
50
+ stddev: result_a[:stddev],
51
+ objects: result_a[:objects],
52
+ memory_mb: result_a[:memory_mb],
53
+ execution_time: result_a[:execution_time]
54
+ },
55
+ code_b_metrics: {
56
+ ips: result_b[:ips],
57
+ stddev: result_b[:stddev],
58
+ objects: result_b[:objects],
59
+ memory_mb: result_b[:memory_mb],
60
+ execution_time: result_b[:execution_time]
61
+ },
62
+ winner: winner,
63
+ performance_ratio: ratio,
64
+ summary: generate_summary(winner, ratio)
65
+ }
66
+ end
67
+
68
+ # Determines which code snippet is the winner
69
+ # @param ips_a [Float] Iterations per second for code A
70
+ # @param ips_b [Float] Iterations per second for code B
71
+ # @return [String] 'code_a', 'code_b', or 'tie'
72
+ def determine_winner(ips_a, ips_b)
73
+ # Consider it a tie if difference is less than 5%
74
+ diff_percentage = ((ips_a - ips_b).abs / [ips_a, ips_b].max) * 100
75
+
76
+ if diff_percentage < 5
77
+ 'tie'
78
+ elsif ips_a > ips_b
79
+ 'code_a'
80
+ else
81
+ 'code_b'
82
+ end
83
+ end
84
+
85
+ # Calculates the performance ratio between the two code snippets
86
+ # @param ips_a [Float] Iterations per second for code A
87
+ # @param ips_b [Float] Iterations per second for code B
88
+ # @param winner [String] The winner ('code_a', 'code_b', or 'tie')
89
+ # @return [Float] Performance ratio (how much faster the winner is)
90
+ def calculate_ratio(ips_a, ips_b, winner)
91
+ return 1.0 if winner == 'tie'
92
+
93
+ if winner == 'code_a'
94
+ (ips_a / ips_b).round(2)
95
+ else
96
+ (ips_b / ips_a).round(2)
97
+ end
98
+ end
99
+
100
+ # Generates a human-readable summary of the results
101
+ # @param winner [String] The winner
102
+ # @param ratio [Float] Performance ratio
103
+ # @return [String] Summary text
104
+ def generate_summary(winner, ratio)
105
+ case winner
106
+ when 'tie'
107
+ 'Both code snippets have similar performance (within 5% difference)'
108
+ when 'code_a'
109
+ "Code A is #{ratio}× faster than Code B"
110
+ when 'code_b'
111
+ "Code B is #{ratio}× faster than Code A"
112
+ end
113
+ end
114
+ end
115
+ end