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 +4 -4
- data/CHANGELOG.md +21 -0
- data/Gemfile.lock +1 -1
- data/README.md +14 -8
- data/lib/rails_benchmark_suite/boot_profiler_script.rb +85 -0
- data/lib/rails_benchmark_suite/reporter.rb +52 -8
- data/lib/rails_benchmark_suite/runner.rb +39 -2
- data/lib/rails_benchmark_suite/templates/report.html.erb +88 -1
- data/lib/rails_benchmark_suite/version.rb +1 -1
- data/lib/rails_benchmark_suite/workload_runner.rb +17 -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,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
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
|
|
|
@@ -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`)
|
|
124
|
-
| `--profile` | **Diagnostic Mode.** Runs
|
|
125
|
-
| `--db` |
|
|
126
|
-
| `-t [N]` | Manually set
|
|
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** |
|
|
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
|
|
@@ -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:
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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:
|
|
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
|
-
#
|
|
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
|
|
@@ -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 =
|
|
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>
|
|
@@ -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)
|
|
@@ -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
|
|
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
|