rails_benchmark_suite 0.2.9 → 0.3.1
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/.gitignore +3 -1
- data/CHANGELOG.md +65 -0
- data/Gemfile.lock +29 -1
- data/README.md +103 -10
- data/bin/rails_benchmark_suite +35 -12
- data/docs/images/report_v0_3_1.png +0 -0
- data/lib/dummy/app/models/benchmark_job.rb +7 -0
- data/lib/dummy/app/models/benchmark_post.rb +9 -0
- data/lib/dummy/app/models/benchmark_user.rb +9 -0
- data/lib/dummy/app/views/rails_benchmark_suite/heft_view.html.erb +11 -0
- data/lib/dummy/config/benchmark_database.yml +16 -0
- data/lib/rails_benchmark_suite/configuration.rb +22 -0
- data/lib/rails_benchmark_suite/database_manager.rb +56 -0
- data/lib/rails_benchmark_suite/db_setup.rb +3 -0
- data/lib/rails_benchmark_suite/models/user.rb +4 -3
- data/lib/rails_benchmark_suite/reporter.rb +215 -5
- data/lib/rails_benchmark_suite/reporters/html_reporter.rb +52 -0
- data/lib/rails_benchmark_suite/runner.rb +46 -191
- data/lib/rails_benchmark_suite/schema.rb +5 -5
- data/lib/rails_benchmark_suite/templates/report.html.erb +187 -0
- data/lib/rails_benchmark_suite/version.rb +1 -1
- data/lib/rails_benchmark_suite/workload_runner.rb +158 -0
- data/lib/rails_benchmark_suite/{suites/active_record_suite.rb → workloads/active_record_workload.rb} +7 -7
- data/lib/rails_benchmark_suite/{suites/cache_heft_suite.rb → workloads/cache_heft_workload.rb} +2 -2
- data/lib/rails_benchmark_suite/{suites/image_heft_suite.rb → workloads/image_heft_workload.rb} +3 -4
- data/lib/rails_benchmark_suite/{suites/job_heft_suite.rb → workloads/job_heft_workload.rb} +4 -4
- data/lib/rails_benchmark_suite/workloads/view_heft_workload.rb +36 -0
- data/lib/rails_benchmark_suite.rb +3 -22
- data/rails_benchmark_suite.gemspec +7 -2
- metadata +92 -10
- data/lib/rails_benchmark_suite/suites/view_heft_suite.rb +0 -44
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-bs-theme="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Rails Benchmark Suite Report</title>
|
|
7
|
+
<!-- Bootstrap 5 CDN -->
|
|
8
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
9
|
+
<!-- Chart.js 4 CDN -->
|
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
11
|
+
|
|
12
|
+
<style>
|
|
13
|
+
body { background-color: #121212; color: #e0e0e0; font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; }
|
|
14
|
+
.card { background-color: #1e1e1e; border: 1px solid #333; }
|
|
15
|
+
.table-dark { --bs-table-bg: #1e1e1e; --bs-table-striped-bg: #252525; }
|
|
16
|
+
.efficiency-high { color: #4caf50; font-weight: bold; }
|
|
17
|
+
.efficiency-med { color: #ffc107; font-weight: bold; }
|
|
18
|
+
.efficiency-low { color: #f44336; font-weight: bold; }
|
|
19
|
+
.header-bar { border-bottom: 2px solid #3d3d3d; padding-bottom: 20px; margin-bottom: 30px; }
|
|
20
|
+
</style>
|
|
21
|
+
</head>
|
|
22
|
+
<body class="container py-5">
|
|
23
|
+
|
|
24
|
+
<!-- Header -->
|
|
25
|
+
<div class="header-bar d-flex justify-content-between align-items-center">
|
|
26
|
+
<div>
|
|
27
|
+
<h1 class="display-5 fw-bold text-light">Rails Benchmark Suite</h1>
|
|
28
|
+
<p class="text-secondary mb-0">v<%= RailsBenchmarkSuite::VERSION %> Report</p>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="text-end text-white-50">
|
|
31
|
+
<div>Ruby <%= RUBY_VERSION %></div>
|
|
32
|
+
<div><%= @payload[:threads] %> Threads</div>
|
|
33
|
+
<div><%= Time.now.strftime("%Y-%m-%d %H:%M") %></div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<!-- Score Cards -->
|
|
38
|
+
<div class="row g-4 mb-5">
|
|
39
|
+
<div class="col-md-6">
|
|
40
|
+
<div class="card h-100 p-4 text-center">
|
|
41
|
+
<h3 class="text-secondary">RHI Score</h3>
|
|
42
|
+
<div class="display-1 fw-bold <%= @payload[:total_score] > 200 ? 'text-primary' : 'text-light' %>">
|
|
43
|
+
<%= @payload[:total_score].to_i %>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="badge bg-dark border mt-2"><%= @payload[:tier] %> Tier</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="col-md-6">
|
|
49
|
+
<div class="card h-100 p-4 text-center">
|
|
50
|
+
<h3 class="text-secondary">Avg. Efficiency</h3>
|
|
51
|
+
<%
|
|
52
|
+
total_eff = @payload[:results].sum do |r|
|
|
53
|
+
entry_1t = r[:report].entries.find { |e| e.label.include?("(1 thread)") }
|
|
54
|
+
entry_mt = r[:report].entries.find { |e| e.label.match?(/\(\d+ threads\)/) }
|
|
55
|
+
next 0 unless entry_1t && entry_mt
|
|
56
|
+
(entry_mt.ips / (entry_1t.ips * @payload[:threads])) * 100
|
|
57
|
+
end
|
|
58
|
+
avg_eff = total_eff / [@payload[:results].size, 1].max
|
|
59
|
+
color_class = avg_eff > 75 ? 'efficiency-high' : (avg_eff > 50 ? 'efficiency-med' : 'efficiency-low')
|
|
60
|
+
%>
|
|
61
|
+
<div class="display-1 <%= color_class %>">
|
|
62
|
+
<%= avg_eff.round(1) %>%
|
|
63
|
+
</div>
|
|
64
|
+
<small class="text-muted">Target: > 80% linear scaling</small>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<!-- Chart -->
|
|
70
|
+
<div class="card mb-5">
|
|
71
|
+
<div class="card-header bg-transparent border-bottom border-secondary">
|
|
72
|
+
<h5 class="mb-0">Scaling Curve (1T vs MaxT)</h5>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="card-body">
|
|
75
|
+
<canvas id="scalingChart" style="max-height: 400px;"></canvas>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- Detailed Table -->
|
|
80
|
+
<div class="card">
|
|
81
|
+
<div class="card-header bg-transparent border-bottom border-secondary">
|
|
82
|
+
<h5 class="mb-0">Detailed Metrics</h5>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="table-responsive">
|
|
85
|
+
<table class="table table-dark table-striped table-hover mb-0 align-middle">
|
|
86
|
+
<thead>
|
|
87
|
+
<tr>
|
|
88
|
+
<th style="width: 30%">Workload</th>
|
|
89
|
+
<th class="text-end">1T IPS</th>
|
|
90
|
+
<th class="text-end">MaxT IPS</th>
|
|
91
|
+
<th class="text-end">Scaling (x)</th>
|
|
92
|
+
<th class="text-end">Efficiency</th>
|
|
93
|
+
<th class="text-end">Weight</th>
|
|
94
|
+
</tr>
|
|
95
|
+
</thead>
|
|
96
|
+
<tbody>
|
|
97
|
+
<% @payload[:results].each do |res| %>
|
|
98
|
+
<%
|
|
99
|
+
entry_1t = res[:report].entries.find { |e| e.label.include?("(1 thread)") }
|
|
100
|
+
entry_mt = res[:report].entries.find { |e| e.label.match?(/\(\d+ threads\)/) }
|
|
101
|
+
ips_1t = entry_1t ? entry_1t.ips : 0
|
|
102
|
+
ips_mt = entry_mt ? entry_mt.ips : 0
|
|
103
|
+
|
|
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
|
|
106
|
+
|
|
107
|
+
eff_color = efficiency > 75 ? 'efficiency-high' : (efficiency > 50 ? 'efficiency-med' : 'efficiency-low')
|
|
108
|
+
scale_color = scaling >= 1.0 ? 'text-success' : 'text-danger'
|
|
109
|
+
%>
|
|
110
|
+
<tr>
|
|
111
|
+
<td class="fw-medium"><%= res[:name] %></td>
|
|
112
|
+
<td class="text-end font-monospace"><%= ips_1t.round(1) %></td>
|
|
113
|
+
<td class="text-end font-monospace"><%= ips_mt.round(1) %></td>
|
|
114
|
+
<td class="text-end font-monospace <%= scale_color %>"><%= "%.2fx" % scaling %></td>
|
|
115
|
+
<td class="text-end font-monospace <%= eff_color %>"><%= efficiency.round(1) %>%</td>
|
|
116
|
+
<td class="text-end text-secondary"><%= res[:adjusted_weight].round(2) %></td>
|
|
117
|
+
</tr>
|
|
118
|
+
<% end %>
|
|
119
|
+
</tbody>
|
|
120
|
+
</table>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<!-- JavaScript Injection -->
|
|
125
|
+
<script>
|
|
126
|
+
const CHART_DATA = <%= @chart_payload %>;
|
|
127
|
+
|
|
128
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
129
|
+
const ctx = document.getElementById('scalingChart');
|
|
130
|
+
|
|
131
|
+
// Data is already prepared in Ruby
|
|
132
|
+
const labels = CHART_DATA.labels;
|
|
133
|
+
const data1T = CHART_DATA.data_1t;
|
|
134
|
+
const dataMaxT = CHART_DATA.data_mt;
|
|
135
|
+
|
|
136
|
+
// Color logic for MaxT bars (Red if bad scaling)
|
|
137
|
+
const backgroundColors = dataMaxT.map((val, index) => {
|
|
138
|
+
const ips1 = data1T[index];
|
|
139
|
+
const scaling = ips1 > 0 ? val / ips1 : 0;
|
|
140
|
+
return scaling < 0.8 ? 'rgba(239, 83, 80, 0.8)' : 'rgba(156, 39, 176, 0.8)';
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
new Chart(ctx, {
|
|
144
|
+
type: 'bar',
|
|
145
|
+
data: {
|
|
146
|
+
labels: labels,
|
|
147
|
+
datasets: [
|
|
148
|
+
{
|
|
149
|
+
label: '1-Thread IPS',
|
|
150
|
+
data: data1T,
|
|
151
|
+
backgroundColor: 'rgba(33, 150, 243, 0.8)',
|
|
152
|
+
borderColor: 'rgba(33, 150, 243, 1)',
|
|
153
|
+
borderWidth: 1
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
label: '<%= @payload[:threads] %>-Thread IPS',
|
|
157
|
+
data: dataMaxT,
|
|
158
|
+
backgroundColor: backgroundColors,
|
|
159
|
+
borderColor: backgroundColors.map(c => c.replace('0.8)', '1)')),
|
|
160
|
+
borderWidth: 1
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
options: {
|
|
165
|
+
responsive: true,
|
|
166
|
+
maintainAspectRatio: false,
|
|
167
|
+
scales: {
|
|
168
|
+
y: {
|
|
169
|
+
beginAtZero: true,
|
|
170
|
+
grid: { color: '#333' },
|
|
171
|
+
ticks: { color: '#aaa' },
|
|
172
|
+
title: { display: true, text: 'Iterations Per Second (IPS)', color: '#aaa' }
|
|
173
|
+
},
|
|
174
|
+
x: {
|
|
175
|
+
grid: { display: false },
|
|
176
|
+
ticks: { color: '#fff' }
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
plugins: {
|
|
180
|
+
legend: { labels: { color: '#fff' } }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
</script>
|
|
186
|
+
</body>
|
|
187
|
+
</html>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "benchmark/ips"
|
|
4
|
+
require "get_process_mem"
|
|
5
|
+
require "tty-spinner"
|
|
6
|
+
|
|
7
|
+
module RailsBenchmarkSuite
|
|
8
|
+
class WorkloadRunner
|
|
9
|
+
# Base weights for each workload
|
|
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
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def initialize(workloads, options: {}, show_progress: true)
|
|
19
|
+
@workloads = workloads
|
|
20
|
+
@options = options
|
|
21
|
+
@threads = options[:threads] || 4
|
|
22
|
+
@profile_mode = options[:profile] || false
|
|
23
|
+
@show_progress = show_progress
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def execute
|
|
27
|
+
if @profile_mode && @show_progress
|
|
28
|
+
puts "\nRunning Scaling Diagnostic (Profile Mode)..."
|
|
29
|
+
elsif @show_progress
|
|
30
|
+
puts "\n⏳ Running Benchmarks..."
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Initializing MultiSpinner if progress is enabled
|
|
34
|
+
multi_spinner = TTY::Spinner::Multi.new("[:spinner] Running Workloads", format: :dots) if @show_progress
|
|
35
|
+
|
|
36
|
+
# Run all workloads and collect results
|
|
37
|
+
results = @workloads.map do |w|
|
|
38
|
+
spinner = nil
|
|
39
|
+
if @show_progress
|
|
40
|
+
spinner = multi_spinner.register("[:spinner] #{w[:name]}", format: :dots)
|
|
41
|
+
spinner.auto_spin
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
result = run_single_workload(w, spinner)
|
|
45
|
+
|
|
46
|
+
if @show_progress && spinner
|
|
47
|
+
ips_1t = result[:report].entries.find { |e| e.label.include?("(1 thread)") }&.ips || 0
|
|
48
|
+
ips_mt = result[:report].entries.find { |e| e.label.include?("(#{@threads} threads)") }&.ips || 0
|
|
49
|
+
|
|
50
|
+
# Dynamic Success Message without duplicate name
|
|
51
|
+
success_msg = " ... #{Reporter.humanize(ips_1t)} IPS (1T) | #{Reporter.humanize(ips_mt)} IPS (#{@threads}T)"
|
|
52
|
+
spinner.success(success_msg)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
result
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Calculate normalized weights
|
|
59
|
+
weight_pool = results.sum { |r| BASE_WEIGHTS[r[:name]] || 0 }
|
|
60
|
+
|
|
61
|
+
results.each do |r|
|
|
62
|
+
base_weight = BASE_WEIGHTS[r[:name]] || 1.0
|
|
63
|
+
r[:adjusted_weight] = base_weight / weight_pool
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Calculate total score
|
|
67
|
+
total_score = results.sum do |r|
|
|
68
|
+
entries = r[:report].entries
|
|
69
|
+
entry_mt = entries.find { |e| e.label.include?("(#{@threads} threads)") }
|
|
70
|
+
ips_mt = entry_mt ? entry_mt.ips : 0
|
|
71
|
+
ips_mt * r[:adjusted_weight]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Determine tier
|
|
75
|
+
tier = if total_score < 50
|
|
76
|
+
"Entry/Dev"
|
|
77
|
+
elsif total_score < 200
|
|
78
|
+
"Production-Ready"
|
|
79
|
+
else
|
|
80
|
+
"High-Performance"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Return complete payload
|
|
84
|
+
{
|
|
85
|
+
results: results,
|
|
86
|
+
total_score: total_score,
|
|
87
|
+
tier: tier,
|
|
88
|
+
threads: @threads,
|
|
89
|
+
profile_mode: @profile_mode
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def run_single_workload(workload, spinner)
|
|
96
|
+
mem_before = GetProcessMem.new.mb
|
|
97
|
+
|
|
98
|
+
# Run benchmark
|
|
99
|
+
report = Benchmark.ips do |x|
|
|
100
|
+
# Silence output to allow spinner to own the UI
|
|
101
|
+
x.config(:time => 5, :warmup => 2, :quiet => true)
|
|
102
|
+
|
|
103
|
+
# Single Threaded
|
|
104
|
+
x.report("#{workload[:name]} (1 thread)") do
|
|
105
|
+
with_retries { workload[:block].call }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Multi Threaded
|
|
109
|
+
x.report("#{workload[:name]} (#{@threads} threads)") do
|
|
110
|
+
threads = @threads.times.map do
|
|
111
|
+
Thread.new do
|
|
112
|
+
ActiveRecord::Base.connection_pool.with_connection do
|
|
113
|
+
with_retries { workload[:block].call }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
threads.each(&:join)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# x.compare! removed to prevent STDOUT pollution
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
mem_after = GetProcessMem.new.mb
|
|
124
|
+
|
|
125
|
+
# Calculate Scaling Efficiency if in profile mode
|
|
126
|
+
# Efficiency = (Multi Score / (Single Score * Threads)) * 100
|
|
127
|
+
entries = report.entries
|
|
128
|
+
entry_1t = entries.find { |e| e.label.include?("(1 thread)") }
|
|
129
|
+
entry_mt = entries.find { |e| e.label.include?("(#{@threads} threads)") }
|
|
130
|
+
|
|
131
|
+
efficiency = 0.0
|
|
132
|
+
if entry_1t && entry_mt && entry_1t.ips > 0
|
|
133
|
+
single_score = entry_1t.ips
|
|
134
|
+
multi_score = entry_mt.ips
|
|
135
|
+
efficiency = (multi_score / (single_score * @threads)) * 100
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
{
|
|
139
|
+
name: workload[:name],
|
|
140
|
+
report: report,
|
|
141
|
+
memory_delta_mb: mem_after - mem_before,
|
|
142
|
+
efficiency: efficiency
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def with_retries
|
|
147
|
+
yield
|
|
148
|
+
rescue ActiveRecord::StatementInvalid => e
|
|
149
|
+
if e.message =~ /locked/i
|
|
150
|
+
ActiveRecord::Base.connection.reset!
|
|
151
|
+
sleep(rand(0.01..0.05))
|
|
152
|
+
retry
|
|
153
|
+
else
|
|
154
|
+
raise e
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
data/lib/rails_benchmark_suite/{suites/active_record_suite.rb → workloads/active_record_workload.rb}
RENAMED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
require "active_record"
|
|
4
4
|
|
|
5
|
-
# Benchmark
|
|
6
|
-
RailsBenchmarkSuite.
|
|
5
|
+
# Benchmark Workload
|
|
6
|
+
RailsBenchmarkSuite::Runner.register_workload("Active Record Heft", weight: 0.4) do
|
|
7
7
|
# Workload: Create User with Posts, Join Query, Update
|
|
8
8
|
# Use transaction rollback to keep the DB clean and avoid costly destroy callbacks
|
|
9
9
|
ActiveRecord::Base.transaction do
|
|
10
10
|
# 1. Create - with unique email per thread
|
|
11
|
-
user = RailsBenchmarkSuite::
|
|
11
|
+
user = RailsBenchmarkSuite::Dummy::BenchmarkUser.create!(
|
|
12
12
|
name: "Benchmark User",
|
|
13
13
|
email: "test-#{Thread.current.object_id}@example.com"
|
|
14
14
|
)
|
|
@@ -20,10 +20,10 @@ RailsBenchmarkSuite.register_suite("Active Record Heft", weight: 0.4) do
|
|
|
20
20
|
|
|
21
21
|
# 3. Complex Query (Join + Order)
|
|
22
22
|
# Unloading the relation to force execution
|
|
23
|
-
RailsBenchmarkSuite::
|
|
24
|
-
.where(
|
|
25
|
-
.where("
|
|
26
|
-
.order("
|
|
23
|
+
RailsBenchmarkSuite::Dummy::BenchmarkUser.joins(:posts)
|
|
24
|
+
.where(benchmark_users: { id: user.id })
|
|
25
|
+
.where("benchmark_posts.views >= ?", 0)
|
|
26
|
+
.order("benchmark_posts.created_at DESC")
|
|
27
27
|
.to_a
|
|
28
28
|
|
|
29
29
|
# 4. Update
|
data/lib/rails_benchmark_suite/{suites/cache_heft_suite.rb → workloads/cache_heft_workload.rb}
RENAMED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
require "active_support/cache"
|
|
4
4
|
require "securerandom"
|
|
5
5
|
|
|
6
|
-
# Benchmark
|
|
7
|
-
RailsBenchmarkSuite.
|
|
6
|
+
# Benchmark Workload
|
|
7
|
+
RailsBenchmarkSuite::Runner.register_workload("Cache Heft", weight: 0.1) do
|
|
8
8
|
# Simulate SolidCache using MemoryStore
|
|
9
9
|
@cache ||= ActiveSupport::Cache::MemoryStore.new
|
|
10
10
|
|
data/lib/rails_benchmark_suite/{suites/image_heft_suite.rb → workloads/image_heft_workload.rb}
RENAMED
|
@@ -11,7 +11,7 @@ begin
|
|
|
11
11
|
SAMPLE_IMAGE = File.join(ASSET_DIR, "sample.jpg")
|
|
12
12
|
|
|
13
13
|
# Only register if vips is actually available
|
|
14
|
-
RailsBenchmarkSuite.
|
|
14
|
+
RailsBenchmarkSuite::Runner.register_workload("Image Heft", weight: 0.1) do
|
|
15
15
|
# Gracefully handle missing dependencies
|
|
16
16
|
if File.exist?(SAMPLE_IMAGE)
|
|
17
17
|
ImageProcessing::Vips
|
|
@@ -25,7 +25,6 @@ begin
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
rescue LoadError, StandardError
|
|
28
|
-
# Don't register the
|
|
29
|
-
puts "⚠️ Skipping Image
|
|
28
|
+
# Don't register the workload at all if vips is unavailable
|
|
29
|
+
puts "\n⚠️ Skipping Image Workload: libvips not available. Install with: 'brew install vips' (macOS) or 'sudo apt install libvips-dev' (Linux)\n\n"
|
|
30
30
|
end
|
|
31
|
-
|
|
@@ -4,12 +4,12 @@ require "active_record"
|
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
RailsBenchmarkSuite.
|
|
7
|
+
RailsBenchmarkSuite::Runner.register_workload("Solid Queue Heft", weight: 0.2) do
|
|
8
8
|
# Simulation: Enqueue 100 jobs, then work them off
|
|
9
9
|
|
|
10
10
|
# 1. Enqueue Loop
|
|
11
11
|
100.times do |i|
|
|
12
|
-
RailsBenchmarkSuite::
|
|
12
|
+
RailsBenchmarkSuite::Dummy::BenchmarkJob.create!(
|
|
13
13
|
queue_name: "default",
|
|
14
14
|
arguments: { job_id: i, payload: "x" * 100 }.to_json,
|
|
15
15
|
scheduled_at: Time.now
|
|
@@ -23,12 +23,12 @@ RailsBenchmarkSuite.register_suite("Solid Queue Heft", weight: 0.2) do
|
|
|
23
23
|
# Transactional polling
|
|
24
24
|
ActiveRecord::Base.transaction do
|
|
25
25
|
# Fetch batch
|
|
26
|
-
jobs = RailsBenchmarkSuite::
|
|
26
|
+
jobs = RailsBenchmarkSuite::Dummy::BenchmarkJob.where("scheduled_at <= ?", Time.now).order(:created_at).limit(10)
|
|
27
27
|
|
|
28
28
|
if jobs.any?
|
|
29
29
|
# Simulate processing time and delete
|
|
30
30
|
ids = jobs.map(&:id)
|
|
31
|
-
RailsBenchmarkSuite::
|
|
31
|
+
RailsBenchmarkSuite::Dummy::BenchmarkJob.where(id: ids).delete_all
|
|
32
32
|
processed = true
|
|
33
33
|
end
|
|
34
34
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "action_view"
|
|
4
|
+
require "ostruct"
|
|
5
|
+
|
|
6
|
+
# Benchmark Workload
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Helper for the workload - Mixed into ActionView::Base instance automatically by Rails usually,
|
|
10
|
+
# but here we might need to include it or just rely on standard helpers if ActionView loads them.
|
|
11
|
+
# The template uses number_with_delimiter which is standard.
|
|
12
|
+
module RailsBenchmarkSuiteNumberHelper
|
|
13
|
+
# No-op or keep provided helper if standard library fails in isolation
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
RailsBenchmarkSuite::Runner.register_workload("View Heft", weight: 0.2) do
|
|
17
|
+
# Setup context once
|
|
18
|
+
@view_renderer ||= begin
|
|
19
|
+
# Use the "Dummy" app views folder
|
|
20
|
+
views_path = File.expand_path("../../dummy/app/views", __dir__)
|
|
21
|
+
lookup_context = ActionView::LookupContext.new([views_path])
|
|
22
|
+
ActionView::Base.with_empty_template_cache.new(lookup_context, {}, nil)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Workload: Render template from file
|
|
26
|
+
# Previously inline, now isolated in lib/dummy/app/views
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Dummy Objects
|
|
30
|
+
user = OpenStruct.new(name: "Speedy")
|
|
31
|
+
posts = 100.times.map { |i| OpenStruct.new(title: "Post #{i}", body: "Content " * 10, views: i * 1000) }
|
|
32
|
+
|
|
33
|
+
# Execution
|
|
34
|
+
# Render the namespaced template 'rails_benchmark_suite/heft_view'
|
|
35
|
+
@view_renderer.render(template: "rails_benchmark_suite/heft_view", locals: { user: user, posts: posts })
|
|
36
|
+
end
|
|
@@ -1,27 +1,8 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "concurrent"
|
|
4
1
|
require "rails_benchmark_suite/version"
|
|
5
|
-
require "rails_benchmark_suite/reporter"
|
|
6
2
|
require "rails_benchmark_suite/runner"
|
|
7
|
-
require "rails_benchmark_suite/
|
|
8
|
-
require "rails_benchmark_suite/
|
|
9
|
-
require "rails_benchmark_suite/models/user"
|
|
10
|
-
require "rails_benchmark_suite/models/post"
|
|
11
|
-
require "rails_benchmark_suite/models/simulated_job"
|
|
3
|
+
require "rails_benchmark_suite/reporter"
|
|
4
|
+
require "rails_benchmark_suite/configuration"
|
|
12
5
|
|
|
13
6
|
module RailsBenchmarkSuite
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def self.register_suite(name, weight: 1.0, &block)
|
|
17
|
-
@suites << { name: name, weight: weight, block: block }
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def self.run(json: false)
|
|
21
|
-
# Load suites
|
|
22
|
-
Dir[File.join(__dir__, "rails_benchmark_suite", "suites", "*.rb")].each { |f| require f }
|
|
23
|
-
|
|
24
|
-
runner = Runner.new(@suites, json: json)
|
|
25
|
-
runner.run
|
|
26
|
-
end
|
|
7
|
+
class Error < StandardError; end
|
|
27
8
|
end
|
|
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ["RailsBenchmarkSuite Contributors"]
|
|
9
9
|
spec.email = ["team@rails.org"]
|
|
10
10
|
|
|
11
|
-
spec.summary = "Rails-
|
|
12
|
-
spec.description = "Measures the
|
|
11
|
+
spec.summary = "Rails Heft Index (RHI) - Hardware benchmarking using realistic workloads"
|
|
12
|
+
spec.description = "Measures the Rails Heft Index (RHI), a weighted performance score based on realistic Rails 8+ workloads across Active Record, caching, views, jobs, and image processing."
|
|
13
13
|
spec.homepage = "https://github.com/overnet/rails_benchmark_suite"
|
|
14
14
|
spec.license = "MIT"
|
|
15
15
|
|
|
@@ -32,10 +32,15 @@ Gem::Specification.new do |spec|
|
|
|
32
32
|
spec.add_dependency "sqlite3", "~> 2.8"
|
|
33
33
|
spec.add_dependency "concurrent-ruby", "~> 1.3"
|
|
34
34
|
spec.add_dependency "get_process_mem", "~> 1.0"
|
|
35
|
+
spec.add_dependency "tty-spinner", "~> 0.9"
|
|
36
|
+
spec.add_dependency "tty-table", "~> 0.12"
|
|
37
|
+
spec.add_dependency "tty-box", "~> 0.7"
|
|
38
|
+
spec.add_dependency "pastel", "~> 0.8"
|
|
35
39
|
|
|
36
40
|
spec.add_development_dependency "bundler", "~> 2.5"
|
|
37
41
|
spec.add_development_dependency "rake", "~> 13.0"
|
|
38
42
|
spec.add_development_dependency "minitest", "~> 5.0"
|
|
43
|
+
spec.add_development_dependency "ostruct", "~> 0.6"
|
|
39
44
|
|
|
40
45
|
spec.required_ruby_version = ">= 3.4.0"
|
|
41
46
|
end
|