topfunky-bong 0.0.2

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.
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
+