kronk 1.4.0 → 1.5.0

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