2013-08-15 01:19:23 -04:00
|
|
|
require "socket"
|
|
|
|
require "csv"
|
2013-08-15 02:35:57 -04:00
|
|
|
require "yaml"
|
2013-12-10 18:32:23 -05:00
|
|
|
require "optparse"
|
2014-12-03 20:30:00 -05:00
|
|
|
require "fileutils"
|
2013-12-10 18:32:23 -05:00
|
|
|
|
|
|
|
@include_env = false
|
|
|
|
@result_file = nil
|
2013-12-29 23:15:30 -05:00
|
|
|
@iterations = 500
|
2014-01-08 23:56:03 -05:00
|
|
|
@best_of = 1
|
2014-02-13 23:43:08 -05:00
|
|
|
@mem_stats = false
|
2014-02-16 00:44:51 -05:00
|
|
|
@unicorn = false
|
2014-12-07 17:54:35 -05:00
|
|
|
@dump_heap = false
|
2014-02-13 23:43:08 -05:00
|
|
|
|
2013-12-10 18:32:23 -05:00
|
|
|
opts = OptionParser.new do |o|
|
|
|
|
o.banner = "Usage: ruby bench.rb [options]"
|
|
|
|
|
|
|
|
o.on("-n", "--with_default_env", "Include recommended Discourse env") do
|
|
|
|
@include_env = true
|
|
|
|
end
|
|
|
|
o.on("-o", "--output [FILE]", "Output results to this file") do |f|
|
|
|
|
@result_file = f
|
|
|
|
end
|
2013-12-29 23:15:30 -05:00
|
|
|
o.on("-i", "--iterations [ITERATIONS]", "Number of iterations to run the bench for") do |i|
|
|
|
|
@iterations = i.to_i
|
|
|
|
end
|
2014-01-08 23:56:03 -05:00
|
|
|
o.on("-b", "--best_of [NUM]", "Number of times to run the bench taking best as result") do |i|
|
|
|
|
@best_of = i.to_i
|
|
|
|
end
|
2014-12-07 17:54:35 -05:00
|
|
|
o.on("-d", "--heap_dump") do
|
|
|
|
@dump_heap = true
|
|
|
|
# We need an env var for config/boot.rb to enable allocation tracing prior to framework init
|
|
|
|
ENV['DISCOURSE_DUMP_HEAP'] = "1"
|
|
|
|
end
|
2014-02-13 23:43:08 -05:00
|
|
|
o.on("-m", "--memory_stats") do
|
|
|
|
@mem_stats = true
|
|
|
|
end
|
2014-02-16 00:44:51 -05:00
|
|
|
o.on("-u", "--unicorn", "Use unicorn to serve pages as opposed to thin") do
|
|
|
|
@unicorn = true
|
|
|
|
end
|
2013-12-10 18:32:23 -05:00
|
|
|
end
|
|
|
|
opts.parse!
|
2013-08-15 02:35:57 -04:00
|
|
|
|
2014-01-08 23:56:03 -05:00
|
|
|
def run(command, opt = nil)
|
2014-12-23 01:29:44 -05:00
|
|
|
exit_status =
|
|
|
|
if opt == :quiet
|
|
|
|
system(command, out: "/dev/null", err: :out)
|
|
|
|
else
|
|
|
|
system(command, out: $stdout, err: :out)
|
|
|
|
end
|
|
|
|
|
2017-04-14 12:58:35 -04:00
|
|
|
abort("Command '#{command}' failed with exit status #{$?}") unless exit_status
|
2013-08-15 01:32:07 -04:00
|
|
|
end
|
|
|
|
|
2013-08-29 07:23:00 -04:00
|
|
|
begin
|
|
|
|
require 'facter'
|
|
|
|
rescue LoadError
|
|
|
|
run "gem install facter"
|
2017-04-14 12:58:35 -04:00
|
|
|
# Facter requires CFPropertyList, but doesn't install it.
|
|
|
|
run "gem install CFPropertyList"
|
2014-01-02 21:03:58 -05:00
|
|
|
puts "please rerun script"
|
2014-01-01 18:21:01 -05:00
|
|
|
exit
|
2013-08-29 07:23:00 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
@timings = {}
|
|
|
|
|
2013-08-15 02:35:57 -04:00
|
|
|
def measure(name)
|
|
|
|
start = Time.now
|
|
|
|
yield
|
|
|
|
@timings[name] = ((Time.now - start) * 1000).to_i
|
|
|
|
end
|
|
|
|
|
2013-08-15 01:37:33 -04:00
|
|
|
def prereqs
|
|
|
|
puts "Be sure to following packages are installed:
|
|
|
|
|
2014-05-29 00:10:34 -04:00
|
|
|
sudo apt-get -y install build-essential libssl-dev libyaml-dev git libtool libxslt-dev libxml2-dev libpq-dev gawk curl pngcrush python-software-properties software-properties-common tasksel
|
2014-05-29 00:35:41 -04:00
|
|
|
|
2013-08-15 01:37:33 -04:00
|
|
|
sudo tasksel install postgresql-server
|
2014-05-29 00:35:41 -04:00
|
|
|
OR
|
|
|
|
apt-get install postgresql-server^
|
2013-08-15 01:37:33 -04:00
|
|
|
|
|
|
|
sudo apt-add-repository -y ppa:rwky/redis
|
|
|
|
sudo apt-get update
|
|
|
|
sudo apt-get install redis-server
|
|
|
|
"
|
|
|
|
end
|
|
|
|
|
2013-08-15 01:19:23 -04:00
|
|
|
puts "Running bundle"
|
2014-12-23 01:29:44 -05:00
|
|
|
if run("bundle", :quiet)
|
2013-08-15 01:32:07 -04:00
|
|
|
puts "Quitting, some of the gems did not install"
|
2013-08-15 01:37:33 -04:00
|
|
|
prereqs
|
2013-08-15 01:32:07 -04:00
|
|
|
exit
|
|
|
|
end
|
2013-08-15 01:19:23 -04:00
|
|
|
|
|
|
|
puts "Ensuring config is setup"
|
|
|
|
|
2014-01-01 18:21:01 -05:00
|
|
|
%x{which ab > /dev/null 2>&1}
|
|
|
|
unless $? == 0
|
2013-08-15 01:19:23 -04:00
|
|
|
abort "Apache Bench is not installed. Try: apt-get install apache2-utils or brew install ab"
|
|
|
|
end
|
|
|
|
|
|
|
|
unless File.exists?("config/database.yml")
|
|
|
|
puts "Copying database.yml.development.sample to database.yml"
|
|
|
|
`cp config/database.yml.development-sample config/database.yml`
|
|
|
|
end
|
|
|
|
|
|
|
|
ENV["RAILS_ENV"] = "profile"
|
2013-10-12 17:06:45 -04:00
|
|
|
|
2014-12-07 17:54:35 -05:00
|
|
|
discourse_env_vars = %w(DISCOURSE_DUMP_HEAP RUBY_GC_HEAP_INIT_SLOTS RUBY_GC_HEAP_FREE_SLOTS RUBY_GC_HEAP_GROWTH_FACTOR RUBY_GC_HEAP_GROWTH_MAX_SLOTS RUBY_GC_MALLOC_LIMIT RUBY_GC_OLDMALLOC_LIMIT RUBY_GC_MALLOC_LIMIT_MAX RUBY_GC_OLDMALLOC_LIMIT_MAX RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR)
|
2014-12-03 10:11:03 -05:00
|
|
|
|
2013-12-10 18:32:23 -05:00
|
|
|
if @include_env
|
|
|
|
puts "Running with tuned environment"
|
2014-12-07 17:54:35 -05:00
|
|
|
discourse_env_vars - %w(RUBY_GC_MALLOC_LIMIT).each do |v|
|
2014-12-03 10:11:03 -05:00
|
|
|
ENV.delete v
|
|
|
|
end
|
2013-12-10 18:32:23 -05:00
|
|
|
else
|
|
|
|
# clean env
|
2013-12-29 23:15:30 -05:00
|
|
|
puts "Running with the following custom environment"
|
2014-12-07 17:54:35 -05:00
|
|
|
discourse_env_vars.each do |w|
|
2013-12-29 23:15:30 -05:00
|
|
|
puts "#{w}: #{ENV[w]}"
|
|
|
|
end
|
2013-10-12 17:06:45 -04:00
|
|
|
end
|
2013-08-15 02:35:57 -04:00
|
|
|
|
2017-07-27 21:20:09 -04:00
|
|
|
def port_available?(port)
|
2013-08-17 05:36:41 -04:00
|
|
|
server = TCPServer.open("0.0.0.0", port)
|
2013-08-15 01:19:23 -04:00
|
|
|
server.close
|
|
|
|
true
|
|
|
|
rescue Errno::EADDRINUSE
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
2013-08-15 03:13:05 -04:00
|
|
|
@port = 60079
|
2013-08-15 01:19:23 -04:00
|
|
|
|
2013-08-15 03:13:05 -04:00
|
|
|
while !port_available? @port
|
|
|
|
@port += 1
|
2013-08-15 01:19:23 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
puts "Ensuring profiling DB exists and is migrated"
|
|
|
|
puts `bundle exec rake db:create`
|
|
|
|
`bundle exec rake db:migrate`
|
|
|
|
|
2013-08-15 02:59:38 -04:00
|
|
|
puts "Timing loading Rails"
|
2013-08-15 02:35:57 -04:00
|
|
|
measure("load_rails") do
|
|
|
|
`bundle exec rake middleware`
|
|
|
|
end
|
2013-08-15 01:19:23 -04:00
|
|
|
|
2013-08-15 02:59:38 -04:00
|
|
|
puts "Populating Profile DB"
|
|
|
|
run("bundle exec ruby script/profile_db_generator.rb")
|
2013-08-15 01:19:23 -04:00
|
|
|
|
2013-09-10 02:03:11 -04:00
|
|
|
puts "Getting api key"
|
2013-09-10 02:22:58 -04:00
|
|
|
api_key = `bundle exec rake api_key:get`.split("\n")[-1]
|
2013-09-10 02:03:11 -04:00
|
|
|
|
2017-04-14 12:58:35 -04:00
|
|
|
def bench(path, name)
|
2013-08-15 01:19:23 -04:00
|
|
|
puts "Running apache bench warmup"
|
2014-02-16 00:44:51 -05:00
|
|
|
add = ""
|
|
|
|
add = "-c 3 " if @unicorn
|
|
|
|
`ab #{add} -n 10 "http://127.0.0.1:#{@port}#{path}"`
|
2017-04-14 12:58:35 -04:00
|
|
|
puts "Benchmarking #{name} @ #{path}"
|
2013-12-29 23:15:30 -05:00
|
|
|
`ab -n #{@iterations} -e tmp/ab.csv "http://127.0.0.1:#{@port}#{path}"`
|
2013-08-15 01:19:23 -04:00
|
|
|
|
|
|
|
percentiles = Hash[*[50, 75, 90, 99].zip([]).flatten]
|
|
|
|
CSV.foreach("tmp/ab.csv") do |percent, time|
|
|
|
|
percentiles[percent.to_i] = time.to_i if percentiles.key? percent.to_i
|
|
|
|
end
|
|
|
|
|
2013-08-15 03:13:05 -04:00
|
|
|
percentiles
|
|
|
|
end
|
|
|
|
|
|
|
|
begin
|
2013-09-10 02:03:11 -04:00
|
|
|
# critical cause cache may be incompatible
|
|
|
|
puts "precompiling assets"
|
|
|
|
run("bundle exec rake assets:precompile")
|
|
|
|
|
2017-07-27 21:20:09 -04:00
|
|
|
pid =
|
|
|
|
if @unicorn
|
|
|
|
ENV['UNICORN_PORT'] = @port.to_s
|
|
|
|
FileUtils.mkdir_p(File.join('tmp', 'pids'))
|
|
|
|
spawn("bundle exec unicorn -c config/unicorn.conf.rb")
|
|
|
|
else
|
|
|
|
spawn("bundle exec thin start -p #{@port}")
|
|
|
|
end
|
2013-08-15 03:13:05 -04:00
|
|
|
|
|
|
|
while port_available? @port
|
|
|
|
sleep 1
|
|
|
|
end
|
|
|
|
|
2013-08-17 05:36:41 -04:00
|
|
|
puts "Starting benchmark..."
|
2014-01-08 23:56:03 -05:00
|
|
|
append = "?api_key=#{api_key}&api_username=admin1"
|
2013-08-17 05:36:41 -04:00
|
|
|
|
2013-09-03 04:58:56 -04:00
|
|
|
# asset precompilation is a dog, wget to force it
|
2014-01-09 00:39:30 -05:00
|
|
|
run "wget http://127.0.0.1:#{@port}/ -o /dev/null"
|
2013-08-15 03:13:05 -04:00
|
|
|
|
2014-01-08 23:56:03 -05:00
|
|
|
tests = [
|
2014-02-15 23:11:25 -05:00
|
|
|
["categories", "/categories"],
|
2014-01-08 23:56:03 -05:00
|
|
|
["home", "/"],
|
2014-02-15 23:11:25 -05:00
|
|
|
["topic", "/t/oh-how-i-wish-i-could-shut-up-like-a-tunnel-for-so/69"]
|
2017-03-28 14:27:54 -04:00
|
|
|
# ["user", "/u/admin1/activity"],
|
2014-01-08 23:56:03 -05:00
|
|
|
]
|
|
|
|
|
2017-07-27 21:20:09 -04:00
|
|
|
tests = tests.map { |k, url| ["#{k}_admin", "#{url}#{append}"] } + tests
|
2014-02-15 23:11:25 -05:00
|
|
|
|
|
|
|
# NOTE: we run the most expensive page first in the bench
|
2014-01-08 23:56:03 -05:00
|
|
|
|
|
|
|
def best_of(a, b)
|
|
|
|
return a unless b
|
|
|
|
return b unless a
|
|
|
|
|
|
|
|
a[50] < b[50] ? a : b
|
|
|
|
end
|
|
|
|
|
|
|
|
results = {}
|
|
|
|
@best_of.times do
|
|
|
|
tests.each do |name, url|
|
2017-07-27 21:20:09 -04:00
|
|
|
results[name] = best_of(bench(url, name), results[name])
|
2014-01-08 23:56:03 -05:00
|
|
|
end
|
|
|
|
end
|
2013-09-10 02:22:58 -04:00
|
|
|
|
2013-08-15 03:48:11 -04:00
|
|
|
puts "Your Results: (note for timings- percentile is first, duration is second in millisecs)"
|
2013-08-15 01:19:23 -04:00
|
|
|
|
2016-09-21 16:15:58 -04:00
|
|
|
# Prevent using external facts because it breaks when running in the
|
|
|
|
# discourse/discourse_bench docker container.
|
|
|
|
Facter::Util::Config.external_facts_dirs = []
|
2013-08-29 07:34:32 -04:00
|
|
|
facts = Facter.to_hash
|
|
|
|
|
2017-07-27 21:20:09 -04:00
|
|
|
facts.delete_if { |k, v|
|
|
|
|
!["operatingsystem", "architecture", "kernelversion",
|
2013-08-29 07:34:32 -04:00
|
|
|
"memorysize", "physicalprocessorcount", "processor0",
|
|
|
|
"virtual"].include?(k)
|
|
|
|
}
|
|
|
|
|
2013-09-10 02:03:11 -04:00
|
|
|
run("RAILS_ENV=profile bundle exec rake assets:clean")
|
|
|
|
|
2014-02-16 00:44:51 -05:00
|
|
|
def get_mem(pid)
|
|
|
|
YAML.load `ruby script/memstats.rb #{pid} --yaml`
|
|
|
|
end
|
|
|
|
|
|
|
|
mem = get_mem(pid)
|
2014-01-02 19:51:12 -05:00
|
|
|
|
2017-07-27 21:20:09 -04:00
|
|
|
results = results.merge("timings" => @timings,
|
|
|
|
"ruby-version" => "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}",
|
|
|
|
"rss_kb" => mem["rss_kb"],
|
|
|
|
"pss_kb" => mem["pss_kb"]).merge(facts)
|
2013-12-10 18:32:23 -05:00
|
|
|
|
2014-02-16 00:44:51 -05:00
|
|
|
if @unicorn
|
|
|
|
child_pids = `ps --ppid #{pid} | awk '{ print $1; }' | grep -v PID`.split("\n")
|
|
|
|
child_pids.each do |child|
|
|
|
|
mem = get_mem(child)
|
|
|
|
results["rss_kb_#{child}"] = mem["rss_kb"]
|
|
|
|
results["pss_kb_#{child}"] = mem["pss_kb"]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-01-08 23:56:03 -05:00
|
|
|
puts results.to_yaml
|
2013-12-10 18:32:23 -05:00
|
|
|
|
2014-02-13 23:43:08 -05:00
|
|
|
if @mem_stats
|
|
|
|
puts
|
|
|
|
puts open("http://127.0.0.1:#{@port}/admin/memory_stats#{append}").read
|
|
|
|
end
|
|
|
|
|
2014-12-07 17:54:35 -05:00
|
|
|
if @dump_heap
|
|
|
|
puts
|
|
|
|
puts open("http://127.0.0.1:#{@port}/admin/dump_heap#{append}").read
|
|
|
|
end
|
|
|
|
|
2013-12-10 18:32:23 -05:00
|
|
|
if @result_file
|
2017-07-27 21:20:09 -04:00
|
|
|
File.open(@result_file, "wb") do |f|
|
2013-12-10 18:32:23 -05:00
|
|
|
f.write(results)
|
|
|
|
end
|
|
|
|
end
|
2013-08-15 01:19:23 -04:00
|
|
|
|
|
|
|
ensure
|
|
|
|
Process.kill "KILL", pid
|
|
|
|
end
|