autograph 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,26 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ webgen*
21
+
22
+ ## PROJECT::SPECIFIC
23
+ load_test*
24
+ eapis*
25
+ *.svg
26
+ *.gemspec
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.0.1 2009-06-30
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/README.rdoc ADDED
@@ -0,0 +1,93 @@
1
+ = autograph
2
+
3
+ == DESCRIPTION:
4
+
5
+ Autograph drives httperf, varying the request rate and graphing the output. This exercise provides graphical data showing how the requested resources hold up under an increasing rate, particularly with request to response time and achieved request rate.
6
+
7
+ Can work with SOAP.
8
+
9
+ == NOTES:
10
+
11
+ A note on stats, from Zed Shaw: http://zedshaw.com/essays/programmer_stats.html
12
+
13
+ == PROBLEMS:
14
+
15
+ * No 'AcceptEncoding' support
16
+ * Limited to the load generation capabilities of a single server.
17
+
18
+ == SYNOPSIS:
19
+
20
+ autograph --host example.com --low-rate 5 --high-rate 25 --rate-step 5 --num-conns 25
21
+
22
+ autograph --host example.com --low-rate 10 --high-rate 50 --rate-step 10 --output-file my_load_test.html
23
+
24
+ == INSTALL:
25
+
26
+ * Get httperf installed ('brew install httperf' if you've got homebrew installed)
27
+ * Download project and run 'rake build install'
28
+
29
+ nick-stielaus-computer-3:autograph nick$ httperf -h
30
+ Usage: httperf [-hdvV] [--add-header S] [--burst-length N] [--client N/N]
31
+ [--close-with-reset] [--debug N] [--failure-status N]
32
+ [--help] [--hog] [--http-version S] [--max-connections N]
33
+ [--max-piped-calls N] [--method S] [--no-host-hdr]
34
+ [--num-calls N] [--num-conns N] [--period [d|u|e]T1[,T2]]
35
+ [--port N] [--print-reply [header|body]] [--print-request [header|body]]
36
+ [--rate X] [--recv-buffer N] [--retry-on-failure] [--send-buffer N]
37
+ [--server S] [--server-name S] [--session-cookies]
38
+ [--ssl] [--ssl-ciphers L] [--ssl-no-reuse]
39
+ [--think-timeout X] [--timeout X] [--uri S] [--verbose] [--version]
40
+ [--wlog y|n,file] [--wsess N,N,X] [--wsesslog N,X,file]
41
+ [--wset N,X]
42
+ nick-stielaus-computer-3:autograph nick$ autograph
43
+ Usage: autograph [options]
44
+ --host HOST The host to load test
45
+ --port PORT The port to load test
46
+ --uris PATH,PATH A comma separated list of pages to cycle through
47
+ --output-file PATH Specify the file to output to.
48
+ --output-dir PATH Specify a directory to write output files to.
49
+ --notes NOTES Notes to be written to the report.
50
+ --test Do not run benchmarks. Use test data to generate reports.
51
+ Httperf Knobs:
52
+ --timeout SECONDS The length in seconds before a request is marked as errored
53
+ --num-call NUMCALLS The number of calls to make for each connection in the test (defaults to one).
54
+ --num-conns NUMCONNS The number of connections to make for each test
55
+ --low-rate LOWRATE The starting rate
56
+ --high-rate HIGHRATE The highest rate at which to perform a test
57
+ --rate-step RATESTEP The ammount at which to increment the rate for each interation of the test
58
+ --wsesslog PATH Path to the wsesslog file.
59
+ Common options:
60
+ -h, --help Displays this help info
61
+ -v, --verbose Verbose output
62
+
63
+
64
+ == THANKS:
65
+
66
+ Thanks to
67
+
68
+ Julian T J Midgley
69
+ Autobench
70
+ http://www.xenoclast.org/autobench/
71
+
72
+ Ilya Grigorik
73
+ Autoperf
74
+ http://github.com/igrigorik/autoperf/tree
75
+
76
+ HP
77
+ httperf
78
+ http://www.hpl.hp.com/research/linux/httperf/
79
+
80
+
81
+ == LICENSE:
82
+
83
+
84
+ Autograph
85
+ Copyright (c) 2009 Nick Stielau
86
+
87
+ Based on ideas and code from autoperf.rb
88
+ http://github.com/igrigorik/autoperf/tree
89
+ Copyright (C)2008 Ilya Grigorik
90
+
91
+
92
+
93
+ You can redistribute this under the terms of the Ruby license
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ require 'rake'
2
+ require File.dirname(__FILE__) + '/lib/autograph'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "autograph"
8
+ gem.summary = %Q{Simple httperf runner with nice html reports with graphs.}
9
+ gem.description = %Q{Autograph wraps httperf, running multiple tests while varying the parameters, graphing the output.}
10
+ gem.email = "nick.stielau@gmail.com"
11
+ gem.homepage = "http://github.com/nstielau/autograph"
12
+ gem.authors = ["Nick Stielau"]
13
+ gem.add_runtime_dependency 'builder', '= 2.1.2'
14
+ gem.add_runtime_dependency 'ruport', '= 1.6.3'
15
+ gem.add_runtime_dependency 'scruffy', '= 0.2.6'
16
+ gem.add_runtime_dependency 'gchart', '= 1.0.0'
17
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
22
+ end
23
+
24
+ require 'rake/testtask'
25
+ Rake::TestTask.new(:test) do |test|
26
+ test.libs << 'lib' << 'test'
27
+ test.pattern = 'test/**/test_*.rb'
28
+ test.verbose = true
29
+ end
30
+
31
+
32
+ task :test => :check_dependencies
33
+ task :default => :test
data/TODO ADDED
@@ -0,0 +1,32 @@
1
+ # Report for AVG says "Report for http://127.0.0.1Avg"
2
+ # Skin report page
3
+ # Compare two hosts
4
+ # Config file?
5
+
6
+ # Deal with AcceptEncoding, add options
7
+
8
+ # Factor out outputs types into a report_renderers dir
9
+ # Control verbose output with a Logger, instead of just going to STDOUT
10
+
11
+ # Don't include overview for one page...
12
+ # Flag maxes
13
+ # Set report overview y-axis to the max request rate
14
+
15
+ # Pass-through httperf args
16
+ # Pick output format
17
+ # Pick graph types
18
+ # take a txt file for the urls
19
+
20
+ # Remove default timeout httperf option
21
+ # Add interactive mode
22
+ # Add badass testing server
23
+
24
+ # JS for selecting right view after page refresh
25
+
26
+ # Fix Graphs x=y!
27
+
28
+ # --group graphs, or separate into different pages
29
+
30
+
31
+ # Trap exit and write output to file? (Can you have two traps?)
32
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.2
data/bin/autograph ADDED
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'autograph'
5
+
6
+ options = {}
7
+
8
+ command_parts = [File.basename(__FILE__)]
9
+
10
+ opt_parse_opts = OptionParser.new do |opts|
11
+ opts.banner = "Usage: #{File.basename($0)} [options]"
12
+
13
+ opts.on("--host HOST", "The host to load test") do |opt|
14
+ options['host'] = opt
15
+ command_parts << "--host #{opt}"
16
+ end
17
+
18
+ opts.on("--port PORT", Integer, "The port to load test") do |opt|
19
+ options['port'] = opt
20
+ command_parts << "--port #{opt}"
21
+ end
22
+
23
+ opts.on("--uris PATH,PATH", String, "A comma separated list of pages to cycle through") do |opt|
24
+ options['uris'] = opt.split(',')
25
+ command_parts << "--uris #{opt}"
26
+ end
27
+
28
+ opts.on("--output-file PATH", "Specify the file to output to.") do |opt|
29
+ options['output_file'] = opt
30
+ command_parts << "--output-file #{opt}"
31
+ end
32
+
33
+ opts.on("--output-dir PATH", "Specify a directory to write output files to.") do |opt|
34
+ options["output_dir"] = opt;
35
+ command_parts << "--output-dir"
36
+ end
37
+
38
+ opts.on("--notes NOTES", "Notes to be written to the report.") do |opt|
39
+ options['notes'] = opt
40
+ command_parts << "--notes '#{opt}'"
41
+ end
42
+
43
+ # opts.on("--average", "Run a round-robin test of all URIs") do
44
+ # options["average"] = true;
45
+ # command_parts << "--average"
46
+ # end
47
+
48
+ opts.on("--test", "Do not run benchmarks. Use test data to generate reports.") do |opt|
49
+ options['use_test_data'] = true
50
+ command_parts << "--test"
51
+ end
52
+
53
+ opts.separator ""
54
+ opts.separator "Httperf Knobs:"
55
+
56
+ opts.on("--timeout SECONDS", Integer, "The length in seconds before a request is marked as errored") do |opt|
57
+ options['httperf_timeout'] = opt
58
+ command_parts << "--timeout #{opt}"
59
+ end
60
+
61
+ opts.on("--num-call NUMCALLS", Integer, "The number of calls to make for each connection in the test (defaults to one).") do |opt|
62
+ options['httperf_num-call'] = opt
63
+ command_parts << "--num-call #{opt}"
64
+ end
65
+
66
+ opts.on("--num-conns NUMCONNS", Integer, "The number of connections to make for each test") do |opt|
67
+ options['httperf_num-conns'] = opt
68
+ command_parts << "--num-conns #{opt}"
69
+ end
70
+
71
+ # opts.on("--rate RATE", Integer, "The rate at which to make requests") do |opt|
72
+ # options['httperf_rate'] = opt
73
+ # command_parts << "--rate #{opt}"
74
+ # end
75
+
76
+ opts.on("--low-rate LOWRATE", Integer, "The starting rate") do |opt|
77
+ options['low_rate'] = opt
78
+ command_parts << "-low-rate #{opt}"
79
+ end
80
+
81
+ opts.on("--high-rate HIGHRATE", Integer, "The highest rate at which to perform a test") do |opt|
82
+ options['high_rate'] = opt
83
+ command_parts << "--high-rate #{opt}"
84
+ end
85
+
86
+ opts.on("--rate-step RATESTEP", Integer, "The ammount at which to increment the rate for each interation of the test") do |opt|
87
+ options['rate_step'] = opt
88
+ command_parts << "--rate-step #{opt}"
89
+ end
90
+
91
+ # opts.on("--wlog PATH", String, "A file a ASCII nul terminated pages to cycle through") do |opt|
92
+ # options['httperf_wlog'] = "y,#{opt}";
93
+ # command_parts << "--wlog #{opt}"
94
+ # end
95
+
96
+ opts.on("--wsesslog PATH", "Path to the wsesslog file.") do |opt|
97
+ options['httperf_wsesslog'] = opt
98
+ command_parts << "--wsesslog '#{opt}'"
99
+ end
100
+
101
+ opts.separator ""
102
+ opts.separator "Common options:"
103
+
104
+ opts.on("-h", "--help", "Displays this help info") do
105
+ puts opts
106
+ exit 0
107
+ end
108
+
109
+ opts.on("-v", "--verbose", "Verbose output") do
110
+ options["verbose"] = true;
111
+ command_parts << "-v"
112
+ end
113
+
114
+ begin
115
+ opts.parse!(ARGV)
116
+ rescue OptionParser::ParseError => e
117
+ warn e.message
118
+ puts opts
119
+ exit 1
120
+ end
121
+
122
+ opts
123
+ end
124
+
125
+ if options.length == 0
126
+ puts opt_parse_opts
127
+ exit
128
+ end
129
+
130
+ options["command_run"] = command_parts.join(" ")
131
+
132
+ trap("INT") {
133
+ abort("Terminating autograph.")
134
+ }
135
+
136
+ if options['verbose']
137
+ puts
138
+ puts "These args were specified from the command line:"
139
+ options.sort.each do |key, value|
140
+ puts " #{key}=#{value}"
141
+ end
142
+ puts
143
+ end
144
+
145
+ begin
146
+ AutoPerf.new(options)
147
+ rescue => e
148
+ abort(e.to_s)
149
+ end
data/lib/autograph.rb ADDED
@@ -0,0 +1,15 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
2
+
3
+ require 'rubygems'
4
+ require 'optparse'
5
+ require 'ruport'
6
+ require 'pp'
7
+
8
+ require 'autograph/autoperf'
9
+ require 'autograph/graph_series'
10
+ require 'autograph/html_report'
11
+ require 'autograph/configuration'
12
+
13
+ require 'autograph/graph_renderers/base_renderer'
14
+ require 'autograph/graph_renderers/gchart_renderer'
15
+ require 'autograph/graph_renderers/scruffy_renderer'
@@ -0,0 +1,105 @@
1
+ class AutoPerf
2
+ COLUMN_NAMES = ['rate', 'conn/s', 'req/s', 'replies/s avg', 'errors', 'net io (KB/s)', 'reply time']
3
+
4
+ def initialize(opts = {})
5
+ conf = Configuration.new(opts)
6
+
7
+ puts configuration.pretty_print if opts['verbose']
8
+
9
+ if conf['use_test_data']
10
+ conf['uris'] = ['/', '/page1', '/page2']
11
+ @reports = load_test_data(conf)
12
+ else
13
+ @reports = run_tests(conf)
14
+ end
15
+
16
+ graphs = BaseRenderer.generate_graphs(@reports, conf)
17
+ HtmlReport.new(@reports, graphs, conf)
18
+ end
19
+
20
+ def benchmark(conf)
21
+ raise "You must specify a host." if conf['host'].nil?
22
+ httperf_opt = conf.keys.grep(/httperf/).collect {|k| "--#{k.gsub(/httperf_/, '')} #{conf[k]}"}.join(" ")
23
+ httperf_cmd = "httperf --hog --server #{conf['host']} --port #{conf['port']} #{httperf_opt}"
24
+
25
+ res = Hash.new("")
26
+ IO.popen("#{httperf_cmd} 2>&1") do |pipe|
27
+ puts "\n#{httperf_cmd}"
28
+
29
+ while((line = pipe.gets))
30
+ res['output'] += line
31
+
32
+ case line
33
+ when /^Total: .*replies (\d+)/ then res['replies'] = $1
34
+ when /^Connection rate: (\d+\.\d)/ then res['conn/s'] = $1
35
+ when /^Request rate: (\d+\.\d)/ then res['req/s'] = $1
36
+ when /^Reply time .* response (\d+\.\d)/ then res['reply time'] = $1
37
+ when /^Net I\/O: (\d+\.\d)/ then res['net io (KB/s)'] = $1
38
+ when /^Errors: total (\d+)/ then res['errors'] = $1
39
+ when /^Reply rate .*min (\d+\.\d) avg (\d+\.\d) max (\d+\.\d) stddev (\d+\.\d)/ then
40
+ res['replies/s min'] = $1
41
+ res['replies/s avg'] = $2
42
+ res['replies/s max'] = $3
43
+ res['replies/s stddev'] = $4
44
+ end
45
+ end
46
+ end
47
+
48
+ return res
49
+ end
50
+
51
+ def vary_rate(uri, configuration)
52
+ puts "Config is #{configuration.inspect}" if configuration['verbose']
53
+ results = {}
54
+ report = Table(:column_names => COLUMN_NAMES)
55
+
56
+ (configuration['low_rate']..configuration['high_rate']).step(configuration['rate_step']) do |rate|
57
+ results[rate] = benchmark(configuration.merge({'httperf_rate' => rate, 'httperf_uri' => uri}))
58
+ report << results[rate].merge({'rate' => rate})
59
+
60
+ puts report.to_s
61
+ puts results[rate]['output'] if results[rate]['errors'].to_i > 0
62
+ end
63
+
64
+ report
65
+ end
66
+
67
+ def run_tests(configuration)
68
+ reports = {}
69
+ configuration['uris'].uniq.each do |uri|
70
+ reports[uri] = vary_rate(uri, configuration)
71
+ end
72
+
73
+ # TODO: Factor out to create_httperf_wlog
74
+ if !configuration['httperf_wlog'] && configuration['uris'].length > 1 && configuration['average']
75
+ replay_log = File.open('tmp_replay_log', 'w')
76
+ path = replay_log.path
77
+ puts "Tmp replay log is at #{path}" if configuration['verbose']
78
+ index = 1
79
+ configuration['uris'].each do |uri|
80
+ replay_log.print uri
81
+ replay_log.putc 0 if index < configuration['uris'].length # ASCII NUL Terminate join paths
82
+ index = index + 1
83
+ end
84
+ replay_log.close
85
+ puts "Replay log is at #{path}" if configuration['verbose']
86
+ reports["Avg"] = vary_rate('httperf_wlog' => "y,#{path}")
87
+ end
88
+ reports
89
+ end
90
+
91
+ def load_test_data(configuration)
92
+ reports = {}
93
+ configuration['uris'].each do |uri|
94
+ reports[uri] = ::Ruport::Data::Table.new(:column_names => COLUMN_NAMES)
95
+ times = [130.7, 132.7, 180.4, 438.3, 591.9, 686.9, 739.4, 661.3, 727.1, 546.5, 711.1, 893.7, 870.0]
96
+ conns = [5.0, 21.5, 28.8, 30.6, 26.3, 24.7, 23.0, 25.8, 28.0, 27.4, 27.9, 22.2, 22.7]
97
+ 1.upto(10) do |i|
98
+ reports[uri] << {'rate' => i*10 - 10,
99
+ 'conn/s' => conns[i % conns.length],
100
+ 'reply time' => times[i % times.length]}
101
+ end
102
+ end
103
+ reports
104
+ end
105
+ end
@@ -0,0 +1,63 @@
1
+ class Configuration
2
+ def initialize(opts={})
3
+ @conf = {'httperf_timeout' => 20,
4
+ 'httperf_num-call' => 1,
5
+ 'httperf_num-conns' => 100,
6
+ 'httperf_rate' => 5,
7
+ 'port' => 80,
8
+ 'uri' => '/',
9
+ 'low_conns' => 50,
10
+ 'high_conns' => 550,
11
+ 'conns_step' => 100,
12
+ 'low_rate' => 5,
13
+ 'high_rate' => 250,
14
+ 'rate_step' => 20,
15
+ 'uris' => ['/'],
16
+ 'use_test_data' => false,
17
+ 'batik-rasterizer-jar' => '/opt/batik/batik-rasterizer.jar',
18
+ 'graph_renderer' => 'GChartRenderer',
19
+ 'average' => false,
20
+ 'output_dir' => './'
21
+ }.merge(opts)
22
+
23
+ # This is a little too much 'magic'
24
+ if @conf['httperf_wsesslog']
25
+ puts "Using httperf_wsesslog"
26
+ @conf.delete("httperf_num-call")
27
+ @conf.delete("httperf_num-conns")
28
+ @conf["httperf_add-header"] = "'Content-Type: application/x-www-form-urlencoded\\n'"
29
+ # TODO: Add AcceptEncoding: gzip,deflate option
30
+ end
31
+
32
+ @conf
33
+ end
34
+
35
+ def [](prop)
36
+ @conf[prop]
37
+ end
38
+
39
+ def []=(prop, value)
40
+ @conf[prop] = value
41
+ end
42
+
43
+ def merge(other_hash)
44
+ @conf.dup.merge(other_hash)
45
+ end
46
+
47
+ def inspect
48
+ @conf.inspect
49
+ end
50
+
51
+ def pretty_print
52
+ io = StringIO.new
53
+ io.puts
54
+ io.puts "Using these parameters:"
55
+ @conf.sort.each{ |k, v| io.puts " #{k}=#{v}"}
56
+ io.puts
57
+ io.read
58
+ end
59
+
60
+ def graph_renderer_class
61
+ Object.const_get(@conf['graph_renderer'].to_s)
62
+ end
63
+ end
@@ -0,0 +1,76 @@
1
+ class BaseRenderer
2
+ attr :title, true
3
+ attr :width, true
4
+ attr :height, true
5
+ attr :path, true
6
+ attr :series
7
+
8
+ def initialize
9
+ @series = []
10
+ end
11
+
12
+ def add_series(new_series)
13
+ series.push(new_series)
14
+ end
15
+
16
+ def to_html
17
+ return "DATA"
18
+ end
19
+
20
+ def find_max_y_value
21
+ series.map{|s| s.y_values.max}.max
22
+ end
23
+
24
+ def self.generate_graphs(reports, configuration)
25
+ graphs = {}
26
+ reports.each do |uri, report|
27
+ graphs[uri] = []
28
+
29
+ puts "For '#{uri}' the values are #{report.column('reply time').join(', ')}" if configuration['verbose']
30
+ request_rate_graph = configuration.graph_renderer_class.new
31
+ request_rate_graph.title = "Demanded vs. Achieved Request Rate (r/s)"
32
+ request_rate_graph.path = uri
33
+ request_rate_graph.width = 600
34
+ request_rate_graph.height = 300
35
+
36
+ if reports['Avg']
37
+ avg_request_rate = GraphSeries.new(:area, report.column('rate'), reports['Avg'].column('conn/s').map{|x| x.to_f}, "Avg")
38
+ request_rate_graph.add_series(avg_request_rate)
39
+ end
40
+
41
+ request_rate = GraphSeries.new(:line, report.column('rate'), report.column('conn/s').map{|x| x.to_f}, "Requests for '#{uri}'")
42
+ request_rate_graph.add_series(request_rate)
43
+
44
+ graphs[uri] << request_rate_graph.to_html
45
+
46
+ response_time_graph = configuration.graph_renderer_class.new
47
+ response_time_graph.path = uri
48
+ response_time_graph.title = "Demanded Request Rate (r/s) vs. Response Time"
49
+ response_time_graph.width = 600
50
+ response_time_graph.height = 300
51
+
52
+ if reports['Avg']
53
+ avg_response_time = GraphSeries.new(:area, report.column('rate'), reports['Avg'].column('reply time').map{|x| x.to_f}, "Avg")
54
+ response_time_graph.add_series(avg_response_time)
55
+ end
56
+
57
+ response_time = GraphSeries.new(:line, report.column('rate'), report.column('reply time'), "Requests for '#{uri}'")
58
+ response_time_graph.add_series(response_time)
59
+
60
+ graphs[uri] << response_time_graph.to_html
61
+ end
62
+
63
+ max_rate_graph = configuration.graph_renderer_class.new
64
+ max_rate_graph.title = "Max Achieved Connection Rate"
65
+ max_rate_graph.width = 600
66
+ max_rate_graph.height = 300
67
+
68
+ reports.keys.each do |key|
69
+ max = reports[key].column('conn/s').map{|x| x.to_i}.max.to_i
70
+ max_request_rate = GraphSeries.new(:bar, [key], [max], "Max Request Rate for '#{key}'")
71
+ max_rate_graph.add_series(max_request_rate)
72
+ end
73
+ graphs['summary_graph'] = max_rate_graph
74
+ graphs
75
+ end
76
+ end
@@ -0,0 +1,82 @@
1
+ require 'gchart'
2
+
3
+ class GChartRenderer < BaseRenderer
4
+
5
+ def to_html
6
+ #"<img src='#{render_graph.to_url}'/>"
7
+ render_graph.to_url
8
+ end
9
+
10
+ private
11
+ def available_colors
12
+ [:red, :yellow, :green, :blue, :black]
13
+ end
14
+
15
+ def render_graph
16
+ chart_type = series[0].type.to_s
17
+ chart_type = 'line' if chart_type == 'area'
18
+
19
+ chart = GChart.send(chart_type) do |g|
20
+
21
+ x_values = series[0].x_values
22
+ y_values = series[0].y_values
23
+
24
+ x_values = series.map{|s| s.x_values} if chart_type.to_sym == :bar
25
+
26
+ series_data = series.map{|s| s.y_values}
27
+
28
+ g.data = series_data
29
+
30
+ # chg, grid lines
31
+ # chm data point makers
32
+ # chma, margins
33
+ # chm, flags :chm => "fMax,FF0000,0,#{y_values.index(y_values.max)},15"
34
+ # cht, bvg (bar vertical grouped)
35
+ # chbh, bar spacing
36
+ # chdlp, legend on bottom, vertical
37
+
38
+ # TODO: Should case off of chart types
39
+
40
+ extras = {:chg => "#{100/(y_values.size)},20,1,5",
41
+ :chm => "o,0066FF,0,-1.0,6",
42
+ :chma => "20,20,20,30|80,20",
43
+ :chdlp => "bv"}
44
+
45
+ if chart_type.to_sym == :bar
46
+ extras = extras.merge({:chm => '', :chbh => 'a,20,20'})
47
+ # chm=N*f0*,000000,1,-1,11|N*f0*,000000,2,-1,11|N*f1*,000000,3,-1,11|N*f2*,FF0000,0,0,18
48
+ g.grouped = true
49
+ end
50
+
51
+ g.extras = extras
52
+
53
+ g.axis(:bottom) do |a|
54
+ a.labels = [0] << x_values
55
+ a.text_color = :black
56
+ end
57
+
58
+ g.axis(:left) do |a|
59
+ interval = (y_values.max/10).to_i
60
+ interval = 1 if interval == 0
61
+ a.labels = (0..(y_values.max.to_i)).to_a.select{|y| y % interval == 0}
62
+ a.text_color = :black
63
+ end
64
+
65
+ if chart_type.to_sym == :bar
66
+ g.orientation = :vertical
67
+ end
68
+
69
+ colors = available_colors
70
+ g.colors = series.map{|s| colors.pop}
71
+
72
+ g.legend = series.map{|s| s.label}
73
+
74
+ g.title = title
75
+
76
+ g.width = width
77
+ g.height = height
78
+
79
+ g.entire_background = "f4f4f4"
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,20 @@
1
+ def rasterize_graphs
2
+ batik_location = @conf['batik-rasterizer-jar']
3
+ if File.exist?(batik_location)
4
+ `java -jar #{batik_location} *.svg > /dev/null 2>&1`
5
+
6
+ Dir.glob('*.png').each do |png|
7
+ if File.size(png) > 0
8
+ File.delete(png.sub('.png', '.svg'))
9
+ else
10
+ puts "Error rasterizing #{png}, leaving SVG intact."
11
+ end
12
+ end
13
+
14
+ @graphs.each do |uri, graph|
15
+ @graphs[uri].each do |file_name|
16
+ @graphs[uri] = @graphs[uri].map{|x| File.exist?(x.sub('.svg', '.png')) ? x.sub('.svg', '.png') : x}
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,31 @@
1
+ require 'scruffy'
2
+
3
+ class ScruffyRenderer < BaseRenderer
4
+
5
+ def to_html
6
+ render_graph
7
+ "<img src='#{@file_name}'/>"
8
+ end
9
+
10
+ private
11
+ def format_file_name(desired_name)
12
+ desired_name.gsub("/","_").gsub("=","_").gsub("?","_")
13
+ end
14
+
15
+ def render_graph
16
+ graph = Scruffy::Graph.new
17
+ graph.title = title
18
+ graph.renderer = Scruffy::Renderers::Standard.new(:values => series[0].x_values.size + 1)
19
+ graph.point_markers = series[0].x_values
20
+
21
+ series.each do |s|
22
+ graph.add s.type.to_sym, s.label, s.y_values
23
+ end
24
+
25
+ @file_name = format_file_name("request_graph_#{path}.svg")
26
+
27
+ graph.render :to => @file_name, :min_value => 0, :max_value => find_max_y_value
28
+
29
+ graph
30
+ end
31
+ end
@@ -0,0 +1,16 @@
1
+ class GraphSeries
2
+ attr :x_values, true
3
+ attr :y_values, true
4
+ attr :label, true
5
+ attr :type, true
6
+ attr :path, true
7
+
8
+ def initialize(t, xs, ys, l, p=nil)
9
+ @type = t
10
+ @x_values = xs.map{|x| x.to_f}
11
+ @y_values = ys.map{|x| x.to_f}
12
+ @label = l
13
+ @path = p
14
+ end
15
+
16
+ end
@@ -0,0 +1,33 @@
1
+ class HtmlReport
2
+ require 'erb'
3
+
4
+ def initialize(reports, graphs, configuration)
5
+ date = Time.now
6
+ host = configuration['host']
7
+ title = "Report for #{host}"
8
+ uris = configuration['uris']
9
+ command_run = configuration["command_run"]
10
+ notes = configuration["notes"]
11
+ summary_graph = graphs['summary_graph']
12
+
13
+ output_file = HtmlReport.determine_output_file(configuration['output_file'], configuration['output_dir'])
14
+
15
+ template = File.read(File.dirname(__FILE__) + '/report.html.erb')
16
+ result = ERB.new(template).result(binding).to_s
17
+
18
+ File.open(output_file, "w") do |file|
19
+ file.puts result
20
+ end
21
+ end
22
+
23
+ def self.determine_output_file(output_file, output_dir)
24
+ return output_file if output_file
25
+ file = File.join(output_dir, "load_test.html")
26
+ i = 0
27
+ while File.exist?(file) do
28
+ i = i + 1
29
+ file = File.join(output_dir, "load_test_#{i}.html")
30
+ end
31
+ file
32
+ end
33
+ end
@@ -0,0 +1,174 @@
1
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
2
+ "http://www.w3.org/TR/html4/strict.dtd">
3
+ <html lang="en">
4
+ <head>
5
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
6
+ <title><%= title %></title>
7
+ <style type="text/css">
8
+ body,
9
+ html {
10
+ margin:0;
11
+ padding:0;
12
+ color:#000;
13
+ }
14
+ body {
15
+ min-width:950px;
16
+ }
17
+ #wrap {
18
+ margin:0 auto;
19
+ width:950px;
20
+ }
21
+ #header h1 {
22
+ padding:5px;
23
+ margin:0;
24
+ text-align: center;
25
+ }
26
+ #header h3 {
27
+ margin: 0;
28
+ }
29
+ #nav {
30
+
31
+ padding:5px;
32
+ }
33
+ #nav ul{
34
+ margin:0;
35
+ padding:0;
36
+ list-style:none;
37
+ }
38
+ #nav li{
39
+ display:inline;
40
+ margin:0;
41
+ padding:0;
42
+ }
43
+ #main {
44
+ float:right;
45
+ width:700px;
46
+ }
47
+ #main h2, #main h3, #main p {
48
+ padding:0 10px;
49
+ }
50
+ #sidebar {
51
+ float:left;
52
+ width:240px;
53
+ }
54
+ #sidebar ul {
55
+ margin-bottom:0;
56
+ }
57
+ #sidebar h3, #sidebar p {
58
+ padding:0 10px 0 0;
59
+ }
60
+ #footer {
61
+ clear:both;
62
+ }
63
+ #footer p {
64
+ padding:5px;
65
+ margin:0;
66
+ }
67
+
68
+ div.report {
69
+ background: #FFFFFF;
70
+ }
71
+ </style>
72
+ <script type="text/javascript"``>
73
+ function hide_reports() {
74
+ var reports = document.getElementsByClassName('report');
75
+ for (var i=0; i < reports.length; i++) {
76
+ reports[i].style.display = 'none';
77
+ }
78
+ }
79
+
80
+ function show_report(uri) {
81
+ hide_reports();
82
+ document.getElementById(uri).style.display = '';
83
+ }
84
+
85
+ function uri_change_handler(uri) {
86
+ var page_select = document.getElementById('pages');
87
+ show_report(page_select.value);
88
+ }
89
+ </script>
90
+
91
+ </head>
92
+ <body>
93
+ <div id="wrap">
94
+ <div id="header">
95
+ <h1>Load testing report</h1>
96
+ <h3>Date: <%= date %></h3>
97
+ <h3>Host: <%= host %></h3>
98
+ <h3>Page:
99
+ <select id="pages" onchange="uri_change_handler()">
100
+ <option value='overview'>Overview</option>
101
+ <% uris.each do |uri|%>
102
+ <option value='page_<%= uri %>'><%=uri%></option>
103
+ <% end %>
104
+ </select>
105
+ </h3>
106
+
107
+ </div>
108
+ <div id="nav">
109
+
110
+ </div>
111
+ <div id="sidebar">
112
+ <div style="display:none">
113
+ <h3>Tested Pages</h3>
114
+ <ul>
115
+ <li><a href="#" onclick="show_report('overview');" title="Overview">Overview</a></li>
116
+ <% uris.each do |uri|%>
117
+ <li><a href="#" onclick="show_report('page_<%= uri %>');" title="<%= uri %>"><%= uri %></a></li>
118
+ <% end %>
119
+ </ul>
120
+ </div>
121
+ <% if notes %>
122
+ <h3>Report Notes</h3>
123
+ <p><%= notes %></p>
124
+ <% end %>
125
+ <h3>Info</h3>
126
+ <ul>
127
+ <li><a href="#" onclick="show_report('graph_discussion');" title="Discussion">Discussion</a></li>
128
+ </ul>
129
+
130
+ </div>
131
+ <div id="main">
132
+ <h2>Report Detail</h2>
133
+
134
+ <div id="overview" class='report'>
135
+ <img src="<%= summary_graph.to_html %>"/>
136
+ </div>
137
+
138
+ <div id="graph_discussion" class='report' style="display:none;">
139
+ Discussion on what these graphs mean.
140
+
141
+ • Increase concurrent connections, and see at which point your app will no longer serve at the demanded request rate
142
+ • Find which pages are heavier/lighter than others
143
+ • Replay a log (with a little work)
144
+
145
+ • Actually hits your app with load, doesn't just extrapolate out from a few data points
146
+ • Start broad and zero in on connection limits
147
+ • Run from a server, not your laptop
148
+ </div>
149
+
150
+ <% reports.each do |uri, report| %>
151
+ <div id="page_<%= uri %>" class='report' style="display:none;">
152
+ <p>Report for <%= "http://#{host}#{uri}" %></p>
153
+ <% graphs[uri].each do |graph| %>
154
+ <img src="<%= graph %>"/>
155
+ <% end %>
156
+ <pre>
157
+ <%= report.to_s %>
158
+ </pre>
159
+ </div>
160
+ <% end %>
161
+ </div>
162
+ <div id="footer" align="center">
163
+ <p>Generated at <%= date %></p>
164
+ <div>
165
+ <p><a onclick="document.getElementById('command_container').style.display = 'block';">Show Command<a/></p>
166
+ <div id="command_container" style="display:none">
167
+ <pre><%= command_run %></pre>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </body>
173
+ </html>
174
+
data/script/console ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # File: script/console
3
+ irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
+
5
+ libs = " -r irb/completion"
6
+ # Perhaps use a console_lib to store any extra methods I may want available in the cosole
7
+ # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
8
+ libs << " -r #{File.dirname(__FILE__) + '/../lib/autograph.rb'}"
9
+ puts "Loading autograph gem"
10
+ exec "#{irb} #{libs} --simple-prompt"
data/script/destroy ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/destroy'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Destroy.new.run(ARGV)
data/script/generate ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..'))
3
+
4
+ begin
5
+ require 'rubigen'
6
+ rescue LoadError
7
+ require 'rubygems'
8
+ require 'rubigen'
9
+ end
10
+ require 'rubigen/scripts/generate'
11
+
12
+ ARGV.shift if ['--help', '-h'].include?(ARGV[0])
13
+ RubiGen::Base.use_component_sources! [:rubygems, :newgem, :newgem_theme, :test_unit]
14
+ RubiGen::Scripts::Generate.new.run(ARGV)
@@ -0,0 +1,18 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestAutograph < Test::Unit::TestCase
4
+ require 'mocha'
5
+
6
+ def setup
7
+ end
8
+
9
+ def test_truth
10
+ assert(true)
11
+ end
12
+
13
+ # def test_uniq_uris
14
+ # runner = AutoPerf.new(:host => 'example.com')
15
+ # AutoPerf.any_instance.expects(:generate_graphs)
16
+ # runner.expects(:generate_graphs)
17
+ # end
18
+ end
@@ -0,0 +1,29 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestHtmlReport < Test::Unit::TestCase
4
+
5
+ def test_takes_a_hash
6
+ c = Configuration.new(:a => 1)
7
+ assert_equal c[:a], 1
8
+ end
9
+
10
+ def test_has_defaults
11
+ c = Configuration.new()
12
+ assert_equal c['low_rate'], 5
13
+ end
14
+
15
+ def test_can_override_defaults
16
+ c = Configuration.new('low_rate' => 1)
17
+ assert_equal c['low_rate'], 1
18
+ end
19
+
20
+ def test_graph_render_klazz_helper
21
+ c = Configuration.new()
22
+ assert_equal c.graph_renderer_class, GChartRenderer
23
+ end
24
+
25
+ def test_pretty_print_returns_a_string
26
+ c = Configuration.new().pretty_print
27
+ assert_equal c.class, String
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestGraphSeries < Test::Unit::TestCase
4
+ require 'mocha'
5
+ require 'autograph'
6
+
7
+ def setup
8
+ end
9
+
10
+ def test_constructor
11
+ x_values = [1,2,3]
12
+ y_values = [4,5,6]
13
+
14
+ gs = GraphSeries.new(:type, x_values, y_values, 'label')
15
+ assert_equal(gs.type, :type)
16
+ assert_equal(gs.x_values, x_values)
17
+ assert_equal(gs.y_values, y_values)
18
+ assert_equal(gs.label, 'label')
19
+ end
20
+
21
+ def test_constructor_converts_to_floats
22
+ x_values = [1,2,3]
23
+ y_values = [4,5,6]
24
+
25
+ gs = GraphSeries.new(:type, x_values, y_values, 'label')
26
+ gs.y_values.each{|y| assert_equal(y.class, Float) }
27
+ gs.x_values.each{|x| assert_equal(x.class, Float) }
28
+ end
29
+
30
+ end
@@ -0,0 +1,3 @@
1
+ require 'stringio'
2
+ require 'test/unit'
3
+ require File.dirname(__FILE__) + '/../lib/autograph'
@@ -0,0 +1,38 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class TestHtmlReport < Test::Unit::TestCase
4
+ require 'mocha'
5
+ require 'autograph'
6
+
7
+ def setup
8
+ end
9
+
10
+ def test_requires_params
11
+ error = nil
12
+ begin
13
+ HtmlReport.new({})
14
+ rescue => e
15
+ error = e
16
+ end
17
+ assert(!e.nil?)
18
+ end
19
+
20
+ def test_uses_defined_output_file
21
+ output_file = HtmlReport.determine_output_file('file.txt', nil)
22
+ assert_equal(output_file, 'file.txt')
23
+ end
24
+
25
+ def test_uses_defined_output_dir
26
+ output_file = HtmlReport.determine_output_file(nil, '/opt')
27
+ assert_equal(output_file, '/opt/load_test.html')
28
+ end
29
+
30
+ def test_generate_output_file
31
+ File.expects(:exist?).with('./load_test.html').returns(true)
32
+ File.expects(:exist?).with('./load_test_1.html').returns(true)
33
+ File.expects(:exist?).with('./load_test_2.html').returns(false)
34
+ output_file = HtmlReport.determine_output_file(nil, '.')
35
+ assert_equal(output_file, './load_test_2.html')
36
+ end
37
+
38
+ end
metadata ADDED
@@ -0,0 +1,147 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: autograph
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 2
9
+ version: 0.1.2
10
+ platform: ruby
11
+ authors:
12
+ - Nick Stielau
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-05-20 00:00:00 -07:00
18
+ default_executable: autograph
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: builder
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 2
29
+ - 1
30
+ - 2
31
+ version: 2.1.2
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: ruport
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 1
43
+ - 6
44
+ - 3
45
+ version: 1.6.3
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: scruffy
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "="
54
+ - !ruby/object:Gem::Version
55
+ segments:
56
+ - 0
57
+ - 2
58
+ - 6
59
+ version: 0.2.6
60
+ type: :runtime
61
+ version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ name: gchart
64
+ prerelease: false
65
+ requirement: &id004 !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - "="
68
+ - !ruby/object:Gem::Version
69
+ segments:
70
+ - 1
71
+ - 0
72
+ - 0
73
+ version: 1.0.0
74
+ type: :runtime
75
+ version_requirements: *id004
76
+ description: Autograph wraps httperf, running multiple tests while varying the parameters, graphing the output.
77
+ email: nick.stielau@gmail.com
78
+ executables:
79
+ - autograph
80
+ extensions: []
81
+
82
+ extra_rdoc_files:
83
+ - README.rdoc
84
+ - TODO
85
+ files:
86
+ - .gitignore
87
+ - History.txt
88
+ - README.rdoc
89
+ - Rakefile
90
+ - TODO
91
+ - VERSION
92
+ - bin/autograph
93
+ - lib/autograph.rb
94
+ - lib/autograph/autoperf.rb
95
+ - lib/autograph/configuration.rb
96
+ - lib/autograph/graph_renderers/base_renderer.rb
97
+ - lib/autograph/graph_renderers/gchart_renderer.rb
98
+ - lib/autograph/graph_renderers/rasterized_scruffy_renderer.rb
99
+ - lib/autograph/graph_renderers/scruffy_renderer.rb
100
+ - lib/autograph/graph_series.rb
101
+ - lib/autograph/html_report.rb
102
+ - lib/autograph/report.html.erb
103
+ - load_test_report_example.html
104
+ - script/console
105
+ - script/destroy
106
+ - script/generate
107
+ - test/test_autograph.rb
108
+ - test/test_configuration.rb
109
+ - test/test_graph_series.rb
110
+ - test/test_helper.rb
111
+ - test/test_html_report.rb
112
+ has_rdoc: true
113
+ homepage: http://github.com/nstielau/autograph
114
+ licenses: []
115
+
116
+ post_install_message:
117
+ rdoc_options:
118
+ - --charset=UTF-8
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ segments:
126
+ - 0
127
+ version: "0"
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ segments:
133
+ - 0
134
+ version: "0"
135
+ requirements: []
136
+
137
+ rubyforge_project:
138
+ rubygems_version: 1.3.6
139
+ signing_key:
140
+ specification_version: 3
141
+ summary: Simple httperf runner with nice html reports with graphs.
142
+ test_files:
143
+ - test/test_autograph.rb
144
+ - test/test_configuration.rb
145
+ - test/test_graph_series.rb
146
+ - test/test_helper.rb
147
+ - test/test_html_report.rb