rails_benchmark_suite 0.3.1 → 0.3.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 473aa21869380dae81d4f9ee8668ccf161721923fd6c453cc711caf9d6303fd1
4
- data.tar.gz: 5101bb8d63885263dc3fcbb8851d52f3ed48a1f919b0abfab38d43dc3f3df2a2
3
+ metadata.gz: 836276b8a2872af0084df8bbad548c23cd1102aefb9d8e8399107c79a154a237
4
+ data.tar.gz: 174cc4856a798619687f55ceb182d03f423d33ccd830a90ec5c9d538a0411f2a
5
5
  SHA512:
6
- metadata.gz: 11311aa76df4c103c8b3ea2e12a803ddeb339cd454127b246557341ddd5c7872fa01c4ff00f4c14ae711d95b48f0d84b148ec7f8b0a39fda7ab70cac0d070bad
7
- data.tar.gz: 3f141277e2daae966aa4d7d7be684e112889fa757282e36a4f35ac34d60ac91ca76903281d9e091b9549ef567d6e8b7a26b13221326bf8273c3c33fe7e6082fc
6
+ metadata.gz: 626e81a963ca2a345a51236c24188caaec3d8b871392262cd6189551ed66997cd7722573327645445cd5a068be7f32b3e918fe4fb8e4d0f0518cf65094fbe3ef
7
+ data.tar.gz: f9893a9176305a878e9a2be2f0bb80e9fb4f4613837e98c74042f74917fa79e7498618cc1c75de4ac026f9ce4ab9b7152c7721d8db5dae89b5b3ea0dd232c9fb
data/CHANGELOG.md CHANGED
@@ -1,4 +1,25 @@
1
1
  # Changelog
2
+ ## [0.3.3] - 2026-01-06
3
+ ### Added
4
+ - **Request Heft Workload**: A new benchmark measuring the full Rails stack overhead (Middleware + Router + Controller + View).
5
+ - **Boot Structure Analysis**: Auto-Boot Profiler detects which `app/` folders are slowing down startup time.
6
+ - **Security**: Uses ephemeral in-memory route injection (zero production footprint/risk).
7
+ - **Docs**: Updated weights and workload descriptions in README.
8
+
9
+ ### Fixed
10
+ - **Boot Profiler Accuracy**: Now fully initializes the Rails environment (instead of just application config) to ensure autoloaders/Zeitwerk function correctly during file profiling.
11
+ - **Boot Profiler Stability**: The boot profiler now suppresses Rails logs and warnings to prevent JSON corruption.
12
+ - **Infinite Score Bug**: Workloads now strictly check dependencies before registering. Missing dependencies (e.g. `--skip-rails`) correctly trigger weight redistribution instead of reporting infinite IPS.
13
+ - **JSON Output**: Boot Structure Analysis is now included in the JSON report (`--json`) for CI/CD visibility.
14
+ - **UI**: Fixed character encoding issues in terminal reporter emojis.
15
+
16
+ ## [0.3.2] - 2026-01-05
17
+ ### Refactoring & Polish
18
+ - **DRY Logic**: Centralized "Efficiency" calculation; removed duplicate math from HTML templates.
19
+ - **Responsive UI**: Terminal output now adapts to screen width (instead of hardcoded 84 chars).
20
+ - **Tests**: Added unit tests for HTML Reporter generation.
21
+ - **Docs**: Improved formatting of the Command Line Options table.
22
+
2
23
  ## [0.3.1] - 2026-01-04
3
24
  *Major Architectural Repair & TTY Overhaul*
4
25
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rails_benchmark_suite (0.3.1)
4
+ rails_benchmark_suite (0.3.3)
5
5
  actionview (~> 8.1)
6
6
  activerecord (~> 8.1)
7
7
  activestorage (~> 8.1)
data/README.md CHANGED
@@ -8,6 +8,10 @@ A standardized performance suite designed to measure the "Heft" of a machine usi
8
8
 
9
9
  Think of this as a **"Test Track" for Rails servers**. Unlike profilers that measure your specific application code, this gem runs a **fixed, standardized set of Rails operations** (Active Record object allocation, SQL query complexity, ActionView rendering, and background job throughput) to measure the raw performance of your server and Ruby configuration.
10
10
 
11
+ **Key Features:**
12
+ - **Auto-Boot Profiler (Default):** Automatically detects slow folders (Models, Controllers, etc.) during startup—runs on every benchmark.
13
+ - **Isolated Environment:** Uses in-memory SQLite—never touches your production data.
14
+
11
15
  To ensure a level playing field, the gem boots an **isolated, in-memory SQLite environment**. It creates its own schema and records, meaning it **never touches your production data** and returns comparable results across any machine.
12
16
 
13
17
  ## 📊 The "Heft" Score
@@ -100,7 +104,7 @@ Enable YJIT for maximum performance measurement accuracy. This is the recommende
100
104
  ```bash
101
105
  bundle exec rails_benchmark_suite --json > report.json
102
106
  ```
103
- Perfect for CI/CD pipelines and programmatic analysis. Outputs clean JSON without any UI elements.
107
+ Perfect for CI/CD pipelines and programmatic analysis. Outputs clean JSON including Boot Structure Analysis and all workload metrics.
104
108
 
105
109
  ### 📊 Visual Diagnostics (HTML Report)
106
110
 
@@ -120,10 +124,10 @@ bundle exec rails_benchmark_suite --profile --html
120
124
 
121
125
  | Flag | Description |
122
126
  | :--- | :--- |
123
- | `--html` | Generates a visual dashboard (`tmp/rails_benchmark_report.html`). **Best used with `--profile`.** |
124
- | `--profile` | **Diagnostic Mode.** Runs the benchmark twice (1 Thread vs Max Threads) to calculate "Scaling Efficiency." Required to populate the "Scaling Curve" charts. |
125
- | `--db` | Uses your local `config/database.yml` (Postgres/MySQL) instead of the synthetic in-memory SQLite. |
126
- | `-t [N]` | Manually set the thread count (Default: Auto-detects CPU cores). |
127
+ | `--html` | Generates a visual dashboard (`tmp/rails_benchmark_report.html`).<br>**Best used with `--profile`.** |
128
+ | `--profile` | **Diagnostic Mode.** Runs benchmark twice (1T vs MaxT) to calc efficiency.<br>Required for "Scaling Curve". |
129
+ | `--db` | Connects to local `config/database.yml` (Postgres/MySQL).<br>Bypasses in-memory SQLite. |
130
+ | `-t [N]` | Manually set thread count.<br>(Default: Auto-detects CPU cores). |
127
131
 
128
132
  ### Configuration Flags
129
133
  - `--json`: Output results in JSON format
@@ -156,9 +160,10 @@ RHI Score = Σ (4-Thread IPS × Weight)
156
160
 
157
161
  | Workload | Weight | Rationale |
158
162
  |----------|--------|-----------|
159
- | **Active Record** | 40% | Database operations are the core of most Rails apps |
160
- | **View Rendering** | 20% | ERB/ActionView processing |
161
- | **Solid Queue** | 20% | Background job throughput |
163
+ | **Active Record** | 30% | Database operations are the core of most Rails apps |
164
+ | **Request Heft** | 30% | Full Stack Overhead (Middleware → Router → Controller) |
165
+ | **View Rendering** | 10% | ERB/ActionView processing |
166
+ | **Solid Queue** | 10% | Background job throughput |
162
167
  | **Cache Operations** | 10% | Memory store performance |
163
168
  | **Image Processing** | 10% | Optional - requires libvips |
164
169
 
@@ -181,6 +186,7 @@ Your RHI score maps to these performance tiers:
181
186
  The gem measures performance across critical Rails subsystems using a dedicated, isolated schema:
182
187
 
183
188
  * **Active Record Heft:** Standardized CRUD: Creation, indexing, and complex querying.
189
+ * **Request Heft:** Full-stack request throughput (Middleware → Router → Controller).
184
190
  * **Cache Heft:** High-frequency read/writes to the Rails memory store.
185
191
  * **Solid Queue Heft:** Background job enqueuing and database-backed polling stress.
186
192
  * **View Heft:** Partial rendering overhead and ActionView throughput.
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Boot Structure Analysis - Standalone Profiler Script
5
+ # Executed as subprocess to measure cold boot times per app/ directory
6
+ # Outputs JSON to STDOUT
7
+ #
8
+ # IMPORTANT: Uses stdout silencing to handle noisy Rails boots that may
9
+ # output deprecation warnings or logs during require statements.
10
+
11
+ require "json"
12
+ require "benchmark"
13
+
14
+ # Output Silencer - swallows all puts/print from Rails boot
15
+ def silence_stdout
16
+ original_stdout = $stdout
17
+ original_stderr = $stderr
18
+ $stdout = File.open(File::NULL, "w")
19
+ $stderr = File.open(File::NULL, "w")
20
+ yield
21
+ ensure
22
+ $stdout = original_stdout
23
+ $stderr = original_stderr
24
+ end
25
+
26
+ results = []
27
+
28
+ begin
29
+ # Boot Phase: Minimal Rails setup (silenced to avoid noisy output)
30
+ silence_stdout do
31
+ require "bundler/setup"
32
+
33
+ # Check if we're in a Rails app
34
+ env_file = File.join(Dir.pwd, "config", "environment.rb")
35
+ unless File.exist?(env_file)
36
+ # Not in a Rails app - will output empty results after restore
37
+ break
38
+ end
39
+
40
+ # Load full Rails environment to initialize Zeitwerk autoloading
41
+ ENV["RAILS_ENV"] ||= "development"
42
+ require File.join(Dir.pwd, "config", "environment")
43
+ end
44
+
45
+ # Check if we're in a Rails app (outside silence block)
46
+ env_file = File.join(Dir.pwd, "config", "environment.rb")
47
+ unless File.exist?(env_file)
48
+ puts "[]"
49
+ exit 0
50
+ end
51
+
52
+ # Profiling Phase: Measure each app/ subdirectory
53
+ target_dirs = Dir.glob("app/*").select { |f| File.directory?(f) }
54
+
55
+ target_dirs.each do |dir|
56
+ files = Dir.glob("#{dir}/**/*.rb")
57
+ next if files.empty?
58
+
59
+ time_ms = Benchmark.realtime do
60
+ # Silence loading phase - models/controllers often trigger logs
61
+ silence_stdout do
62
+ files.each do |file|
63
+ begin
64
+ require file
65
+ rescue LoadError, NameError, StandardError
66
+ # Silently skip files with missing dependencies
67
+ end
68
+ end
69
+ end
70
+ end * 1000 # Convert to milliseconds
71
+
72
+ results << {
73
+ path: dir,
74
+ time_ms: time_ms.round(2),
75
+ file_count: files.size
76
+ }
77
+ end
78
+
79
+ rescue => e
80
+ # If anything fails, return error info for debugging
81
+ results = [{ error: e.message, backtrace: e.backtrace&.first(3) }]
82
+ end
83
+
84
+ # Output JSON to STDOUT (only after restoring stdout)
85
+ puts results.to_json
@@ -5,6 +5,7 @@ require "tty-table"
5
5
  require "tty-box"
6
6
  require "pastel"
7
7
  require "tty-cursor"
8
+ require "tty-screen"
8
9
 
9
10
  module RailsBenchmarkSuite
10
11
  module Reporter
@@ -42,8 +43,10 @@ module RailsBenchmarkSuite
42
43
  "DB: #{info[:db_mode] || 'SQLite (Memory)'} | YJIT: #{yjit_status}#{yjit_hint}"
43
44
  ].join("\n")
44
45
 
46
+ box_width = [TTY::Screen.width, 84].min
47
+
45
48
  print TTY::Box.frame(
46
- width: 80,
49
+ width: box_width,
47
50
  title: { top_left: " Rails Benchmark Suite v#{RailsBenchmarkSuite::VERSION} " },
48
51
  padding: 1,
49
52
  style: {
@@ -113,18 +116,56 @@ module RailsBenchmarkSuite
113
116
  renderer.border.separator = :each_row
114
117
  renderer.border.style = :blue
115
118
  end
119
+
120
+ # 2. Boot Structure Analysis
121
+ render_boot_analysis(payload[:boot_analysis])
116
122
 
117
- # 2. Insights List
123
+ # 3. Insights List
118
124
  puts ""
119
125
  check_scaling_insights(results)
120
126
  check_yjit_insight
121
127
  check_memory_insights(results)
122
128
 
123
- # 3. Final Score Dashboard
129
+ # 4. Final Score Dashboard
124
130
  render_final_score(total_score)
125
131
  show_hardware_tier(tier)
126
132
  end
127
133
 
134
+ def render_boot_analysis(boot_data)
135
+ return unless boot_data&.any?
136
+
137
+ puts ""
138
+ puts pastel.bold("\u{1F4C2} Boot Structure Analysis")
139
+ puts ""
140
+
141
+ rows = boot_data.map do |entry|
142
+ time_ms = entry[:time_ms] || 0
143
+
144
+ # Color coding: Red > 500ms, Yellow > 200ms, Green otherwise
145
+ time_color = if time_ms > 500
146
+ :red
147
+ elsif time_ms > 200
148
+ :yellow
149
+ else
150
+ :green
151
+ end
152
+
153
+ time_str = pastel.decorate("#{time_ms.round(0)}ms", time_color)
154
+
155
+ [entry[:path], time_str, entry[:file_count]]
156
+ end
157
+
158
+ table = TTY::Table.new(
159
+ header: ["Directory", "Load Time", "Files"],
160
+ rows: rows
161
+ )
162
+
163
+ puts table.render(:unicode, padding: [0, 1]) do |renderer|
164
+ renderer.border.separator = :each_row
165
+ renderer.border.style = :cyan
166
+ end
167
+ end
168
+
128
169
  def check_scaling_insights(results)
129
170
  poor_scaling = results.select do |r|
130
171
  entries = r[:report].entries
@@ -138,7 +179,7 @@ module RailsBenchmarkSuite
138
179
  end
139
180
 
140
181
  if poor_scaling.any?
141
- puts pastel.yellow.bold("💡 Insight (Scaling):") + " Scaling below 1.0x detected."
182
+ puts pastel.yellow.bold("\u{1F4A1} Insight (Scaling):") + " Scaling below 1.0x detected."
142
183
  puts " This indicates SQLite lock contention or Ruby GIL saturation."
143
184
  end
144
185
  end
@@ -146,7 +187,7 @@ module RailsBenchmarkSuite
146
187
  def check_yjit_insight
147
188
  unless yjit_enabled?
148
189
  puts ""
149
- puts pastel.yellow.bold("💡 Insight (YJIT):") + " YJIT is OFF."
190
+ puts pastel.yellow.bold("\u{1F4A1} Insight (YJIT):") + " YJIT is OFF."
150
191
  puts " Run with RUBY_OPT=\"--yjit\" for ~20% boost."
151
192
  end
152
193
  end
@@ -154,7 +195,7 @@ module RailsBenchmarkSuite
154
195
  def check_memory_insights(results)
155
196
  results.select { |r| r[:memory_delta_mb] > 20 }.each do |r|
156
197
  puts ""
157
- puts pastel.yellow.bold("💡 Insight (Memory):") + " High growth in #{r[:name]} (#{r[:memory_delta_mb].round(1)}MB)"
198
+ puts pastel.yellow.bold("\u{1F4A1} Insight (Memory):") + " High growth in #{r[:name]} (#{r[:memory_delta_mb].round(1)}MB)"
158
199
  puts " Suggests heavy object allocation."
159
200
  end
160
201
  end
@@ -170,7 +211,7 @@ module RailsBenchmarkSuite
170
211
  end
171
212
 
172
213
  puts ""
173
- puts pastel.bold("📊 Performance Tier: ") + comparison
214
+ puts pastel.bold("\u{1F4CA} Performance Tier: ") + comparison
174
215
  puts ""
175
216
  print cursor.show
176
217
  end
@@ -179,8 +220,10 @@ module RailsBenchmarkSuite
179
220
  score_str = "#{score.round(0)}"
180
221
  puts ""
181
222
 
223
+ box_width = [TTY::Screen.width, 84].min
224
+
182
225
  print TTY::Box.frame(
183
- width: 40,
226
+ width: box_width,
184
227
  height: 5,
185
228
  align: :center,
186
229
  padding: 1,
@@ -196,6 +239,7 @@ module RailsBenchmarkSuite
196
239
  system: system_info,
197
240
  total_score: payload[:total_score].round(0),
198
241
  tier: payload[:tier],
242
+ boot_analysis: payload[:boot_analysis] || [],
199
243
  workloads: []
200
244
  }
201
245
 
@@ -5,6 +5,8 @@ require_relative "schema"
5
5
  require_relative "../dummy/app/models/benchmark_user"
6
6
  require_relative "../dummy/app/models/benchmark_post"
7
7
  require_relative "../dummy/app/models/benchmark_job"
8
+ require "open3"
9
+ require "json"
8
10
 
9
11
  module RailsBenchmarkSuite
10
12
  class Runner
@@ -49,19 +51,54 @@ module RailsBenchmarkSuite
49
51
  options: runner_options,
50
52
  show_progress: !@config.json
51
53
  ).execute
54
+
55
+ # 4. Boot Structure Analysis (run for all modes including JSON)
56
+ payload[:boot_analysis] = run_boot_analysis
52
57
 
53
- # 4. Report Results
58
+ # 5. Report Results
54
59
  if @config.json
55
60
  Reporter.as_json(payload)
56
61
  else
57
62
  Reporter.render(payload)
58
63
  end
59
64
 
60
- # 5. HTML Report Generation
65
+ # 6. HTML Report Generation
61
66
  if @config.html
62
67
  require_relative "reporters/html_reporter"
63
68
  Reporters::HtmlReporter.new(payload).generate
64
69
  end
65
70
  end
71
+
72
+ private
73
+
74
+ def run_boot_analysis
75
+ script_path = File.join(__dir__, "boot_profiler_script.rb")
76
+ return nil unless File.exist?(script_path)
77
+
78
+ begin
79
+ stdout, status = Open3.capture2("bundle", "exec", "ruby", script_path)
80
+ return nil unless status.success?
81
+
82
+ # Try to parse the whole output first
83
+ results = begin
84
+ JSON.parse(stdout, symbolize_names: true)
85
+ rescue JSON::ParserError
86
+ # Fallback: Try last line (in case silence failed and noise leaked)
87
+ last_line = stdout.strip.lines.last
88
+ begin
89
+ JSON.parse(last_line, symbolize_names: true)
90
+ rescue JSON::ParserError
91
+ nil
92
+ end
93
+ end
94
+
95
+ return nil if results.nil? || results.empty? || results.first&.dig(:error)
96
+
97
+ # Sort by load time descending (slowest first)
98
+ results.sort_by { |r| -(r[:time_ms] || 0) }
99
+ rescue StandardError
100
+ nil
101
+ end
102
+ end
66
103
  end
67
104
  end
@@ -102,7 +102,7 @@
102
102
  ips_mt = entry_mt ? entry_mt.ips : 0
103
103
 
104
104
  scaling = ips_1t > 0 ? (ips_mt / ips_1t) : 0
105
- efficiency = (ips_1t.to_f > 0) ? (ips_mt.to_f / (ips_1t * @payload[:threads])) * 100 : 0.0
105
+ efficiency = res[:efficiency] || 0.0
106
106
 
107
107
  eff_color = efficiency > 75 ? 'efficiency-high' : (efficiency > 50 ? 'efficiency-med' : 'efficiency-low')
108
108
  scale_color = scaling >= 1.0 ? 'text-success' : 'text-danger'
@@ -121,6 +121,46 @@
121
121
  </div>
122
122
  </div>
123
123
 
124
+ <% if @payload[:boot_analysis]&.any? %>
125
+ <!-- Boot Structure Analysis -->
126
+ <div class="card mt-5">
127
+ <div class="card-header bg-transparent border-bottom border-secondary">
128
+ <h5 class="mb-0">📂 Boot Structure Analysis</h5>
129
+ </div>
130
+ <div class="card-body">
131
+ <div class="row">
132
+ <div class="col-md-6">
133
+ <canvas id="bootChart" style="max-height: 300px;"></canvas>
134
+ </div>
135
+ <div class="col-md-6">
136
+ <table class="table table-dark table-striped table-hover mb-0 align-middle">
137
+ <thead>
138
+ <tr>
139
+ <th>Directory</th>
140
+ <th class="text-end">Load Time</th>
141
+ <th class="text-end">Files</th>
142
+ </tr>
143
+ </thead>
144
+ <tbody>
145
+ <% @payload[:boot_analysis].each do |entry| %>
146
+ <%
147
+ time_ms = entry[:time_ms] || 0
148
+ time_class = time_ms > 500 ? 'text-danger' : (time_ms > 200 ? 'text-warning' : 'text-success')
149
+ %>
150
+ <tr>
151
+ <td class="font-monospace"><%= entry[:path] %></td>
152
+ <td class="text-end font-monospace <%= time_class %>"><%= time_ms.round(0) %>ms</td>
153
+ <td class="text-end text-secondary"><%= entry[:file_count] %></td>
154
+ </tr>
155
+ <% end %>
156
+ </tbody>
157
+ </table>
158
+ </div>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ <% end %>
163
+
124
164
  <!-- JavaScript Injection -->
125
165
  <script>
126
166
  const CHART_DATA = <%= @chart_payload %>;
@@ -181,6 +221,53 @@
181
221
  }
182
222
  }
183
223
  });
224
+
225
+ <% if @payload[:boot_analysis]&.any? %>
226
+ // Boot Structure Analysis Chart
227
+ const bootCtx = document.getElementById('bootChart');
228
+ const bootLabels = <%= @payload[:boot_analysis].map { |e| e[:path] }.to_json %>;
229
+ const bootData = <%= @payload[:boot_analysis].map { |e| e[:time_ms] }.to_json %>;
230
+
231
+ const bootColors = bootData.map(ms => {
232
+ if (ms > 500) return 'rgba(244, 67, 54, 0.8)';
233
+ if (ms > 200) return 'rgba(255, 193, 7, 0.8)';
234
+ return 'rgba(76, 175, 80, 0.8)';
235
+ });
236
+
237
+ new Chart(bootCtx, {
238
+ type: 'bar',
239
+ data: {
240
+ labels: bootLabels,
241
+ datasets: [{
242
+ label: 'Load Time (ms)',
243
+ data: bootData,
244
+ backgroundColor: bootColors,
245
+ borderColor: bootColors.map(c => c.replace('0.8)', '1)')),
246
+ borderWidth: 1
247
+ }]
248
+ },
249
+ options: {
250
+ indexAxis: 'y',
251
+ responsive: true,
252
+ maintainAspectRatio: false,
253
+ scales: {
254
+ x: {
255
+ beginAtZero: true,
256
+ grid: { color: '#333' },
257
+ ticks: { color: '#aaa' },
258
+ title: { display: true, text: 'Milliseconds', color: '#aaa' }
259
+ },
260
+ y: {
261
+ grid: { display: false },
262
+ ticks: { color: '#fff' }
263
+ }
264
+ },
265
+ plugins: {
266
+ legend: { display: false }
267
+ }
268
+ }
269
+ });
270
+ <% end %>
184
271
  });
185
272
  </script>
186
273
  </body>
@@ -1,3 +1,3 @@
1
1
  module RailsBenchmarkSuite
2
- VERSION = "0.3.1"
2
+ VERSION = "0.3.3"
3
3
  end
@@ -6,13 +6,14 @@ require "tty-spinner"
6
6
 
7
7
  module RailsBenchmarkSuite
8
8
  class WorkloadRunner
9
- # Base weights for each workload
9
+ # Base weights for each workload (must sum to 1.0)
10
10
  BASE_WEIGHTS = {
11
- "Active Record Heft" => 0.4,
12
- "View Heft" => 0.2,
13
- "Solid Queue Heft" => 0.2,
14
- "Cache Heft" => 0.1,
15
- "Image Heft" => 0.1
11
+ "Active Record Heft" => 0.3, # Reduced from 0.4
12
+ "Request Heft" => 0.3, # NEW: Full Stack Overhead
13
+ "View Heft" => 0.1, # Reduced from 0.2
14
+ "Solid Queue Heft" => 0.1, # Reduced from 0.2
15
+ "Cache Heft" => 0.1,
16
+ "Image Heft" => 0.1
16
17
  }.freeze
17
18
 
18
19
  def initialize(workloads, options: {}, show_progress: true)
@@ -61,6 +62,16 @@ module RailsBenchmarkSuite
61
62
  results.each do |r|
62
63
  base_weight = BASE_WEIGHTS[r[:name]] || 1.0
63
64
  r[:adjusted_weight] = base_weight / weight_pool
65
+
66
+ # Calculate Efficiency (DRY)
67
+ entries = r[:report].entries
68
+ entry_mt = entries.find { |e| e.label.include?("(#{@threads} threads)") }
69
+ entry_1t = entries.find { |e| e.label.include?("(1 thread)") }
70
+
71
+ ips_mt = entry_mt ? entry_mt.ips : 0
72
+ ips_1t = entry_1t ? entry_1t.ips : 0
73
+
74
+ r[:efficiency] = (ips_1t > 0) ? (ips_mt / (ips_1t * @threads)) * 100 : 0.0
64
75
  end
65
76
 
66
77
  # Calculate total score
@@ -10,18 +10,16 @@ begin
10
10
  FileUtils.mkdir_p(ASSET_DIR)
11
11
  SAMPLE_IMAGE = File.join(ASSET_DIR, "sample.jpg")
12
12
 
13
- # Only register if vips is actually available
14
- RailsBenchmarkSuite::Runner.register_workload("Image Heft", weight: 0.1) do
15
- # Gracefully handle missing dependencies
16
- if File.exist?(SAMPLE_IMAGE)
13
+ # Only register if vips is available AND sample image exists
14
+ if File.exist?(SAMPLE_IMAGE)
15
+ RailsBenchmarkSuite::Runner.register_workload("Image Heft", weight: 0.1) do
17
16
  ImageProcessing::Vips
18
17
  .source(SAMPLE_IMAGE)
19
18
  .resize_to_limit(800, 800)
20
19
  .call
21
- else
22
- # Maintain benchmark stability if asset is missing
23
- true
24
20
  end
21
+ else
22
+ puts "\n⚠️ Skipping Image Workload: sample.jpg not found in assets/\n\n"
25
23
  end
26
24
 
27
25
  rescue LoadError, StandardError
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Request Heft Workload
4
+ # Benchmarks the full Rails request lifecycle (Middleware → Router → Controller → View)
5
+ # Uses ephemeral in-memory route injection - zero production footprint
6
+
7
+ # Check dependencies BEFORE registering to prevent infinite IPS bug
8
+ has_rails = begin
9
+ require "rails"
10
+ require "action_controller/railtie"
11
+ defined?(Rails.application) && Rails.application
12
+ rescue LoadError
13
+ false
14
+ end
15
+
16
+ # Only register if Rails is present
17
+ if has_rails
18
+ RailsBenchmarkSuite::Runner.register_workload("Request Heft", weight: 0.3) do
19
+ begin
20
+ # Step A: Define Ephemeral Controller
21
+ # Anonymous class avoids constant pollution
22
+ controller_class = Class.new(ActionController::Base) do
23
+ layout false
24
+
25
+ def index
26
+ render plain: "OK"
27
+ end
28
+ end
29
+
30
+ # Step B: Inject Ephemeral Route
31
+ # Obscure path to avoid collisions with host app routes
32
+ Rails.application.routes.draw do
33
+ get "/_rbs_benchmark/heft", to: controller_class.action(:index)
34
+ end
35
+
36
+ # Step C: Execute Full-Stack Request
37
+ env = Rack::MockRequest.env_for("/_rbs_benchmark/heft")
38
+ status, _headers, _body = Rails.application.call(env)
39
+
40
+ # Step D: Validation
41
+ # Don't read/close body to avoid I/O overhead skewing benchmark
42
+ raise RuntimeError, "Request Heft failed: status=#{status}" unless status == 200
43
+
44
+ true
45
+ rescue => e
46
+ # Error handling - don't crash the entire suite
47
+ warn "[Request Heft] Error: #{e.message}" if ENV["DEBUG"]
48
+ false
49
+ end
50
+ end
51
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_benchmark_suite
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - RailsBenchmarkSuite Contributors
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-01-04 00:00:00.000000000 Z
10
+ date: 2026-01-24 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: benchmark-ips
@@ -259,6 +259,7 @@ files:
259
259
  - lib/dummy/app/views/rails_benchmark_suite/heft_view.html.erb
260
260
  - lib/dummy/config/benchmark_database.yml
261
261
  - lib/rails_benchmark_suite.rb
262
+ - lib/rails_benchmark_suite/boot_profiler_script.rb
262
263
  - lib/rails_benchmark_suite/configuration.rb
263
264
  - lib/rails_benchmark_suite/database_manager.rb
264
265
  - lib/rails_benchmark_suite/db_setup.rb
@@ -276,6 +277,7 @@ files:
276
277
  - lib/rails_benchmark_suite/workloads/cache_heft_workload.rb
277
278
  - lib/rails_benchmark_suite/workloads/image_heft_workload.rb
278
279
  - lib/rails_benchmark_suite/workloads/job_heft_workload.rb
280
+ - lib/rails_benchmark_suite/workloads/request_heft_workload.rb
279
281
  - lib/rails_benchmark_suite/workloads/view_heft_workload.rb
280
282
  - rails_benchmark_suite.gemspec
281
283
  homepage: https://github.com/overnet/rails_benchmark_suite