rails_benchmark_suite 0.3.0 → 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 +2 -0
- data/CHANGELOG.md +19 -0
- data/Gemfile.lock +27 -1
- data/README.md +39 -2
- data/bin/rails_benchmark_suite +29 -10
- 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 +36 -18
- 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 +54 -11
- 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 +54 -17
- data/lib/rails_benchmark_suite/workloads/active_record_workload.rb +6 -6
- data/lib/rails_benchmark_suite/workloads/cache_heft_workload.rb +1 -1
- data/lib/rails_benchmark_suite/workloads/image_heft_workload.rb +2 -2
- data/lib/rails_benchmark_suite/workloads/job_heft_workload.rb +4 -4
- data/lib/rails_benchmark_suite/workloads/view_heft_workload.rb +13 -21
- data/lib/rails_benchmark_suite.rb +3 -25
- data/rails_benchmark_suite.gemspec +4 -0
- metadata +67 -3
- data/lib/rails_benchmark_suite/formatter.rb +0 -206
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
require "json"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
module RailsBenchmarkSuite
|
|
8
|
+
module Reporters
|
|
9
|
+
class HtmlReporter
|
|
10
|
+
def initialize(payload)
|
|
11
|
+
@payload = payload
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def generate
|
|
15
|
+
template_path = File.expand_path("../templates/report.html.erb", __dir__)
|
|
16
|
+
template = File.read(template_path)
|
|
17
|
+
|
|
18
|
+
# Prepare data for JS injection (Flatten complex objects to simple Hash)
|
|
19
|
+
chart_data = {
|
|
20
|
+
labels: @payload[:results].map { |r| r[:name] },
|
|
21
|
+
data_1t: [],
|
|
22
|
+
data_mt: []
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@payload[:results].each do |res|
|
|
26
|
+
entry_1t = res[:report].entries.find { |e| e.label.include?("(1 thread)") }
|
|
27
|
+
entry_mt = res[:report].entries.find { |e| e.label.match?(/\(\d+ threads\)/) }
|
|
28
|
+
|
|
29
|
+
chart_data[:data_1t] << (entry_1t ? entry_1t.ips : 0)
|
|
30
|
+
chart_data[:data_mt] << (entry_mt ? entry_mt.ips : 0)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
@chart_payload = chart_data.to_json
|
|
34
|
+
|
|
35
|
+
# Render
|
|
36
|
+
html = ERB.new(template).result(binding)
|
|
37
|
+
|
|
38
|
+
# Output file
|
|
39
|
+
dir = Dir.exist?("tmp") ? "tmp" : "."
|
|
40
|
+
file_path = File.join(dir, "rails_benchmark_report.html")
|
|
41
|
+
File.write(file_path, html)
|
|
42
|
+
|
|
43
|
+
puts "\n"
|
|
44
|
+
puts "✅ HTML Report Generated!"
|
|
45
|
+
puts "📂 Location: #{File.expand_path(file_path)}"
|
|
46
|
+
puts "👉 View (Local): open '#{file_path}'"
|
|
47
|
+
puts "👉 View (Remote): scp user@server:#{File.expand_path(file_path)} ."
|
|
48
|
+
puts "\n"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -1,23 +1,66 @@
|
|
|
1
|
-
|
|
1
|
+
require_relative "database_manager"
|
|
2
|
+
require_relative "workload_runner"
|
|
3
|
+
require_relative "reporter"
|
|
4
|
+
require_relative "schema"
|
|
5
|
+
require_relative "../dummy/app/models/benchmark_user"
|
|
6
|
+
require_relative "../dummy/app/models/benchmark_post"
|
|
7
|
+
require_relative "../dummy/app/models/benchmark_job"
|
|
2
8
|
|
|
3
9
|
module RailsBenchmarkSuite
|
|
4
10
|
class Runner
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
# Registry for workloads
|
|
12
|
+
@workloads = []
|
|
13
|
+
|
|
14
|
+
def self.register_workload(name, weight: 1.0, &block)
|
|
15
|
+
@workloads << { name: name, weight: weight, block: block }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def initialize(config)
|
|
19
|
+
@config = config
|
|
8
20
|
end
|
|
9
21
|
|
|
10
22
|
def run
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
# Load workloads dynamically if not already loaded (idempotent)
|
|
24
|
+
if Runner.instance_variable_get(:@workloads).empty?
|
|
25
|
+
Dir[File.join(__dir__, "workloads", "*.rb")].each { |f| require f }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 1. Setup Database
|
|
29
|
+
DatabaseManager.new.setup(use_local_db: @config.db)
|
|
13
30
|
|
|
14
|
-
#
|
|
15
|
-
|
|
31
|
+
# 2. Display Header
|
|
32
|
+
header_info = Reporter.system_info.merge(
|
|
33
|
+
threads: @config.threads,
|
|
34
|
+
db_mode: @config.db_mode
|
|
35
|
+
)
|
|
36
|
+
Reporter.header(header_info) unless @config.json
|
|
16
37
|
|
|
17
|
-
|
|
18
|
-
|
|
38
|
+
# 3. Execute Workloads
|
|
39
|
+
# Passing config values as options to WorkloadRunner for compatibility
|
|
40
|
+
# Ideally we'd pass the config object but WorkloadRunner expects a hash currently
|
|
41
|
+
# We will refactor WorkloadRunner to accept config later or wrap it here
|
|
42
|
+
runner_options = {
|
|
43
|
+
threads: @config.threads,
|
|
44
|
+
profile: @config.profile
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
payload = WorkloadRunner.new(
|
|
48
|
+
Runner.instance_variable_get(:@workloads),
|
|
49
|
+
options: runner_options,
|
|
50
|
+
show_progress: !@config.json
|
|
51
|
+
).execute
|
|
52
|
+
|
|
53
|
+
# 4. Report Results
|
|
54
|
+
if @config.json
|
|
55
|
+
Reporter.as_json(payload)
|
|
19
56
|
else
|
|
20
|
-
|
|
57
|
+
Reporter.render(payload)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# 5. HTML Report Generation
|
|
61
|
+
if @config.html
|
|
62
|
+
require_relative "reporters/html_reporter"
|
|
63
|
+
Reporters::HtmlReporter.new(payload).generate
|
|
21
64
|
end
|
|
22
65
|
end
|
|
23
66
|
end
|
|
@@ -6,16 +6,16 @@ module RailsBenchmarkSuite
|
|
|
6
6
|
module Schema
|
|
7
7
|
def self.load
|
|
8
8
|
ActiveRecord::Schema.define do
|
|
9
|
-
#
|
|
10
|
-
create_table :
|
|
9
|
+
# BenchmarkUsers
|
|
10
|
+
create_table :benchmark_users, force: true do |t|
|
|
11
11
|
t.string :name
|
|
12
12
|
t.string :email
|
|
13
13
|
t.timestamps
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
create_table :
|
|
18
|
-
t.references :
|
|
16
|
+
# BenchmarkPosts
|
|
17
|
+
create_table :benchmark_posts, force: true do |t|
|
|
18
|
+
t.references :benchmark_user, foreign_key: { to_table: :benchmark_users }
|
|
19
19
|
t.string :title
|
|
20
20
|
t.text :body
|
|
21
21
|
t.integer :views, default: 0
|
|
@@ -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>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "benchmark/ips"
|
|
4
4
|
require "get_process_mem"
|
|
5
|
+
require "tty-spinner"
|
|
5
6
|
|
|
6
7
|
module RailsBenchmarkSuite
|
|
7
8
|
class WorkloadRunner
|
|
@@ -14,22 +15,41 @@ module RailsBenchmarkSuite
|
|
|
14
15
|
"Image Heft" => 0.1
|
|
15
16
|
}.freeze
|
|
16
17
|
|
|
17
|
-
def initialize(workloads, show_progress: true)
|
|
18
|
+
def initialize(workloads, options: {}, show_progress: true)
|
|
18
19
|
@workloads = workloads
|
|
20
|
+
@options = options
|
|
21
|
+
@threads = options[:threads] || 4
|
|
22
|
+
@profile_mode = options[:profile] || false
|
|
19
23
|
@show_progress = show_progress
|
|
20
24
|
end
|
|
21
25
|
|
|
22
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
|
+
|
|
23
36
|
# Run all workloads and collect results
|
|
24
|
-
results = @workloads.map
|
|
37
|
+
results = @workloads.map do |w|
|
|
38
|
+
spinner = nil
|
|
25
39
|
if @show_progress
|
|
26
|
-
|
|
40
|
+
spinner = multi_spinner.register("[:spinner] #{w[:name]}", format: :dots)
|
|
41
|
+
spinner.auto_spin
|
|
27
42
|
end
|
|
28
43
|
|
|
29
|
-
result = run_single_workload(w)
|
|
44
|
+
result = run_single_workload(w, spinner)
|
|
30
45
|
|
|
31
|
-
if @show_progress
|
|
32
|
-
|
|
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)
|
|
33
53
|
end
|
|
34
54
|
|
|
35
55
|
result
|
|
@@ -46,9 +66,9 @@ module RailsBenchmarkSuite
|
|
|
46
66
|
# Calculate total score
|
|
47
67
|
total_score = results.sum do |r|
|
|
48
68
|
entries = r[:report].entries
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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]
|
|
52
72
|
end
|
|
53
73
|
|
|
54
74
|
# Determine tier
|
|
@@ -64,27 +84,30 @@ module RailsBenchmarkSuite
|
|
|
64
84
|
{
|
|
65
85
|
results: results,
|
|
66
86
|
total_score: total_score,
|
|
67
|
-
tier: tier
|
|
87
|
+
tier: tier,
|
|
88
|
+
threads: @threads,
|
|
89
|
+
profile_mode: @profile_mode
|
|
68
90
|
}
|
|
69
91
|
end
|
|
70
92
|
|
|
71
93
|
private
|
|
72
94
|
|
|
73
|
-
def run_single_workload(workload)
|
|
95
|
+
def run_single_workload(workload, spinner)
|
|
74
96
|
mem_before = GetProcessMem.new.mb
|
|
75
97
|
|
|
76
98
|
# Run benchmark
|
|
77
99
|
report = Benchmark.ips do |x|
|
|
78
|
-
|
|
100
|
+
# Silence output to allow spinner to own the UI
|
|
101
|
+
x.config(:time => 5, :warmup => 2, :quiet => true)
|
|
79
102
|
|
|
80
103
|
# Single Threaded
|
|
81
104
|
x.report("#{workload[:name]} (1 thread)") do
|
|
82
105
|
with_retries { workload[:block].call }
|
|
83
106
|
end
|
|
84
107
|
|
|
85
|
-
# Multi Threaded
|
|
86
|
-
x.report("#{workload[:name]} (
|
|
87
|
-
threads =
|
|
108
|
+
# Multi Threaded
|
|
109
|
+
x.report("#{workload[:name]} (#{@threads} threads)") do
|
|
110
|
+
threads = @threads.times.map do
|
|
88
111
|
Thread.new do
|
|
89
112
|
ActiveRecord::Base.connection_pool.with_connection do
|
|
90
113
|
with_retries { workload[:block].call }
|
|
@@ -94,15 +117,29 @@ module RailsBenchmarkSuite
|
|
|
94
117
|
threads.each(&:join)
|
|
95
118
|
end
|
|
96
119
|
|
|
97
|
-
x.compare!
|
|
120
|
+
# x.compare! removed to prevent STDOUT pollution
|
|
98
121
|
end
|
|
99
122
|
|
|
100
123
|
mem_after = GetProcessMem.new.mb
|
|
101
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
|
+
|
|
102
138
|
{
|
|
103
139
|
name: workload[:name],
|
|
104
140
|
report: report,
|
|
105
|
-
memory_delta_mb: mem_after - mem_before
|
|
141
|
+
memory_delta_mb: mem_after - mem_before,
|
|
142
|
+
efficiency: efficiency
|
|
106
143
|
}
|
|
107
144
|
end
|
|
108
145
|
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
require "active_record"
|
|
4
4
|
|
|
5
5
|
# Benchmark Workload
|
|
6
|
-
RailsBenchmarkSuite.register_workload("Active Record Heft", weight: 0.4) do
|
|
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_workload("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
|
|
@@ -4,7 +4,7 @@ require "active_support/cache"
|
|
|
4
4
|
require "securerandom"
|
|
5
5
|
|
|
6
6
|
# Benchmark Workload
|
|
7
|
-
RailsBenchmarkSuite.register_workload("Cache Heft", weight: 0.1) do
|
|
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
|
|
|
@@ -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.register_workload("Image Heft", weight: 0.1) do
|
|
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
|
|
@@ -26,5 +26,5 @@ begin
|
|
|
26
26
|
|
|
27
27
|
rescue LoadError, StandardError
|
|
28
28
|
# Don't register the workload at all if vips is unavailable
|
|
29
|
-
puts "⚠️ Skipping Image Workload: libvips not available. Install with: 'brew install vips' (macOS) or 'sudo apt install libvips-dev' (Linux)"
|
|
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
|
|
@@ -4,12 +4,12 @@ require "active_record"
|
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
RailsBenchmarkSuite.register_workload("Solid Queue Heft", weight: 0.2) do
|
|
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_workload("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
|
|
@@ -6,39 +6,31 @@ require "ostruct"
|
|
|
6
6
|
# Benchmark Workload
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
# Helper for the workload
|
|
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.
|
|
10
12
|
module RailsBenchmarkSuiteNumberHelper
|
|
11
|
-
|
|
12
|
-
number.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")
|
|
13
|
-
end
|
|
13
|
+
# No-op or keep provided helper if standard library fails in isolation
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
RailsBenchmarkSuite.register_workload("View Heft", weight: 0.2) do
|
|
16
|
+
RailsBenchmarkSuite::Runner.register_workload("View Heft", weight: 0.2) do
|
|
17
17
|
# Setup context once
|
|
18
18
|
@view_renderer ||= begin
|
|
19
|
-
|
|
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])
|
|
20
22
|
ActionView::Base.with_empty_template_cache.new(lookup_context, {}, nil)
|
|
21
23
|
end
|
|
22
24
|
|
|
23
|
-
# Workload: Render
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<ul>
|
|
27
|
-
<% posts.each do |post| %>
|
|
28
|
-
<li>
|
|
29
|
-
<strong><%= post.title %></strong>
|
|
30
|
-
<p><%= post.body.truncate(50) %></p>
|
|
31
|
-
<small>Views: <%= RailsBenchmarkSuiteNumberHelper.number_with_delimiter(post.views) %></small>
|
|
32
|
-
</li>
|
|
33
|
-
<% end %>
|
|
34
|
-
</ul>
|
|
35
|
-
<footer>Generated at <%= Time.now.to_s %></footer>
|
|
36
|
-
ERB
|
|
25
|
+
# Workload: Render template from file
|
|
26
|
+
# Previously inline, now isolated in lib/dummy/app/views
|
|
27
|
+
|
|
37
28
|
|
|
38
29
|
# Dummy Objects
|
|
39
30
|
user = OpenStruct.new(name: "Speedy")
|
|
40
31
|
posts = 100.times.map { |i| OpenStruct.new(title: "Post #{i}", body: "Content " * 10, views: i * 1000) }
|
|
41
32
|
|
|
42
33
|
# Execution
|
|
43
|
-
|
|
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 })
|
|
44
36
|
end
|
|
@@ -1,30 +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
|
-
require "rails_benchmark_suite/database_manager"
|
|
7
|
-
require "rails_benchmark_suite/workload_runner"
|
|
8
|
-
require "rails_benchmark_suite/formatter"
|
|
9
2
|
require "rails_benchmark_suite/runner"
|
|
10
|
-
require "rails_benchmark_suite/
|
|
11
|
-
require "rails_benchmark_suite/
|
|
12
|
-
require "rails_benchmark_suite/models/user"
|
|
13
|
-
require "rails_benchmark_suite/models/post"
|
|
14
|
-
require "rails_benchmark_suite/models/simulated_job"
|
|
3
|
+
require "rails_benchmark_suite/reporter"
|
|
4
|
+
require "rails_benchmark_suite/configuration"
|
|
15
5
|
|
|
16
6
|
module RailsBenchmarkSuite
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def self.register_workload(name, weight: 1.0, &block)
|
|
20
|
-
@workloads << { name: name, weight: weight, block: block }
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.run(json: false)
|
|
24
|
-
# Load workloads
|
|
25
|
-
Dir[File.join(__dir__, "rails_benchmark_suite", "workloads", "*.rb")].each { |f| require f }
|
|
26
|
-
|
|
27
|
-
runner = Runner.new(@workloads, json: json)
|
|
28
|
-
runner.run
|
|
29
|
-
end
|
|
7
|
+
class Error < StandardError; end
|
|
30
8
|
end
|