abcrunch 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ .idea
2
+ .idea/*
3
+ log
4
+ tmp
5
+ bin/*
6
+ .DS_Store
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.2-p290@abcrunch --create
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in abcrunch.gemspec
4
+ gemspec
@@ -0,0 +1,28 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ abcrunch (0.0.4)
5
+ colorize
6
+ rr
7
+ rspec
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ colorize (0.5.8)
13
+ diff-lcs (1.1.3)
14
+ rr (1.0.4)
15
+ rspec (2.6.0)
16
+ rspec-core (~> 2.6.0)
17
+ rspec-expectations (~> 2.6.0)
18
+ rspec-mocks (~> 2.6.0)
19
+ rspec-core (2.6.4)
20
+ rspec-expectations (2.6.0)
21
+ diff-lcs (~> 1.1.2)
22
+ rspec-mocks (2.6.0)
23
+
24
+ PLATFORMS
25
+ ruby
26
+
27
+ DEPENDENCIES
28
+ abcrunch!
data/README ADDED
@@ -0,0 +1,176 @@
1
+ Ab Crunch
2
+
3
+ The idea behind Ab Crunch is that basic performance metrics and standards should
4
+ be effortless, first-class citizens in the development process, with frequent visibility
5
+ and immediate feedback when performance issues are introduced.
6
+
7
+ Other tools exist for measuring performance, but we found that they had some drawbacks:
8
+ - Not easily integrated into routine development practices, such as automated testing and CI
9
+ - Take a long time to set up.
10
+ - Take a long time to use.
11
+
12
+ We wanted a tool that, while simple, was valid enough to surface basic performance
13
+ issues and fast/easy enough to use throughout all our projects.
14
+
15
+ Ab Crunch uses Apache Bench to run various strategies for load testing web sites.
16
+ It generates rake tasks for running all or some of our tests. These can be configured
17
+ to be just informational, or to fail when specified standards are not met. The rake
18
+ tasks can then be added to our Continuous Integration (CI) builds, so builds fail when
19
+ performance degrades.
20
+
21
+
22
+ Credits
23
+ Christopher "Kai" Lichti, Author
24
+ Aaron Hopkins, adviser / contributed strategies
25
+ John Williams, adviser / contributed strategies
26
+
27
+
28
+ Prerequisites
29
+
30
+ Must have Apache Bench installed and 'ab' on your path
31
+
32
+
33
+ Quick Start Guide
34
+
35
+ To see some immediate action, require the gem, and run 'rake abcrunch:example'
36
+
37
+ Now to use it on your own pages:
38
+
39
+ First, define the pages you want to test, and (optionally), the performance
40
+ requirements you want them to meet. If you exclude any requirements, your
41
+ load test will be informational only, and won't log or raise any errors
42
+ based on performance standards.
43
+
44
+ For Example:
45
+
46
+ @load_test_page_sets = {
47
+ :production => [
48
+ {
49
+ :name => "Google home page",
50
+ :url => "http://www.google.com/",
51
+ :min_queries_per_second => 20,
52
+ :max_avg_response_time => 1000,
53
+ },
54
+ {
55
+ :name => "Facebook home page",
56
+ :url => "http://www.facebook.com/",
57
+ }
58
+ ],
59
+ :staging => [
60
+ {
61
+ :name => "Github home page",
62
+ :url => "http://www.github.com/",
63
+ :max_avg_response_time => 1000,
64
+ }
65
+ ]
66
+ }
67
+
68
+ require 'abcrunch'
69
+ AbCrunch::Config.page_sets = @load_test_page_sets
70
+
71
+ In Rails, you can do this in your development and test environments.
72
+
73
+ Once you've configured Ab Crunch, you can run rake tasks to load test your pages, like this:
74
+
75
+ rake abcrunch:staging
76
+ - or -
77
+ rake abcrunch:all
78
+
79
+
80
+ Configuring Pages
81
+
82
+ :name - (required) User-friendly name for the page.
83
+ :url - (required) Url to test. Can be a string or a Proc. Proc example:
84
+ :url => proc do
85
+ "http://www.google.com/?q=#{['food','coma','weirds','code'][rand(4)]}"
86
+ end,
87
+
88
+ Performance requirements (will raise so CI builds break when requirements fail)
89
+ :min_queries_per_second - page must support at least this many QPS
90
+ :max_avg_response_time - latency for the page cannot go higher than this
91
+
92
+ Other Options
93
+ :num_requests - how many requests to make during each (of many) runs [Default: 50]
94
+ :max_latency - global maximum latency (in ms) considered to be acceptable [Default: 1000]
95
+ :max_degradation_percent - global max percent latency can degrade before being considered unacceptable [Default: 0.5 (iow 50%)]
96
+
97
+
98
+ Example: Iterative Optimization
99
+ Running a focus load test to iterate fixing a performance issue.
100
+
101
+ If a specific page is too slow, you can iterate on that page using a focus rake task, like so:
102
+
103
+ rake abcrunch:dev:focus[3]
104
+
105
+
106
+ Example: Configuring the same URLS in multiple environments (dev, qa, staging, prod...)
107
+
108
+ Here's an example showing how you might dry up the AbCrunch configuration to support multiple environments.
109
+
110
+ def init_env
111
+ if ['development', 'test'].include? RAILS_ENV
112
+ require 'abcrunch'
113
+ AbCrunch::Config.page_sets = ab_crunch_page_sets
114
+ end
115
+ end
116
+
117
+ def ab_crunch_page_sets
118
+ def page_with_domain(page, domain)
119
+ new = page.clone
120
+ new[:url] = page[:url].gsub '<domain>', domain
121
+ new
122
+ end
123
+
124
+ result = {
125
+ :dev => AB_CRUNCH_PAGE_SET_TEMPLATE.collect { |page| page_with_domain(page, 'http://localhost:3000') },
126
+ :qa => AB_CRUNCH_PAGE_SET_TEMPLATE.collect { |page| page_with_domain(page, 'http://qa.myapp.com') },
127
+ :staging => AB_CRUNCH_PAGE_SET_TEMPLATE.collect { |page| page_with_domain(page, 'http://staging.myapp.com') },
128
+ :prod => AB_CRUNCH_PAGE_SET_TEMPLATE.collect { |page| page_with_domain(page, 'http://www.myapp.com') },
129
+ }
130
+
131
+ result
132
+ end
133
+
134
+ AB_CRUNCH_PAGE_SET_TEMPLATE = [
135
+ {
136
+ :name => "Home",
137
+ :url => "<domain>/",
138
+ :min_queries_per_second => 50,
139
+ },
140
+ {
141
+ :name => "Blog",
142
+ :url => "<domain>/blog?user=honest_auto",
143
+ :max_avg_response_time => 450,
144
+ }
145
+ ]
146
+
147
+
148
+ KNOWN GOTCHA: Apache Bench does not like urls that just end with the domain. For example:
149
+ http://www.google.com is BAD, but
150
+ http://www.google.com/ is fine, for reasons surpassing understanding.
151
+ ...so for root level urls, be sure to add a trailing slash.
152
+
153
+ <<-LICENSE
154
+
155
+ The MIT License
156
+ Copyright (c) 2011 TrueCar, Inc.
157
+
158
+ Permission is hereby granted, free of charge, to any person obtaining a copy
159
+ of this software and associated documentation files (the "Software"), to deal
160
+ in the Software without restriction, including without limitation the rights
161
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
162
+ copies of the Software, and to permit persons to whom the Software is
163
+ furnished to do so, subject to the following conditions:
164
+
165
+ The above copyright notice and this permission notice shall be included in
166
+ all copies or substantial portions of the Software.
167
+
168
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
169
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
170
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
171
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
172
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
173
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
174
+ THE SOFTWARE.
175
+
176
+ LICENSE
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ Dir[File.join(File.dirname(__FILE__), "lib/**/*.rb")].each { |f| require f }
4
+
5
+ Dir[File.join(File.dirname(__FILE__), "lib/**/*.rake")].
6
+ concat(Dir[File.join(File.dirname(__FILE__), "spec/tasks/**/*.rake")]).
7
+ concat(Dir[File.join(File.dirname(__FILE__), "tasks/**/*.rake")]).
8
+ concat(Dir[File.join(File.dirname(__FILE__), "{test,spec}/*.rake")]).each { |rake| load(rake) }
9
+
10
+ task default: [:spec]
@@ -0,0 +1,104 @@
1
+ test_pages = [
2
+ {:name => 'Price Drop Widget', :url => 'http://widgets.qa.honk.com/wsj/price-drop?pag_id=179'},
3
+ {:name => 'Histogram Widget', :url => 'http://widgets.qa.honk.com/wsj/histogram?pag_id=179&make=toyota&model=corolla&year=2010&body_type=sedan'},
4
+ {:name => 'ES Hybrids Widget', :url => 'http://widgets.qa.honk.com/wsj/search?preset=hybrids&zip=95101'},
5
+ {:name => 'Makes list page', :url => 'https://staging.honk.com/wsj/makes'},
6
+ {:name => 'Landing Page', :url => 'https://staging.honk.com/wsj/new-cars'},
7
+ {:name => 'Trends Page', :url => 'https://staging.honk.com/wsj/new-car-trends'},
8
+ {:name => 'Explore Search Tool', :url => 'https://staging.honk.com/wsj/explore'},
9
+ {:name => 'Make Landing Page', :url => 'https://staging.honk.com/wsj/toyota'},
10
+ {:name => 'Model Landing Page', :url => 'https://staging.honk.com/wsj/toyota/corolla/sedan/2010/4dr-sedan-man'},
11
+ {:name => 'Model Reviews Page', :url => 'https://staging.honk.com/wsj/toyota/corolla/sedan/2010/4dr-sedan-man/reviews'},
12
+ {:name => 'Model Price Page', :url => 'https://staging.honk.com/wsj/toyota/corolla/sedan/2010/4dr-sedan-man/price'},
13
+ {:name => 'Model Specs Page', :url => 'https://staging.honk.com/wsj/toyota/corolla/sedan/2010/4dr-sedan-man/tech_specs'},
14
+ {:name => 'Model Photos Page', :url => 'https://staging.honk.com/wsj/toyota/corolla/sedan/2010/4dr-sedan-man/media'}
15
+ ]
16
+
17
+ def parse_ab_data(ab_log)
18
+ {
19
+ :response_time => ab_log.match(/Time per request:\s*([\d\.]+)\s\[ms\]\s\(mean\)/)[1].to_f,
20
+ :queries_per_second => ab_log.match(/Requests per second:\s*([\d\.]+)\s\[#\/sec\]\s\(mean\)/)[1].to_f,
21
+ :failed_requests => ab_log.match(/Failed requests:\s*([\d\.]+)/)[1].to_f
22
+ }
23
+ end
24
+
25
+ def ab(url, concurrency, num_requests)
26
+ `ab -c #{concurrency} -n #{num_requests} #{url}`
27
+ end
28
+
29
+ def best_of(url, concurrency, num_requests, num_runs)
30
+ min_response_time = 999999
31
+ min_response_log = ''
32
+ puts "\nBest of #{num_runs} at concurrency: #{concurrency} and num_requests: #{num_requests}"
33
+ puts "Collecting average response times for each run:"
34
+ num_runs.times do
35
+ ab_log = ab(url, concurrency, num_requests)
36
+ response_time = parse_ab_data(ab_log)[:response_time]
37
+ print "#{response_time} ... "
38
+ STDOUT.flush
39
+ if response_time < min_response_time
40
+ min_response_time = response_time
41
+ min_response_log = ab_log
42
+ end
43
+ end
44
+ [min_response_time, min_response_log]
45
+ end
46
+
47
+ def baseline_page(page)
48
+ puts "\nCalculating Baseline (min average response time over multiple runs)"
49
+ best_of(page[:url], 1, 10, 5)
50
+ end
51
+
52
+ def find_max_concurrency(page, baseline_response_time, baseline_log, threshold_percent)
53
+ puts "\nFinding the max concurrency without degrading performance beyond a threshold"
54
+ threshold_ms = [1000.0, baseline_response_time * (1 + threshold_percent)].min
55
+ puts "Threshold: #{threshold_ms} (ms)"
56
+ print "Trying ever increasing concurrency until we bust the threshold"
57
+ concurrency = 0
58
+ ab_log = baseline_log
59
+ begin
60
+ concurrency += 1
61
+ prev_log = ab_log
62
+ response_time, ab_log = best_of(page[:url], concurrency, 10, 3)
63
+ end while response_time < threshold_ms
64
+ [concurrency - 1, threshold_ms, prev_log]
65
+ end
66
+
67
+ test_pages.each do |page|
68
+ puts "\n\n\n=========== Testing #{page[:name]}..."
69
+
70
+ baseline_response_time, baseline_output = baseline_page(page)
71
+ page[:baseline_response_time] = baseline_response_time
72
+ puts "\n----- Min Average Response Time was #{baseline_response_time}"
73
+ #puts "----- Matching AB Output"
74
+ #puts baseline_output
75
+
76
+ max_concurrency, threshold, mc_output = find_max_concurrency(page, baseline_response_time, baseline_output, 0.5)
77
+ page[:max_concurrency] = max_concurrency
78
+ page[:threshold] = threshold
79
+ page[:queries_per_second] = parse_ab_data(mc_output)[:queries_per_second]
80
+ puts "\n----- Max Concurrency was #{max_concurrency}"
81
+ puts "----- Threshold was #{threshold}"
82
+ puts "----- Matching AB Output at max concurrency"
83
+ puts mc_output
84
+ end
85
+
86
+ class Float
87
+ alias_method :orig_to_s, :to_s
88
+ def to_s(arg = nil)
89
+ if arg.nil?
90
+ orig_to_s
91
+ else
92
+ sprintf("%.#{arg}f", self)
93
+ end
94
+ end
95
+ end
96
+
97
+ puts "\nSummary"
98
+ puts "#{"Page".ljust(30, ' ')}#{"Baseline".rjust(10, ' ')} #{"Max Concurrency".rjust(16, ' ')} #{"Threshold".rjust(10, ' ')} #{"Queries/sec".rjust(12, ' ')}"
99
+ test_pages.each do |page|
100
+ puts "#{page[:name].ljust(30, ' ')}#{page[:baseline_response_time].to_s(2).rjust(10, ' ')} #{page[:max_concurrency].to_s.rjust(16, ' ')} #{page[:threshold].to_s(0).rjust(10, ' ')} #{page[:queries_per_second].to_s(2).rjust(12, ' ')}"
101
+ end
102
+ puts "\nBaseline = Best average response time (ms) over multiple runs with no concurrency"
103
+ puts "Max Concurrency = Most concurrent requests where best average response time doesn't bust our performance threshold"
104
+ puts "Queries/sec = Queries per second at max concurrency"
@@ -0,0 +1,36 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "abcrunch/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "abcrunch"
7
+ s.version = AbCrunch::VERSION
8
+ s.authors = ["Chris Lichti"]
9
+ s.email = ["kai@truecar.com"]
10
+ s.homepage = "https://github.com/kurisu/abcrunch"
11
+ s.summary = "Automated load testing in ruby"
12
+ s.description = <<-DESC
13
+ The idea behind Ab Crunch is that basic performance metrics and standards should
14
+ be effortless, first-class citizens in the development process, with frequent visibility
15
+ and immediate feedback when performance issues are introduced.
16
+
17
+ Ab Crunch uses Apache Bench to run various strategies for load testing web projects,
18
+ and provides rake tasks for analyzing performance and enforcing performance
19
+ standards on CI.
20
+ DESC
21
+
22
+ s.rubyforge_project = "abcrunch"
23
+
24
+ s.files = `git ls-files`.split("\n")
25
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
26
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
27
+ s.require_paths = ["lib"]
28
+
29
+ s.add_runtime_dependency "rspec"
30
+ s.add_runtime_dependency "rr"
31
+ s.add_runtime_dependency "colorize"
32
+
33
+ # specify any dependencies here; for example:
34
+ # s.add_development_dependency "rspec"
35
+ # s.add_runtime_dependency "rest-client"
36
+ end
@@ -0,0 +1,11 @@
1
+ require "abcrunch/version"
2
+ require 'rake'
3
+
4
+ Dir[File.join(File.dirname(__FILE__), "**/*.rb")].each { |f| require f }
5
+ Dir[File.join(File.dirname(__FILE__), "**/*.rake")].each { |rake| load(rake) }
6
+
7
+ module AbCrunch
8
+ def self.root
9
+ File.dirname(File.dirname(__FILE__))
10
+ end
11
+ end
@@ -0,0 +1,33 @@
1
+ module AbCrunch
2
+ class AbResult
3
+ attr_accessor :raw, :ab_options
4
+
5
+ def initialize(raw_ab_output, ab_options)
6
+ @raw = raw_ab_output
7
+ @ab_options = ab_options
8
+ end
9
+
10
+ def command
11
+ AbCrunch::AbRunner.ab_command(@ab_options)
12
+ end
13
+
14
+ def avg_response_time
15
+ raw.match(/Time per request:\s*([\d\.]+)\s\[ms\]\s\(mean\)/)[1].to_f
16
+ end
17
+
18
+ def queries_per_second
19
+ raw.match(/Requests per second:\s*([\d\.]+)\s\[#\/sec\]\s\(mean\)/)[1].to_f
20
+ end
21
+
22
+ def failed_requests
23
+ raw.match(/Failed requests:\s*([\d\.]+)/)[1].to_i
24
+ end
25
+
26
+ def log
27
+ AbCrunch::Logger.log :ab_result, "#{command}"
28
+ AbCrunch::Logger.log :ab_result, "Average Response Time: #{avg_response_time}"
29
+ AbCrunch::Logger.log :ab_result, "Queries per Second: #{queries_per_second}"
30
+ AbCrunch::Logger.log :ab_result, "Failed requests: #{failed_requests}"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ require 'open3'
2
+ require 'pp'
3
+
4
+ module AbCrunch
5
+ class AbRunner
6
+ def self.validate_options(options)
7
+ raise "AB Options missing :url" unless options.has_key? :url
8
+ AbCrunch::Config.ab_options.merge(options)
9
+ end
10
+
11
+ def self.ab_command(options)
12
+ options = validate_options(options)
13
+ url = AbCrunch::Page.get_url(options, true)
14
+ AbCrunch::Logger.log(:info, "Calling ab on: #{url}")
15
+ "ab -c #{options[:concurrency]} -n #{options[:num_requests]} -k -H 'Accept-Encoding: gzip' #{url}"
16
+ end
17
+
18
+ def self.ab(options)
19
+ cmd = ab_command(options)
20
+ result = nil
21
+
22
+ Open3.popen3(cmd) do |stdin, stdout, stderr|
23
+ err_lines = stderr.readlines
24
+ if err_lines.length > 0
25
+ cmd = cmd.cyan
26
+ err = "AB command failed".red
27
+ err_s = err_lines.reduce {|line, memo| memo += line}.red
28
+ raise "#{err}\nCommand: #{cmd}\n#{err_s}"
29
+ end
30
+ result = AbCrunch::AbResult.new stdout.readlines.reduce {|line, memo| memo += line}, options
31
+ end
32
+
33
+ result
34
+ end
35
+ end
36
+ end