Skip to content

Instantly share code, notes, and snippets.

@alexisbernard
Last active November 6, 2025 08:34
Show Gist options
  • Select an option

  • Save alexisbernard/1ae2fee6f5b158858f0f5729a0fe7d72 to your computer and use it in GitHub Desktop.

Select an option

Save alexisbernard/1ae2fee6f5b158858f0f5729a0fe7d72 to your computer and use it in GitHub Desktop.
Simple Rails app to measure latency and throughput when multithreaded given a specific IO rate
# frozen_string_literal: true
# Simple Rails app to measure latency and throughput when multithreaded given a specific IO rate.
# Start the server with: MAX_THREADS=10 RAILS_ENV=production ruby threads_contention_app.rb
# Then run load tests with concurrency from 1 to 10:
# ab -n 100 -c 1 -l http://localhost:3000/?io_rate=0.5
# ab -n 100 -c 2 -l http://localhost:3000/?io_rate=0.5
# ab -n 100 -c 3 -l http://localhost:3000/?io_rate=0.5
# ...
# ab -n 100 -c 10 -l http://localhost:3000/?io_rate=0.5
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
gem "rails", "8.1"
gem "puma"
end
require "action_controller/railtie"
class App < Rails::Application
config.secret_key_base = "i_am_a_secret"
config.logger = Logger.new($stdout)
routes.append do
root to: "benchmark#index"
end
end
class BenchmarkController < ActionController::Base
WORK_DIFFICULTY = 46_368
WORKING_MS = [1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
def index
working_ms = WORKING_MS.shuffle
io_rate = params[:io_rate].to_f
raise ArgumentError.new("params[:io_rate] (#{io_rate}) must be between 0 and 1") if !(0..1).include?(io_rate)
cpu_rate = 1 - io_rate
total_cpu_ms = total_io_ms = 0
while ms = working_ms.pop
total_cpu_ms += cpu_ms = work(ms)
total_io_ms += io_ms = io_rate * cpu_ms / cpu_rate
sleep(io_ms / 1000)
end
real_io_rate = total_io_ms / (total_cpu_ms + total_io_ms) * 100
render inline: "CPU: #{total_cpu_ms.round.to_i}ms (#{100 - real_io_rate.round(2)}%)\nIO: #{total_io_ms.to_i}ms (#{real_io_rate.round(2)}%)"
end
private
def work(target_ms)
started_at = clock_ms
fibonacci(WORK_DIFFICULTY) while (real_ms = clock_ms - started_at) < target_ms
real_ms
end
def clock_ms
Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
end
# Curious about this super fast fibonacci? You should read that article:
# https://www.rorvswild.com/blog/2025/fibonacci-ruby-algorithms
def fibonacci(n)
fib_hash = Hash.new do |h, k|
h[k] = _fib(h, k)
end
fib_hash[0] = 0
fib_hash[1] = 1
fib_hash[2] = 1
fib_hash[n]
end
def _fib(hash, n)
half_n = n / 2
a = hash[half_n + 1]
b = hash[half_n]
if n.odd?
a * a + b * b
else
2 * a * b - b * b
end
end
end
App.initialize!
require "puma"
require "puma/cli"
# Choose port dynamically or fallback to 3000
port = ENV.fetch("PORT", 3000)
# Temporary rackup file
rackup_path = File.join(Dir.tmpdir, "tmp_rackup.ru")
File.write(rackup_path, "run App\n")
# Puma configuration
puma_config = <<~PUMA
workers #{ENV.fetch("WEB_CONCURRENCY", 1)}
threads #{ENV.fetch("MAX_THREADS", 5)}, #{ENV.fetch("MAX_THREADS", 5)}
port #{port}
preload_app!
rackup "#{rackup_path}"
PUMA
config_path = File.join(Dir.tmpdir, "tmp_puma.rb")
File.write(config_path, puma_config)
cli = Puma::CLI.new(["-C", config_path])
cli.run
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment