abcrunch 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +6 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +28 -0
- data/README +176 -0
- data/Rakefile +10 -0
- data/ab_honk.rb +104 -0
- data/abcrunch.gemspec +36 -0
- data/lib/abcrunch.rb +11 -0
- data/lib/abcrunch/ab_result.rb +33 -0
- data/lib/abcrunch/ab_runner.rb +36 -0
- data/lib/abcrunch/best_run.rb +22 -0
- data/lib/abcrunch/config.rb +35 -0
- data/lib/abcrunch/log_console_writer.rb +44 -0
- data/lib/abcrunch/logger.rb +13 -0
- data/lib/abcrunch/page.rb +26 -0
- data/lib/abcrunch/page_tester.rb +44 -0
- data/lib/abcrunch/strategy_best_concurrency.rb +48 -0
- data/lib/abcrunch/tasks/default.rake +12 -0
- data/lib/abcrunch/tasks/example.rake +33 -0
- data/lib/abcrunch/tasks/generated.rake +36 -0
- data/lib/abcrunch/tester.rb +56 -0
- data/lib/abcrunch/version.rb +3 -0
- data/spec/helpers/page_helper.rb +62 -0
- data/spec/lib/ab_result_spec.rb +87 -0
- data/spec/lib/ab_runner_spec.rb +47 -0
- data/spec/lib/best_run_spec.rb +56 -0
- data/spec/lib/config_spec.rb +22 -0
- data/spec/lib/log_console_writer_spec.rb +82 -0
- data/spec/lib/logger_spec.rb +12 -0
- data/spec/lib/page_spec.rb +64 -0
- data/spec/lib/page_tester_spec.rb +85 -0
- data/spec/lib/strategy_best_concurrency_spec.rb +124 -0
- data/spec/lib/tester_spec.rb +73 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/tasks/spec.rake +15 -0
- metadata +141 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
module AbCrunch
|
2
|
+
class BestRun
|
3
|
+
def self.of_avg_response_time(num_runs, ab_options)
|
4
|
+
AbCrunch::Logger.log :task, "Best of #{num_runs} runs at concurrency: #{ab_options[:concurrency]} and num_requests: #{ab_options[:num_requests]}"
|
5
|
+
AbCrunch::Logger.log :info, "for #{AbCrunch::Page.get_url(ab_options)}"
|
6
|
+
AbCrunch::Logger.log :info, "Collecting average response times for each run:"
|
7
|
+
|
8
|
+
min_response_time = 999999
|
9
|
+
min_response_result = nil
|
10
|
+
num_runs.times do
|
11
|
+
abr = AbCrunch::AbRunner.ab(ab_options)
|
12
|
+
AbCrunch::Logger.log :info, "Average response time: #{abr.avg_response_time} (ms) "
|
13
|
+
if abr.avg_response_time < min_response_time
|
14
|
+
min_response_time = abr.avg_response_time
|
15
|
+
min_response_result = abr
|
16
|
+
end
|
17
|
+
end
|
18
|
+
AbCrunch::Logger.log :progress, "Best response time was #{min_response_time}"
|
19
|
+
min_response_result
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module AbCrunch
|
2
|
+
module Config
|
3
|
+
class << self
|
4
|
+
attr_accessor :page_sets, :best_concurrency_options, :ab_options
|
5
|
+
end
|
6
|
+
|
7
|
+
@best_concurrency_options = {
|
8
|
+
:max_degradation_percent => 0.5,
|
9
|
+
:max_latency => 1000.0,
|
10
|
+
:num_baseline_runs => 5,
|
11
|
+
:num_concurrency_runs => 3
|
12
|
+
}
|
13
|
+
|
14
|
+
@ab_options = {
|
15
|
+
:concurrency => 1,
|
16
|
+
:num_requests => 100
|
17
|
+
}
|
18
|
+
|
19
|
+
@page_sets = {
|
20
|
+
:localhost => [
|
21
|
+
{
|
22
|
+
:name => 'localhost',
|
23
|
+
:url => 'http://localhost:3000/',
|
24
|
+
:max_avg_response_time => 500
|
25
|
+
}
|
26
|
+
]
|
27
|
+
}
|
28
|
+
|
29
|
+
def self.page_sets=(new_page_sets)
|
30
|
+
@page_sets = new_page_sets
|
31
|
+
require 'rake'
|
32
|
+
load File.join(AbCrunch.root, "lib/abcrunch/tasks/generated.rake")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'colorize'
|
2
|
+
|
3
|
+
module AbCrunch
|
4
|
+
class LogConsoleWriter
|
5
|
+
@@last_inline = false
|
6
|
+
|
7
|
+
TYPE_STYLES = {
|
8
|
+
:info => { :color => :white, :prefix => ' ' },
|
9
|
+
:test => { :color => :light_white, :prefix => "\n\n-----" },
|
10
|
+
:strategy => { :color => :light_white, :prefix => "\n" },
|
11
|
+
:task => { :color => :white, :prefix => "\n " },
|
12
|
+
:progress => { :color => :green, :prefix => ' ' },
|
13
|
+
:result => { :color => :light_green, :prefix => ' ' },
|
14
|
+
:ab_result => { :color => :cyan, :prefix => ' ' },
|
15
|
+
:success => { :color => :light_green, :prefix => ' ' },
|
16
|
+
:failure => { :color => :light_red, :prefix => ' ' },
|
17
|
+
:summary => { :color => :light_white, :prefix => '' },
|
18
|
+
:summary_title => { :color => :light_white, :prefix => "\n==================== " },
|
19
|
+
:summary_passed => { :color => :light_green, :prefix => '' },
|
20
|
+
:summary_failed => { :color => :light_red, :prefix => '' },
|
21
|
+
|
22
|
+
}
|
23
|
+
|
24
|
+
def self.color_for_type(type)
|
25
|
+
TYPE_STYLES[type] ? TYPE_STYLES[type][:color] : :white
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.prefix_for_type(type)
|
29
|
+
TYPE_STYLES[type] ? TYPE_STYLES[type][:prefix] : ''
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.log(type, message, options = {})
|
33
|
+
a_message = prefix_for_type(type) + message
|
34
|
+
if options[:inline]
|
35
|
+
print a_message.send(color_for_type type)
|
36
|
+
@@last_inline = true
|
37
|
+
else
|
38
|
+
a_message = "\n#{a_message}" if @@last_inline
|
39
|
+
puts a_message.send(color_for_type type)
|
40
|
+
@@last_inline = false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module AbCrunch
|
2
|
+
module Page
|
3
|
+
def self.get_url(page, force_new = false)
|
4
|
+
if force_new || !page[:testing_url]
|
5
|
+
url = page[:url]
|
6
|
+
if url.respond_to? :call
|
7
|
+
url = url.call
|
8
|
+
end
|
9
|
+
page[:testing_url] = url
|
10
|
+
else
|
11
|
+
url = page[:testing_url]
|
12
|
+
end
|
13
|
+
|
14
|
+
url
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.get_display_url(page)
|
18
|
+
if page[:url].respond_to? :call
|
19
|
+
return "Dynamic url example: #{page[:url].call}"
|
20
|
+
else
|
21
|
+
page[:url]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module AbCrunch
|
2
|
+
module PageTester
|
3
|
+
def self.test(page)
|
4
|
+
AbCrunch::Logger.log :test, "Testing #{page[:name]}"
|
5
|
+
AbCrunch::Logger.log :info, "#{AbCrunch::Page.get_display_url(page)}"
|
6
|
+
|
7
|
+
if page[:max_avg_response_time]
|
8
|
+
page.merge!({:max_latency => page[:max_avg_response_time]})
|
9
|
+
end
|
10
|
+
qps_result = AbCrunch::StrategyBestConcurrency.run(page)
|
11
|
+
|
12
|
+
passed = true
|
13
|
+
errors = []
|
14
|
+
|
15
|
+
if page[:max_avg_response_time]
|
16
|
+
if qps_result.avg_response_time > page[:max_avg_response_time]
|
17
|
+
passed = false
|
18
|
+
errors << "#{page[:name]}: Avg response time of #{qps_result.avg_response_time} must be <= #{page[:max_avg_response_time]}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
if page[:min_queries_per_second]
|
23
|
+
if qps_result.queries_per_second < page[:min_queries_per_second]
|
24
|
+
passed = false
|
25
|
+
errors << "#{page[:name]}: QPS of #{qps_result.queries_per_second} must be >= #{page[:min_queries_per_second]}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
if qps_result.failed_requests > 0
|
30
|
+
passed = false
|
31
|
+
errors << "#{page[:name]}: Load test invalidated: #{qps_result.failed_requests} requests failed"
|
32
|
+
end
|
33
|
+
|
34
|
+
if passed
|
35
|
+
AbCrunch::Logger.log :success, "PASSED"
|
36
|
+
else
|
37
|
+
errors.each { |error| AbCrunch::Logger.log :failure, error }
|
38
|
+
AbCrunch::Logger.log :failure, "#{page[:name]} FAILED"
|
39
|
+
end
|
40
|
+
|
41
|
+
[passed, qps_result, errors]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module AbCrunch
|
2
|
+
class StrategyBestConcurrency
|
3
|
+
|
4
|
+
def self.calc_threshold(baseline_latency, percent_buffer, max_latency)
|
5
|
+
[max_latency, baseline_latency * (1 + percent_buffer)].min
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.find_best_concurrency(page, baseline_result)
|
9
|
+
threshold_ms = calc_threshold(baseline_result.avg_response_time, page[:max_degradation_percent], page[:max_latency].to_f)
|
10
|
+
|
11
|
+
AbCrunch::Logger.log :task, "Finding the max concurrency without degrading performance beyond a threshold"
|
12
|
+
AbCrunch::Logger.log :info, "Threshold: #{threshold_ms} (ms)"
|
13
|
+
AbCrunch::Logger.log :info, "Trying ever increasing concurrency until we bust the threshold"
|
14
|
+
|
15
|
+
fmc_page = page.clone
|
16
|
+
fmc_page[:concurrency] = 0
|
17
|
+
abr = baseline_result
|
18
|
+
begin
|
19
|
+
fmc_page[:concurrency] += 1
|
20
|
+
prev_result = abr
|
21
|
+
abr = AbCrunch::BestRun.of_avg_response_time(fmc_page[:num_concurrency_runs], fmc_page)
|
22
|
+
end while abr.avg_response_time < threshold_ms and fmc_page[:concurrency] < fmc_page[:num_requests]
|
23
|
+
if abr.avg_response_time < threshold_ms
|
24
|
+
return abr || baseline_result
|
25
|
+
else
|
26
|
+
fmc_page[:concurrency] -= 1
|
27
|
+
return prev_result || baseline_result
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.run(page)
|
32
|
+
page = AbCrunch::Config.best_concurrency_options.merge(page)
|
33
|
+
|
34
|
+
AbCrunch::Logger.log :strategy, "Strategy: find queries per second (QPS) at highest concurrency before latency degrades"
|
35
|
+
|
36
|
+
AbCrunch::Logger.log :task, "Calculating Baseline (min average response time over multiple runs)"
|
37
|
+
baseline_result = AbCrunch::BestRun.of_avg_response_time(page[:num_baseline_runs], page)
|
38
|
+
|
39
|
+
best_result = find_best_concurrency(page, baseline_result)
|
40
|
+
|
41
|
+
AbCrunch::Logger.log :progress, "Highest concurrency without degrading latency: #{best_result.ab_options[:concurrency]}"
|
42
|
+
AbCrunch::Logger.log :result, "Queries Per Second (QPS): #{best_result.queries_per_second}"
|
43
|
+
best_result.log
|
44
|
+
|
45
|
+
best_result
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
namespace :abcrunch do
|
2
|
+
desc "Run load tests against ALL page sets"
|
3
|
+
task :all do
|
4
|
+
if AbCrunch::Config.page_sets.length == 0
|
5
|
+
AbCrunch::Logger.log(:failure, "No page sets defined. Nothing to do")
|
6
|
+
end
|
7
|
+
|
8
|
+
AbCrunch::Config.page_sets.keys.each do |page_set_key|
|
9
|
+
AbCrunch::Tester.test(AbCrunch::Config.page_sets[page_set_key])
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
namespace :abcrunch do
|
2
|
+
|
3
|
+
desc "Test example pages"
|
4
|
+
task :example do
|
5
|
+
AbCrunch::Config.page_sets = @sample_page_sets
|
6
|
+
Rake::Task['abcrunch:sample_1'].invoke
|
7
|
+
end
|
8
|
+
|
9
|
+
@sample_page_sets = {
|
10
|
+
:sample_1 => [
|
11
|
+
{
|
12
|
+
:name => "Google home page",
|
13
|
+
:url => proc do
|
14
|
+
"http://www.google.com/?q=#{['food','coma','weirds','code'][rand(4)]}"
|
15
|
+
end,
|
16
|
+
:min_queries_per_second => 10,
|
17
|
+
:max_avg_response_time => 1000,
|
18
|
+
},
|
19
|
+
{
|
20
|
+
:name => "Facebook home page",
|
21
|
+
:url => "http://www.facebook.com/",
|
22
|
+
:max_avg_response_time => 1000,
|
23
|
+
},
|
24
|
+
{
|
25
|
+
:name => "Bing Homepage",
|
26
|
+
:url => "http://www.bing.com/",
|
27
|
+
:min_queries_per_second => 50,
|
28
|
+
:max_avg_response_time => 200
|
29
|
+
}
|
30
|
+
]
|
31
|
+
}
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
namespace :abcrunch do
|
2
|
+
|
3
|
+
AbCrunch::Config.page_sets.keys.each do |page_set_key|
|
4
|
+
unless Rake::Task.task_defined? page_set_key
|
5
|
+
desc "Run load tests for #{page_set_key}"
|
6
|
+
task page_set_key do
|
7
|
+
AbCrunch::Tester.test(AbCrunch::Config.page_sets[page_set_key])
|
8
|
+
end
|
9
|
+
|
10
|
+
namespace page_set_key do
|
11
|
+
desc "Run a focused page load test in #{page_set_key}. Example rake abcrunch:#{page_set_key}:focus[0] runs the first page"
|
12
|
+
task :focus, [:page_index] do |t, args|
|
13
|
+
unless verify_abcrunch_args(args, page_set_key)
|
14
|
+
raise "usage: rake abcrunch:#{page_set_key}:focus[<page_index>]"
|
15
|
+
end
|
16
|
+
|
17
|
+
orig_page = AbCrunch::Config.page_sets[page_set_key][args[:page_index].to_i]
|
18
|
+
AbCrunch::PageTester.test(orig_page)
|
19
|
+
end
|
20
|
+
|
21
|
+
def verify_abcrunch_args(args, page_set_key)
|
22
|
+
max_idx = AbCrunch::Config.page_sets[page_set_key].length-1
|
23
|
+
if !args[:page_index]
|
24
|
+
puts "Focusing on... NOTHING! (dork)"
|
25
|
+
return false
|
26
|
+
elsif !(0..max_idx).include?(args[:page_index].to_i)
|
27
|
+
puts "Page index (#{args[:page_index]}) not in range (0..#{max_idx})"
|
28
|
+
return false
|
29
|
+
end
|
30
|
+
true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module AbCrunch
|
2
|
+
module Tester
|
3
|
+
def self.test(pages)
|
4
|
+
results = []
|
5
|
+
|
6
|
+
pages.each do |page|
|
7
|
+
passed, qps_result, errors = AbCrunch::PageTester.test(page)
|
8
|
+
results << {
|
9
|
+
:page => page,
|
10
|
+
:passed => passed,
|
11
|
+
:qps_result => qps_result,
|
12
|
+
:errors => errors
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
log_result_summary results
|
17
|
+
|
18
|
+
passed = results.reduce(true) { |value, result| value && result[:passed] }
|
19
|
+
|
20
|
+
if not passed
|
21
|
+
errors = results.reduce('') do |value, result|
|
22
|
+
page_set_errors = result[:errors].reduce('') do |val, error|
|
23
|
+
"#{val}#{val.length > 0 ? "\n" : ''}#{error}"
|
24
|
+
end
|
25
|
+
"#{value}#{value.length > 0 ? "\n" : ''}#{page_set_errors}"
|
26
|
+
end
|
27
|
+
|
28
|
+
raise "Load tests FAILED\n#{errors}"
|
29
|
+
end
|
30
|
+
|
31
|
+
results
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.log_result_summary(results)
|
35
|
+
AbCrunch::Logger.log :summary_title, "Summary"
|
36
|
+
AbCrunch::Logger.log :summary, "#{"Page".ljust(30, ' ')}#{"Response time".rjust(10, ' ')} #{"Concurrency".rjust(16, ' ')} #{"Queries/sec".rjust(12, ' ')}"
|
37
|
+
results.each do |result|
|
38
|
+
page_name = result[:page][:name].ljust(30, ' ')
|
39
|
+
base_response_time = sprintf("%.2f", result[:qps_result].avg_response_time).rjust(10, ' ')
|
40
|
+
max_concurrency = result[:qps_result].ab_options[:concurrency].to_s.rjust(16, ' ')
|
41
|
+
queries_per_second = sprintf("%.2f", result[:qps_result].queries_per_second).rjust(12, ' ')
|
42
|
+
if result[:passed]
|
43
|
+
AbCrunch::Logger.log :summary_passed, "#{page_name}#{base_response_time} #{max_concurrency} #{queries_per_second}"
|
44
|
+
else
|
45
|
+
AbCrunch::Logger.log :summary_failed, "#{page_name}#{base_response_time} #{max_concurrency} #{queries_per_second}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
AbCrunch::Logger.log :summary_title, "Legend"
|
49
|
+
AbCrunch::Logger.log :summary, "Response Time = Best average response time (ms) at max concurrency"
|
50
|
+
AbCrunch::Logger.log :summary, "Concurrency = Best concurrency where response time doesn't bust our performance threshold"
|
51
|
+
AbCrunch::Logger.log :summary, "Queries/sec = Queries per second at best concurrency"
|
52
|
+
|
53
|
+
results
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module AbCrunchSpec
|
2
|
+
|
3
|
+
FULL_TEST_PAGE = {
|
4
|
+
:name => "some page",
|
5
|
+
:url => "some url",
|
6
|
+
}
|
7
|
+
|
8
|
+
def self.new_page(override_options = {})
|
9
|
+
FULL_TEST_PAGE.merge(override_options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.new_result(page = FULL_TEST_PAGE)
|
13
|
+
AbCrunch::AbResult.new(FAKE_AB_RESULT_TEXT, page)
|
14
|
+
end
|
15
|
+
|
16
|
+
FAKE_AB_RESULT_TEXT = <<-ABRESULT
|
17
|
+
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
|
18
|
+
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
|
19
|
+
Licensed to The Apache Software Foundation, http://www.apache.org/
|
20
|
+
|
21
|
+
Benchmarking www.google.com (be patient).....done
|
22
|
+
|
23
|
+
|
24
|
+
Server Software: gws
|
25
|
+
Server Hostname: www.google.com
|
26
|
+
Server Port: 80
|
27
|
+
|
28
|
+
Document Path: /
|
29
|
+
Document Length: 10372 bytes
|
30
|
+
|
31
|
+
Concurrency Level: 1
|
32
|
+
Time taken for tests: 0.880 seconds
|
33
|
+
Complete requests: 10
|
34
|
+
Failed requests: 0
|
35
|
+
(Connect: 0, Receive: 0, Length: 8, Exceptions: 0)
|
36
|
+
Write errors: 0
|
37
|
+
Total transferred: 109542 bytes
|
38
|
+
HTML transferred: 103762 bytes
|
39
|
+
Requests per second: 11.36 [#/sec] (mean)
|
40
|
+
Time per request: 88.019 [ms] (mean)
|
41
|
+
Time per request: 88.019 [ms] (mean, across all concurrent requests)
|
42
|
+
Transfer rate: 121.54 [Kbytes/sec] received
|
43
|
+
|
44
|
+
Connection Times (ms)
|
45
|
+
min mean[+/-sd] median max
|
46
|
+
Connect: 24 29 4.4 28 38
|
47
|
+
Processing: 56 59 3.2 59 66
|
48
|
+
Waiting: 52 55 3.4 55 64
|
49
|
+
Total: 80 88 6.4 88 103
|
50
|
+
|
51
|
+
Percentage of the requests served within a certain time (ms)
|
52
|
+
50% 88
|
53
|
+
66% 89
|
54
|
+
75% 89
|
55
|
+
80% 91
|
56
|
+
90% 103
|
57
|
+
95% 103
|
58
|
+
98% 103
|
59
|
+
99% 103
|
60
|
+
100% 103 (longest request)
|
61
|
+
ABRESULT
|
62
|
+
end
|