rails_benchmark_suite 0.3.2 → 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 +4 -4
- data/CHANGELOG.md +14 -0
- data/Gemfile.lock +1 -1
- data/README.md +10 -4
- data/lib/rails_benchmark_suite/boot_profiler_script.rb +85 -0
- data/lib/rails_benchmark_suite/reporter.rb +45 -6
- data/lib/rails_benchmark_suite/runner.rb +39 -2
- data/lib/rails_benchmark_suite/templates/report.html.erb +87 -0
- data/lib/rails_benchmark_suite/version.rb +1 -1
- data/lib/rails_benchmark_suite/workload_runner.rb +7 -6
- data/lib/rails_benchmark_suite/workloads/image_heft_workload.rb +5 -7
- data/lib/rails_benchmark_suite/workloads/request_heft_workload.rb +51 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 836276b8a2872af0084df8bbad548c23cd1102aefb9d8e8399107c79a154a237
|
|
4
|
+
data.tar.gz: 174cc4856a798619687f55ceb182d03f423d33ccd830a90ec5c9d538a0411f2a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 626e81a963ca2a345a51236c24188caaec3d8b871392262cd6189551ed66997cd7722573327645445cd5a068be7f32b3e918fe4fb8e4d0f0518cf65094fbe3ef
|
|
7
|
+
data.tar.gz: f9893a9176305a878e9a2be2f0bb80e9fb4f4613837e98c74042f74917fa79e7498618cc1c75de4ac026f9ce4ab9b7152c7721d8db5dae89b5b3ea0dd232c9fb
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
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
|
+
|
|
2
16
|
## [0.3.2] - 2026-01-05
|
|
3
17
|
### Refactoring & Polish
|
|
4
18
|
- **DRY Logic**: Centralized "Efficiency" calculation; removed duplicate math from HTML templates.
|
data/Gemfile.lock
CHANGED
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
|
|
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
|
|
|
@@ -156,9 +160,10 @@ RHI Score = Σ (4-Thread IPS × Weight)
|
|
|
156
160
|
|
|
157
161
|
| Workload | Weight | Rationale |
|
|
158
162
|
|----------|--------|-----------|
|
|
159
|
-
| **Active Record** |
|
|
160
|
-
| **
|
|
161
|
-
| **
|
|
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
|
|
@@ -116,18 +116,56 @@ module RailsBenchmarkSuite
|
|
|
116
116
|
renderer.border.separator = :each_row
|
|
117
117
|
renderer.border.style = :blue
|
|
118
118
|
end
|
|
119
|
+
|
|
120
|
+
# 2. Boot Structure Analysis
|
|
121
|
+
render_boot_analysis(payload[:boot_analysis])
|
|
119
122
|
|
|
120
|
-
#
|
|
123
|
+
# 3. Insights List
|
|
121
124
|
puts ""
|
|
122
125
|
check_scaling_insights(results)
|
|
123
126
|
check_yjit_insight
|
|
124
127
|
check_memory_insights(results)
|
|
125
128
|
|
|
126
|
-
#
|
|
129
|
+
# 4. Final Score Dashboard
|
|
127
130
|
render_final_score(total_score)
|
|
128
131
|
show_hardware_tier(tier)
|
|
129
132
|
end
|
|
130
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
|
+
|
|
131
169
|
def check_scaling_insights(results)
|
|
132
170
|
poor_scaling = results.select do |r|
|
|
133
171
|
entries = r[:report].entries
|
|
@@ -141,7 +179,7 @@ module RailsBenchmarkSuite
|
|
|
141
179
|
end
|
|
142
180
|
|
|
143
181
|
if poor_scaling.any?
|
|
144
|
-
puts pastel.yellow.bold("
|
|
182
|
+
puts pastel.yellow.bold("\u{1F4A1} Insight (Scaling):") + " Scaling below 1.0x detected."
|
|
145
183
|
puts " This indicates SQLite lock contention or Ruby GIL saturation."
|
|
146
184
|
end
|
|
147
185
|
end
|
|
@@ -149,7 +187,7 @@ module RailsBenchmarkSuite
|
|
|
149
187
|
def check_yjit_insight
|
|
150
188
|
unless yjit_enabled?
|
|
151
189
|
puts ""
|
|
152
|
-
puts pastel.yellow.bold("
|
|
190
|
+
puts pastel.yellow.bold("\u{1F4A1} Insight (YJIT):") + " YJIT is OFF."
|
|
153
191
|
puts " Run with RUBY_OPT=\"--yjit\" for ~20% boost."
|
|
154
192
|
end
|
|
155
193
|
end
|
|
@@ -157,7 +195,7 @@ module RailsBenchmarkSuite
|
|
|
157
195
|
def check_memory_insights(results)
|
|
158
196
|
results.select { |r| r[:memory_delta_mb] > 20 }.each do |r|
|
|
159
197
|
puts ""
|
|
160
|
-
puts pastel.yellow.bold("
|
|
198
|
+
puts pastel.yellow.bold("\u{1F4A1} Insight (Memory):") + " High growth in #{r[:name]} (#{r[:memory_delta_mb].round(1)}MB)"
|
|
161
199
|
puts " Suggests heavy object allocation."
|
|
162
200
|
end
|
|
163
201
|
end
|
|
@@ -173,7 +211,7 @@ module RailsBenchmarkSuite
|
|
|
173
211
|
end
|
|
174
212
|
|
|
175
213
|
puts ""
|
|
176
|
-
puts pastel.bold("
|
|
214
|
+
puts pastel.bold("\u{1F4CA} Performance Tier: ") + comparison
|
|
177
215
|
puts ""
|
|
178
216
|
print cursor.show
|
|
179
217
|
end
|
|
@@ -201,6 +239,7 @@ module RailsBenchmarkSuite
|
|
|
201
239
|
system: system_info,
|
|
202
240
|
total_score: payload[:total_score].round(0),
|
|
203
241
|
tier: payload[:tier],
|
|
242
|
+
boot_analysis: payload[:boot_analysis] || [],
|
|
204
243
|
workloads: []
|
|
205
244
|
}
|
|
206
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
@@ -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>
|
|
@@ -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
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
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)
|
|
@@ -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
|
|
14
|
-
|
|
15
|
-
|
|
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.
|
|
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-
|
|
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
|