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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 473aa21869380dae81d4f9ee8668ccf161721923fd6c453cc711caf9d6303fd1
|
|
4
|
+
data.tar.gz: 5101bb8d63885263dc3fcbb8851d52f3ed48a1f919b0abfab38d43dc3f3df2a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 11311aa76df4c103c8b3ea2e12a803ddeb339cd454127b246557341ddd5c7872fa01c4ff00f4c14ae711d95b48f0d84b148ec7f8b0a39fda7ab70cac0d070bad
|
|
7
|
+
data.tar.gz: 3f141277e2daae966aa4d7d7be684e112889fa757282e36a4f35ac34d60ac91ca76903281d9e091b9549ef567d6e8b7a26b13221326bf8273c3c33fe7e6082fc
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
|
+
## [0.3.1] - 2026-01-04
|
|
3
|
+
*Major Architectural Repair & TTY Overhaul*
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **--html**: Static HTML Reporter with Chart.js visualization.
|
|
7
|
+
- **--profile**: Automated "Scaling Efficiency" calculation (1T vs MaxT).
|
|
8
|
+
- **--db**: Real database integration (Postgres/MySQL support).
|
|
9
|
+
- **Hardware Awareness**: Auto-detection of CPU cores for thread defaults.
|
|
10
|
+
- **UI**: Rich terminal output using `tty-spinner`, `tty-table`, and `tty-box` for a dashboard-style report.
|
|
11
|
+
- **Architecture**: Implemented the standard `lib/dummy` Rails Engine pattern for internal tests.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- **Refactor**: Complete structural overhaul. Moved monolithic logic from `lib/rails_benchmark_suite.rb` into a proper namespace (`lib/rails_benchmark_suite/`).
|
|
15
|
+
- **Concurrency**: Default thread count is now dynamic (`Etc.nprocessors`) instead of hardcoded to 4.
|
|
16
|
+
- **Safety**: Renamed internal test model from `User` to `BenchmarkUser` to prevent collisions when running inside host apps.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **CLI**: Fixed the non-functional `--help` flag (now implemented via `OptionParser`).
|
|
20
|
+
- **Reporting**: Restored the "Scaling (x)" column in the final report to correctly visualize performance degradation.
|
|
2
21
|
|
|
3
22
|
## [0.3.0] - 2025-01-03
|
|
4
23
|
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
rails_benchmark_suite (0.3.
|
|
4
|
+
rails_benchmark_suite (0.3.1)
|
|
5
5
|
actionview (~> 8.1)
|
|
6
6
|
activerecord (~> 8.1)
|
|
7
7
|
activestorage (~> 8.1)
|
|
@@ -9,7 +9,11 @@ PATH
|
|
|
9
9
|
concurrent-ruby (~> 1.3)
|
|
10
10
|
get_process_mem (~> 1.0)
|
|
11
11
|
image_processing (~> 1.14)
|
|
12
|
+
pastel (~> 0.8)
|
|
12
13
|
sqlite3 (~> 2.8)
|
|
14
|
+
tty-box (~> 0.7)
|
|
15
|
+
tty-spinner (~> 0.9)
|
|
16
|
+
tty-table (~> 0.12)
|
|
13
17
|
|
|
14
18
|
GEM
|
|
15
19
|
remote: https://rubygems.org/
|
|
@@ -117,6 +121,8 @@ GEM
|
|
|
117
121
|
nokogiri (1.19.0-x86_64-linux-musl)
|
|
118
122
|
racc (~> 1.4)
|
|
119
123
|
ostruct (0.6.3)
|
|
124
|
+
pastel (0.8.0)
|
|
125
|
+
tty-color (~> 0.5)
|
|
120
126
|
racc (1.8.1)
|
|
121
127
|
rack (3.2.4)
|
|
122
128
|
rack-session (2.1.1)
|
|
@@ -146,9 +152,29 @@ GEM
|
|
|
146
152
|
sqlite3 (2.9.0-x86_64-darwin)
|
|
147
153
|
sqlite3 (2.9.0-x86_64-linux-gnu)
|
|
148
154
|
sqlite3 (2.9.0-x86_64-linux-musl)
|
|
155
|
+
strings (0.2.1)
|
|
156
|
+
strings-ansi (~> 0.2)
|
|
157
|
+
unicode-display_width (>= 1.5, < 3.0)
|
|
158
|
+
unicode_utils (~> 1.4)
|
|
159
|
+
strings-ansi (0.2.0)
|
|
149
160
|
timeout (0.6.0)
|
|
161
|
+
tty-box (0.7.0)
|
|
162
|
+
pastel (~> 0.8)
|
|
163
|
+
strings (~> 0.2.0)
|
|
164
|
+
tty-cursor (~> 0.7)
|
|
165
|
+
tty-color (0.6.0)
|
|
166
|
+
tty-cursor (0.7.1)
|
|
167
|
+
tty-screen (0.8.2)
|
|
168
|
+
tty-spinner (0.9.3)
|
|
169
|
+
tty-cursor (~> 0.7)
|
|
170
|
+
tty-table (0.12.0)
|
|
171
|
+
pastel (~> 0.8)
|
|
172
|
+
strings (~> 0.2.0)
|
|
173
|
+
tty-screen (~> 0.8)
|
|
150
174
|
tzinfo (2.0.6)
|
|
151
175
|
concurrent-ruby (~> 1.0)
|
|
176
|
+
unicode-display_width (2.6.0)
|
|
177
|
+
unicode_utils (1.4.0)
|
|
152
178
|
uri (1.1.1)
|
|
153
179
|
useragent (0.16.11)
|
|
154
180
|
|
data/README.md
CHANGED
|
@@ -26,6 +26,19 @@ The Heft Score is a weighted metric representing a machine's ability to handle R
|
|
|
26
26
|
| **150+** | 🚀 High Performance | Apple M-series Pro/Max, Ryzen 5000+ |
|
|
27
27
|
| **300+** | ⚡ Blazing | Server-grade Metal, M3 Ultra |
|
|
28
28
|
|
|
29
|
+
### 📊 Visual Reports (HTML)
|
|
30
|
+
|
|
31
|
+
Visualize your threading efficiency and bottlenecks with a self-contained dashboard:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bundle exec rails_benchmark_suite -t 8 --html
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
This generates `rails_benchmark_report.html` containing:
|
|
38
|
+
- **Scaling Curve**: Interactive bar chart comparing 1-thread vs Max-thread throughput.
|
|
39
|
+
- **Efficiency Heatmap**: Score cards identifying which workloads are Gil-bound vs CPU-bound.
|
|
40
|
+
- **Detailed Metrics**: Raw IPS and detailed scaling factors.
|
|
41
|
+
|
|
29
42
|
---
|
|
30
43
|
|
|
31
44
|
## 🚀 Quick Start
|
|
@@ -89,10 +102,34 @@ bundle exec rails_benchmark_suite --json > report.json
|
|
|
89
102
|
```
|
|
90
103
|
Perfect for CI/CD pipelines and programmatic analysis. Outputs clean JSON without any UI elements.
|
|
91
104
|
|
|
92
|
-
###
|
|
105
|
+
### 📊 Visual Diagnostics (HTML Report)
|
|
106
|
+
|
|
107
|
+

|
|
108
|
+
|
|
109
|
+
To diagnose scaling bottlenecks, run the tool in Profile Mode (`--profile`) and generate the HTML Report (`--html`). This compares Single-Thread vs Multi-Thread performance side-by-side.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
bundle exec rails_benchmark_suite --profile --html
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Opening the Report**:
|
|
116
|
+
- **Local:** `open tmp/rails_benchmark_report.html`
|
|
117
|
+
- **Remote:** `scp user@server:/current/path/tmp/rails_benchmark_report.html .`
|
|
118
|
+
|
|
119
|
+
### Command Line Options
|
|
120
|
+
|
|
121
|
+
| Flag | Description |
|
|
122
|
+
| :--- | :--- |
|
|
123
|
+
| `--html` | Generates a visual dashboard (`tmp/rails_benchmark_report.html`). **Best used with `--profile`.** |
|
|
124
|
+
| `--profile` | **Diagnostic Mode.** Runs the benchmark twice (1 Thread vs Max Threads) to calculate "Scaling Efficiency." Required to populate the "Scaling Curve" charts. |
|
|
125
|
+
| `--db` | Uses your local `config/database.yml` (Postgres/MySQL) instead of the synthetic in-memory SQLite. |
|
|
126
|
+
| `-t [N]` | Manually set the thread count (Default: Auto-detects CPU cores). |
|
|
127
|
+
|
|
128
|
+
### Configuration Flags
|
|
129
|
+
- `--json`: Output results in JSON format
|
|
93
130
|
- `--skip-rails`: Run in isolated mode without loading Rails environment
|
|
94
131
|
- `--version`: Display gem version
|
|
95
|
-
- `--help`: Show
|
|
132
|
+
- `-h` / `--help`: Show usage help
|
|
96
133
|
|
|
97
134
|
### Standalone Usage
|
|
98
135
|
|
data/bin/rails_benchmark_suite
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "optparse"
|
|
5
|
+
require "etc"
|
|
5
6
|
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
6
7
|
require "rails_benchmark_suite"
|
|
7
8
|
require "rails_benchmark_suite/version"
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
config = RailsBenchmarkSuite::Configuration.new
|
|
11
|
+
|
|
10
12
|
OptionParser.new do |opts|
|
|
11
13
|
opts.banner = "Usage: rails_benchmark_suite [options]"
|
|
12
14
|
|
|
@@ -15,12 +17,28 @@ OptionParser.new do |opts|
|
|
|
15
17
|
exit 0
|
|
16
18
|
end
|
|
17
19
|
|
|
20
|
+
opts.on("-t", "--threads N", Integer, "Number of threads (default: auto set to #{Etc.nprocessors} cores)") do |n|
|
|
21
|
+
config.threads = n
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
opts.on("-p", "--profile", "Enable scaling efficiency profile mode") do
|
|
25
|
+
config.profile = true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
opts.on("-d", "--db", "Use real database from config/database.yml instead of in-memory SQLite") do
|
|
29
|
+
config.db = true
|
|
30
|
+
end
|
|
31
|
+
|
|
18
32
|
opts.on("--skip-rails", "Skip Rails environment loading") do
|
|
19
|
-
|
|
33
|
+
config.skip_rails = true
|
|
20
34
|
end
|
|
21
35
|
|
|
22
36
|
opts.on("-j", "--json", "Output results as JSON") do
|
|
23
|
-
|
|
37
|
+
config.json = true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
opts.on("--html", "Generate a visual HTML report (rails_benchmark_report.html)") do
|
|
41
|
+
config.html = true
|
|
24
42
|
end
|
|
25
43
|
|
|
26
44
|
opts.on("-h", "--help", "Prints this help") do
|
|
@@ -29,20 +47,21 @@ OptionParser.new do |opts|
|
|
|
29
47
|
end
|
|
30
48
|
end.parse!
|
|
31
49
|
|
|
32
|
-
unless
|
|
50
|
+
unless config.json
|
|
33
51
|
puts "RailsBenchmarkSuite v#{RailsBenchmarkSuite::VERSION}"
|
|
52
|
+
puts "Configuration: Threads=#{config.threads} | Profile=#{config.profile} | Local DB=#{config.db}"
|
|
34
53
|
end
|
|
35
54
|
|
|
36
55
|
# Rails Detection
|
|
37
56
|
rails_env_path = File.join(Dir.pwd, "config", "environment.rb")
|
|
38
57
|
|
|
39
|
-
if !
|
|
40
|
-
puts "Rails environment detected at #{rails_env_path}. Loading..." unless
|
|
58
|
+
if !config.skip_rails && File.exist?(rails_env_path)
|
|
59
|
+
puts "Rails environment detected at #{rails_env_path}. Loading..." unless config.json
|
|
41
60
|
require rails_env_path
|
|
42
|
-
puts "Rails loaded successfully." unless
|
|
61
|
+
puts "Rails loaded successfully." unless config.json
|
|
43
62
|
else
|
|
44
|
-
unless
|
|
45
|
-
if
|
|
63
|
+
unless config.json
|
|
64
|
+
if config.skip_rails
|
|
46
65
|
puts "Skipping Rails loading (requested via --skip-rails)."
|
|
47
66
|
else
|
|
48
67
|
puts "No Rails environment detected (config/environment.rb not found)."
|
|
@@ -50,4 +69,4 @@ else
|
|
|
50
69
|
end
|
|
51
70
|
end
|
|
52
71
|
|
|
53
|
-
RailsBenchmarkSuite.
|
|
72
|
+
RailsBenchmarkSuite::Runner.new(config).run
|
|
Binary file
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<h1>Dashboard for <%= user.name %></h1>
|
|
2
|
+
<ul>
|
|
3
|
+
<% posts.each do |post| %>
|
|
4
|
+
<li>
|
|
5
|
+
<strong><%= post.title %></strong>
|
|
6
|
+
<p><%= post.body.truncate(50) %></p>
|
|
7
|
+
<small>Views: <%= number_with_delimiter(post.views) %></small>
|
|
8
|
+
</li>
|
|
9
|
+
<% end %>
|
|
10
|
+
</ul>
|
|
11
|
+
<footer>Generated at <%= Time.now.to_s %></footer>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Dummy DB Config for isolated benchmarking
|
|
2
|
+
test:
|
|
3
|
+
adapter: sqlite3
|
|
4
|
+
database: ":memory:"
|
|
5
|
+
pool: 50
|
|
6
|
+
timeout: 5000
|
|
7
|
+
|
|
8
|
+
development:
|
|
9
|
+
adapter: sqlite3
|
|
10
|
+
database: "file:dummy_bench.sqlite3?mode=memory&cache=shared"
|
|
11
|
+
pool: 50
|
|
12
|
+
timeout: 30000
|
|
13
|
+
# High performance settings
|
|
14
|
+
variables:
|
|
15
|
+
journal_mode: WAL
|
|
16
|
+
synchronous: NORMAL
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "etc"
|
|
4
|
+
|
|
5
|
+
module RailsBenchmarkSuite
|
|
6
|
+
class Configuration
|
|
7
|
+
attr_accessor :threads, :profile, :db, :skip_rails, :json, :html
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@threads = Etc.nprocessors
|
|
11
|
+
@profile = false
|
|
12
|
+
@db = false
|
|
13
|
+
@skip_rails = false
|
|
14
|
+
@json = false
|
|
15
|
+
@html = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def db_mode
|
|
19
|
+
@db ? "Local DB" : "SQLite (Memory)"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -1,38 +1,56 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "active_record"
|
|
4
|
+
require "yaml"
|
|
4
5
|
|
|
5
6
|
module RailsBenchmarkSuite
|
|
6
7
|
class DatabaseManager
|
|
7
8
|
SETUP_MUTEX = Mutex.new
|
|
8
9
|
|
|
9
|
-
def setup
|
|
10
|
+
def setup(use_local_db: false)
|
|
10
11
|
# Silence migrations
|
|
11
12
|
ActiveRecord::Migration.verbose = false
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
if use_local_db
|
|
15
|
+
setup_real_database
|
|
16
|
+
else
|
|
17
|
+
setup_dummy_database
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
def setup_real_database
|
|
24
|
+
config_path = File.join(Dir.pwd, "config", "database.yml")
|
|
25
|
+
unless File.exist?(config_path)
|
|
26
|
+
raise "Database config not found at #{config_path} (required for --db option)"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
db_config = YAML.load_file(config_path)
|
|
30
|
+
env = defined?(Rails) ? Rails.env : "development"
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
ActiveRecord::Base.establish_connection(db_config[env])
|
|
33
|
+
puts "Connected to local database (#{env})"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def setup_dummy_database
|
|
37
|
+
# Load internal dummy config
|
|
38
|
+
config_path = File.expand_path("../dummy/config/benchmark_database.yml", __dir__)
|
|
39
|
+
db_config = YAML.load_file(config_path)
|
|
40
|
+
|
|
41
|
+
# Use "development" profile which has the PRAGMA optimizations
|
|
42
|
+
ActiveRecord::Base.establish_connection(db_config["development"])
|
|
43
|
+
|
|
44
|
+
# Apply manual optimizations if needed (though config should handle it)
|
|
45
|
+
conn = ActiveRecord::Base.connection.raw_connection
|
|
46
|
+
conn.busy_timeout = 10000
|
|
47
|
+
|
|
48
|
+
# Setup Schema once safely
|
|
25
49
|
SETUP_MUTEX.synchronize do
|
|
26
|
-
|
|
27
|
-
unless ActiveRecord::Base.connection.table_exists?(:users)
|
|
50
|
+
unless ActiveRecord::Base.connection.table_exists?(:benchmark_users)
|
|
28
51
|
RailsBenchmarkSuite::Schema.load
|
|
29
52
|
end
|
|
30
53
|
end
|
|
31
|
-
|
|
32
|
-
# High-Performance Pragmas for WAL + NORMAL sync
|
|
33
|
-
ActiveRecord::Base.connection.raw_connection.execute("PRAGMA journal_mode = WAL")
|
|
34
|
-
ActiveRecord::Base.connection.raw_connection.execute("PRAGMA synchronous = NORMAL")
|
|
35
|
-
ActiveRecord::Base.connection.raw_connection.execute("PRAGMA mmap_size = 268435456") # 256MB - reduce disk I/O
|
|
36
54
|
end
|
|
37
55
|
end
|
|
38
56
|
end
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "rails_benchmark_suite/dummy/app/models/benchmark_user"
|
|
4
|
+
require "rails_benchmark_suite/dummy/app/models/benchmark_post"
|
|
5
|
+
|
|
3
6
|
module RailsBenchmarkSuite
|
|
4
7
|
module Models
|
|
5
|
-
|
|
6
|
-
has_many :posts, class_name: "RailsBenchmarkSuite::Models::Post", dependent: :destroy
|
|
7
|
-
end
|
|
8
|
+
# Proxy or Legacy support if needed, but primary logic is now in Dummy namespace
|
|
8
9
|
end
|
|
9
10
|
end
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
require "tty-table"
|
|
5
|
+
require "tty-box"
|
|
6
|
+
require "pastel"
|
|
7
|
+
require "tty-cursor"
|
|
8
|
+
|
|
3
9
|
module RailsBenchmarkSuite
|
|
4
10
|
module Reporter
|
|
5
11
|
module_function
|
|
6
12
|
|
|
13
|
+
def pastel
|
|
14
|
+
@pastel ||= Pastel.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def cursor
|
|
18
|
+
@cursor ||= TTY::Cursor
|
|
19
|
+
end
|
|
20
|
+
|
|
7
21
|
def system_info
|
|
8
22
|
{
|
|
9
23
|
ruby_version: RUBY_VERSION,
|
|
10
24
|
platform: RUBY_PLATFORM,
|
|
11
|
-
processors: Concurrent.processor_count,
|
|
12
|
-
libvips: libvips?,
|
|
25
|
+
processors: defined?(Etc) ? Etc.nprocessors : Concurrent.processor_count,
|
|
13
26
|
yjit: yjit_enabled?
|
|
14
27
|
}
|
|
15
28
|
end
|
|
@@ -18,9 +31,206 @@ module RailsBenchmarkSuite
|
|
|
18
31
|
defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
|
|
19
32
|
end
|
|
20
33
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
def header(info)
|
|
35
|
+
print cursor.hide
|
|
36
|
+
# Build YJIT Status
|
|
37
|
+
yjit_status = info[:yjit] ? pastel.green("ON") : pastel.red("OFF")
|
|
38
|
+
yjit_hint = info[:yjit] ? "" : " (use RUBY_OPT=\"--yjit\")"
|
|
39
|
+
|
|
40
|
+
content = [
|
|
41
|
+
"System: #{info[:processors]} Cores | Ruby #{info[:ruby_version]}",
|
|
42
|
+
"DB: #{info[:db_mode] || 'SQLite (Memory)'} | YJIT: #{yjit_status}#{yjit_hint}"
|
|
43
|
+
].join("\n")
|
|
44
|
+
|
|
45
|
+
print TTY::Box.frame(
|
|
46
|
+
width: 80,
|
|
47
|
+
title: { top_left: " Rails Benchmark Suite v#{RailsBenchmarkSuite::VERSION} " },
|
|
48
|
+
padding: 1,
|
|
49
|
+
style: {
|
|
50
|
+
fg: :white,
|
|
51
|
+
border: { fg: :bright_blue }
|
|
52
|
+
}
|
|
53
|
+
) { content }
|
|
54
|
+
puts ""
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render(payload)
|
|
58
|
+
results = payload[:results]
|
|
59
|
+
total_score = payload[:total_score]
|
|
60
|
+
tier = payload[:tier]
|
|
61
|
+
threads = payload[:threads] || 4
|
|
62
|
+
|
|
63
|
+
puts ""
|
|
64
|
+
|
|
65
|
+
# 1. Comparison Table
|
|
66
|
+
rows = results.map do |data|
|
|
67
|
+
report = data[:report]
|
|
68
|
+
entries = report.entries
|
|
69
|
+
|
|
70
|
+
entry_1t = entries.find { |e| e.label.include?("(1 thread)") }
|
|
71
|
+
entry_mt = entries.find { |e| e.label.match?(/\(\d+ threads\)/) }
|
|
72
|
+
|
|
73
|
+
ips_1t = entry_1t ? entry_1t.ips : 0
|
|
74
|
+
ips_mt = entry_mt ? entry_mt.ips : 0
|
|
75
|
+
|
|
76
|
+
scaling = ips_1t > 0 ? (ips_mt / ips_1t) : 0
|
|
77
|
+
efficiency = (ips_mt / (ips_1t * threads)) * 100 if ips_1t > 0 && threads > 0
|
|
78
|
+
efficiency ||= 0
|
|
79
|
+
|
|
80
|
+
# Color coding
|
|
81
|
+
eff_color = if efficiency >= 75
|
|
82
|
+
:green
|
|
83
|
+
elsif efficiency >= 50
|
|
84
|
+
:yellow
|
|
85
|
+
else
|
|
86
|
+
:red
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Colorize scaling (Simple heuristic per user request)
|
|
90
|
+
scale_str = "%.2fx" % scaling
|
|
91
|
+
if scaling >= 1.0
|
|
92
|
+
scale_str = pastel.green(scale_str)
|
|
93
|
+
else
|
|
94
|
+
scale_str = pastel.red(scale_str)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
[
|
|
98
|
+
data[:name],
|
|
99
|
+
humanize(ips_1t),
|
|
100
|
+
humanize(ips_mt),
|
|
101
|
+
scale_str,
|
|
102
|
+
pastel.decorate("#{efficiency.round(1)}%", eff_color),
|
|
103
|
+
data[:adjusted_weight].round(2)
|
|
104
|
+
]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
table = TTY::Table.new(
|
|
108
|
+
header: ["Workload", "1T IPS", "MaxT IPS", "Scaling", "Efficiency", "Weight"],
|
|
109
|
+
rows: rows
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
puts table.render(:unicode, padding: [0, 1]) do |renderer|
|
|
113
|
+
renderer.border.separator = :each_row
|
|
114
|
+
renderer.border.style = :blue
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# 2. Insights List
|
|
118
|
+
puts ""
|
|
119
|
+
check_scaling_insights(results)
|
|
120
|
+
check_yjit_insight
|
|
121
|
+
check_memory_insights(results)
|
|
122
|
+
|
|
123
|
+
# 3. Final Score Dashboard
|
|
124
|
+
render_final_score(total_score)
|
|
125
|
+
show_hardware_tier(tier)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def check_scaling_insights(results)
|
|
129
|
+
poor_scaling = results.select do |r|
|
|
130
|
+
entries = r[:report].entries
|
|
131
|
+
entry_1t = entries.find { |e| e.label.include?("(1 thread)") }
|
|
132
|
+
entry_mt = entries.find { |e| e.label.match?(/\(\d+ threads\)/) }
|
|
133
|
+
|
|
134
|
+
ips_1t = entry_1t ? entry_1t.ips : 0
|
|
135
|
+
ips_mt = entry_mt ? entry_mt.ips : 0
|
|
136
|
+
scaling = ips_1t > 0 ? (ips_mt / ips_1t) : 0
|
|
137
|
+
scaling < 0.8
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
if poor_scaling.any?
|
|
141
|
+
puts pastel.yellow.bold("💡 Insight (Scaling):") + " Scaling below 1.0x detected."
|
|
142
|
+
puts " This indicates SQLite lock contention or Ruby GIL saturation."
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def check_yjit_insight
|
|
147
|
+
unless yjit_enabled?
|
|
148
|
+
puts ""
|
|
149
|
+
puts pastel.yellow.bold("💡 Insight (YJIT):") + " YJIT is OFF."
|
|
150
|
+
puts " Run with RUBY_OPT=\"--yjit\" for ~20% boost."
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def check_memory_insights(results)
|
|
155
|
+
results.select { |r| r[:memory_delta_mb] > 20 }.each do |r|
|
|
156
|
+
puts ""
|
|
157
|
+
puts pastel.yellow.bold("💡 Insight (Memory):") + " High growth in #{r[:name]} (#{r[:memory_delta_mb].round(1)}MB)"
|
|
158
|
+
puts " Suggests heavy object allocation."
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def show_hardware_tier(tier)
|
|
163
|
+
comparison = case tier
|
|
164
|
+
when "Entry/Dev"
|
|
165
|
+
"Entry-Level (Suitable for dev/testing)"
|
|
166
|
+
when "Production-Ready"
|
|
167
|
+
"Professional-Grade (Matches dedicated instances)"
|
|
168
|
+
else
|
|
169
|
+
"High-Performance (Bare-metal speed)"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
puts ""
|
|
173
|
+
puts pastel.bold("📊 Performance Tier: ") + comparison
|
|
174
|
+
puts ""
|
|
175
|
+
print cursor.show
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def render_final_score(score)
|
|
179
|
+
score_str = "#{score.round(0)}"
|
|
180
|
+
puts ""
|
|
181
|
+
|
|
182
|
+
print TTY::Box.frame(
|
|
183
|
+
width: 40,
|
|
184
|
+
height: 5,
|
|
185
|
+
align: :center,
|
|
186
|
+
padding: 1,
|
|
187
|
+
title: { top_left: " RAILS HEFT INDEX " },
|
|
188
|
+
style: { border: { fg: :green }, fg: :green }
|
|
189
|
+
) {
|
|
190
|
+
pastel.bold(score_str)
|
|
191
|
+
}
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def as_json(payload)
|
|
195
|
+
out = {
|
|
196
|
+
system: system_info,
|
|
197
|
+
total_score: payload[:total_score].round(0),
|
|
198
|
+
tier: payload[:tier],
|
|
199
|
+
workloads: []
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
payload[:results].each do |data|
|
|
203
|
+
entries = data[:report].entries
|
|
204
|
+
entry_1t = entries.find { |e| e.label.include?("(1 thread)") }
|
|
205
|
+
entry_mt = entries.find { |e| e.label.match?(/\(\d+ threads\)/) }
|
|
206
|
+
|
|
207
|
+
ips_1t = entry_1t ? entry_1t.ips : 0
|
|
208
|
+
ips_mt = entry_mt ? entry_mt.ips : 0
|
|
209
|
+
|
|
210
|
+
out[:workloads] << {
|
|
211
|
+
name: data[:name],
|
|
212
|
+
adjusted_weight: data[:adjusted_weight],
|
|
213
|
+
ips_1t: ips_1t,
|
|
214
|
+
ips_mt: ips_mt,
|
|
215
|
+
threads: payload[:threads],
|
|
216
|
+
scaling: ips_1t > 0 ? (ips_mt / ips_1t) : 0,
|
|
217
|
+
efficiency: data[:efficiency],
|
|
218
|
+
memory_delta_mb: data[:memory_delta_mb]
|
|
219
|
+
}
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
puts out.to_json
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def humanize(ips)
|
|
226
|
+
return "0" if ips.nil? || ips == 0
|
|
227
|
+
if ips >= 1_000_000
|
|
228
|
+
"#{(ips / 1_000_000.0).round(1)}M"
|
|
229
|
+
elsif ips >= 1_000
|
|
230
|
+
"#{(ips / 1_000.0).round(1)}k"
|
|
231
|
+
else
|
|
232
|
+
ips.round(1).to_s
|
|
233
|
+
end
|
|
24
234
|
end
|
|
25
235
|
end
|
|
26
236
|
end
|