Benchmarks - add ResponseTimeWrk files (#2895)
* Update & add new test/rackup files * Add benchmarks/local files
This commit is contained in:
parent
528912e9eb
commit
f02cdcd5c4
|
@ -0,0 +1,392 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'optparse'
|
||||
|
||||
module TestPuma
|
||||
|
||||
HOST4 = ENV.fetch('PUMA_TEST_HOST4', '127.0.0.1')
|
||||
HOST6 = ENV.fetch('PUMA_TEST_HOST6', '::1')
|
||||
PORT = ENV.fetch('PUMA_TEST_PORT', 40001).to_i
|
||||
|
||||
# Array of response body sizes. If specified, set by ENV['PUMA_TEST_SIZES']
|
||||
#
|
||||
SIZES = if (t = ENV['PUMA_TEST_SIZES'])
|
||||
t.split(',').map(&:to_i).freeze
|
||||
else
|
||||
[1, 10, 100, 256, 512, 1024, 2048].freeze
|
||||
end
|
||||
|
||||
TYPES = [[:a, 'array'].freeze, [:c, 'chunk'].freeze,
|
||||
[:s, 'string'].freeze, [:i, 'io'].freeze].freeze
|
||||
|
||||
# Creates files used by 'i' (File/IO) responses. Placed in
|
||||
# "#{Dir.tmpdir}/.puma_response_body_io"
|
||||
# @param sizes [Array <Integer>] Array of sizes
|
||||
#
|
||||
def self.create_io_files(sizes = SIZES)
|
||||
require 'tmpdir'
|
||||
tmp_folder = "#{Dir.tmpdir}/.puma_response_body_io"
|
||||
Dir.mkdir(tmp_folder) unless Dir.exist? tmp_folder
|
||||
fn_format = "#{tmp_folder}/body_io_%04d.txt"
|
||||
str = ("── Puma Hello World! ── " * 31) + "── Puma Hello World! ──\n" # 1 KB
|
||||
sizes.each do |len|
|
||||
suf = format "%04d", len
|
||||
fn = format fn_format, len
|
||||
unless File.exist? fn
|
||||
body = "Hello World\n#{str}".byteslice(0,1023) + "\n" + (str * (len-1))
|
||||
File.write fn, body
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Base class for generating client request streams
|
||||
#
|
||||
class BenchBase
|
||||
# We're running under GitHub Actions
|
||||
IS_GHA = ENV['GITHUB_ACTIONS'] == 'true'
|
||||
|
||||
WRK_PERCENTILE = [0.50, 0.75, 0.9, 0.99, 1.0].freeze
|
||||
|
||||
HDR_BODY_CONF = "Body-Conf: "
|
||||
|
||||
# extracts 'type' string from `-b` argument
|
||||
TYPES_RE = /\A[acis]+/.freeze
|
||||
|
||||
# extracts 'size' string from `-b` argument
|
||||
SIZES_RE = /\d[\d,]*\z/.freeze
|
||||
|
||||
def initialize
|
||||
sleep 5 # wait for server to boot
|
||||
|
||||
@thread_loops = nil
|
||||
@clients_per_thread = nil
|
||||
@req_per_client = nil
|
||||
@body_sizes = SIZES
|
||||
@body_types = TYPES
|
||||
@dly_app = nil
|
||||
@bind_type = :tcp
|
||||
|
||||
@ios_to_close = []
|
||||
|
||||
setup_options
|
||||
|
||||
unless File.exist? @state_file
|
||||
puts "Can't find state file '#{@state_file}'"
|
||||
exit 1
|
||||
end
|
||||
|
||||
mstr_pid = File.binread(@state_file)[/^pid: +(\d+)/, 1].to_i
|
||||
begin
|
||||
Process.kill 0, mstr_pid
|
||||
rescue Errno::ESRCH
|
||||
puts 'Puma server stopped?'
|
||||
exit 1
|
||||
rescue Errno::EPERM
|
||||
end
|
||||
|
||||
case @bind_type
|
||||
when :ssl, :ssl4, :tcp, :tcp4
|
||||
@bind_host = HOST4
|
||||
@bind_port = PORT
|
||||
when :ssl6, :tcp6
|
||||
@bind_host = HOST6
|
||||
@bind_port = PORT
|
||||
when :unix
|
||||
@bind_path = 'tmp/benchmark_skt.unix'
|
||||
when :aunix
|
||||
@bind_path = '@benchmark_skt.aunix'
|
||||
else
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
def setup_options
|
||||
OptionParser.new do |o|
|
||||
o.on "-T", "--stream-threads THREADS", OptionParser::DecimalInteger, "request_stream: loops/threads" do |arg|
|
||||
@stream_threads = arg.to_i
|
||||
end
|
||||
|
||||
o.on "-c", "--wrk-connections CONNECTIONS", OptionParser::DecimalInteger, "request_stream: clients_per_thread" do |arg|
|
||||
@wrk_connections = arg.to_i
|
||||
end
|
||||
|
||||
o.on "-R", "--requests REQUESTS", OptionParser::DecimalInteger, "request_stream: requests per socket" do |arg|
|
||||
@req_per_socket = arg.to_i
|
||||
end
|
||||
|
||||
o.on "-D", "--duration DURATION", OptionParser::DecimalInteger, "wrk/stream: duration" do |arg|
|
||||
@duration = arg.to_i
|
||||
end
|
||||
|
||||
o.on "-b", "--body_conf BODY_CONF", String, "CI RackUp: type and size of response body in kB" do |arg|
|
||||
if (types = arg[TYPES_RE])
|
||||
@body_types = TYPES.select { |a| types.include? a[0].to_s }
|
||||
end
|
||||
|
||||
if (sizes = arg[SIZES_RE])
|
||||
@body_sizes = sizes.split(',').map(&:to_i).sort
|
||||
end
|
||||
end
|
||||
|
||||
o.on "-d", "--dly_app DELAYAPP", Float, "CI RackUp: app response delay" do |arg|
|
||||
@dly_app = arg.to_f
|
||||
end
|
||||
|
||||
o.on "-s", "--socket SOCKETTYPE", String, "Bind type: tcp, ssl, tcp6, ssl6, unix, aunix" do |arg|
|
||||
@bind_type = arg.to_sym
|
||||
end
|
||||
|
||||
o.on "-S", "--state PUMA_STATEFILE", String, "Puma Server: state file" do |arg|
|
||||
@state_file = arg
|
||||
end
|
||||
|
||||
o.on "-t", "--threads PUMA_THREADS", String, "Puma Server: threads" do |arg|
|
||||
@threads = arg
|
||||
end
|
||||
|
||||
o.on "-w", "--workers PUMA_WORKERS", OptionParser::DecimalInteger, "Puma Server: workers" do |arg|
|
||||
@workers = arg.to_i
|
||||
end
|
||||
|
||||
o.on "-W", "--wrk_bind WRK_STR", String, "wrk: bind string" do |arg|
|
||||
@wrk_bind_str = arg
|
||||
end
|
||||
|
||||
o.on("-h", "--help", "Prints this help") do
|
||||
puts o
|
||||
exit
|
||||
end
|
||||
end.parse! ARGV
|
||||
end
|
||||
|
||||
def close_clients
|
||||
closed = 0
|
||||
@ios_to_close.each do |socket|
|
||||
if socket && socket.to_io.is_a?(IO) && !socket.closed?
|
||||
begin
|
||||
if @bind_type == :ssl
|
||||
socket.sysclose
|
||||
else
|
||||
socket.close
|
||||
end
|
||||
closed += 1
|
||||
rescue Errno::EBADF
|
||||
end
|
||||
end
|
||||
end
|
||||
puts "Closed #{closed} sockets" unless closed.zero?
|
||||
end
|
||||
|
||||
# Runs wrk and returns data from its output.
|
||||
# @param cmd [String] The wrk command string, with arguments
|
||||
# @return [Hash] The wrk data
|
||||
#
|
||||
def run_wrk_parse(cmd, log: false)
|
||||
STDOUT.syswrite cmd.ljust 55
|
||||
|
||||
if @dly_app
|
||||
cmd.sub! ' -H ', " -H 'Dly: #{@dly_app.round 4}' -H "
|
||||
end
|
||||
|
||||
wrk_output = %x[#{cmd}]
|
||||
if log
|
||||
puts '', wrk_output, ''
|
||||
end
|
||||
|
||||
wrk_data = "#{wrk_output[/\A.+ connections/m]}\n#{wrk_output[/ Thread Stats.+\z/m]}"
|
||||
|
||||
ary = wrk_data[/^ +\d+ +requests.+/].strip.split ' '
|
||||
|
||||
fmt = " | %6s %s %s %7s %8s %s\n"
|
||||
|
||||
STDOUT.syswrite format(fmt, *ary)
|
||||
|
||||
hsh = {}
|
||||
|
||||
rps = wrk_data[/^Requests\/sec: +([\d.]+)/, 1].to_f
|
||||
requests = wrk_data[/^ +(\d+) +requests/, 1].to_i
|
||||
|
||||
transfer = wrk_data[/^Transfer\/sec: +([\d.]+)/, 1].to_f
|
||||
transfer_unit = wrk_data[/^Transfer\/sec: +[\d.]+(GB|KB|MB)/, 1]
|
||||
transfer_mult = mult_for_unit transfer_unit
|
||||
|
||||
read = wrk_data[/ +([\d.]+)(GB|KB|MB) +read$/, 1].to_f
|
||||
read_unit = wrk_data[/ +[\d.]+(GB|KB|MB) +read$/, 1]
|
||||
read_mult = mult_for_unit read_unit
|
||||
|
||||
resp_transfer = (transfer * transfer_mult)/rps
|
||||
resp_read = (read * read_mult)/requests.to_f
|
||||
|
||||
mult = transfer/read
|
||||
|
||||
hsh[:resp_size] = ((resp_transfer * mult + resp_read)/(mult + 1)).round
|
||||
|
||||
hsh[:resp_size] = hsh[:resp_size] - 1770 - hsh[:resp_size].to_s.length
|
||||
|
||||
hsh[:rps] = rps.round
|
||||
hsh[:requests] = requests
|
||||
|
||||
if (t = wrk_data[/^ +Socket errors: +(.+)/, 1])
|
||||
hsh[:errors] = t
|
||||
end
|
||||
|
||||
read = wrk_data[/ +([\d.]+)(GB|KB|MB) +read$/, 1].to_f
|
||||
unit = wrk_data[/ +[\d.]+(GB|KB|MB) +read$/, 1]
|
||||
|
||||
mult = mult_for_unit unit
|
||||
|
||||
hsh[:read] = (mult * read).round
|
||||
|
||||
if hsh[:errors]
|
||||
t = hsh[:errors]
|
||||
hsh[:errors] = t.sub('connect ', 'c').sub('read ', 'r')
|
||||
.sub('write ', 'w').sub('timeout ', 't')
|
||||
end
|
||||
|
||||
t_re = ' +([\d.ums]+)'
|
||||
|
||||
latency =
|
||||
wrk_data.match(/^ +50%#{t_re}\s+75%#{t_re}\s+90%#{t_re}\s+99%#{t_re}/).captures
|
||||
# add up max time
|
||||
latency.push wrk_data[/^ +Latency.+/].split(' ')[-2]
|
||||
|
||||
hsh[:times_summary] = WRK_PERCENTILE.zip(latency.map do |t|
|
||||
if t.end_with?('ms')
|
||||
t.to_f
|
||||
elsif t.end_with?('us')
|
||||
t.to_f/1000
|
||||
elsif t.end_with?('s')
|
||||
t.to_f * 1000
|
||||
else
|
||||
0
|
||||
end
|
||||
end).to_h
|
||||
hsh
|
||||
end
|
||||
|
||||
def mult_for_unit(unit)
|
||||
case unit
|
||||
when 'KB' then 1_024
|
||||
when 'MB' then 1_024**2
|
||||
when 'GB' then 1_024**3
|
||||
end
|
||||
end
|
||||
|
||||
# Outputs info about the run. Example output:
|
||||
#
|
||||
# benchmarks/local/response_time_wrk.sh -w2 -t5:5 -s tcp6
|
||||
# Server cluster mode -w2 -t5:5, bind: tcp6
|
||||
# Puma repo branch 00-response-refactor
|
||||
# ruby 3.2.0dev (2022-06-11T12:26:03Z master 28e27ee76e) +YJIT [x86_64-linux]
|
||||
#
|
||||
def env_log
|
||||
puts "#{ENV['PUMA_BENCH_CMD']} #{ENV['PUMA_BENCH_ARGS']}"
|
||||
puts @workers ?
|
||||
"Server cluster mode -w#{@workers} -t#{@threads}, bind: #{@bind_type}" :
|
||||
"Server single mode -t#{@threads}, bind: #{@bind_type}"
|
||||
|
||||
branch = %x[git branch][/^\* (.*)/, 1]
|
||||
if branch
|
||||
puts "Puma repo branch #{branch.strip}", RUBY_DESCRIPTION
|
||||
else
|
||||
const = File.read File.expand_path('../../lib/puma/const.rb', __dir__)
|
||||
puma_version = const[/^ +PUMA_VERSION[^'"]+['"]([^\s'"]+)/, 1]
|
||||
puts "Puma version #{puma_version}", RUBY_DESCRIPTION
|
||||
end
|
||||
end
|
||||
|
||||
# Parses data returned by `PumaInfo.run stats`
|
||||
# @return [Hash] The data from Puma stats
|
||||
#
|
||||
def parse_stats
|
||||
stats = {}
|
||||
|
||||
obj = @puma_info.run 'stats'
|
||||
|
||||
worker_status = obj[:worker_status]
|
||||
|
||||
worker_status.each do |w|
|
||||
pid = w[:pid]
|
||||
req_cnt = w[:last_status][:requests_count]
|
||||
id = format 'worker-%01d-%02d', w[:phase], w[:index]
|
||||
hsh = {
|
||||
pid: pid,
|
||||
requests: req_cnt - @worker_req_ttl[pid],
|
||||
backlog: w[:last_status][:backlog]
|
||||
}
|
||||
@pids[pid] = id
|
||||
@worker_req_ttl[pid] = req_cnt
|
||||
stats[id] = hsh
|
||||
end
|
||||
|
||||
stats
|
||||
end
|
||||
|
||||
# Runs gc in the server, then parses data from
|
||||
# `smem -c 'pid rss pss uss command'`
|
||||
# @return [Hash] The data from smem
|
||||
#
|
||||
def parse_smem
|
||||
@puma_info.run 'gc'
|
||||
sleep 1
|
||||
|
||||
hsh_smem = Hash.new []
|
||||
pids = @pids.keys
|
||||
|
||||
smem_info = %x[smem -c 'pid rss pss uss command']
|
||||
|
||||
smem_info.lines.each do |l|
|
||||
ary = l.strip.split ' ', 5
|
||||
if pids.include? ary[0].to_i
|
||||
hsh_smem[@pids[ary[0].to_i]] = {
|
||||
pid: ary[0].to_i,
|
||||
rss: ary[1].to_i,
|
||||
pss: ary[2].to_i,
|
||||
uss: ary[3].to_i
|
||||
}
|
||||
end
|
||||
end
|
||||
hsh_smem.sort.to_h
|
||||
end
|
||||
end
|
||||
|
||||
class ResponseTimeBase < BenchBase
|
||||
def run
|
||||
@puma_info = PumaInfo.new ['-S', @state_file]
|
||||
end
|
||||
|
||||
# Prints summarized data. Example:
|
||||
# ```
|
||||
# Body ────────── req/sec ────────── ─────── req 50% times ───────
|
||||
# KB array chunk string io array chunk string io
|
||||
# 1 13760 13492 13817 9610 0.744 0.759 0.740 1.160
|
||||
# 10 13536 13077 13492 9269 0.759 0.785 0.760 1.190
|
||||
# ```
|
||||
#
|
||||
# @param summaries [Hash] generated in subclasses
|
||||
#
|
||||
def overall_summary(summaries)
|
||||
names = ''.dup
|
||||
@body_types.each { |_, t_desc| names << t_desc.rjust(8) }
|
||||
|
||||
puts "\nBody ────────── req/sec ────────── ─────── req 50% times ───────" \
|
||||
"\n KB #{names.ljust 32}#{names}"
|
||||
|
||||
len = @body_types.length
|
||||
digits = [4 - Math.log10(@max_050_time).to_i, 3].min
|
||||
|
||||
fmt_rps = ('%6d ' * len).strip
|
||||
fmt_times = (digits < 0 ? " %6d" : " %6.#{digits}f") * len
|
||||
|
||||
@body_sizes.each do |size|
|
||||
line = format '%-5d ', size
|
||||
resp = ''
|
||||
line << format(fmt_rps , *@body_types.map { |_, t_desc| summaries[size][t_desc][:rps] }).ljust(30)
|
||||
line << format(fmt_times, *@body_types.map { |_, t_desc| summaries[size][t_desc][:times_summary][0.5] })
|
||||
puts line
|
||||
end
|
||||
puts '─' * 69
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,175 @@
|
|||
#!/bin/bash
|
||||
|
||||
# -T client threads (wrk -t)
|
||||
# -c connections per client thread
|
||||
# -R requests per client
|
||||
#
|
||||
# Total connections/requests = l * c * r
|
||||
#
|
||||
# -b response body size kB
|
||||
# -d app delay
|
||||
#
|
||||
# -s Puma bind socket type, default ssl, also tcp or unix
|
||||
# -t Puma threads
|
||||
# -w Puma workers
|
||||
# -r Puma rackup file
|
||||
|
||||
if [[ "$@" =~ ^[^-].* ]]; then
|
||||
echo "Error: Invalid option was specified $1"
|
||||
exit
|
||||
fi
|
||||
|
||||
PUMA_BENCH_CMD=$0
|
||||
PUMA_BENCH_ARGS=$@
|
||||
|
||||
export PUMA_BENCH_CMD
|
||||
export PUMA_BENCH_ARGS
|
||||
|
||||
if [ -z "$PUMA_TEST_HOST4" ]; then export PUMA_TEST_HOST4=127.0.0.1; fi
|
||||
if [ -z "$PUMA_TEST_HOST6" ]; then export PUMA_TEST_HOST6=::1; fi
|
||||
if [ -z "$PUMA_TEST_PORT" ]; then export PUMA_TEST_PORT=40001; fi
|
||||
if [ -z "$PUMA_TEST_CTRL" ]; then export PUMA_TEST_CTRL=40010; fi
|
||||
if [ -z "$PUMA_TEST_STATE" ]; then export PUMA_TEST_STATE=tmp/bench_test_puma.state; fi
|
||||
|
||||
export PUMA_CTRL=$PUMA_TEST_HOST4:$PUMA_TEST_CTRL
|
||||
|
||||
while getopts :b:C:c:D:d:R:r:s:T:t:w:Y option
|
||||
do
|
||||
case "${option}" in
|
||||
#———————————————————— RUBY options
|
||||
Y) export RUBYOPT=--yjit;;
|
||||
#———————————————————— Puma options
|
||||
C) conf=${OPTARG};;
|
||||
t) threads=${OPTARG};;
|
||||
w) workers=${OPTARG};;
|
||||
r) rackup_file=${OPTARG};;
|
||||
#———————————————————— app/common options
|
||||
b) body_conf=${OPTARG};;
|
||||
s) skt_type=${OPTARG};;
|
||||
d) dly_app=${OPTARG};;
|
||||
#———————————————————— request_stream options
|
||||
T) stream_threads=${OPTARG};;
|
||||
D) duration=${OPTARG};;
|
||||
R) req_per_socket=${OPTARG};;
|
||||
#———————————————————— wrk options
|
||||
c) connections=${OPTARG};;
|
||||
# T) stream_threads=${OPTARG};;
|
||||
# D) duration=${OPTARG};;
|
||||
?) echo "Error: Invalid option was specified -$OPTARG"; exit;;
|
||||
esac
|
||||
done
|
||||
|
||||
# -n not empty, -z is empty
|
||||
|
||||
ruby_args="-S $PUMA_TEST_STATE"
|
||||
|
||||
if [ -n "$connections" ]; then
|
||||
ruby_args="$ruby_args -c$connections"
|
||||
fi
|
||||
|
||||
if [ -n "$stream_threads" ]; then
|
||||
ruby_args="$ruby_args -T$stream_threads"
|
||||
fi
|
||||
|
||||
if [ -n "$duration" ] ; then
|
||||
ruby_args="$ruby_args -D$duration"
|
||||
fi
|
||||
|
||||
if [ -n "$req_per_socket" ]; then
|
||||
ruby_args="$ruby_args -R$req_per_socket"
|
||||
fi
|
||||
|
||||
if [ -n "$dly_app" ]; then
|
||||
ruby_args="$ruby_args -d$dly_app"
|
||||
fi
|
||||
|
||||
if [ -n "$body_conf" ]; then
|
||||
ruby_args="$ruby_args -b $body_conf"
|
||||
export CI_BODY_CONF=$body_conf
|
||||
fi
|
||||
|
||||
if [ -z "$skt_type" ]; then
|
||||
skt_type=tcp
|
||||
fi
|
||||
|
||||
ruby_args="$ruby_args -s $skt_type"
|
||||
|
||||
puma_args="-S $PUMA_TEST_STATE"
|
||||
|
||||
if [ -n "$workers" ]; then
|
||||
puma_args="$puma_args -w$workers"
|
||||
ruby_args="$ruby_args -w$workers"
|
||||
fi
|
||||
|
||||
if [ -z "$threads" ]; then
|
||||
threads=0:5
|
||||
fi
|
||||
|
||||
puma_args="$puma_args -t$threads"
|
||||
ruby_args="$ruby_args -t$threads"
|
||||
|
||||
if [ -n "$conf" ]; then
|
||||
puma_args="$puma_args -C $conf"
|
||||
fi
|
||||
|
||||
if [ -z "$rackup_file" ]; then
|
||||
rackup_file="test/rackup/ci_select.ru"
|
||||
fi
|
||||
|
||||
ip4=$PUMA_TEST_HOST4:$PUMA_TEST_PORT
|
||||
ip6=[$PUMA_TEST_HOST6]:$PUMA_TEST_PORT
|
||||
|
||||
case $skt_type in
|
||||
ssl4)
|
||||
bind="ssl://$PUMA_TEST_HOST4:$PUMA_TEST_PORT?cert=examples/puma/cert_puma.pem&key=examples/puma/puma_keypair.pem&verify_mode=none"
|
||||
curl_str=https://$ip4
|
||||
wrk_str=https://$ip4
|
||||
;;
|
||||
ssl)
|
||||
bind="ssl://$ip4?cert=examples/puma/cert_puma.pem&key=examples/puma/puma_keypair.pem&verify_mode=none"
|
||||
curl_str=https://$ip4
|
||||
wrk_str=https://$ip4
|
||||
;;
|
||||
ssl6)
|
||||
bind="ssl://$ip6?cert=examples/puma/cert_puma.pem&key=examples/puma/puma_keypair.pem&verify_mode=none"
|
||||
curl_str=https://$ip6
|
||||
wrk_str=https://$ip6
|
||||
;;
|
||||
tcp4)
|
||||
bind=tcp://$ip4
|
||||
curl_str=http://$ip4
|
||||
wrk_str=http://$ip4
|
||||
;;
|
||||
tcp)
|
||||
bind=tcp://$ip4
|
||||
curl_str=http://$ip4
|
||||
wrk_str=http://$ip4
|
||||
;;
|
||||
tcp6)
|
||||
bind=tcp://$ip6
|
||||
curl_str=http://$ip6
|
||||
wrk_str=http://$ip6
|
||||
;;
|
||||
unix)
|
||||
bind=unix://tmp/benchmark_skt.unix
|
||||
curl_str="--unix-socket tmp/benchmark_skt.unix http:/n"
|
||||
;;
|
||||
aunix)
|
||||
bind=unix://@benchmark_skt.aunix
|
||||
curl_str="--abstract-unix-socket benchmark_skt.aunix http:/n"
|
||||
;;
|
||||
*)
|
||||
echo "Error: Invalid socket type option was specified '$skt_type'"
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
|
||||
StartPuma()
|
||||
{
|
||||
if [ -n "$1" ]; then
|
||||
rackup_file=$1
|
||||
fi
|
||||
printf "\nbundle exec bin/puma -q -b $bind $puma_args --control-url=tcp://$PUMA_CTRL --control-token=test $rackup_file\n\n"
|
||||
bundle exec bin/puma -q -b $bind $puma_args --control-url=tcp://$PUMA_CTRL --control-token=test $rackup_file &
|
||||
sleep 6s
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'optparse'
|
||||
require_relative '../../lib/puma/state_file'
|
||||
require_relative '../../lib/puma/const'
|
||||
require_relative '../../lib/puma/detect'
|
||||
require_relative '../../lib/puma/configuration'
|
||||
require 'uri'
|
||||
require 'socket'
|
||||
require 'json'
|
||||
|
||||
module TestPuma
|
||||
|
||||
# Similar to puma_ctl.rb, but returns objects. Command list is minimal.
|
||||
#
|
||||
class PumaInfo
|
||||
# @version 5.0.0
|
||||
PRINTABLE_COMMANDS = %w{gc-stats stats stop thread-backtraces}.freeze
|
||||
|
||||
COMMANDS = (PRINTABLE_COMMANDS + %w{gc}).freeze
|
||||
|
||||
attr_reader :master_pid
|
||||
|
||||
def initialize(argv, stdout=STDOUT, stderr=STDERR)
|
||||
@state = nil
|
||||
@quiet = false
|
||||
@pidfile = nil
|
||||
@pid = nil
|
||||
@control_url = nil
|
||||
@control_auth_token = nil
|
||||
@config_file = nil
|
||||
@command = nil
|
||||
@environment = ENV['RACK_ENV'] || ENV['RAILS_ENV']
|
||||
|
||||
@argv = argv
|
||||
@stdout = stdout
|
||||
@stderr = stderr
|
||||
@cli_options = {}
|
||||
|
||||
opts = OptionParser.new do |o|
|
||||
o.banner = "Usage: pumactl (-p PID | -P pidfile | -S status_file | -C url -T token | -F config.rb) (#{PRINTABLE_COMMANDS.join("|")})"
|
||||
|
||||
o.on "-S", "--state PATH", "Where the state file to use is" do |arg|
|
||||
@state = arg
|
||||
end
|
||||
|
||||
o.on "-Q", "--quiet", "Not display messages" do |arg|
|
||||
@quiet = true
|
||||
end
|
||||
|
||||
o.on "-C", "--control-url URL", "The bind url to use for the control server" do |arg|
|
||||
@control_url = arg
|
||||
end
|
||||
|
||||
o.on "-T", "--control-token TOKEN", "The token to use as authentication for the control server" do |arg|
|
||||
@control_auth_token = arg
|
||||
end
|
||||
|
||||
o.on "-F", "--config-file PATH", "Puma config script" do |arg|
|
||||
@config_file = arg
|
||||
end
|
||||
|
||||
o.on "-e", "--environment ENVIRONMENT",
|
||||
"The environment to run the Rack app on (default development)" do |arg|
|
||||
@environment = arg
|
||||
end
|
||||
|
||||
o.on_tail("-H", "--help", "Show this message") do
|
||||
@stdout.puts o
|
||||
exit
|
||||
end
|
||||
|
||||
o.on_tail("-V", "--version", "Show version") do
|
||||
@stdout.puts Const::PUMA_VERSION
|
||||
exit
|
||||
end
|
||||
end
|
||||
|
||||
opts.order!(argv) { |a| opts.terminate a }
|
||||
opts.parse!
|
||||
|
||||
unless @config_file == '-'
|
||||
environment = @environment || 'development'
|
||||
|
||||
if @config_file.nil?
|
||||
@config_file = %W(config/puma/#{environment}.rb config/puma.rb).find do |f|
|
||||
File.exist?(f)
|
||||
end
|
||||
end
|
||||
|
||||
if @config_file
|
||||
config = Puma::Configuration.new({ config_files: [@config_file] }, {})
|
||||
config.load
|
||||
@state ||= config.options[:state]
|
||||
@control_url ||= config.options[:control_url]
|
||||
@control_auth_token ||= config.options[:control_auth_token]
|
||||
@pidfile ||= config.options[:pidfile]
|
||||
end
|
||||
end
|
||||
|
||||
@master_pid = File.binread(@state)[/^pid: +(\d+)/, 1].to_i
|
||||
|
||||
rescue => e
|
||||
@stdout.puts e.message
|
||||
exit 1
|
||||
end
|
||||
|
||||
def message(msg)
|
||||
@stdout.puts msg unless @quiet
|
||||
end
|
||||
|
||||
def prepare_configuration
|
||||
if @state
|
||||
unless File.exist? @state
|
||||
raise "State file not found: #{@state}"
|
||||
end
|
||||
|
||||
sf = Puma::StateFile.new
|
||||
sf.load @state
|
||||
|
||||
@control_url = sf.control_url
|
||||
@control_auth_token = sf.control_auth_token
|
||||
@pid = sf.pid
|
||||
end
|
||||
end
|
||||
|
||||
def send_request
|
||||
uri = URI.parse @control_url
|
||||
|
||||
# create server object by scheme
|
||||
server =
|
||||
case uri.scheme
|
||||
when 'ssl'
|
||||
require 'openssl'
|
||||
OpenSSL::SSL::SSLSocket.new(
|
||||
TCPSocket.new(uri.host, uri.port),
|
||||
OpenSSL::SSL::SSLContext.new)
|
||||
.tap { |ssl| ssl.sync_close = true } # default is false
|
||||
.tap(&:connect)
|
||||
when 'tcp'
|
||||
TCPSocket.new uri.host, uri.port
|
||||
when 'unix'
|
||||
# check for abstract UNIXSocket
|
||||
UNIXSocket.new(@control_url.start_with?('unix://@') ?
|
||||
"\0#{uri.host}#{uri.path}" : "#{uri.host}#{uri.path}")
|
||||
else
|
||||
raise "Invalid scheme: #{uri.scheme}"
|
||||
end
|
||||
|
||||
url = "/#{@command}"
|
||||
|
||||
if @control_auth_token
|
||||
url = url + "?token=#{@control_auth_token}"
|
||||
end
|
||||
|
||||
server.syswrite "GET #{url} HTTP/1.0\r\n\r\n"
|
||||
|
||||
unless data = server.read
|
||||
raise 'Server closed connection before responding'
|
||||
end
|
||||
|
||||
response = data.split("\r\n")
|
||||
|
||||
if response.empty?
|
||||
raise "Server sent empty response"
|
||||
end
|
||||
|
||||
@http, @code, @message = response.first.split(' ',3)
|
||||
|
||||
if @code == '403'
|
||||
raise 'Unauthorized access to server (wrong auth token)'
|
||||
elsif @code == '404'
|
||||
raise "Command error: #{response.last}"
|
||||
elsif @code == '500' && @command == 'stop-sigterm'
|
||||
# expected with stop-sigterm
|
||||
elsif @code != '200'
|
||||
raise "Bad response from server: #{@code}"
|
||||
end
|
||||
return unless PRINTABLE_COMMANDS.include? @command
|
||||
JSON.parse response.last, {symbolize_names: true}
|
||||
ensure
|
||||
if server
|
||||
if uri.scheme == 'ssl'
|
||||
server.sysclose
|
||||
else
|
||||
server.close unless server.closed?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def run(cmd)
|
||||
return unless COMMANDS.include?(cmd)
|
||||
@command = cmd
|
||||
prepare_configuration
|
||||
send_request
|
||||
rescue => e
|
||||
message e.message
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,240 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'bench_base'
|
||||
require_relative 'puma_info'
|
||||
|
||||
module TestPuma
|
||||
|
||||
# This file is called from `response_time_wrk.sh`. It requires `wrk`.
|
||||
# We suggest using https://github.com/ioquatix/wrk
|
||||
#
|
||||
# It starts a `Puma` server, then collects data from one or more runs of wrk.
|
||||
# It logs the wrk data as each wrk runs is done, then summarizes
|
||||
# the data in two tables.
|
||||
#
|
||||
# The default runs a matrix of the following, and takes a bit over 5 minutes,
|
||||
# with 28 (4x7) wrk runs:
|
||||
#
|
||||
# bodies - array, chunk, string, io<br/>
|
||||
#
|
||||
# sizes - 1k, 10k, 100k, 256k, 512k, 1024k, 2048k
|
||||
#
|
||||
# See the file 'Testing - benchmark/local files' for sample output and information
|
||||
# on arguments for the shell script.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# * `benchmarks/local/response_time_wrk.sh -w2 -t5:5 -s tcp6 -Y`<br/>
|
||||
# 2 Puma workers, Puma threads 5:5, IPv6 http, 28 wrk runs with matrix above
|
||||
#
|
||||
# * `benchmarks/local/response_time_wrk.sh -t6:6 -s tcp -Y -b ac10,50,100`<br/>
|
||||
# Puma single mode (0 workers), Puma threads 6:6, IPv4 http, six wrk runs,
|
||||
# [array, chunk] * [10kb, 50kb, 100kb]
|
||||
#
|
||||
class ResponseTimeWrk < ResponseTimeBase
|
||||
|
||||
def run
|
||||
time_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
super
|
||||
# default values
|
||||
@duration ||= 10
|
||||
max_threads = (@threads[/\d+\z/] || 5).to_i
|
||||
@stream_threads ||= (0.8 * (@workers || 1) * max_threads).to_i
|
||||
connections = @stream_threads * (@wrk_connections || 2)
|
||||
|
||||
warm_up
|
||||
|
||||
@max_100_time = 0
|
||||
@max_050_time = 0
|
||||
@errors = false
|
||||
|
||||
summaries = Hash.new { |h,k| h[k] = {} }
|
||||
|
||||
@single_size = @body_sizes.length == 1
|
||||
@single_type = @body_types.length == 1
|
||||
|
||||
@body_sizes.each do |size|
|
||||
@body_types.each do |pre, desc|
|
||||
header = @single_size ? "-H '#{HDR_BODY_CONF}#{pre}#{size}'" :
|
||||
"-H '#{HDR_BODY_CONF}#{pre}#{size}'".ljust(21)
|
||||
|
||||
# warmup?
|
||||
if pre == :i
|
||||
wrk_cmd = %Q[wrk -t#{@stream_threads} -c#{connections} -d1s --latency #{header} #{@wrk_bind_str}]
|
||||
%x[#{wrk_cmd}]
|
||||
end
|
||||
|
||||
wrk_cmd = %Q[wrk -t#{@stream_threads} -c#{connections} -d#{@duration}s --latency #{header} #{@wrk_bind_str}]
|
||||
hsh = run_wrk_parse wrk_cmd
|
||||
|
||||
@errors ||= hsh.key? :errors
|
||||
|
||||
times = hsh[:times_summary]
|
||||
@max_100_time = times[1.0] if times[1.0] > @max_100_time
|
||||
@max_050_time = times[0.5] if times[0.5] > @max_050_time
|
||||
summaries[size][desc] = hsh
|
||||
end
|
||||
sleep 0.5
|
||||
@puma_info.run 'gc'
|
||||
sleep 2.0
|
||||
end
|
||||
|
||||
run_summaries summaries
|
||||
|
||||
if @single_size || @single_type
|
||||
puts ''
|
||||
else
|
||||
overall_summary(summaries) unless @single_size || @single_type
|
||||
end
|
||||
|
||||
puts "wrk -t#{@stream_threads} -c#{connections} -d#{@duration}s"
|
||||
|
||||
env_log
|
||||
|
||||
rescue => e
|
||||
puts e.class, e.message, e.backtrace
|
||||
ensure
|
||||
puts ''
|
||||
@puma_info.run 'stop'
|
||||
sleep 2
|
||||
running_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - time_start
|
||||
puts format("\n%2d:%d Total Time", (running_time/60).to_i, running_time % 60)
|
||||
end
|
||||
|
||||
# Prints parsed data of each wrk run. Similar to:
|
||||
# ```
|
||||
# Type req/sec 50% 75% 90% 99% 100% Resp Size
|
||||
# ───────────────────────────────────────────────────────────────── 1kB
|
||||
# array 13760 0.74 2.51 5.22 7.76 11.18 2797
|
||||
# ```
|
||||
#
|
||||
# @param summaries [Hash]
|
||||
#
|
||||
def run_summaries(summaries)
|
||||
digits = [4 - Math.log10(@max_100_time).to_i, 3].min
|
||||
|
||||
fmt_vals = "%-6s %6d".dup
|
||||
fmt_vals << (digits < 0 ? " %6d" : " %6.#{digits}f")*5
|
||||
fmt_vals << ' %8d'
|
||||
|
||||
label = @single_type ? 'Size' : 'Type'
|
||||
|
||||
if @errors
|
||||
puts "\n#{label} req/sec 50% 75% 90% 99% 100% Resp Size Errors"
|
||||
desc_width = 83
|
||||
else
|
||||
puts "\n#{label} req/sec 50% 75% 90% 99% 100% Resp Size"
|
||||
desc_width = 65
|
||||
end
|
||||
|
||||
puts format("#{'─' * desc_width} %s", @body_types[0][1]) if @single_type
|
||||
|
||||
@body_sizes.each do |size|
|
||||
puts format("#{'─' * desc_width}%5dkB", size) unless @single_type
|
||||
@body_types.each do |_, t_desc|
|
||||
hsh = summaries[size][t_desc]
|
||||
times = hsh[:times_summary].values
|
||||
desc = @single_type ? size : t_desc
|
||||
# puts format(fmt_vals, desc, hsh[:rps], *times, hsh[:read]/hsh[:requests])
|
||||
puts format(fmt_vals, desc, hsh[:rps], *times, hsh[:resp_size])
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Checks if any body files need to be created, reads all the body files,
|
||||
# then runs a quick 'wrk warmup' command for each body type
|
||||
#
|
||||
def warm_up
|
||||
puts "\nwarm-up"
|
||||
if @body_types.map(&:first).include? :i
|
||||
TestPuma.create_io_files @body_sizes
|
||||
|
||||
# get size files cached
|
||||
if @body_types.include? :i
|
||||
2.times do
|
||||
@body_sizes.each do |size|
|
||||
fn = format "#{Dir.tmpdir}/.puma_response_body_io/body_io_%04d.txt", size
|
||||
t = File.read fn, mode: 'rb'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
size = @body_sizes.length == 1 ? @body_sizes.first : 10
|
||||
|
||||
@body_types.each do |pre, _|
|
||||
header = "-H '#{HDR_BODY_CONF}#{pre}#{size}'".ljust(21)
|
||||
warm_up_cmd = %Q[wrk -t2 -c4 -d1s --latency #{header} #{@wrk_bind_str}]
|
||||
run_wrk_parse warm_up_cmd
|
||||
end
|
||||
puts ''
|
||||
end
|
||||
|
||||
# Experimental - try to see how busy a CI system is.
|
||||
def ci_test_rps
|
||||
host = ENV['HOST']
|
||||
port = ENV['PORT'].to_i
|
||||
|
||||
str = 'a' * 65_500
|
||||
|
||||
server = TCPServer.new host, port
|
||||
|
||||
svr_th = Thread.new do
|
||||
loop do
|
||||
begin
|
||||
Thread.new(server.accept) do |client|
|
||||
client.sysread 65_536
|
||||
client.syswrite str
|
||||
client.close
|
||||
end
|
||||
rescue => e
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
threads = []
|
||||
|
||||
t_st = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
|
||||
100.times do
|
||||
threads << Thread.new do
|
||||
100.times {
|
||||
s = TCPSocket.new host, port
|
||||
s.syswrite str
|
||||
s.sysread 65_536
|
||||
s = nil
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
threads.each(&:join)
|
||||
loops_time = (1_000*(Process.clock_gettime(Process::CLOCK_MONOTONIC) - t_st)).to_i
|
||||
|
||||
threads.clear
|
||||
threads = nil
|
||||
|
||||
server.close
|
||||
svr_th.join
|
||||
|
||||
req_limit =
|
||||
if loops_time > 3_050 then 13_000
|
||||
elsif loops_time > 2_900 then 13_500
|
||||
elsif loops_time > 2_500 then 14_000
|
||||
elsif loops_time > 2_200 then 18_000
|
||||
elsif loops_time > 2_100 then 19_000
|
||||
elsif loops_time > 1_900 then 20_000
|
||||
elsif loops_time > 1_800 then 21_000
|
||||
elsif loops_time > 1_600 then 22_500
|
||||
else 23_000
|
||||
end
|
||||
[req_limit, loops_time]
|
||||
end
|
||||
|
||||
def puts(*ary)
|
||||
ary.each { |s| STDOUT.syswrite "#{s}\n" }
|
||||
end
|
||||
end
|
||||
end
|
||||
TestPuma::ResponseTimeWrk.new.run
|
|
@ -0,0 +1,18 @@
|
|||
#!/bin/bash
|
||||
|
||||
# see comments in response_time_wrk.rb
|
||||
|
||||
source benchmarks/local/bench_base.sh
|
||||
|
||||
if [ "$skt_type" == "unix" ] || [ "$skt_type" == "aunix" ]; then
|
||||
printf "\nwrk doesn't support UNIXSockets...\n\n"
|
||||
exit
|
||||
fi
|
||||
|
||||
StartPuma
|
||||
|
||||
ruby -I./lib benchmarks/local/response_time_wrk.rb $ruby_args -W $wrk_str
|
||||
wrk_exit=$?
|
||||
|
||||
printf "\n"
|
||||
exit $wrk_exit
|
|
@ -0,0 +1,150 @@
|
|||
# Testing - benchmark/local files
|
||||
|
||||
These files generate data that shows request-per-second (RPS), etc. Typically, files are in
|
||||
pairs, a shell script and a Ruby script. The shell script starts the server, then runs the
|
||||
Ruby file, which starts client request stream(s), then collects and logs metrics.
|
||||
|
||||
## response_time_wrk.sh
|
||||
|
||||
This uses [wrk] for generating data. One or more wrk runs are performed. Summarizes RPS and
|
||||
wrk latency times. The default for the `-b` argument runs 28 different client request streams,
|
||||
and takes a bit over 5 minutes. See 'Request Stream Configuration' below for `-b` argument
|
||||
description.
|
||||
|
||||
<details>
|
||||
<summary>Summary output for<br/><code>benchmarks/local/response_time_wrk.sh -w2 -t5:5 -s tcp6</code>:</summary>
|
||||
|
||||
```
|
||||
Type req/sec 50% 75% 90% 99% 100% Resp Size
|
||||
───────────────────────────────────────────────────────────────── 1kB
|
||||
array 13710 0.74 2.52 5.23 7.76 37.45 1024
|
||||
chunk 13502 0.76 2.55 5.28 7.84 11.23 1042
|
||||
string 13794 0.74 2.51 5.20 7.75 14.07 1024
|
||||
io 9615 1.16 3.45 7.13 10.57 15.75 1024
|
||||
───────────────────────────────────────────────────────────────── 10kB
|
||||
array 13458 0.76 2.57 5.31 7.93 13.94 10239
|
||||
chunk 13066 0.78 2.64 5.46 8.18 38.48 10320
|
||||
string 13500 0.76 2.55 5.29 7.88 11.42 10240
|
||||
io 9293 1.18 3.59 7.39 10.94 16.99 10240
|
||||
───────────────────────────────────────────────────────────────── 100kB
|
||||
array 11315 0.96 3.06 6.33 9.49 17.69 102424
|
||||
chunk 9916 1.10 3.48 7.20 10.73 15.14 103075
|
||||
string 10948 1.00 3.17 6.57 9.83 17.88 102378
|
||||
io 8901 1.21 3.72 7.48 11.27 59.98 102407
|
||||
───────────────────────────────────────────────────────────────── 256kB
|
||||
array 9217 1.15 3.82 7.88 11.74 17.12 262212
|
||||
chunk 7339 1.45 4.76 9.81 14.63 22.70 264007
|
||||
string 8574 1.19 3.81 7.73 11.21 15.80 262147
|
||||
io 8911 1.19 3.80 7.55 15.25 60.01 262183
|
||||
───────────────────────────────────────────────────────────────── 512kB
|
||||
array 6951 1.49 5.03 10.28 15.90 25.08 524378
|
||||
chunk 5234 2.03 6.56 13.57 20.46 32.15 527862
|
||||
string 6438 1.55 5.04 10.12 16.28 72.87 524275
|
||||
io 8533 1.15 4.62 8.79 48.15 70.51 524327
|
||||
───────────────────────────────────────────────────────────────── 1024kB
|
||||
array 4122 1.80 15.59 41.87 67.79 121.00 1048565
|
||||
chunk 3158 2.82 15.22 31.00 71.39 99.90 1055654
|
||||
string 4710 2.24 6.66 13.65 20.38 70.44 1048575
|
||||
io 8355 1.23 3.95 7.94 14.08 68.54 1048498
|
||||
───────────────────────────────────────────────────────────────── 2048kB
|
||||
array 2454 4.12 14.02 27.70 43.48 88.89 2097415
|
||||
chunk 1743 6.26 17.65 36.98 55.78 92.10 2111358
|
||||
string 2479 4.38 12.52 25.65 38.44 95.62 2097502
|
||||
io 8264 1.25 3.83 7.76 11.73 65.69 2097090
|
||||
|
||||
Body ────────── req/sec ────────── ─────── req 50% times ───────
|
||||
KB array chunk string io array chunk string io
|
||||
1 13710 13502 13794 9615 0.745 0.757 0.741 1.160
|
||||
10 13458 13066 13500 9293 0.760 0.784 0.759 1.180
|
||||
100 11315 9916 10948 8901 0.960 1.100 1.000 1.210
|
||||
256 9217 7339 8574 8911 1.150 1.450 1.190 1.190
|
||||
512 6951 5234 6438 8533 1.490 2.030 1.550 1.150
|
||||
1024 4122 3158 4710 8355 1.800 2.820 2.240 1.230
|
||||
2048 2454 1743 2479 8264 4.120 6.260 4.380 1.250
|
||||
─────────────────────────────────────────────────────────────────────
|
||||
wrk -t8 -c16 -d10s
|
||||
benchmarks/local/response_time_wrk.sh -w2 -t5:5 -s tcp6 -Y
|
||||
Server cluster mode -w2 -t5:5, bind: tcp6
|
||||
Puma repo branch 00-response-refactor
|
||||
ruby 3.2.0dev (2022-06-14T01:21:55Z master 048f14221c) +YJIT [x86_64-linux]
|
||||
|
||||
[2136] - Gracefully shutting down workers...
|
||||
[2136] === puma shutdown: 2022-06-13 21:16:13 -0500 ===
|
||||
[2136] - Goodbye!
|
||||
|
||||
5:15 Total Time
|
||||
```
|
||||
</details><br/>
|
||||
|
||||
## bench_base.sh, bench_base.rb
|
||||
|
||||
These two files setup parameters for the Puma server, which is normally started in a shell
|
||||
script. It then starts a Ruby file (a subclass of BenchBase), passing arguments to it. The
|
||||
Ruby file is normally used to generate a client request stream(s).
|
||||
|
||||
### Puma Configuration
|
||||
|
||||
The following arguments are used for the Puma server:
|
||||
|
||||
* **`-C`** - configuration file
|
||||
* **`-d`** - app delay
|
||||
* **`-r`** - rackup file, often defaults to test/rackup/ci_select.ru
|
||||
* **`-s`** - bind socket type, default is tcp/tcp4, also tcp6, ssl/ssl4, ssl6, unix, or aunix
|
||||
(unix & abstract unix are not available with wrk).
|
||||
* **`-t`** - threads, expressed as '5:5', same as Puma --thread
|
||||
* **`-w`** - workers, same as Puma --worker
|
||||
* **`-Y`** - enable Ruby YJIT
|
||||
|
||||
### Request Stream Configuration
|
||||
|
||||
The following arguments are used for request streams:
|
||||
|
||||
* **`-b`** - response body configuration. Body type options are a array, c chunked, s string,
|
||||
and i for File/IO. None or any combination can be specified, they should start the option.
|
||||
Then, any combination of comma separated integers can be used for the response body size
|
||||
in kB. The string 'ac50,100' would create four runs, 50kb array, 50kB chunked, 100kB array,
|
||||
and 100kB chunked. See 'Testing - test/rackup/ci-*.ru files' for more info.
|
||||
* **`-c`** - connections per client request stream thread, defaults to 2 for wrk.
|
||||
* **`-D`** - duration of client request stream in seconds.
|
||||
* **`-T`** - number of threads in the client request stream. For wrk, this defaults to
|
||||
80% of Puma workers * max_threads.
|
||||
|
||||
### Notes - Configuration
|
||||
|
||||
The above lists script arguments.
|
||||
|
||||
`bench_base.sh` contains most server defaults. Many can be set via ENV variables.
|
||||
|
||||
`bench_base.rb` contains the client request stream defaults. The default value for
|
||||
`-b` is `acsi1,10,100,256,512,1024,2048`, which is a 4 x 7 matrix, and hence, runs
|
||||
28 jobs. Also, the i body type (File/IO) generates files, they are placed in the
|
||||
`"#{Dir.tmpdir}/.puma_response_body_io"` directory, which is created.
|
||||
|
||||
### Notes - wrk
|
||||
|
||||
The shell scripts use `-T` for wrk's thread count, since `-t` is used for Puma
|
||||
server threads. Regarding the `-c` argument, wrk has an interesting behavior.
|
||||
The total number of connections is set by `(connections/threads).to_i`. The scripts
|
||||
here use `-c` as connections per thread. Hence, using `-T4 -c2` will yield a total
|
||||
of eight wrk connections, two per thread. The equivalent wrk arguments would be `-t4 -c8`.
|
||||
|
||||
Puma can only process so many requests, and requests will queue in the backlog
|
||||
until Puma can respond to them. With wrk, if the number of total connections is
|
||||
too high, one will see the upper latency times increase, pushing into the lower
|
||||
latency times as the connections are increased. The default values for wrk's
|
||||
threads and connections were chosen to minimize requests' time in the backlog.
|
||||
|
||||
An example with four wrk runs using `-b s10`. Notice that `req/sec` varies by
|
||||
less than 1%, but the `75%` times increase by an order of magnitude:
|
||||
```
|
||||
req/sec 50% 75% 90% 99% 100% Resp Size wrk cmd line
|
||||
─────────────────────────────────────────────────────────────────────────────
|
||||
13597 0.755 2.550 5.260 7.800 13.310 12040 wrk -t8 -c16 -d10
|
||||
13549 0.793 4.430 8.140 11.220 16.600 12002 wrk -t10 -c20 -d10
|
||||
13570 1.040 25.790 40.010 49.070 58.300 11982 wrk -t8 -c64 -d10
|
||||
13684 1.050 25.820 40.080 49.160 66.190 12033 wrk -t16 -c64 -d10
|
||||
```
|
||||
Finally, wrk's output may cause rounding errors, so the response body size calculation is
|
||||
imprecise.
|
||||
|
||||
[wrk]: <https://github.com/ioquatix/wrk>
|
|
@ -0,0 +1,36 @@
|
|||
# Testing - test/rackup/ci-*.ru files
|
||||
|
||||
## Overview
|
||||
|
||||
Puma should efficiently handle a variety of response bodies, varying both by size
|
||||
and by the type of object used for the body.
|
||||
|
||||
Five rackup files are located in 'test/rackup' that can be used. All have their
|
||||
request body size (in kB) set via `Body-Conf` header or with `ENV['CI_BODY_CONF']`.
|
||||
Additionally, the ci_select.ru file can have it's body type set via a starting
|
||||
character.
|
||||
|
||||
* **ci_array.ru** - body is an `Array` of 1kB strings. `Content-Length` is not set.
|
||||
* **ci_chunked.ru** - body is an `Enumerator` of 1kB strings. `Content-Length` is not set.
|
||||
* **ci_io.ru** - body is a File/IO object. `Content-Length` is set.
|
||||
* **ci_string.ru** - body is a single string. `Content-Length` is set.
|
||||
* **ci_select.ru** - can be any of the above.
|
||||
|
||||
All responses have 25 headers, total length approx 1kB. ci_array.ru and ci_chunked.ru
|
||||
contain 1kB items.
|
||||
|
||||
All can be delayed by a float value (seconds) specified by the `Dly` header
|
||||
|
||||
Note that rhe `Body-Conf` header takes precedence, and `ENV['CI_BODY_CONF']` is
|
||||
only read on load.
|
||||
|
||||
## ci_select.ru
|
||||
|
||||
The ci_select.ru file allows a starting character to specify the body type in the
|
||||
`Body-Conf` header or with `ENV['CI_BODY_CONF']`.
|
||||
* **a** - array of strings
|
||||
* **c** - chunked (enum)
|
||||
* **s** - single string
|
||||
* **i** - File/IO
|
||||
|
||||
A value of `a100` would return a body as an array of 100 1kB strings.
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Generates a response with array bodies, size set via ENV['CI_BODY_CONF'] or
|
||||
# `Body-Conf` request header.
|
||||
# See 'CI - test/rackup/ci-*.ru files' or docs/test_rackup_ci_files.md
|
||||
|
||||
require 'securerandom'
|
||||
|
||||
headers = {}
|
||||
headers['Content-Type'] = 'text/plain; charset=utf-8'
|
||||
25.times { |i| headers["X-My-Header-#{i}"] = SecureRandom.hex(25) }
|
||||
|
||||
hdr_dly = 'HTTP_DLY'
|
||||
hdr_body_conf = 'HTTP_BODY_CONF'
|
||||
|
||||
# length = 1018 bytesize = 1024
|
||||
str_1kb = "──#{SecureRandom.hex 507}─\n".freeze
|
||||
|
||||
env_len = (t = ENV['CI_BODY_CONF']) ? t[/\d+\z/].to_i : 10
|
||||
|
||||
run lambda { |env|
|
||||
info = if (dly = env[hdr_dly])
|
||||
sleep dly.to_f
|
||||
"#{Process.pid}\nHello World\nSlept #{dly}\n"
|
||||
else
|
||||
"#{Process.pid}\nHello World\n"
|
||||
end
|
||||
info_len_adj = 1023 - info.bytesize
|
||||
|
||||
len = (t = env[hdr_body_conf]) ? t[/\d+\z/].to_i : env_len
|
||||
|
||||
body = Array.new len, str_1kb
|
||||
body[0] = info + str_1kb.byteslice(0, info_len_adj) + "\n"
|
||||
headers[hdr_content_length] = (1_024 * len).to_s
|
||||
[200, headers, body]
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Generates a response with chunked bodies, size set via ENV['CI_BODY_CONF'] or
|
||||
# `Body-Conf` request header.
|
||||
# See 'CI - test/rackup/ci-*.ru files' or docs/test_rackup_ci_files.md
|
||||
|
||||
require 'securerandom'
|
||||
|
||||
headers = {}
|
||||
headers['Content-Type'] = 'text/plain; charset=utf-8'
|
||||
25.times { |i| headers["X-My-Header-#{i}"] = SecureRandom.hex(25) }
|
||||
|
||||
hdr_dly = 'HTTP_DLY'
|
||||
hdr_body_conf = 'HTTP_BODY_CONF'
|
||||
|
||||
# length = 1018 bytesize = 1024
|
||||
str_1kb = "──#{SecureRandom.hex 507}─\n".freeze
|
||||
|
||||
env_len = (t = ENV['CI_BODY_CONF']) ? t[/\d+\z/].to_i : 10
|
||||
|
||||
run lambda { |env|
|
||||
info = if (dly = env[hdr_dly])
|
||||
sleep dly.to_f
|
||||
"#{Process.pid}\nHello World\nSlept #{dly}\n"
|
||||
else
|
||||
"#{Process.pid}\nHello World\n"
|
||||
end
|
||||
info_len_adj = 1023 - info.bytesize
|
||||
|
||||
len = (t = env[hdr_body_conf]) ? t[/\d+\z/].to_i : env_len
|
||||
|
||||
temp = Array.new len, str_1kb
|
||||
temp[0] = info + str_1kb.byteslice(0, info_len_adj) + "\n"
|
||||
body = temp.to_enum
|
||||
[200, headers, body]
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Generates a response with File/IO bodies, size set via ENV['CI_BODY_CONF'] or
|
||||
# `Body-Conf` request header.
|
||||
# See 'CI - test/rackup/ci-*.ru files' or docs/test_rackup_ci_files.md
|
||||
|
||||
require 'securerandom'
|
||||
require 'tmpdir'
|
||||
|
||||
headers = {}
|
||||
headers['Content-Type'] = 'text/plain; charset=utf-8'
|
||||
25.times { |i| headers["X-My-Header-#{i}"] = SecureRandom.hex(25) }
|
||||
|
||||
hdr_dly = 'HTTP_DLY'
|
||||
hdr_body_conf = 'HTTP_BODY_CONF'
|
||||
hdr_content_length = 'Content-Length'
|
||||
|
||||
env_len = (t = ENV['CI_BODY_CONF']) ? t[/\d+\z/].to_i : 10
|
||||
|
||||
tmp_folder = "#{Dir.tmpdir}/.puma_response_body_io"
|
||||
|
||||
unless Dir.exist? tmp_folder
|
||||
STDOUT.syswrite "\nNeeded files do not exist. Run `TestPuma.create_io_files" \
|
||||
" contained in benchmarks/local/bench_base.rb\n"
|
||||
exit 1
|
||||
end
|
||||
|
||||
fn_format = "#{tmp_folder}/body_io_%04d.txt"
|
||||
|
||||
run lambda { |env|
|
||||
if (dly = env[hdr_dly])
|
||||
sleep dly.to_f
|
||||
end
|
||||
len = (t = env[hdr_body_conf]) ? t[/\d+\z/].to_i : env_len
|
||||
headers[hdr_content_length] = (1024*len).to_s
|
||||
fn = format fn_format, len
|
||||
body = File.open fn
|
||||
[200, headers, body]
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Generates a response with various body types and sizes, set via ENV['CI_BODY_CONF'] or
|
||||
# `Body-Conf` request header.
|
||||
# See 'CI - test/rackup/ci-*.ru files' or docs/test_rackup_ci_files.md
|
||||
|
||||
require 'securerandom'
|
||||
require 'tmpdir'
|
||||
|
||||
headers = {}
|
||||
headers['Content-Type'] = 'text/plain; charset=utf-8'
|
||||
25.times { |i| headers["X-My-Header-#{i}"] = SecureRandom.hex(25) }
|
||||
|
||||
hdr_dly = 'HTTP_DLY'
|
||||
hdr_body_conf = 'HTTP_BODY_CONF'
|
||||
hdr_content_length = 'Content-Length'
|
||||
|
||||
# length = 1018 bytesize = 1024
|
||||
str_1kb = "──#{SecureRandom.hex 507}─\n".freeze
|
||||
|
||||
fn_format = "#{Dir.tmpdir}/.puma_response_body_io/body_io_%04d.txt"
|
||||
|
||||
body_types = %w[a c i s].freeze
|
||||
|
||||
run lambda { |env|
|
||||
info = if (dly = env[hdr_dly])
|
||||
sleep dly.to_f
|
||||
"#{Process.pid}\nHello World\nSlept #{dly}\n"
|
||||
else
|
||||
"#{Process.pid}\nHello World\n"
|
||||
end
|
||||
info_len_adj = 1023 - info.bytesize
|
||||
|
||||
body_conf = env[hdr_body_conf]
|
||||
|
||||
if body_conf && body_conf.start_with?(*body_types)
|
||||
type = body_conf.slice!(0).to_sym
|
||||
len = body_conf.to_i
|
||||
elsif body_conf
|
||||
type = :s
|
||||
len = body_conf[/\d+\z/].to_i
|
||||
else # default
|
||||
type = :s
|
||||
len = 1
|
||||
end
|
||||
|
||||
case type
|
||||
when :a # body is an array
|
||||
headers[hdr_content_length] = (1_024 * len).to_s
|
||||
body = Array.new len, str_1kb
|
||||
body[0] = info + str_1kb.byteslice(0, info_len_adj) + "\n"
|
||||
when :c # body is chunked
|
||||
headers.delete hdr_content_length
|
||||
temp = Array.new len, str_1kb
|
||||
temp[0] = info + str_1kb.byteslice(0, info_len_adj) + "\n"
|
||||
body = temp.to_enum
|
||||
when :i # body is an io
|
||||
headers[hdr_content_length] = (1_024 * len).to_s
|
||||
fn = format fn_format, len
|
||||
body = File.open fn, 'rb'
|
||||
when :s # body is a single string in an array
|
||||
headers[hdr_content_length] = (1_024 * len).to_s
|
||||
info << str_1kb.byteslice(0, info_len_adj) << "\n" << (str_1kb * (len-1))
|
||||
body = [info]
|
||||
end
|
||||
[200, headers, body]
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Generates a response with single string bodies, size set via ENV['CI_BODY_CONF'] or
|
||||
# `Body-Conf` request header.
|
||||
# See 'CI - test/rackup/ci-*.ru files' or docs/test_rackup_ci_files.md
|
||||
|
||||
require 'securerandom'
|
||||
|
||||
env_len = (t = ENV['CI_BODY_CONF']) ? t[/\d+\z/].to_i : 10
|
||||
|
||||
headers = {}
|
||||
headers['Content-Type'] = 'text/plain; charset=utf-8'
|
||||
25.times { |i| headers["X-My-Header-#{i}"] = SecureRandom.hex(25) }
|
||||
|
||||
hdr_dly = 'HTTP_DLY'
|
||||
hdr_body_conf = 'HTTP_BODY_CONF'
|
||||
hdr_content_length = 'Content-Length'
|
||||
|
||||
# length = 1018 bytesize = 1024
|
||||
str_1kb = "──#{SecureRandom.hex 507}─\n".freeze
|
||||
|
||||
env_len = (t = ENV['CI_BODY_CONF']) ? t[/\d+\z/].to_i : 10
|
||||
|
||||
run lambda { |env|
|
||||
info = if (dly = env[hdr_dly])
|
||||
sleep dly.to_f
|
||||
"#{Process.pid}\nHello World\nSlept #{dly}\n"
|
||||
else
|
||||
"#{Process.pid}\nHello World\n"
|
||||
end
|
||||
info_len_adj = 1023 - info.bytesize
|
||||
|
||||
len = (t = env[hdr_body_conf]) ? t[/\d+\z/].to_i : env_len
|
||||
|
||||
info << str_1kb.byteslice(0, info_len_adj) << "\n" << (str_1kb * (len-1))
|
||||
headers[hdr_content_length] = (1_024 * len).to_s
|
||||
[200, headers, [info]]
|
||||
}
|
|
@ -1 +1,3 @@
|
|||
run lambda { |env| [200, {"Content-Type" => "text/plain"}, ["Hello World"]] }
|
||||
hdrs = {'Content-Type'.freeze => 'text/plain'.freeze}.freeze
|
||||
body = ['Hello World'.freeze].freeze
|
||||
run lambda { |env| [200, hdrs, body] }
|
||||
|
|
Loading…
Reference in New Issue