topfunky-bong 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (7) hide show
  1. data/History.txt +12 -0
  2. data/Manifest.txt +6 -0
  3. data/README.txt +112 -0
  4. data/Rakefile +16 -0
  5. data/bin/bong +102 -0
  6. data/lib/bong.rb +225 -0
  7. metadata +69 -0
@@ -0,0 +1,12 @@
1
+ == 0.0.2 / ???
2
+
3
+ * ???
4
+
5
+ == 0.0.1 / 2007-11-07
6
+
7
+ * First release. See README.txt for the details.
8
+
9
+ == TODO
10
+
11
+ * Compare multiple benchmarking runs and generate a report.
12
+ * Generate Gruff graphs or an HTML report.
@@ -0,0 +1,6 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ bin/bong
6
+ lib/bong.rb
@@ -0,0 +1,112 @@
1
+ bong
2
+ by Geoffrey Grosenbach
3
+ boss@topfunky.com
4
+ http://topfunky.com
5
+
6
+ == DESCRIPTION:
7
+
8
+ Hit your website with bong. Uses httperf to run a suite of benchmarking tests against specified urls on your site.
9
+
10
+ Graphical output and multi-test comparisons are planned. Apache ab support may be added in the future.
11
+
12
+ == USAGE
13
+
14
+ See all options with:
15
+
16
+ % bong --help
17
+
18
+ Generate a config file (writes to config/httperf.yml by default):
19
+
20
+ % bong --generate
21
+
22
+ Edit the config file with a list of servers, urls, and your preferred sample size. NOTE: Don't run this against servers you don't own!
23
+
24
+ ---
25
+ uris:
26
+ - /
27
+ - /pages/about
28
+ samples: 2
29
+ servers:
30
+ - localhost:3000
31
+
32
+ Run the benchmarking suite, label it 'baseline', and output the raw data to a file (writes output to log/httperf-report.yml by default):
33
+
34
+ % bong baseline
35
+
36
+ A report will be printed to the screen and the raw data will be saved to log/httperf-report.yml (change with the --out option). It's a good idea to use a label for each test so you can compare them later and find out what the fastest implementation was. Examples: 'baseline', 'memcached-optimization', 'sql-queries', etc.
37
+
38
+ baseline
39
+ localhost
40
+ / 37-56 req/sec
41
+ /products.rss 395-403 req/sec
42
+ example.com
43
+ / 35-58 req/sec
44
+ /products.rss 400-407 req/sec
45
+
46
+ View the saved report again with:
47
+
48
+ % bong baseline -r log/httperf-report.yml
49
+
50
+ Lather, rinse, repeat.
51
+
52
+ == LIMITATIONS
53
+
54
+ * Can't access pages that require login.
55
+ * HTTP GET only.
56
+
57
+ == REQUIREMENTS
58
+
59
+ The httperf command-line tool must be installed. Get it here:
60
+
61
+ http://www.hpl.hp.com/research/linux/httperf/download.php
62
+
63
+ You must start a webserver (or just Mongrel). Ideally, this would be on a different machine over a fast network. You can also run bong against the local machine or (less ideally) against a remote machine over a slow network, but these will give different performance numbers and may not be accurate.
64
+
65
+ Internally, bong will
66
+
67
+ * Run a short series of 5 hits against a URL.
68
+ * Calculate the number of hits needed to run for 10 seconds, or 2 samples. You can change this in the config file, but 2 is the minimum for getting a meaningful standard deviation out of the report.
69
+ * A test will be run again.
70
+ * A short report will be displayed and the raw data will be saved for later comparison.
71
+
72
+ See http://peepcode.com/products/benchmarking-with-httperf for a full tutorial on httperf (produced by Geoffrey Grosenbach, technical editing by Zed Shaw).
73
+
74
+ == INSTALL:
75
+
76
+ * sudo gem install bong
77
+
78
+ == EXPERIMENTAL GRAPHS
79
+
80
+ After running bong a number of times, provide a visual output with time on the x axis and req/second on the y axis. Intended to show the change in performance throughout the development of a project.
81
+
82
+ Assumptions
83
+
84
+ * Each run will be named "benchmark-1216122887" where the second part can be converted to a Time class using Time.at(..)
85
+ * Runs will be displayed at equal intervals along the x axis regardless of if the time between them is uniform
86
+ * Several URLs may be incldued. rps for each one will be of a similar order of magnitude, so it makes sense to graph them together
87
+ * Not all URLs will have data in all runs. However once a url is added it will be in ALL subsequent runs
88
+
89
+ == LICENSE:
90
+
91
+ (The MIT License)
92
+
93
+ Copyright (c) 2007 Topfunky Corporation
94
+
95
+ Permission is hereby granted, free of charge, to any person obtaining
96
+ a copy of this software and associated documentation files (the
97
+ 'Software'), to deal in the Software without restriction, including
98
+ without limitation the rights to use, copy, modify, merge, publish,
99
+ distribute, sublicense, and/or sell copies of the Software, and to
100
+ permit persons to whom the Software is furnished to do so, subject to
101
+ the following conditions:
102
+
103
+ The above copyright notice and this permission notice shall be
104
+ included in all copies or substantial portions of the Software.
105
+
106
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
107
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
108
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
109
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
110
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
111
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
112
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+
3
+ require 'hoe'
4
+ require './lib/bong.rb'
5
+
6
+ Hoe.new('bong', Bong::VERSION) do |p|
7
+ p.rubyforge_name = 'bong'
8
+ p.author = 'Geoffrey Grosenbach'
9
+ p.email = 'boss@topfunky.com'
10
+ p.summary = 'Website benchmarking helper.'
11
+ p.description = p.paragraphs_of('README.txt', 1..2).join("\n\n")
12
+ p.url = "http://rubyforge.org/projects/bong"
13
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
14
+
15
+ p.remote_rdoc_dir = '' # Release to root
16
+ end
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ ##
4
+ # Created on 2007-11-6.
5
+ # Copyright (c) 2007 Topfunky Corporation
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining
8
+ # a copy of this software and associated documentation files (the
9
+ # "Software"), to deal in the Software without restriction, including
10
+ # without limitation the rights to use, copy, modify, merge, publish,
11
+ # distribute, sublicense, and/or sell copies of the Software, and to
12
+ # permit persons to whom the Software is furnished to do so, subject to
13
+ # the following conditions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be
16
+ # included in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ begin
27
+ require "rubygems"
28
+ require File.dirname(__FILE__) + "/../lib/bong"
29
+ rescue LoadError
30
+ require File.dirname(__FILE__) + "/../lib/bong"
31
+ end
32
+
33
+ require 'optparse'
34
+
35
+ OPTIONS = {
36
+ :config => 'config/httperf.yml',
37
+ :generate => 'config/httperf.yml',
38
+ :out => "log/httperf-report.yml"
39
+ }
40
+ MANDATORY_OPTIONS = %w( )
41
+
42
+ parser = OptionParser.new do |opts|
43
+ opts.banner = <<-BANNER
44
+ A benchmarking tool for staging hits on your website. Uses httperf.
45
+
46
+ Usage: #{File.basename($0)} [label] [options]
47
+
48
+ label is a name for this benchmarking run.
49
+ Examples: 'baseline', 'with-optimizations', 'custom-sql-queries'
50
+
51
+ Other options are:
52
+ BANNER
53
+
54
+ opts.separator ""
55
+
56
+ opts.on("-c", "--config [PATH]", String, "Path to config file.", "Default: config/httperf.yml") do |v|
57
+ OPTIONS[:config] = v || OPTIONS[:config]
58
+ end
59
+
60
+ opts.on("-g", "--generate [PATH]", String, "Generate a config file.", "Default: config/httperf.yml") do |v|
61
+ Bong.generate(v || OPTIONS[:generate]); exit
62
+ end
63
+
64
+ opts.on("-o", "--out [PATH]", String, "Write output to a file.") do |v|
65
+ OPTIONS[:out] = v || OPTIONS[:out]
66
+ end
67
+
68
+ opts.on("-r", "--report PATH", String, "Display the report for a data file.") do |v|
69
+ OPTIONS[:report] = v
70
+ end
71
+
72
+ opts.on("-t", "--time-report PATH", String, "Create a time graph showing performance over time", "Only works when run in combination with -r") do |v|
73
+ OPTIONS[:graph] = v
74
+ end
75
+
76
+ opts.on("-h", "--help", "Show this help message.") { puts opts; exit }
77
+
78
+ opts.parse!(ARGV)
79
+
80
+ if MANDATORY_OPTIONS && MANDATORY_OPTIONS.find { |option| OPTIONS[option.to_sym].nil? }
81
+ puts opts; exit
82
+ end
83
+
84
+ OPTIONS[:label] = ARGV.first || "benchmark-#{Time.now.to_i}"
85
+
86
+ end
87
+
88
+ # Finally, run the app.
89
+
90
+ bong = Bong.new(OPTIONS[:config], OPTIONS[:label])
91
+
92
+ if OPTIONS[:graph] && OPTIONS[:report]
93
+ bong.graph_report(OPTIONS[:graph], OPTIONS[:report])
94
+ exit
95
+ elsif OPTIONS[:report]
96
+ bong.load_report(OPTIONS[:report])
97
+ else
98
+ bong.run
99
+ bong.save_report(OPTIONS[:out])
100
+ end
101
+
102
+ puts bong.report
@@ -0,0 +1,225 @@
1
+ require 'yaml'
2
+ require 'logger'
3
+
4
+ ##
5
+ # A tool for running httperf against a website. Documentation coming soon.
6
+
7
+ class Bong
8
+ VERSION = '0.0.2'
9
+
10
+ ##
11
+ # Generate a sample config file.
12
+
13
+ def self.generate(config_yml_path)
14
+ config_data = {
15
+ 'servers' => ['localhost:3000'],
16
+ 'uris' => ['/', '/pages/about'],
17
+ 'samples' => 2
18
+ }
19
+
20
+ if File.exist?(config_yml_path)
21
+ puts("A config file already exists at '#{config_yml_path}'.")
22
+ exit
23
+ end
24
+
25
+ File.open(config_yml_path, 'w') do |f|
26
+ f.write config_data.to_yaml
27
+ end
28
+ end
29
+
30
+ def initialize(config_yml_path, label)
31
+ unless File.exist?(config_yml_path)
32
+ puts <<-MESSAGE
33
+
34
+ A config file could not be found at '#{config_yml_path}'.
35
+
36
+ Please generate one with the -g option.
37
+
38
+ MESSAGE
39
+ exit
40
+ end
41
+
42
+ @config = YAML.load(File.read(config_yml_path))
43
+ @label = label
44
+ @stats = {}
45
+
46
+ @logger = Logger.new(STDOUT)
47
+ @logger.level = Logger::DEBUG
48
+
49
+ @logger.info "Running suite for label '#{@label}'"
50
+ end
51
+
52
+ def run
53
+ servers.each do |server, port|
54
+ port ||= 80
55
+ @stats[server] = {}
56
+ uris.each do |uri|
57
+ run_benchmark(server, port, uri)
58
+ end
59
+ end
60
+ end
61
+
62
+
63
+ def graph_report(graph_path, report_yml_path)
64
+ # Require gruff here so people can run the rest of the app without gruff.
65
+ require 'gruff'
66
+
67
+ @report = YAML.load(File.read(report_yml_path))
68
+
69
+ # Remove any with no date
70
+ @report.reject! { |name, data| name.split("-").size != 2 }
71
+
72
+ number_of_times = @report.size
73
+
74
+ inverted_report = { }
75
+
76
+ @report.each do |name, data|
77
+ report_time = Time.at(name.split("-").last.to_i)
78
+ date_time = report_time.strftime("%d/%m %H:%M")
79
+
80
+ data.each do |host, urls|
81
+ urls.each do |url, payload|
82
+ inverted_report[url] ||= { }
83
+ inverted_report[url][date_time] = payload['avg'] || nil
84
+ end
85
+ end
86
+ end
87
+
88
+ inverted_report.each do |url, payload|
89
+ inverted_report[url][:array] = inverted_report[url].to_a.sort.map{|ele| ele.last}
90
+ missing_times = number_of_times - inverted_report[url][:array].size
91
+ inverted_report[url][:array] = Array.new(missing_times) + inverted_report[url][:array]
92
+ end
93
+
94
+ g = Gruff::Line.new
95
+ g.title = "Requests per second"
96
+
97
+ inverted_report.each do |url, payload|
98
+ g.data(url, inverted_report[url][:array])
99
+ end
100
+
101
+ g.write(graph_path)
102
+ end
103
+
104
+ def load_report(report_yml_path, label=nil)
105
+ @report = YAML.load(File.read(report_yml_path))
106
+ label = label || @label || @report.keys.first
107
+ @stats = @report[label]
108
+ end
109
+
110
+ def report
111
+ length_of_longest_uri = uris.inject(0) { |length, uri| uri_length = uri.length; (uri_length > length ? uri_length : length) }
112
+
113
+ output = ["\n#{@label}"]
114
+ servers.each do |server, port|
115
+ output << " #{server}"
116
+ uris.each do |uri|
117
+ output << " #{format_string(uri, length_of_longest_uri)} #{format_rounded(@stats[server][uri]['avg_low'])}-#{format_rounded(@stats[server][uri]['avg_high'])} req/sec"
118
+ end
119
+ end
120
+ output.join("\n")
121
+ end
122
+
123
+ def save_report(path)
124
+ @all_stats = {}
125
+ if File.exist?(path)
126
+ @all_stats = YAML.load(File.read(path))
127
+ end
128
+
129
+ @all_stats[@label] = @stats
130
+
131
+ File.open(path, 'w') do |f|
132
+ f.write @all_stats.to_yaml
133
+ end
134
+ end
135
+
136
+ protected
137
+
138
+ ##
139
+ # A list of servers and ports from the config file.
140
+
141
+ def servers
142
+ @config['servers'].map { |server| server.split(/:/) }
143
+ end
144
+
145
+ def uris
146
+ @config['uris']
147
+ end
148
+
149
+ def run_benchmark(server, port, uri)
150
+ until (sufficient_sample_size?(server, uri) && no_errors?)
151
+ increase_num_conns(server, uri)
152
+ exec_command(server, port, uri, @num_conns)
153
+ @stats[server][uri] = parse_results
154
+ end
155
+ end
156
+
157
+ def exec_command(server, port, uri, num_conns)
158
+ @logger.info "Sending #{@num_conns} hits to #{server}:#{port}#{uri}"
159
+ cmd = "httperf --server #{server} --port #{port} --uri #{uri} --num-conns #{num_conns}"
160
+ @output = `#{cmd}`
161
+ end
162
+
163
+ def parse_results
164
+ stat = {}
165
+
166
+ # Total: connections 5 requests 5 replies 5 test-duration 0.013 s
167
+ stat['duration'] = @output.scan(/test-duration ([\d.]+)/).flatten.first.to_f
168
+
169
+ # Reply rate [replies/s]: min 0.0 avg 0.0 max 0.0 stddev 0.0 (0 samples)
170
+ (stat['min'], stat['avg'], stat['max'], stat['stddev'], stat['samples']) = @output.scan(/Reply rate \[replies\/s\]: min ([\d.]+) avg ([\d.]+) max ([\d.]+) stddev ([\d.]+) \((\d+) samples\)/).flatten.map { |i| i.to_f }
171
+
172
+ # Reply status: 1xx=0 2xx=5 3xx=0 4xx=0 5xx=0
173
+ (stat['1xx'], stat['2xx'], stat['3xx'], stat['4xx'], stat['5xx']) = @output.scan(/Reply status: 1xx=(\d+) 2xx=(\d+) 3xx=(\d+) 4xx=(\d+) 5xx=(\d+)/).flatten.map { |i| i.to_f }
174
+
175
+ stat['avg_low'] = stat['avg'].to_f - 2.0 * stat['stddev'].to_f
176
+ stat['avg_high'] = stat['avg'].to_f + 2.0 * stat['stddev'].to_f
177
+
178
+ stat
179
+ end
180
+
181
+ def sufficient_sample_size?(server, uri)
182
+ @stats[server][uri]['samples'] >= @config['samples'].to_f
183
+ rescue
184
+ false
185
+ end
186
+
187
+ def no_errors?
188
+ # TODO
189
+ true
190
+ end
191
+
192
+ def increase_num_conns(server, uri)
193
+ samples = @stats[server][uri]['samples']
194
+ duration = @stats[server][uri]['duration']
195
+ target_samples = @config['samples']
196
+
197
+ seconds_per_request = (duration / @num_conns.to_f) # 0.02
198
+ adjusted_conns = (target_samples * 5.0) / seconds_per_request # 500
199
+
200
+ # Increase the connections by the factor and a bit more.
201
+ @num_conns = (adjusted_conns * 1.2).to_i
202
+ rescue
203
+ @num_conns = 5
204
+ nil
205
+ end
206
+
207
+ ##
208
+ # Return a string with rounding for display.
209
+ #
210
+ # Small numbers will have a decimal point. Larger numbers will be shown
211
+ # as plain integers.
212
+
213
+ def format_rounded(number)
214
+ if number > 20
215
+ number.to_i
216
+ else
217
+ sprintf('%0.1f', number)
218
+ end
219
+ end
220
+
221
+ def format_string(string, length)
222
+ sprintf "%-#{length + 2}s", string
223
+ end
224
+
225
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: topfunky-bong
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Geoffrey Grosenbach
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-07-15 00:00:00 -07:00
13
+ default_executable: bong
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.5.1
23
+ version:
24
+ description: Hit your website with bong. Uses httperf to run a suite of benchmarking tests against specified urls on your site. Graphical output and multi-test comparisons are planned. Apache ab support may be added in the future.
25
+ email: boss@topfunky.com
26
+ executables:
27
+ - bong
28
+ extensions: []
29
+
30
+ extra_rdoc_files:
31
+ - History.txt
32
+ - Manifest.txt
33
+ - README.txt
34
+ files:
35
+ - History.txt
36
+ - Manifest.txt
37
+ - README.txt
38
+ - Rakefile
39
+ - bin/bong
40
+ - lib/bong.rb
41
+ has_rdoc: true
42
+ homepage: http://rubyforge.org/projects/bong
43
+ post_install_message:
44
+ rdoc_options:
45
+ - --main
46
+ - README.txt
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ requirements: []
62
+
63
+ rubyforge_project: bong
64
+ rubygems_version: 1.2.0
65
+ signing_key:
66
+ specification_version: 2
67
+ summary: Website benchmarking helper.
68
+ test_files: []
69
+