kronk 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,261 @@
1
+ class Kronk
2
+
3
+ ##
4
+ # Returns benchmarks for a set of Player results.
5
+ # * Total time taken
6
+ # * Complete requests
7
+ # * Failed requests
8
+ # * Total bytes transferred
9
+ # * Requests per second
10
+ # * Time per request
11
+ # * Transfer rate
12
+ # * Connection times (min mean median max)
13
+ # * Percentage of requests within a certain time
14
+ # * Slowest endpoints
15
+
16
+ class Player::Benchmark < Player::Output
17
+
18
+ class ResultSet
19
+
20
+ attr_reader :byterate, :count, :fastest, :hostname, :precision,
21
+ :slowest, :total_bytes
22
+
23
+ def initialize uri, start_time
24
+ @times = Hash.new(0)
25
+ @count = 0
26
+ @r5XX = 0
27
+ @r4XX = 0
28
+
29
+ @precision = 3
30
+
31
+ @slowest = nil
32
+ @fastest = nil
33
+
34
+ @paths = {}
35
+
36
+ @total_bytes = 0
37
+ @byterate = 0
38
+
39
+ @start_time = start_time
40
+ @total_time = 0
41
+
42
+ @hostname = "#{uri.scheme}://#{uri.host}:#{uri.port}" if uri
43
+ end
44
+
45
+
46
+ def add_result resp
47
+ time = (resp.time * 1000).round
48
+
49
+ @times[time] += 1
50
+ @count += 1
51
+
52
+ @r5XX += 1 if resp.code =~ /^5\d\d$/
53
+ @r4XX += 1 if resp.code =~ /^4\d\d$/
54
+
55
+ @slowest = time if !@slowest || @slowest < time
56
+ @fastest = time if !@fastest || @fastest > time
57
+
58
+ log_path resp.uri.path, time if resp.uri
59
+
60
+ @total_bytes += resp.raw.bytes.count
61
+
62
+ @byterate = (@byterate * (@count-1) + resp.byterate) / @count
63
+
64
+ @total_time = (Time.now - @start_time).to_f
65
+ end
66
+
67
+
68
+ def log_path path, time
69
+ path = "/" if !path || path.empty?
70
+ @paths[path] ||= [0, 0]
71
+ pcount = @paths[path][1] + 1
72
+ @paths[path][0] = (@paths[path][0] * @paths[path][1] + time) / pcount
73
+ @paths[path][0] = @paths[path][0].round @precision
74
+ @paths[path][1] = pcount
75
+ end
76
+
77
+
78
+ def deviation
79
+ return @deviation if @deviation
80
+
81
+ mdiff = @times.to_a.inject(0) do |sum, (time, count)|
82
+ sum + ((time-self.mean)**2) * count
83
+ end
84
+
85
+ @deviation = ((mdiff / @count)**0.5).round @precision
86
+ end
87
+
88
+
89
+ def mean
90
+ @mean ||= (self.sum / @count).round @precision
91
+ end
92
+
93
+
94
+ def median
95
+ @median ||= ((@slowest + @fastest) / 2).round @precision
96
+ end
97
+
98
+
99
+ def percentages
100
+ return @percentages if @percentages
101
+
102
+ @percentages = {}
103
+
104
+ perc_list = [50, 66, 75, 80, 90, 95, 98, 99]
105
+ times_count = 0
106
+ target_perc = perc_list.first
107
+
108
+ i = 0
109
+ @times.keys.sort.each do |time|
110
+ times_count += @times[time]
111
+
112
+ if target_perc <= (100 * times_count / @count)
113
+ @percentages[target_perc] = time
114
+ i += 1
115
+ target_perc = perc_list[i]
116
+
117
+ break unless target_perc
118
+ end
119
+ end
120
+
121
+ perc_list.each{|i| @percentages[i] ||= self.slowest }
122
+ @percentages[100] = self.slowest
123
+ @percentages
124
+ end
125
+
126
+
127
+ def req_per_sec
128
+ (@count / @total_time).round @precision
129
+ end
130
+
131
+
132
+ def transfer_rate
133
+ ((@total_bytes / 1000) / @total_time).round @precision
134
+ end
135
+
136
+
137
+ def sum
138
+ @sum ||= @times.inject(0){|sum, (time,count)| sum + time * count}
139
+ end
140
+
141
+
142
+ def slowest_paths
143
+ @paths.to_a.sort{|x,y| y[1] <=> x[1]}[0..9]
144
+ end
145
+
146
+
147
+ def to_s
148
+ out = <<-STR
149
+ Host: #{@hostname || "<IO>"}
150
+ Completed: #{@count}
151
+ 400s: #{@r4XX}
152
+ 500s: #{@r5XX}
153
+ Req/Sec: #{self.req_per_sec}
154
+ Total Bytes: #{@total_bytes}
155
+ Transfer Rate: #{self.transfer_rate} Kbytes/sec
156
+
157
+ Connection Times (ms)
158
+ Min: #{self.fastest}
159
+ Mean: #{self.mean}
160
+ [+/-sd]: #{self.deviation}
161
+ Median: #{self.median}
162
+ Max: #{self.slowest}
163
+
164
+ Request Percentages (ms)
165
+ 50% #{self.percentages[50]}
166
+ 66% #{self.percentages[66]}
167
+ 75% #{self.percentages[75]}
168
+ 80% #{self.percentages[80]}
169
+ 90% #{self.percentages[90]}
170
+ 95% #{self.percentages[95]}
171
+ 98% #{self.percentages[98]}
172
+ 99% #{self.percentages[99]}
173
+ 100% #{self.percentages[100]} (longest request)
174
+ STR
175
+
176
+ out << "
177
+ Avg. Slowest Paths (ms, #)
178
+ #{slowest_paths.map{|arr| " #{(arr[1])} #{arr[0]}"}.join "\n" }" if @hostname
179
+
180
+ out
181
+ end
182
+ end
183
+
184
+
185
+ def initialize player
186
+ @player = player
187
+ @results = []
188
+ @count = 0
189
+
190
+ @div = nil
191
+ @div = @player.number / 10 if @player.number
192
+ @div = 100 if !@div || @div < 10
193
+ end
194
+
195
+
196
+ def start
197
+ puts "Benchmarking..."
198
+ super
199
+ end
200
+
201
+
202
+ def result kronk, mutex
203
+ kronk.responses.each_with_index do |resp, i|
204
+ mutex.synchronize do
205
+ @count += 1
206
+ @results[i] ||= ResultSet.new(resp.uri, @start_time)
207
+ @results[i].add_result resp
208
+
209
+ puts "#{@count} requests" if @count % @div == 0
210
+ end
211
+ end
212
+ end
213
+
214
+
215
+ def error err, kronk, mutex
216
+ mutex.synchronize do
217
+ @count += 1
218
+ end
219
+ end
220
+
221
+
222
+ def completed
223
+ puts "Finished!"
224
+
225
+ render_head
226
+ render_body
227
+
228
+ true
229
+ end
230
+
231
+
232
+ def render_body
233
+ if @results.length > 1
234
+ puts Diff.new(@results[0].to_s, @results[1].to_s).formatted
235
+ else
236
+ puts @results.first.to_s
237
+ end
238
+ end
239
+
240
+
241
+ def render_head
242
+ puts <<-STR
243
+
244
+ Benchmark Time: #{(Time.now - @start_time).to_f} sec
245
+ Number of Requests: #{@count}
246
+ Concurrency: #{@player.concurrency}
247
+ STR
248
+ end
249
+ end
250
+ end
251
+
252
+
253
+ if Float.instance_method(:round).arity == 0
254
+ class Float
255
+ def round ndigits=0
256
+ num, dec = self.to_s.split(".")
257
+ num = "#{num}.#{dec[0,ndigits]}".sub(/\.$/, "")
258
+ Float num
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,54 @@
1
+ class Kronk
2
+
3
+ ##
4
+ # Reads an IO stream and parses it with the given parser.
5
+ # Parser must respond to the following:
6
+ # * start_new?(line) - Returns true when line is the beginning of a new
7
+ # http request.
8
+ # * parse(str) - Parses the raw string into value Kronk#request options.
9
+
10
+ class Player::InputReader
11
+
12
+ attr_accessor :io, :parser, :buffer
13
+
14
+ def initialize string_or_io, parser=nil
15
+ @buffer = []
16
+ @parser = parser || Kronk::Player::RequestParser
17
+ @io = string_or_io
18
+ @io = StringIO.new(@io) if String === @io
19
+ end
20
+
21
+
22
+ ##
23
+ # Parse the next request in the IO instance.
24
+
25
+ def get_next
26
+ return if eof?
27
+
28
+ @buffer << @io.gets if @buffer.empty?
29
+
30
+ until @io.eof?
31
+ line = @io.gets
32
+ next unless line
33
+
34
+ if @parser.start_new?(line) || @buffer.empty?
35
+ @buffer << line
36
+ break
37
+ else
38
+ @buffer.last << line
39
+ end
40
+ end
41
+
42
+ return if @buffer.empty?
43
+ @parser.parse(@buffer.slice!(0)) || self.get_next
44
+ end
45
+
46
+
47
+ ##
48
+ # Returns true if there is no more input to read from.
49
+
50
+ def eof?
51
+ !@io || (@io.closed? || @io.eof?) && @buffer.empty?
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,49 @@
1
+ class Kronk
2
+
3
+ ##
4
+ # Generic base class to inherit from for creating a player output.
5
+
6
+ class Player::Output
7
+
8
+ ##
9
+ # New instance initializes @player and @start_time
10
+
11
+ def initialize player
12
+ @player = player
13
+ @start_time = Time.now
14
+ end
15
+
16
+
17
+ ##
18
+ # Called right before the queue starts being processed.
19
+ # Sets @start_time to Time.now.
20
+
21
+ def start
22
+ @start_time = Time.now
23
+ end
24
+
25
+
26
+ ##
27
+ # Called after kronk was run without errors.
28
+
29
+ def result kronk, mutex=nil
30
+ end
31
+
32
+
33
+ ##
34
+ # Called if an error was raised while running kronk.
35
+
36
+ def error err, kronk=nil, mutex=nil
37
+ end
38
+
39
+
40
+ ##
41
+ # Called after the queue is done being processed.
42
+ # If the return value is true-ish, command will exit with status 0,
43
+ # otherwise exits with status 1.
44
+
45
+ def completed
46
+ true
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,24 @@
1
+ class Kronk
2
+
3
+ ##
4
+ # Stream-friendly HTTP Request parser for piping into the Kronk player.
5
+ # Uses Kronk::Request for parsing.
6
+
7
+ class Player::RequestParser
8
+
9
+ ##
10
+ # Returns true-ish if the line given is the start of a new request.
11
+
12
+ def self.start_new? line
13
+ line =~ Request::REQUEST_LINE_MATCHER
14
+ end
15
+
16
+
17
+ ##
18
+ # Parse a single http request kronk options hash.
19
+
20
+ def self.parse string
21
+ Kronk::Request.parse_to_hash string
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,50 @@
1
+ class Kronk
2
+
3
+ ##
4
+ # Outputs Player results as a stream of Kronk outputs
5
+ # in chunked form, each chunk being one response and the number
6
+ # of octets being expressed in plain decimal form.
7
+ #
8
+ # out = Player::StreamOutput.new
9
+ #
10
+ # io1 = StringIO.new "this is the first chunk"
11
+ # io2 = StringIO.new "this is the rest"
12
+ #
13
+ # kronk = Kronk.new
14
+ # kronk.retrieve io1
15
+ # out.result kronk
16
+ # #=> "23\r\nthis is the first chunk\r\n"
17
+ #
18
+ # kronk.retrieve io2
19
+ # out.result kronk
20
+ # #=> "16\r\nthis is the rest\r\n"
21
+ #
22
+ # Note: This output class will not render errors.
23
+
24
+ class Player::Stream < Player::Output
25
+
26
+ def result kronk, mutex=nil
27
+ output =
28
+ if kronk.diff
29
+ kronk.diff.formatted
30
+
31
+ elsif kronk.response
32
+ kronk.response.stringify kronk.options
33
+ end
34
+
35
+ output = "#{output.length}\r\n#{output}\r\n"
36
+
37
+ mutex.synchronize do
38
+ $stdout << output
39
+ end
40
+
41
+ output
42
+ end
43
+
44
+
45
+ def completed
46
+ $stdout.flush
47
+ true
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,123 @@
1
+ class Kronk
2
+
3
+ ##
4
+ # Outputs Player requests and results in a test-suite like format.
5
+
6
+ class Player::Suite < Player::Output
7
+
8
+ def start
9
+ @results = []
10
+ $stdout.puts "Started"
11
+ super
12
+ end
13
+
14
+
15
+ def result kronk, mutex=nil
16
+ status = "."
17
+
18
+ @results <<
19
+ if kronk.diff
20
+ status = "F" if kronk.diff.count > 0
21
+ text = diff_text kronk if status == "F"
22
+ time =
23
+ (kronk.responses[0].time.to_f + kronk.responses[1].time.to_f) / 2
24
+
25
+ [status, time, text]
26
+
27
+ elsif kronk.response
28
+ status = "F" if !kronk.response.success?
29
+ text = resp_text kronk if status == "F"
30
+ [status, kronk.response.time, text]
31
+ end
32
+
33
+ $stdout << status
34
+ $stdout.flush
35
+ end
36
+
37
+
38
+ def error err, kronk=nil, mutex=nil
39
+ status = "E"
40
+ @results << [status, 0, error_text(err, kronk)]
41
+
42
+ $stdout << status
43
+ $stdout.flush
44
+ end
45
+
46
+
47
+ def completed
48
+ player_time = (Time.now - @start_time).to_f
49
+ total_time = 0
50
+ bad_count = 0
51
+ failure_count = 0
52
+ error_count = 0
53
+ err_buffer = ""
54
+
55
+ @results.each do |(status, time, text)|
56
+ case status
57
+ when "F"
58
+ total_time += time.to_f
59
+ bad_count += 1
60
+ failure_count += 1
61
+ err_buffer << "\n #{bad_count}) Failure:\n#{text}"
62
+
63
+ when "E"
64
+ bad_count += 1
65
+ error_count += 1
66
+ err_buffer << "\n #{bad_count}) Error:\n#{text}"
67
+
68
+ else
69
+ total_time += time.to_f
70
+ end
71
+ end
72
+
73
+ non_error_count = @results.length - error_count
74
+
75
+ avg_time = non_error_count > 0 ? total_time / non_error_count : "n/a"
76
+ avg_qps = non_error_count > 0 ? non_error_count / player_time : "n/a"
77
+
78
+ $stdout.puts "\nFinished in #{player_time} seconds.\n"
79
+ $stderr.puts err_buffer unless err_buffer.empty?
80
+ $stdout.puts "\n#{@results.length} cases, " +
81
+ "#{failure_count} failures, #{error_count} errors"
82
+
83
+ $stdout.puts "Avg Time: #{avg_time}"
84
+ $stdout.puts "Avg QPS: #{avg_qps}"
85
+
86
+ return bad_count == 0
87
+ end
88
+
89
+
90
+ private
91
+
92
+
93
+ def resp_text kronk
94
+ <<-STR
95
+ Request: #{kronk.response.code} - #{kronk.response.uri}
96
+ Options: #{kronk.options.inspect}
97
+ STR
98
+ end
99
+
100
+
101
+ def diff_text kronk
102
+ <<-STR
103
+ Request: #{kronk.responses[0].code} - #{kronk.responses[0].uri}
104
+ #{kronk.responses[1].code} - #{kronk.responses[1].uri}
105
+ Options: #{kronk.options.inspect}
106
+ Diffs: #{kronk.diff.count}
107
+ STR
108
+ end
109
+
110
+
111
+ def error_text err, kronk=nil
112
+ str = "#{err.class}: #{err.message}"
113
+
114
+ if kronk
115
+ str << "\n Options: #{kronk.options.inspect}\n\n"
116
+ else
117
+ str << "\n #{err.backtrace}\n\n"
118
+ end
119
+
120
+ str
121
+ end
122
+ end
123
+ end