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.
@@ -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,13 @@
1
+ module AbCrunch
2
+ module Logger
3
+ class << self
4
+ attr_accessor :writers
5
+ end
6
+
7
+ @writers = [AbCrunch::LogConsoleWriter]
8
+
9
+ def self.log(type, message, options = {})
10
+ writers.each { |writer| writer.log(type, message, options) }
11
+ end
12
+ end
13
+ 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,3 @@
1
+ module AbCrunch
2
+ VERSION = "0.0.4"
3
+ 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