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.
- data/.gemtest +0 -0
- data/History.rdoc +22 -0
- data/Manifest.txt +15 -6
- data/README.rdoc +5 -6
- data/Rakefile +5 -5
- data/lib/kronk.rb +164 -194
- data/lib/kronk/cmd.rb +188 -74
- data/lib/kronk/constants.rb +90 -0
- data/lib/kronk/data_renderer.rb +146 -0
- data/lib/kronk/diff.rb +4 -92
- data/lib/kronk/path/transaction.rb +2 -0
- data/lib/kronk/player.rb +233 -0
- data/lib/kronk/player/benchmark.rb +261 -0
- data/lib/kronk/player/input_reader.rb +54 -0
- data/lib/kronk/player/output.rb +49 -0
- data/lib/kronk/player/request_parser.rb +24 -0
- data/lib/kronk/player/stream.rb +50 -0
- data/lib/kronk/player/suite.rb +123 -0
- data/lib/kronk/plist_parser.rb +4 -0
- data/lib/kronk/request.rb +265 -241
- data/lib/kronk/response.rb +330 -149
- data/lib/kronk/test/assertions.rb +2 -2
- data/lib/kronk/xml_parser.rb +7 -1
- data/test/mocks/cookies.yml +32 -0
- data/test/mocks/get_request.txt +6 -0
- data/test/test_assertions.rb +6 -6
- data/test/test_cmd.rb +708 -0
- data/test/test_diff.rb +210 -75
- data/test/test_helper.rb +140 -0
- data/test/test_helper_methods.rb +4 -4
- data/test/test_input_reader.rb +103 -0
- data/test/test_kronk.rb +142 -141
- data/test/test_player.rb +589 -0
- data/test/test_request.rb +147 -212
- data/test/test_request_parser.rb +31 -0
- data/test/test_response.rb +206 -15
- metadata +41 -74
- data/bin/yzma +0 -13
- data/lib/kronk/data_set.rb +0 -144
- data/lib/yzma.rb +0 -174
- data/lib/yzma/randomizer.rb +0 -54
- data/lib/yzma/report.rb +0 -47
- data/test/test_data_set.rb +0 -213
@@ -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
|