ThreadAdvisor

A Rails 7.2+ thread count optimization advisor. Measures I/O wait ratio in blocks and recommends optimal thread count based on Amdahl's law.

Features

  • Automatic Measurement: Measures wall time, CPU time, and I/O time automatically
  • Theory-Based: Calculates theoretical optimal thread count using Amdahl's law
  • Real-World Constraints: Considers CPU cores, DB connection pool, and environment variables
  • GVL Support: High-precision measurement with gvl_timing gem (optional)
  • JSON Output: Structured logs for easy aggregation and analysis

Installation

Add this line to your application's Gemfile:

gem 'thread_advisor'

Or if installing from a local path:

gem 'thread_advisor', path: 'vendor/gems/thread_advisor'

Optional: For high-precision GVL measurement:

gem 'gvl_timing'

Then execute:

bundle install

Configuration

Create config/initializers/thread_advisor.rb:

ThreadAdvisor.configure do |c|
  c.logger = Rails.logger
  c.hard_max_threads = 32                    # Absolute upper limit
  c.core_multiplier = 1.0                    # CPU core count multiplier
  c.diminishing_return_threshold = 0.05      # Diminishing return threshold (5%)
  c.max_avg_gvl_stall_ms = 85.0              # GVL stall tolerance
  c.enable_middleware = true                 # Enable per-request measurement
  c.middleware_tag_resolver = ->(env) {
    "#{env['REQUEST_METHOD']} #{env['PATH_INFO']}"
  }

  # Perfm integration (default: enabled)
  c.enable_perfm = true                      # Blend with Perfm historical data

  # Output format (default: :json)
  c.output_format = :json                    # Options: :json or :stdout
end

Quick Start

See the example directory for runnable examples:

# Basic measurement with different workload types
ruby -Ilib example/basic_measurement.rb

# JSON output format
ruby -Ilib example/json_output.rb

# High-precision measurement with gvl_timing
bundle exec ruby -Ilib example/with_gvl_timing.rb

Check out the example README for detailed documentation of all examples.

Usage

1. Block Measurement

result, metrics = ThreadAdvisor.measure("import_products") do
  Product.import_from_api!
end

# Get recommended thread count
recommended = metrics[:advice][:recommended_threads]
Rails.logger.info "Recommended threads: #{recommended}"

2. Rack Middleware (Automatic Measurement)

Enable middleware to automatically measure all requests and output JSON logs:

# config/initializers/thread_advisor.rb
ThreadAdvisor.configure do |c|
  c.enable_middleware = true
end

3. Example Log Output

{
  "lib": "thread_advisor",
  "event": "advice",
  "name": "import_products",
  "wall_s": 2.45,
  "cpu_s": 1.32,
  "io_s": 1.13,
  "stall_s": null,
  "io_ratio": 0.46,
  "speedup_table": [
    {"n": 1, "speedup": 1.0},
    {"n": 2, "speedup": 1.27},
    {"n": 3, "speedup": 1.40},
    {"n": 4, "speedup": 1.48}
  ],
  "recommended_threads": 3,
  "reasons": {
    "diminishing_return_threshold": 0.05,
    "db_pool_cap": 5,
    "cpu_core_cap": 8,
    "hard_cap": 32,
    "gvl_avg_stall_ms": 78.9,
    "stall_threshold_ms": 85.0
  }
}

Algorithm

  1. I/O Ratio Measurement: p = io_time / wall_time
  2. Amdahl's Law: speedup(N) = 1 / ((1 - p) + p / N)
  3. Upper Limit Determination: Minimum of:
    • ENV["PUMA_MAX_THREADS"] or ENV["RAILS_MAX_THREADS"]
    • ActiveRecord::Base.connection_pool.size
    • Etc.nprocessors * core_multiplier
    • hard_max_threads
  4. Diminishing Returns: Stop when incremental gain falls below threshold
  5. GVL Adjustment: Reduce by 1 step if stall time exceeds threshold

Perfm Integration

ThreadAdvisor integrates with the Perfm gem to provide historical stability:

  • Automatic History Fetching: Uses Perfm::GvlMetricsAnalyzer to fetch historical metrics
  • Weighted Blending: Combines current measurement with historical data using sqrt(sample_count) weighting
  • Stabilized Recommendations: Reduces variance from single measurements by incorporating past performance

How Blending Works

# Current measurement weight = 1.0
# Historical weight = sqrt(number_of_samples)
# Blended value = (current + historical * weight) / (1 + weight)

This approach gives more stability as historical data accumulates while still responding to current conditions.

Output Formats

JSON Format (default)

Outputs structured JSON logs through your configured logger:

ThreadAdvisor.configure do |c|
  c.output_format = :json
end

Stdout Format

Outputs human-readable reports directly to stdout:

ThreadAdvisor.configure do |c|
  c.output_format = :stdout
end

Example stdout output:

============================================================
ThreadAdvisor Report: import_products
============================================================

Timing Metrics:
  Wall Time:    2.45s
  CPU Time:     1.32s
  I/O Time:     1.13s
  I/O Ratio:    46.1%

Perfm History:
  Samples:      127
  Weight:       11.27
  Blended I/O:  44.8%

Speedup Table (Amdahl's Law):
  1 threads -> 1.00x speedup
  2 threads -> 1.27x speedup
  3 threads -> 1.40x speedup
  4 threads -> 1.48x speedup

RECOMMENDED THREADS: 3

Decision Factors:
  Threshold:           5.0%
  DB Pool Cap:         5
  CPU Core Cap:        8
  Hard Cap:            32
  GVL Stall (avg):     78.90 ms
  GVL Stall (limit):   85.00 ms

============================================================

Requirements

  • Ruby 3.2+
  • Rails 7.2+
  • ActiveSupport/Railties

Optional Dependencies

  • Perfm (optional): For historical metrics integration. Install separately: gem 'perfm'
  • gvl_timing (optional): For high-precision GVL measurements. Install separately: gem 'gvl_timing'

If these gems are not installed, ThreadAdvisor will fall back to approximation methods.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/thread_advisor. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the ThreadAdvisor project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.