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.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +19 -0
  4. data/Gemfile.lock +27 -1
  5. data/README.md +39 -2
  6. data/bin/rails_benchmark_suite +29 -10
  7. data/docs/images/report_v0_3_1.png +0 -0
  8. data/lib/dummy/app/models/benchmark_job.rb +7 -0
  9. data/lib/dummy/app/models/benchmark_post.rb +9 -0
  10. data/lib/dummy/app/models/benchmark_user.rb +9 -0
  11. data/lib/dummy/app/views/rails_benchmark_suite/heft_view.html.erb +11 -0
  12. data/lib/dummy/config/benchmark_database.yml +16 -0
  13. data/lib/rails_benchmark_suite/configuration.rb +22 -0
  14. data/lib/rails_benchmark_suite/database_manager.rb +36 -18
  15. data/lib/rails_benchmark_suite/models/user.rb +4 -3
  16. data/lib/rails_benchmark_suite/reporter.rb +215 -5
  17. data/lib/rails_benchmark_suite/reporters/html_reporter.rb +52 -0
  18. data/lib/rails_benchmark_suite/runner.rb +54 -11
  19. data/lib/rails_benchmark_suite/schema.rb +5 -5
  20. data/lib/rails_benchmark_suite/templates/report.html.erb +187 -0
  21. data/lib/rails_benchmark_suite/version.rb +1 -1
  22. data/lib/rails_benchmark_suite/workload_runner.rb +54 -17
  23. data/lib/rails_benchmark_suite/workloads/active_record_workload.rb +6 -6
  24. data/lib/rails_benchmark_suite/workloads/cache_heft_workload.rb +1 -1
  25. data/lib/rails_benchmark_suite/workloads/image_heft_workload.rb +2 -2
  26. data/lib/rails_benchmark_suite/workloads/job_heft_workload.rb +4 -4
  27. data/lib/rails_benchmark_suite/workloads/view_heft_workload.rb +13 -21
  28. data/lib/rails_benchmark_suite.rb +3 -25
  29. data/rails_benchmark_suite.gemspec +4 -0
  30. metadata +67 -3
  31. 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
- # frozen_string_literal: true
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
- def initialize(workloads, json: false)
6
- @workloads = workloads
7
- @json_output = json
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
- DatabaseManager.new.setup
12
- Formatter.header(Reporter.system_info) unless @json_output
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
- # Delegate ALL math and execution to the WorkloadRunner
15
- payload = WorkloadRunner.new(@workloads, show_progress: !@json_output).execute
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
- if @json_output
18
- Formatter.as_json(payload)
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
- Formatter.summary_with_insights(payload)
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
- # Users
10
- create_table :users, force: true do |t|
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
- # Posts
17
- create_table :posts, force: true do |t|
18
- t.references :user
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>
@@ -1,3 +1,3 @@
1
1
  module RailsBenchmarkSuite
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
3
3
  end
@@ -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.with_index do |w, index|
37
+ results = @workloads.map do |w|
38
+ spinner = nil
25
39
  if @show_progress
26
- Formatter.render_progress(index + 1, @workloads.size, w[:name], "Running")
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
- Formatter.render_progress(index + 1, @workloads.size, w[:name], "Done ")
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
- entry_4t = entries.find { |e| e.label.include?("(4 threads)") }
50
- ips_4t = entry_4t ? entry_4t.ips : 0
51
- ips_4t * r[:adjusted_weight]
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
- x.config(:time => 5, :warmup => 2)
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 (4 threads)
86
- x.report("#{workload[:name]} (4 threads)") do
87
- threads = 4.times.map do
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::Models::User.create!(
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::Models::User.joins(:posts)
24
- .where(users: { id: user.id })
25
- .where("posts.views >= ?", 0)
26
- .order("posts.created_at DESC")
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::Models::SimulatedJob.create!(
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::Models::SimulatedJob.where("scheduled_at <= ?", Time.now).order(:created_at).limit(10)
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::Models::SimulatedJob.where(id: ids).delete_all
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
- def self.number_with_delimiter(number)
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
- lookup_context = ActionView::LookupContext.new([File.expand_path(__dir__)])
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 a complex ERB template
24
- template = <<~ERB
25
- <h1>Dashboard for <%= user.name %></h1>
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
- @view_renderer.render(inline: template, locals: { user: user, posts: posts })
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/db_setup"
11
- require "rails_benchmark_suite/schema"
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
- @workloads = []
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