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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 150b032cfebbd12d60c2a407a292cf04367c4b821d44541aa21c7b01243b5288
4
- data.tar.gz: 5fcffc2550db42c299f3c417abb11c9b2d8c3bdea7a27ecf3f8fd7c1d624bf31
3
+ metadata.gz: 473aa21869380dae81d4f9ee8668ccf161721923fd6c453cc711caf9d6303fd1
4
+ data.tar.gz: 5101bb8d63885263dc3fcbb8851d52f3ed48a1f919b0abfab38d43dc3f3df2a2
5
5
  SHA512:
6
- metadata.gz: 785219aa58ac8b7353bf67920c4f419bf628d92f2fc539f02bd09408fb7aac13a6352d5aa68a09f0c60a7f1d95c34e016d12bb4ab015544ecf6962ca0c72aa54
7
- data.tar.gz: 81706842da33247fbace2b62c4dee9f42332ff83a9ed1ab905960296f4954f6915c4055abe97910d9a2e572472d9454ebe7e01fce17dba0b604cd4b43f2f4988
6
+ metadata.gz: 11311aa76df4c103c8b3ea2e12a803ddeb339cd454127b246557341ddd5c7872fa01c4ff00f4c14ae711d95b48f0d84b148ec7f8b0a39fda7ab70cac0d070bad
7
+ data.tar.gz: 3f141277e2daae966aa4d7d7be684e112889fa757282e36a4f35ac34d60ac91ca76903281d9e091b9549ef567d6e8b7a26b13221326bf8273c3c33fe7e6082fc
data/.gitignore CHANGED
@@ -3,3 +3,5 @@
3
3
  vendor/bundle/
4
4
  Gemfile.lock
5
5
  .ruby-lsp/
6
+ *.gem
7
+ rails_benchmark_report.html
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.0)
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
- ### Additional Options
105
+ ### 📊 Visual Diagnostics (HTML Report)
106
+
107
+ ![Rails Benchmark Suite Report](docs/images/report_v0_3_1.png)
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 all available options
132
+ - `-h` / `--help`: Show usage help
96
133
 
97
134
  ### Standalone Usage
98
135
 
@@ -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
- options = {}
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
- options[:skip_rails] = true
33
+ config.skip_rails = true
20
34
  end
21
35
 
22
36
  opts.on("-j", "--json", "Output results as JSON") do
23
- options[:json] = true
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 options[:json]
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 !options[:skip_rails] && File.exist?(rails_env_path)
40
- puts "Rails environment detected at #{rails_env_path}. Loading..." unless options[:json]
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 options[:json]
61
+ puts "Rails loaded successfully." unless config.json
43
62
  else
44
- unless options[:json]
45
- if options[:skip_rails]
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.run(json: options[:json])
72
+ RailsBenchmarkSuite::Runner.new(config).run
Binary file
@@ -0,0 +1,7 @@
1
+ module RailsBenchmarkSuite
2
+ module Dummy
3
+ class BenchmarkJob < ActiveRecord::Base
4
+ self.table_name = "simulated_jobs" # Keep table name consistent or rename? Let's use simulated_jobs as it's descriptive
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ module RailsBenchmarkSuite
2
+ module Dummy
3
+ class BenchmarkPost < ActiveRecord::Base
4
+ self.table_name = "benchmark_posts"
5
+
6
+ belongs_to :user, class_name: "RailsBenchmarkSuite::Dummy::BenchmarkUser", foreign_key: "benchmark_user_id"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module RailsBenchmarkSuite
2
+ module Dummy
3
+ class BenchmarkUser < ActiveRecord::Base
4
+ self.table_name = "benchmark_users"
5
+
6
+ has_many :posts, class_name: "RailsBenchmarkSuite::Dummy::BenchmarkPost", foreign_key: "benchmark_user_id"
7
+ end
8
+ end
9
+ end
@@ -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
- # Ultimate Hardening: Massive pool and timeout for zero lock contention (v0.3.0)
14
- ActiveRecord::Base.establish_connection(
15
- adapter: "sqlite3",
16
- database: "file:heft_db?mode=memory&cache=shared",
17
- pool: 50,
18
- timeout: 30000
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
- # The 'Busy Timeout' Hammer - force it directly on the raw connection
22
- ActiveRecord::Base.connection.raw_connection.busy_timeout = 10000
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
- # Setup Schema once safely with Mutex
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
- # Verify if schema already loaded by checking for a table
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
- class User < ActiveRecord::Base
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 libvips?
22
- # Naive check for libvips presence
23
- system("vips --version", out: File::NULL, err: File::NULL)
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