rails_benchmark_suite 0.2.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 67247c6ec0477941501c1ab15dcbfabd044faa44e790a55e3cf522013bdced08
4
+ data.tar.gz: bbeab62787dc1a0a2d09dd0069ba3fd963a76de3a15fcdd950c49e0d36a8e388
5
+ SHA512:
6
+ metadata.gz: c66eaaedc346d2f66f36412d3032de8ef516cca05a2b36c603e3b449ec3ace814370e5347df5ce59acf0126fb3b10c16e5c91950b6a7e3d3a8f5e326d6b92452
7
+ data.tar.gz: f0cccded74018456e96b160301e59f0beb4bf787146b4f3939d63e999e48a040c256c8bb17bac3e9b7e41c68895b21dd96827cc2cb4ed54408a402b6a6927b53
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle/
3
+ vendor/bundle/
4
+ Gemfile.lock
5
+ .ruby-lsp/
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0] - 2025-12-31
4
+
5
+ ### Added
6
+ - **Suite Expansion**: Added 4 new "Omakase" benchmark suites:
7
+ - `ViewHeft`: ActionView ERB rendering performance.
8
+ - `ImageHeft`: ActiveStorage/Libvips image resizing.
9
+ - `CacheHeft`: In-memory key/value throughput (SolidCache simulation).
10
+ - `SearchHeft`: ActiveRecord text search queries.
11
+ - **Reporting**: Added `--json` flag for CI/CD compatible output.
12
+ - **Scoring**: Re-calibrated scoring weights:
13
+ - Active Record: 40%
14
+ - Job Processing: 20%
15
+ - View Rendering: 20%
16
+ - Image Processing: 10%
17
+ - Cache Operations: 10%
18
+
19
+ ### Changed
20
+ - **Dependencies**: Added `actionview`, `activestorage`, `image_processing` as runtime dependencies.
21
+ - **Calibration**: Adjusted weights to better reflect "Heft" on modern applications.
22
+
23
+ ## [0.1.0] - 2025-12-29
24
+ - Initial Release ("Rails Awareness" update).
25
+ - CLI auto-detects Rails environment.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ ruby "3.4.1"
data/Gemfile.lock ADDED
@@ -0,0 +1,175 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rails_benchmark_suite (0.2.0)
5
+ actionview
6
+ activerecord
7
+ activestorage
8
+ benchmark-ips
9
+ concurrent-ruby
10
+ get_process_mem
11
+ image_processing
12
+ sqlite3
13
+
14
+ GEM
15
+ remote: https://rubygems.org/
16
+ specs:
17
+ actionpack (8.1.1)
18
+ actionview (= 8.1.1)
19
+ activesupport (= 8.1.1)
20
+ nokogiri (>= 1.8.5)
21
+ rack (>= 2.2.4)
22
+ rack-session (>= 1.0.1)
23
+ rack-test (>= 0.6.3)
24
+ rails-dom-testing (~> 2.2)
25
+ rails-html-sanitizer (~> 1.6)
26
+ useragent (~> 0.16)
27
+ actionview (8.1.1)
28
+ activesupport (= 8.1.1)
29
+ builder (~> 3.1)
30
+ erubi (~> 1.11)
31
+ rails-dom-testing (~> 2.2)
32
+ rails-html-sanitizer (~> 1.6)
33
+ activejob (8.1.1)
34
+ activesupport (= 8.1.1)
35
+ globalid (>= 0.3.6)
36
+ activemodel (8.1.1)
37
+ activesupport (= 8.1.1)
38
+ activerecord (8.1.1)
39
+ activemodel (= 8.1.1)
40
+ activesupport (= 8.1.1)
41
+ timeout (>= 0.4.0)
42
+ activestorage (8.1.1)
43
+ actionpack (= 8.1.1)
44
+ activejob (= 8.1.1)
45
+ activerecord (= 8.1.1)
46
+ activesupport (= 8.1.1)
47
+ marcel (~> 1.0)
48
+ activesupport (8.1.1)
49
+ base64
50
+ bigdecimal
51
+ concurrent-ruby (~> 1.0, >= 1.3.1)
52
+ connection_pool (>= 2.2.5)
53
+ drb
54
+ i18n (>= 1.6, < 2)
55
+ json
56
+ logger (>= 1.4.2)
57
+ minitest (>= 5.1)
58
+ securerandom (>= 0.3)
59
+ tzinfo (~> 2.0, >= 2.0.5)
60
+ uri (>= 0.13.1)
61
+ base64 (0.3.0)
62
+ benchmark-ips (2.14.0)
63
+ bigdecimal (4.0.1)
64
+ builder (3.3.0)
65
+ concurrent-ruby (1.3.6)
66
+ connection_pool (3.0.2)
67
+ crass (1.0.6)
68
+ drb (2.2.3)
69
+ erubi (1.13.1)
70
+ ffi (1.17.3-aarch64-linux-gnu)
71
+ ffi (1.17.3-aarch64-linux-musl)
72
+ ffi (1.17.3-arm-linux-gnu)
73
+ ffi (1.17.3-arm-linux-musl)
74
+ ffi (1.17.3-arm64-darwin)
75
+ ffi (1.17.3-x86-linux-gnu)
76
+ ffi (1.17.3-x86-linux-musl)
77
+ ffi (1.17.3-x86_64-darwin)
78
+ ffi (1.17.3-x86_64-linux-gnu)
79
+ ffi (1.17.3-x86_64-linux-musl)
80
+ get_process_mem (1.0.0)
81
+ bigdecimal (>= 2.0)
82
+ ffi (~> 1.0)
83
+ globalid (1.3.0)
84
+ activesupport (>= 6.1)
85
+ i18n (1.14.8)
86
+ concurrent-ruby (~> 1.0)
87
+ image_processing (1.14.0)
88
+ mini_magick (>= 4.9.5, < 6)
89
+ ruby-vips (>= 2.0.17, < 3)
90
+ json (2.18.0)
91
+ logger (1.7.0)
92
+ loofah (2.25.0)
93
+ crass (~> 1.0.2)
94
+ nokogiri (>= 1.12.0)
95
+ marcel (1.1.0)
96
+ mini_magick (5.3.1)
97
+ logger
98
+ mini_portile2 (2.8.9)
99
+ minitest (6.0.1)
100
+ prism (~> 1.5)
101
+ nokogiri (1.19.0)
102
+ mini_portile2 (~> 2.8.2)
103
+ racc (~> 1.4)
104
+ nokogiri (1.19.0-aarch64-linux-gnu)
105
+ racc (~> 1.4)
106
+ nokogiri (1.19.0-aarch64-linux-musl)
107
+ racc (~> 1.4)
108
+ nokogiri (1.19.0-arm-linux-gnu)
109
+ racc (~> 1.4)
110
+ nokogiri (1.19.0-arm-linux-musl)
111
+ racc (~> 1.4)
112
+ nokogiri (1.19.0-arm64-darwin)
113
+ racc (~> 1.4)
114
+ nokogiri (1.19.0-x86_64-darwin)
115
+ racc (~> 1.4)
116
+ nokogiri (1.19.0-x86_64-linux-gnu)
117
+ racc (~> 1.4)
118
+ nokogiri (1.19.0-x86_64-linux-musl)
119
+ racc (~> 1.4)
120
+ prism (1.7.0)
121
+ racc (1.8.1)
122
+ rack (3.2.4)
123
+ rack-session (2.1.1)
124
+ base64 (>= 0.1.0)
125
+ rack (>= 3.0.0)
126
+ rack-test (2.2.0)
127
+ rack (>= 1.3)
128
+ rails-dom-testing (2.3.0)
129
+ activesupport (>= 5.0.0)
130
+ minitest
131
+ nokogiri (>= 1.6)
132
+ rails-html-sanitizer (1.6.2)
133
+ loofah (~> 2.21)
134
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
135
+ rake (13.3.1)
136
+ ruby-vips (2.3.0)
137
+ ffi (~> 1.12)
138
+ logger
139
+ securerandom (0.4.1)
140
+ sqlite3 (2.9.0-aarch64-linux-gnu)
141
+ sqlite3 (2.9.0-aarch64-linux-musl)
142
+ sqlite3 (2.9.0-arm-linux-gnu)
143
+ sqlite3 (2.9.0-arm-linux-musl)
144
+ sqlite3 (2.9.0-arm64-darwin)
145
+ sqlite3 (2.9.0-x86-linux-gnu)
146
+ sqlite3 (2.9.0-x86-linux-musl)
147
+ sqlite3 (2.9.0-x86_64-darwin)
148
+ sqlite3 (2.9.0-x86_64-linux-gnu)
149
+ sqlite3 (2.9.0-x86_64-linux-musl)
150
+ timeout (0.6.0)
151
+ tzinfo (2.0.6)
152
+ concurrent-ruby (~> 1.0)
153
+ uri (1.1.1)
154
+ useragent (0.16.11)
155
+
156
+ PLATFORMS
157
+ aarch64-linux-gnu
158
+ aarch64-linux-musl
159
+ arm-linux-gnu
160
+ arm-linux-musl
161
+ arm64-darwin
162
+ x86-linux-gnu
163
+ x86-linux-musl
164
+ x86_64-darwin
165
+ x86_64-linux-gnu
166
+ x86_64-linux-musl
167
+
168
+ DEPENDENCIES
169
+ bundler
170
+ minitest
171
+ rails_benchmark_suite!
172
+ rake
173
+
174
+ BUNDLED WITH
175
+ 2.5.9
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 overnet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # Rails Benchmark Suite
2
+
3
+ A standardized performance suite designed to measure the "Heft" of a machine using realistic, high-throughput Rails 8+ workloads.
4
+
5
+ Unlike synthetic CPU benchmarks, **Rails Benchmark Suite** simulates Active Record object allocation, SQL query complexity, ActionView rendering, and background job throughput.
6
+
7
+ ## 📊 The "Heft" Score
8
+
9
+ The Heft Score is a weighted metric representing a machine's ability to handle Rails tasks.
10
+ - **Baseline:** A score of **100** is calibrated to represent an **AWS c6g.large** (ARM) instance.
11
+ - **Objective:** To provide a simple, comparable number for evaluating different computing platforms (Cloud VMs, bare-metal, or local dev rigs).
12
+
13
+ ### Baseline Comparisons
14
+ | Score | Classification | Comparable Hardware |
15
+ | :--- | :--- | :--- |
16
+ | < 40 | 🐢 Sluggish | Older Intel Macs, Entry-level VPS |
17
+ | 60 | 🚙 Capable | Standard Cloud VM (c5.large/standard) |
18
+ | **100** | **🏎️ Baseline** | **AWS c6g.large (2 vCPU ARM)** |
19
+ | 150+ | 🚀 High Performance | Apple M-series Pro/Max, Ryzen 5000+ |
20
+ | 300+ | ⚡ Blazing | Server-grade Metal, M3 Ultra |
21
+
22
+ ## 🛠 Technical Philosophy
23
+
24
+ Rails Benchmark Suite prioritizes **Benchmarking** (via `benchmark-ips`) over **Profiling**.
25
+
26
+ * **Benchmarking:** Focuses on macro-throughput—"How many iterations can the hardware handle?" This provides the final Heft Score.
27
+ * **Why no Profiling?** Profiling tools (like `StackProf` or `Vernier`) introduce instrumentation overhead that skews hardware metrics. We aim for "Conceptual Compression"—one clear number to inform infrastructure decisions.
28
+
29
+ ## 🚀 Installation & Usage
30
+
31
+ ### Prerequisites
32
+ * **Ruby:** 3.4.1+ (Recommended for latest YJIT/Prism performance)
33
+ * **Database:** SQLite3
34
+
35
+ ### Standalone Usage
36
+ If you want to test hardware performance without an existing application:
37
+
38
+ ```bash
39
+ git clone https://github.com/overnet/rails_benchmark_suite.git
40
+ cd rails_benchmark_suite
41
+ bundle install
42
+ bin/rails_benchmark_suite
43
+ ```
44
+
45
+ ### Use within a Rails Application
46
+ Rails Benchmark Suite is "Rails-aware." Adding it to your app allows you to benchmark your specific configuration and custom suites.
47
+
48
+ Add to your Gemfile:
49
+
50
+ ```ruby
51
+ gem "rails_benchmark_suite", group: :development
52
+ ```
53
+
54
+ Run via bundle:
55
+
56
+ ```bash
57
+ bundle exec rails_benchmark_suite
58
+ ```
59
+
60
+ > **Note:** Use `--skip-rails` to ignore the host application and run in isolated mode.
61
+
62
+ ## 🏗 Architecture
63
+ * **Engine:** Built on `benchmark-ips`.
64
+ * **Database:** Uses In-Memory SQLite with `cache=shared` for multi-threaded accuracy.
65
+ * **Isolation:** Uses transactional rollbacks (`ActiveRecord::Rollback`) to ensure test isolation without the overhead of row deletion.
66
+ * **Threading:** Supports 1-thread and 4-thread scaling tests to measure vertical efficiency.
67
+ * **Modern Stack:** Optimized for Rails 8+ defaults, including Solid Queue simulation and YJIT detection.
68
+
69
+ ## 📜 Credits
70
+ This project is a functional implementation of the performance benchmark vision discussed in the Rails community.
71
+
72
+ * **Vision:** Inspired by @dhh in [rails/rails#50451](https://github.com/rails/rails/issues/50451).
73
+ * **Initial Roadmap:** Based on suggestions by @JoeDupuis.
74
+ * **Implementation:** The Rails Community.
75
+
76
+ ## 📄 License
77
+ The gem is available as open source under the terms of the MIT License.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "optparse"
5
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
6
+ require "rails_benchmark_suite"
7
+ require "rails_benchmark_suite/version"
8
+
9
+ options = {}
10
+ OptionParser.new do |opts|
11
+ opts.banner = "Usage: rails_benchmark_suite [options]"
12
+
13
+ opts.on("-v", "--version", "Print version") do
14
+ puts "RailsBenchmarkSuite v#{RailsBenchmarkSuite::VERSION}"
15
+ exit 0
16
+ end
17
+
18
+ opts.on("--skip-rails", "Skip Rails environment loading") do
19
+ options[:skip_rails] = true
20
+ end
21
+
22
+ opts.on("-j", "--json", "Output results as JSON") do
23
+ options[:json] = true
24
+ end
25
+
26
+ opts.on("-h", "--help", "Prints this help") do
27
+ puts opts
28
+ exit 0
29
+ end
30
+ end.parse!
31
+
32
+ puts "RailsBenchmarkSuite v#{RailsBenchmarkSuite::VERSION}"
33
+
34
+ # Rails Detection
35
+ rails_env_path = File.join(Dir.pwd, "config", "environment.rb")
36
+
37
+ if !options[:skip_rails] && File.exist?(rails_env_path)
38
+ puts "Rails environment detected at #{rails_env_path}. Loading..."
39
+ require rails_env_path
40
+ puts "Rails loaded successfully."
41
+ else
42
+ if options[:skip_rails]
43
+ puts "Skipping Rails loading (requested via --skip-rails)."
44
+ else
45
+ puts "No Rails environment detected (config/environment.rb not found)."
46
+ end
47
+ end
48
+
49
+ RailsBenchmarkSuite.run(json: options[:json])
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "sqlite3"
5
+
6
+ # Silence ActiveRecord logs during benchmarks to avoid IO bottlenecks
7
+ ActiveRecord::Base.logger = nil
8
+
9
+ # Setup In-Memory SQLite globally for all suites
10
+ # storage_config = ActiveRecord::DatabaseConfigurations::HashConfig.new("test", "sqlite3", { adapter: "sqlite3", database: ":memory:" })
11
+ # Use shared cache to allow threads to see the same in-memory database
12
+ # Skip internal database setup if running within a Rails application
13
+ unless defined?(Rails)
14
+ ActiveRecord::Base.establish_connection(
15
+ adapter: "sqlite3",
16
+ database: "file:rails_benchmark_suite_mem?mode=memory&cache=shared",
17
+ pool: 20,
18
+ timeout: 10000
19
+ )
20
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsBenchmarkSuite
4
+ module Models
5
+ class Post < ActiveRecord::Base
6
+ belongs_to :user, class_name: "RailsBenchmarkSuite::Models::User"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsBenchmarkSuite
4
+ module Models
5
+ class SimulatedJob < ActiveRecord::Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsBenchmarkSuite
4
+ module Models
5
+ class User < ActiveRecord::Base
6
+ has_many :posts, class_name: "RailsBenchmarkSuite::Models::Post", dependent: :destroy
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsBenchmarkSuite
4
+ module Reporter
5
+ module_function
6
+
7
+ def system_info
8
+ {
9
+ ruby_version: RUBY_VERSION,
10
+ platform: RUBY_PLATFORM,
11
+ processors: Concurrent.processor_count,
12
+ libvips: libvips?,
13
+ yjit: yjit_enabled?
14
+ }
15
+ end
16
+
17
+ def yjit_enabled?
18
+ defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
19
+ end
20
+
21
+ def libvips?
22
+ # Naive check for libvips presence
23
+ system("vips --version", out: File::NULL, err: File::NULL)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark/ips"
4
+ require "get_process_mem"
5
+
6
+ module RailsBenchmarkSuite
7
+ class Runner
8
+ def initialize(suites, json: false)
9
+ @suites = suites
10
+ @json_output = json
11
+ end
12
+
13
+ def register(name, &block)
14
+ @suites << { name: name, block: block }
15
+ end
16
+
17
+ def run
18
+ puts "Running RailsBenchmarkSuite Benchmarks..." unless @json_output
19
+ puts system_report unless @json_output
20
+ puts "\n" unless @json_output
21
+
22
+ results = {}
23
+
24
+ @suites.each do |suite|
25
+ puts "== Running Suite: #{suite[:name]} ==" unless @json_output
26
+
27
+ # Capture memory before
28
+ mem_before = GetProcessMem.new.mb
29
+
30
+ # Run benchmark
31
+ report = Benchmark.ips do |x|
32
+ x.config(:time => 5, :warmup => 2)
33
+
34
+ # Single Threaded
35
+ x.report("#{suite[:name]} (1 thread)") do
36
+ suite[:block].call
37
+ end
38
+
39
+ # Multi Threaded (4 threads)
40
+ x.report("#{suite[:name]} (4 threads)") do
41
+ threads = 4.times.map do
42
+ Thread.new do
43
+ # Ensure each thread gets a dedicated connection
44
+ ActiveRecord::Base.connection_pool.with_connection do
45
+ suite[:block].call
46
+ end
47
+ end
48
+ end
49
+ threads.each(&:join)
50
+ end
51
+
52
+ x.compare!
53
+ end
54
+
55
+ # Capture memory after
56
+ mem_after = GetProcessMem.new.mb
57
+
58
+ results[suite[:name]] = {
59
+ report: report,
60
+ memory_delta_mb: mem_after - mem_before,
61
+ weight: suite[:weight]
62
+ }
63
+
64
+ puts "Memory Footprint: #{mem_after.round(2)} MB (+#{(mem_after - mem_before).round(2)} MB)" unless @json_output
65
+ puts "\n" unless @json_output
66
+ end
67
+
68
+ print_summary(results)
69
+ results
70
+ end
71
+
72
+ private
73
+
74
+ def system_report
75
+ info = RailsBenchmarkSuite::Reporter.system_info
76
+ "System: Ruby #{info[:ruby_version]} (#{info[:platform]}), #{info[:processors]} Cores. YJIT: #{info[:yjit] ? 'Enabled' : 'Disabled'}. Libvips: #{info[:libvips]}"
77
+ end
78
+
79
+ def print_summary(results)
80
+ if @json_output
81
+ print_json(results)
82
+ return
83
+ end
84
+
85
+ puts "\n"
86
+ puts "=========================================================================================="
87
+ puts "| %-25s | %-25s | %-12s | %-15s |" % ["Suite", "IPS (1t / 4t)", "Scaling", "Mem Delta"]
88
+ puts "=========================================================================================="
89
+
90
+ total_score = 0
91
+
92
+ results.each do |name, data|
93
+ report = data[:report]
94
+ entries = report.entries
95
+
96
+ entry_1t = entries.find { |e| e.label.include?("(1 thread)") }
97
+ entry_4t = entries.find { |e| e.label.include?("(4 threads)") }
98
+
99
+ ips_1t = entry_1t ? entry_1t.ips : 0
100
+ ips_4t = entry_4t ? entry_4t.ips : 0
101
+
102
+ scaling = ips_1t > 0 ? (ips_4t / ips_1t) : 0
103
+ mem = data[:memory_delta_mb]
104
+
105
+ # Heft Score: Weighted Sum of 4t IPS
106
+ weight = data[:weight] || 1.0
107
+ weighted_score = ips_4t * weight
108
+ total_score += weighted_score
109
+
110
+ puts "| %-25s | %-25s | x%-11.2f | +%-14.2fMB |" % [
111
+ name + " (w: #{weight})",
112
+ "#{humanize(ips_1t)} / #{humanize(ips_4t)}",
113
+ scaling,
114
+ mem
115
+ ]
116
+ end
117
+ puts "=========================================================================================="
118
+ puts "\n"
119
+ puts " >>> FINAL HEFT SCORE: #{total_score.round(0)} <<<"
120
+ puts "\n"
121
+ end
122
+
123
+ def print_json(results)
124
+ require "json"
125
+
126
+ out = {
127
+ system: RailsBenchmarkSuite::Reporter.system_info,
128
+ total_score: 0,
129
+ suites: []
130
+ }
131
+
132
+ total_score = 0
133
+
134
+ results.each do |name, data|
135
+ weight = data[:weight] || 1.0
136
+
137
+ # Parse reports
138
+ ips_1t = data[:report].entries.find { |e| e.label.include?("(1 thread)") }&.ips || 0
139
+ ips_4t = data[:report].entries.find { |e| e.label.include?("(4 threads)") }&.ips || 0
140
+
141
+ weighted_score = ips_4t * weight
142
+ total_score += weighted_score
143
+
144
+ out[:suites] << {
145
+ name: name,
146
+ weight: weight,
147
+ ips_1t: ips_1t,
148
+ ips_4t: ips_4t,
149
+ scaling: ips_1t > 0 ? (ips_4t / ips_1t) : 0,
150
+ memory_delta_mb: data[:memory_delta_mb],
151
+ score: weighted_score
152
+ }
153
+ end
154
+
155
+ out[:total_score] = total_score.round(0)
156
+ puts out.to_json
157
+ end
158
+
159
+ def humanize(ips)
160
+ return "0" if ips.nil?
161
+ if ips > 1000
162
+ "%.1fk" % (ips / 1000.0)
163
+ else
164
+ "%.1f" % ips
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module RailsBenchmarkSuite
6
+ module Schema
7
+ def self.load
8
+ ActiveRecord::Schema.define do
9
+ # Users
10
+ create_table :users, force: true do |t|
11
+ t.string :name
12
+ t.string :email
13
+ t.timestamps
14
+ end
15
+
16
+ # Posts
17
+ create_table :posts, force: true do |t|
18
+ t.references :user
19
+ t.string :title
20
+ t.text :body
21
+ t.integer :views, default: 0
22
+ t.timestamps
23
+ end
24
+
25
+ # Simulated Jobs (for Job Heft)
26
+ create_table :simulated_jobs, force: true do |t|
27
+ t.string :queue_name
28
+ t.text :arguments
29
+ t.datetime :scheduled_at
30
+ t.timestamps
31
+ end
32
+ add_index :simulated_jobs, [:queue_name, :scheduled_at]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ # Benchmark Suite
6
+ RailsBenchmarkSuite.register_suite("Active Record Heft", weight: 0.4) do
7
+ # Workload: Create User with Posts, Join Query, Update
8
+ # Use transaction rollback to keep the DB clean and avoid costly destroy callbacks
9
+ ActiveRecord::Base.transaction do
10
+ # 1. Create
11
+ user = RailsBenchmarkSuite::Models::User.create!(name: "Speedy Gonzales", email: "speedy@example.com")
12
+
13
+ # 2. Create associated records (simulate some weight)
14
+ 10.times do |i|
15
+ user.posts.create!(title: "Post #{i}", body: "Content " * 50)
16
+ end
17
+
18
+ # 3. Complex Query (Join + Order)
19
+ # Unloading the relation to force execution
20
+ RailsBenchmarkSuite::Models::User.joins(:posts)
21
+ .where(users: { id: user.id })
22
+ .where("posts.views >= ?", 0)
23
+ .order("posts.created_at DESC")
24
+ .to_a
25
+
26
+ # 4. Update
27
+ user.update!(name: "Slowpoke Rodriguez")
28
+
29
+ # Rollback everything to leave the DB clean for next iteration
30
+ raise ActiveRecord::Rollback
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/cache"
4
+ require "securerandom"
5
+
6
+ # Benchmark Suite
7
+ RailsBenchmarkSuite.register_suite("Cache Heft", weight: 0.1) do
8
+ # Simulate SolidCache using MemoryStore
9
+ @cache ||= ActiveSupport::Cache::MemoryStore.new
10
+
11
+ # Workload: Measure serialization and storage throughput
12
+ key_prefix = "cache_test_#{SecureRandom.hex(4)}"
13
+
14
+ # 1. Bulk Write
15
+ 1000.times do |i|
16
+ @cache.write("#{key_prefix}/#{i}", { data: "Precious Data " * 20, index: i })
17
+ end
18
+
19
+ # 2. Bulk Read
20
+ 1000.times do |i|
21
+ val = @cache.read("#{key_prefix}/#{i}")
22
+ next if val.nil?
23
+ end
24
+
25
+ # 3. Clean up
26
+ @cache.clear
27
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "image_processing/vips"
5
+ require "fileutils"
6
+
7
+ # Ensure asset directory exists
8
+ ASSET_DIR = File.expand_path("../../assets", __dir__)
9
+ FileUtils.mkdir_p(ASSET_DIR)
10
+ SAMPLE_IMAGE = File.join(ASSET_DIR, "sample.jpg")
11
+
12
+ RailsBenchmarkSuite.register_suite("Image Heft", weight: 0.1) do
13
+ # Gracefully handle missing dependencies
14
+ if File.exist?(SAMPLE_IMAGE)
15
+ ImageProcessing::Vips
16
+ .source(SAMPLE_IMAGE)
17
+ .resize_to_limit(800, 800)
18
+ .call
19
+ else
20
+ # Maintain benchmark stability if asset is missing
21
+ true
22
+ end
23
+ end
24
+
25
+ rescue LoadError, StandardError => e
26
+ # Register a skipped suite if Libvips is unavailable
27
+ RailsBenchmarkSuite.register_suite("Image Heft (Skipped)", weight: 0.0) do
28
+ @warned ||= begin
29
+ warn "⚠️ [RailsBenchmarkSuite] ImageHeft skipped: #{e.message}. Install libvips to enable."
30
+ true
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "json"
5
+
6
+
7
+ RailsBenchmarkSuite.register_suite("Solid Queue Heft", weight: 0.2) do
8
+ # Simulation: Enqueue 100 jobs, then work them off
9
+
10
+ # 1. Enqueue Loop
11
+ 100.times do |i|
12
+ RailsBenchmarkSuite::Models::SimulatedJob.create!(
13
+ queue_name: "default",
14
+ arguments: { job_id: i, payload: "x" * 100 }.to_json,
15
+ scheduled_at: Time.now
16
+ )
17
+ end
18
+
19
+ # 2. Worker Loop (Drain the queue)
20
+ loop do
21
+ processed = false
22
+
23
+ # Transactional polling
24
+ ActiveRecord::Base.transaction do
25
+ # Fetch batch
26
+ jobs = RailsBenchmarkSuite::Models::SimulatedJob.where("scheduled_at <= ?", Time.now).order(:created_at).limit(10)
27
+
28
+ if jobs.any?
29
+ # Simulate processing time and delete
30
+ ids = jobs.map(&:id)
31
+ RailsBenchmarkSuite::Models::SimulatedJob.where(id: ids).delete_all
32
+ processed = true
33
+ end
34
+ end
35
+
36
+ break unless processed
37
+
38
+ # Simulate worker internal latency
39
+ sleep(0.001)
40
+ end
41
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view"
4
+ require "ostruct"
5
+
6
+ # Benchmark Suite
7
+
8
+
9
+ # Helper for the suite
10
+ module RailsBenchmarkSuiteNumberHelper
11
+ def self.number_with_delimiter(number)
12
+ number.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")
13
+ end
14
+ end
15
+
16
+ RailsBenchmarkSuite.register_suite("View Heft", weight: 0.2) do
17
+ # Setup context once
18
+ @view_renderer ||= begin
19
+ lookup_context = ActionView::LookupContext.new([File.expand_path(__dir__)])
20
+ ActionView::Base.with_empty_template_cache.new(lookup_context, {}, nil)
21
+ end
22
+
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
37
+
38
+ # Dummy Objects
39
+ user = OpenStruct.new(name: "Speedy")
40
+ posts = 100.times.map { |i| OpenStruct.new(title: "Post #{i}", body: "Content " * 10, views: i * 1000) }
41
+
42
+ # Execution
43
+ @view_renderer.render(inline: template, locals: { user: user, posts: posts })
44
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module RailsBenchmarkSuite
3
+ VERSION = "0.2.0"
4
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+ require "rails_benchmark_suite/version"
5
+ require "rails_benchmark_suite/reporter"
6
+ require "rails_benchmark_suite/runner"
7
+ require "rails_benchmark_suite/db_setup"
8
+ require "rails_benchmark_suite/schema"
9
+ require "rails_benchmark_suite/models/user"
10
+ require "rails_benchmark_suite/models/post"
11
+ require "rails_benchmark_suite/models/simulated_job"
12
+
13
+ module RailsBenchmarkSuite
14
+ @suites = []
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 Schema
22
+ RailsBenchmarkSuite::Schema.load
23
+
24
+ # Load suites
25
+ Dir[File.join(__dir__, "rails_benchmark_suite", "suites", "*.rb")].each { |f| require f }
26
+
27
+ runner = Runner.new(@suites, json: json)
28
+ runner.run
29
+ end
30
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/rails_benchmark_suite/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "rails_benchmark_suite"
7
+ spec.version = RailsBenchmarkSuite::VERSION
8
+ spec.authors = ["RailsBenchmarkSuite Contributors"]
9
+ spec.email = ["team@rails.org"]
10
+
11
+ spec.summary = "Rails-style functionality & performance benchmark tool"
12
+ spec.description = "Measures the 'Heft' (processing power) of a machine using realistic Rails workloads."
13
+ spec.homepage = "https://github.com/overnet/rails_benchmark_suite"
14
+ spec.license = "MIT"
15
+
16
+ spec.metadata["source_code_uri"] = "https://github.com/overnet/rails_benchmark_suite"
17
+ spec.metadata["bug_tracker_uri"] = "https://github.com/overnet/rails_benchmark_suite/issues"
18
+ spec.metadata["changelog_uri"] = "https://github.com/overnet/rails_benchmark_suite/blob/main/CHANGELOG.md"
19
+
20
+ spec.files = Dir.chdir(__dir__) do
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
+ end
23
+ spec.bindir = "bin"
24
+ spec.executables = ["rails_benchmark_suite"]
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_dependency "benchmark-ips", "~> 2.14"
28
+ spec.add_dependency "activerecord", "~> 8.1"
29
+ spec.add_dependency "actionview", "~> 8.1"
30
+ spec.add_dependency "activestorage", "~> 8.1"
31
+ spec.add_dependency "image_processing", "~> 1.14"
32
+ spec.add_dependency "sqlite3", "~> 2.8"
33
+ spec.add_dependency "concurrent-ruby", "~> 1.3"
34
+ spec.add_dependency "get_process_mem", "~> 1.0"
35
+
36
+ spec.add_development_dependency "bundler", "~> 2.5"
37
+ spec.add_development_dependency "rake", "~> 13.0"
38
+ spec.add_development_dependency "minitest", "~> 5.0"
39
+
40
+ spec.required_ruby_version = ">= 3.4.0"
41
+ end
metadata ADDED
@@ -0,0 +1,223 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_benchmark_suite
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - RailsBenchmarkSuite Contributors
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: benchmark-ips
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.14'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.14'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activerecord
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '8.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '8.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: actionview
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '8.1'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '8.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: activestorage
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '8.1'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '8.1'
68
+ - !ruby/object:Gem::Dependency
69
+ name: image_processing
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.14'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.14'
82
+ - !ruby/object:Gem::Dependency
83
+ name: sqlite3
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.8'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.8'
96
+ - !ruby/object:Gem::Dependency
97
+ name: concurrent-ruby
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '1.3'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '1.3'
110
+ - !ruby/object:Gem::Dependency
111
+ name: get_process_mem
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '1.0'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '1.0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: bundler
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '2.5'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '2.5'
138
+ - !ruby/object:Gem::Dependency
139
+ name: rake
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - "~>"
143
+ - !ruby/object:Gem::Version
144
+ version: '13.0'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: '13.0'
152
+ - !ruby/object:Gem::Dependency
153
+ name: minitest
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '5.0'
159
+ type: :development
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '5.0'
166
+ description: Measures the 'Heft' (processing power) of a machine using realistic Rails
167
+ workloads.
168
+ email:
169
+ - team@rails.org
170
+ executables:
171
+ - rails_benchmark_suite
172
+ extensions: []
173
+ extra_rdoc_files: []
174
+ files:
175
+ - ".gitignore"
176
+ - ".ruby-version"
177
+ - CHANGELOG.md
178
+ - Gemfile
179
+ - Gemfile.lock
180
+ - LICENSE.txt
181
+ - README.md
182
+ - Rakefile
183
+ - bin/rails_benchmark_suite
184
+ - lib/rails_benchmark_suite.rb
185
+ - lib/rails_benchmark_suite/db_setup.rb
186
+ - lib/rails_benchmark_suite/models/post.rb
187
+ - lib/rails_benchmark_suite/models/simulated_job.rb
188
+ - lib/rails_benchmark_suite/models/user.rb
189
+ - lib/rails_benchmark_suite/reporter.rb
190
+ - lib/rails_benchmark_suite/runner.rb
191
+ - lib/rails_benchmark_suite/schema.rb
192
+ - lib/rails_benchmark_suite/suites/active_record_suite.rb
193
+ - lib/rails_benchmark_suite/suites/cache_heft_suite.rb
194
+ - lib/rails_benchmark_suite/suites/image_heft_suite.rb
195
+ - lib/rails_benchmark_suite/suites/job_heft_suite.rb
196
+ - lib/rails_benchmark_suite/suites/view_heft_suite.rb
197
+ - lib/rails_benchmark_suite/version.rb
198
+ - rails_benchmark_suite.gemspec
199
+ homepage: https://github.com/overnet/rails_benchmark_suite
200
+ licenses:
201
+ - MIT
202
+ metadata:
203
+ source_code_uri: https://github.com/overnet/rails_benchmark_suite
204
+ bug_tracker_uri: https://github.com/overnet/rails_benchmark_suite/issues
205
+ changelog_uri: https://github.com/overnet/rails_benchmark_suite/blob/main/CHANGELOG.md
206
+ rdoc_options: []
207
+ require_paths:
208
+ - lib
209
+ required_ruby_version: !ruby/object:Gem::Requirement
210
+ requirements:
211
+ - - ">="
212
+ - !ruby/object:Gem::Version
213
+ version: 3.4.0
214
+ required_rubygems_version: !ruby/object:Gem::Requirement
215
+ requirements:
216
+ - - ">="
217
+ - !ruby/object:Gem::Version
218
+ version: '0'
219
+ requirements: []
220
+ rubygems_version: 3.6.2
221
+ specification_version: 4
222
+ summary: Rails-style functionality & performance benchmark tool
223
+ test_files: []