DEV: Improve/Fix script/bench.rb (#19646)

1. Fix bug where we were not waiting for all unicorn workers to start up
before running benchmarks.

2. Fix a bug where headers were not used when benchmarking. Admin
benchmarks were basically running as anon user.

3. Disable rate limits when in profile env. We're pretty much going to
hit the rate limit every time as a normal user.

4. Benchmark against topic with a fixed posts count of 100. Previously profiling script was just randomly creating posts
and we would benchmark against a topic with a fixed posts count of 30.
Sometimes, the script fails because no topics with a posts count of 30
exists.

5. Benchmarks are not run against a normal user on top of anon and
admin.

6. Add script option to select tests that should be run.
This commit is contained in:
Alan Guo Xiang Tan 2022-12-30 07:25:11 +08:00 committed by GitHub
parent 63debd6d33
commit 0da79561c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 136 additions and 50 deletions

View File

@ -17,6 +17,8 @@ class RateLimiter
@disabled = false @disabled = false
end end
disable if Rails.env.profile?
# We don't observe rate limits in test mode # We don't observe rate limits in test mode
def self.disabled? def self.disabled?
@disabled @disabled

24
lib/tasks/profile.rake Normal file
View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
desc "generate a user api key for given user in profiling environment"
task "user_api_key:create", [:username] => :environment do |task, args|
raise "user_api_key:create rake task is only meant for the profiling env" if ENV["RAILS_ENV"] != "profile"
raise "Supply a username for the key" if !args[:username]
user = User.find_by_username(args[:username])
raise "'#{args[:username]}' is not a valid username" if !user
application_name = 'perf test application'
user_api_key = UserApiKey.where(application_name: application_name).destroy_all
user_api_key = UserApiKey.create!(
application_name: application_name,
client_id: '1234',
scopes: ['read'].map { |name| UserApiKeyScope.new(name: name) },
user_id: user.id
)
puts user_api_key.key
end

View File

@ -5,6 +5,8 @@ require "csv"
require "yaml" require "yaml"
require "optparse" require "optparse"
require "fileutils" require "fileutils"
require "net/http"
require "uri"
@include_env = false @include_env = false
@result_file = nil @result_file = nil
@ -52,6 +54,10 @@ opts = OptionParser.new do |o|
o.on("-s", "--skip-bundle-assets", "Skip bundling assets") do o.on("-s", "--skip-bundle-assets", "Skip bundling assets") do
@skip_asset_bundle = true @skip_asset_bundle = true
end end
o.on("-t", "--tests [STRING]", "List of tests to run. Example: '--tests topic,categories')") do |i|
@tests = i.split(",")
end
end end
opts.parse! opts.parse!
@ -182,17 +188,23 @@ end
puts "Populating Profile DB" puts "Populating Profile DB"
run("bundle exec ruby script/profile_db_generator.rb") run("bundle exec ruby script/profile_db_generator.rb")
puts "Getting api key" puts "Getting admin api key"
api_key = `bundle exec rake api_key:create_master[bench]`.split("\n")[-1] admin_api_key = `bundle exec rake api_key:create_master[bench]`.split("\n")[-1]
raise "Failed to obtain a user API key" if admin_api_key.to_s.empty?
def bench(path, name) puts "Getting user api key"
user_api_key = `bundle exec rake user_api_key:create[user1]`.split("\n")[-1]
raise "Failed to obtain a user API key" if user_api_key.to_s.empty?
def bench(path, name, headers)
puts "Running apache bench warmup" puts "Running apache bench warmup"
add = "" add = ""
add = "-c #{@concurrency} " if @concurrency > 1 add = "-c #{@concurrency} " if @concurrency > 1
`ab #{add} -n 20 -l "http://127.0.0.1:#{@port}#{path}"` header_string = headers&.map { |k, v| "-H \"#{k}:#{v}\"" }&.join(" ")
`ab #{add} #{header_string} -n 20 -l "http://127.0.0.1:#{@port}#{path}"`
puts "Benchmarking #{name} @ #{path}" puts "Benchmarking #{name} @ #{path}"
`ab #{add} -n #{@iterations} -l -e tmp/ab.csv "http://127.0.0.1:#{@port}#{path}"` `ab #{add} #{header_string} -n #{@iterations} -l -e tmp/ab.csv "http://127.0.0.1:#{@port}#{path}"`
percentiles = Hash[*[50, 75, 90, 99].zip([]).flatten] percentiles = Hash[*[50, 75, 90, 99].zip([]).flatten]
CSV.foreach("tmp/ab.csv") do |percent, time| CSV.foreach("tmp/ab.csv") do |percent, time|
@ -214,7 +226,17 @@ begin
ENV['UNICORN_PORT'] = @port.to_s ENV['UNICORN_PORT'] = @port.to_s
ENV['UNICORN_WORKERS'] = @unicorn_workers.to_s ENV['UNICORN_WORKERS'] = @unicorn_workers.to_s
FileUtils.mkdir_p(File.join('tmp', 'pids')) FileUtils.mkdir_p(File.join('tmp', 'pids'))
spawn("bundle exec unicorn -c config/unicorn.conf.rb") unicorn_pid = spawn("bundle exec unicorn -c config/unicorn.conf.rb")
while (unicorn_master_pid = `ps aux | grep "unicorn master" | grep -v "grep" | awk '{print $2}'`.strip.to_i) == 0
sleep 1
end
while `ps -f --ppid #{unicorn_master_pid} | grep worker | awk '{ print $2 }'`.split("\n").map(&:to_i).size != @unicorn_workers.to_i
sleep 1
end
unicorn_pid
else else
spawn("bundle exec puma -p #{@port} -e production") spawn("bundle exec puma -p #{@port} -e production")
end end
@ -224,8 +246,15 @@ begin
end end
puts "Starting benchmark..." puts "Starting benchmark..."
headers = { 'Api-Key' => api_key,
'Api-Username' => "admin1" } admin_headers = {
'Api-Key' => admin_api_key,
'Api-Username' => "admin1"
}
user_headers = {
'User-Api-Key' => user_api_key
}
# asset precompilation is a dog, wget to force it # asset precompilation is a dog, wget to force it
run "curl -s -o /dev/null http://127.0.0.1:#{@port}/" run "curl -s -o /dev/null http://127.0.0.1:#{@port}/"
@ -237,20 +266,38 @@ begin
topic_url = redirect_response.match(/^location: .+(\/t\/i-am-a-topic-used-for-perf-tests\/.+)$/i)[1].strip topic_url = redirect_response.match(/^location: .+(\/t\/i-am-a-topic-used-for-perf-tests\/.+)$/i)[1].strip
tests = [ all_tests = [
["categories", "/categories"], ["categories", "/categories"],
["home", "/"], ["home", "/"],
["topic", topic_url] ["topic", topic_url],
# ["user", "/u/admin1/activity"], ["topic.json", "#{topic_url}.json"],
["user activity", "/u/admin1/activity"],
] ]
tests.concat(tests.map { |k, url| ["#{k}_admin", "#{url}", headers] }) @tests ||= %w{categories home topic}
tests.each do |_, path, headers_for_path| tests_to_run = all_tests.select do |test_name, path|
header_string = headers_for_path&.map { |k, v| "-H \"#{k}: #{v}\"" }&.join(" ") @tests.include?(test_name)
end
if `curl -s -I "http://127.0.0.1:#{@port}#{path}" #{header_string}` !~ /200 OK/ tests_to_run.concat(
raise "#{path} returned non 200 response code" tests_to_run.map { |k, url| ["#{k} user", "#{url}", user_headers] },
tests_to_run.map { |k, url| ["#{k} admin", "#{url}", admin_headers] }
)
tests_to_run.each do |test_name, path, headers_for_path|
uri = URI.parse("http://127.0.0.1:#{@port}#{path}")
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Get.new(uri.request_uri)
headers_for_path&.each do |key, value|
request[key] = value
end
response = http.request(request)
if response.code != "200"
raise "#{test_name} #{path} returned non 200 response code"
end end
end end
@ -265,8 +312,8 @@ begin
results = {} results = {}
@best_of.times do @best_of.times do
tests.each do |name, url| tests_to_run.each do |name, url, headers|
results[name] = best_of(bench(url, name), results[name]) results[name] = best_of(bench(url, name, headers), results[name])
end end
end end
@ -303,7 +350,7 @@ begin
mem = get_mem(pid) mem = get_mem(pid)
results = results.merge("timings" => @timings, results = results.merge("timings" => @timings,
"ruby-version" => "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}", "ruby-version" => "#{RUBY_DESCRIPTION}",
"rss_kb" => mem["rss_kb"], "rss_kb" => mem["rss_kb"],
"pss_kb" => mem["pss_kb"]).merge(facts) "pss_kb" => mem["pss_kb"]).merge(facts)

View File

@ -43,16 +43,20 @@ def sentence
sentence sentence
end end
def create_admin(seq) def create_user(seq, admin: false, username: nil)
User.new.tap { |admin| User.new.tap do |user|
admin.email = "admin@localhost#{seq}.fake" user.email = "user@localhost#{seq}.fake"
admin.username = "admin#{seq}" user.username = username || "user#{seq}"
admin.password = "password12345abc" user.password = "password12345abc"
admin.save! user.save!
admin.grant_admin!
admin.change_trust_level!(TrustLevel[4]) if admin
admin.activate user.grant_admin!
} user.change_trust_level!(TrustLevel[4])
end
user.activate
end
end end
require File.expand_path(File.dirname(__FILE__) + "/../config/environment") require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
@ -64,20 +68,9 @@ unless Rails.env == "profile"
exit exit
end end
def ensure_perf_test_topic_has_right_title!
title = "I am a topic used for perf tests"
# in case we have an old run and picked the wrong topic
Topic.where(title: title).update_all(title: "Test topic #{SecureRandom.hex}")
t = Topic.where(archetype: :regular, posts_count: 30).order(id: :desc).first
t.title = title
t.save!
end
# by default, Discourse has a "system" and `discobot` account # by default, Discourse has a "system" and `discobot` account
if User.count > 2 if User.count > 2
puts "Only run this script against an empty DB" puts "Only run this script against an empty DB"
ensure_perf_test_topic_has_right_title!
exit exit
end end
@ -90,38 +83,58 @@ rescue LoadError
unbundled_require 'gabbler' unbundled_require 'gabbler'
end end
puts "Creating 100 users" number_of_users = 100
users = 100.times.map do |i| puts "Creating #{number_of_users} users"
number_of_users.times.map do |i|
putc "." putc "."
create_admin(i) create_user(i)
end end
puts
puts "Creating 1 admin user"
admin_user = create_user(number_of_users + 1, admin: true, username: "admin1")
users = User.human_users.all
puts puts
puts "Creating 10 categories" puts "Creating 10 categories"
categories = 10.times.map do |i| categories = 10.times.map do |i|
putc "." putc "."
Category.create(name: "category#{i}", text_color: "ffffff", color: "000000", user: users.first) Category.create(name: "category#{i}", text_color: "ffffff", color: "000000", user: admin_user)
end end
puts puts
puts "Creating 100 topics" puts "Creating 100 topics"
topic_ids = 100.times.map do topic_ids = 100.times.map do
post = PostCreator.create(users.sample, raw: sentence, title: sentence[0..50].strip, category: categories.sample.id, skip_validations: true) post = PostCreator.create(admin_user, raw: sentence, title: sentence[0..50].strip, category: categories.sample.id, skip_validations: true)
putc "." putc "."
post.topic_id post.topic_id
end end
puts puts
puts "creating 2000 replies" puts "Creating 2000 replies"
2000.times do 2000.times do
putc "." putc "."
PostCreator.create(users.sample, raw: sentence, topic_id: topic_ids.sample, skip_validations: true) PostCreator.create(users.sample, raw: sentence, topic_id: topic_ids.sample, skip_validations: true)
end end
puts
puts "creating perf test topic"
first_post = PostCreator.create(
users.sample,
raw: sentence,
title: "I am a topic used for perf tests",
category: categories.sample.id,
skip_validations: true
)
puts
puts "Creating 100 replies for perf test topic"
100.times do
putc "."
PostCreator.create(users.sample, raw: sentence, topic_id: first_post.topic_id, skip_validations: true)
end
# no sidekiq so update some stuff # no sidekiq so update some stuff
Category.update_stats Category.update_stats
Jobs::PeriodicalUpdates.new.execute(nil) Jobs::PeriodicalUpdates.new.execute(nil)
ensure_perf_test_topic_has_right_title!