hey.rb 1.0.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,312 @@
1
+ # Copyright 2014 Google Inc. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Translated from requester/report.go.
16
+
17
+ require "async/queue"
18
+
19
+ module Hey
20
+ module Requester
21
+ BAR_CHAR = "■"
22
+
23
+ # We report for max 1M results.
24
+ MAX_RES = 1_000_000
25
+
26
+ # Go's exported LatencyDistribution struct.
27
+ LatencyDistribution = Struct.new(:percentage, :latency)
28
+
29
+ # Go's exported Bucket struct.
30
+ Bucket = Struct.new(:mark, :count, :frequency)
31
+
32
+ # Report is the snapshot handed to a template. It corresponds to Go's
33
+ # exported `Report` struct; the unexported `report` is `Reporter` below,
34
+ # since Ruby cannot distinguish the two by case alone.
35
+ class Report
36
+ attr_accessor :avg_total, :fastest, :slowest, :average, :rps,
37
+ :avg_conn, :avg_dns, :avg_req, :avg_res, :avg_delay,
38
+ :conn_max, :conn_min, :dns_max, :dns_min,
39
+ :req_max, :req_min, :res_max, :res_min,
40
+ :delay_max, :delay_min,
41
+ :lats, :conn_lats, :dns_lats, :req_lats, :res_lats,
42
+ :delay_lats, :offsets, :status_codes,
43
+ :total, :error_dist, :status_code_dist,
44
+ :size_total, :size_req, :num_res,
45
+ :latency_distribution, :histogram
46
+
47
+ # Mirrors Go's zero values, so a snapshot taken when nothing succeeded
48
+ # still renders rather than blowing up on nil.
49
+ def initialize
50
+ @avg_total = 0.0
51
+ @fastest = 0.0
52
+ @slowest = 0.0
53
+ @average = 0.0
54
+ @rps = 0.0
55
+
56
+ @avg_conn = 0.0
57
+ @avg_dns = 0.0
58
+ @avg_req = 0.0
59
+ @avg_res = 0.0
60
+ @avg_delay = 0.0
61
+ @conn_max = 0.0
62
+ @conn_min = 0.0
63
+ @dns_max = 0.0
64
+ @dns_min = 0.0
65
+ @req_max = 0.0
66
+ @req_min = 0.0
67
+ @res_max = 0.0
68
+ @res_min = 0.0
69
+ @delay_max = 0.0
70
+ @delay_min = 0.0
71
+
72
+ @lats = []
73
+ @conn_lats = []
74
+ @dns_lats = []
75
+ @req_lats = []
76
+ @res_lats = []
77
+ @delay_lats = []
78
+ @offsets = []
79
+ @status_codes = []
80
+
81
+ @total = 0.0
82
+
83
+ @error_dist = {}
84
+ @status_code_dist = {}
85
+ @size_total = 0
86
+ @size_req = 0
87
+ @num_res = 0
88
+
89
+ @latency_distribution = []
90
+ @histogram = []
91
+ end
92
+ end
93
+
94
+ # Reporter is Go's unexported `report`: it drains the result queue while
95
+ # the workers run, then renders the summary.
96
+ class Reporter
97
+ attr_reader :done
98
+
99
+ def initialize(writer, results, output, n)
100
+ # Go preallocates the slices to min(n, maxRes); Ruby arrays grow on
101
+ # demand, so the capacity is recorded only for parity.
102
+ @cap = [n, MAX_RES].min
103
+
104
+ @output = output
105
+ @results = results
106
+ @done = Async::LimitedQueue.new(1)
107
+ @error_dist = {}
108
+ @w = writer
109
+
110
+ @avg_total = 0.0
111
+ @fastest = 0.0
112
+ @slowest = 0.0
113
+ @average = 0.0
114
+ @rps = 0.0
115
+
116
+ @avg_conn = 0.0
117
+ @avg_dns = 0.0
118
+ @avg_req = 0.0
119
+ @avg_res = 0.0
120
+ @avg_delay = 0.0
121
+
122
+ @conn_lats = []
123
+ @dns_lats = []
124
+ @req_lats = []
125
+ @res_lats = []
126
+ @delay_lats = []
127
+ @offsets = []
128
+ @status_codes = []
129
+ @lats = []
130
+
131
+ @total = 0.0
132
+ @size_total = 0
133
+ @num_res = 0
134
+ end
135
+
136
+ # Go: runReporter(r). Loops until the queue is closed.
137
+ def run
138
+ @results.each do |res|
139
+ @num_res += 1
140
+ if res.err
141
+ key = res.err
142
+ @error_dist[key] = @error_dist.fetch(key, 0) + 1
143
+ else
144
+ @avg_total += res.duration
145
+ @avg_conn += res.conn_duration
146
+ @avg_delay += res.delay_duration
147
+ @avg_dns += res.dns_duration
148
+ @avg_req += res.req_duration
149
+ @avg_res += res.res_duration
150
+ if @res_lats.length < MAX_RES
151
+ @lats << res.duration
152
+ @conn_lats << res.conn_duration
153
+ @dns_lats << res.dns_duration
154
+ @req_lats << res.req_duration
155
+ @delay_lats << res.delay_duration
156
+ @res_lats << res.res_duration
157
+ @status_codes << res.status_code
158
+ @offsets << res.offset
159
+ end
160
+ @size_total += res.content_length if res.content_length > 0
161
+ end
162
+ end
163
+
164
+ # Signal reporter is done.
165
+ @done.enqueue(true)
166
+ end
167
+
168
+ def finalize(total)
169
+ @total = total
170
+ @rps = @num_res / @total
171
+ @average = @avg_total / @lats.length
172
+ @avg_conn = @avg_conn / @lats.length
173
+ @avg_delay = @avg_delay / @lats.length
174
+ @avg_dns = @avg_dns / @lats.length
175
+ @avg_req = @avg_req / @lats.length
176
+ @avg_res = @avg_res / @lats.length
177
+ print_report
178
+ end
179
+
180
+ def print_report
181
+ begin
182
+ buf = TemplateContext.new(snapshot).render(Requester.new_template(@output))
183
+ rescue StandardError => e
184
+ # Go logs and returns without printing anything further.
185
+ $stderr.puts("error: #{e.message}")
186
+ return
187
+ end
188
+ printf("%s", buf)
189
+
190
+ printf("\n")
191
+ end
192
+
193
+ def printf(fmt, *args)
194
+ @w.write(format(fmt, *args))
195
+ end
196
+
197
+ def snapshot
198
+ snapshot = Report.new
199
+ snapshot.avg_total = @avg_total
200
+ snapshot.average = @average
201
+ snapshot.rps = @rps
202
+ snapshot.size_total = @size_total
203
+ snapshot.avg_conn = @avg_conn
204
+ snapshot.avg_dns = @avg_dns
205
+ snapshot.avg_req = @avg_req
206
+ snapshot.avg_res = @avg_res
207
+ snapshot.avg_delay = @avg_delay
208
+ snapshot.total = @total
209
+ snapshot.error_dist = @error_dist
210
+ snapshot.num_res = @num_res
211
+
212
+ return snapshot if @lats.empty?
213
+
214
+ snapshot.size_req = @size_total / @lats.length
215
+
216
+ # Copy before sorting: the snapshot's series must stay in the order
217
+ # the requests completed (the CSV output depends on it), while the
218
+ # percentile and histogram maths below sort in place, as Go does.
219
+ snapshot.lats = @lats.dup
220
+ snapshot.conn_lats = @conn_lats.dup
221
+ snapshot.dns_lats = @dns_lats.dup
222
+ snapshot.req_lats = @req_lats.dup
223
+ snapshot.res_lats = @res_lats.dup
224
+ snapshot.delay_lats = @delay_lats.dup
225
+ snapshot.status_codes = @status_codes.dup
226
+ snapshot.offsets = @offsets.dup
227
+
228
+ @lats.sort!
229
+ @fastest = @lats[0]
230
+ @slowest = @lats[@lats.length - 1]
231
+
232
+ @conn_lats.sort!
233
+ @dns_lats.sort!
234
+ @req_lats.sort!
235
+ @res_lats.sort!
236
+ @delay_lats.sort!
237
+
238
+ snapshot.histogram = histogram
239
+ snapshot.latency_distribution = latencies
240
+
241
+ snapshot.fastest = @fastest
242
+ snapshot.slowest = @slowest
243
+ snapshot.conn_max = @conn_lats[0]
244
+ snapshot.conn_min = @conn_lats[@conn_lats.length - 1]
245
+ snapshot.dns_max = @dns_lats[0]
246
+ snapshot.dns_min = @dns_lats[@dns_lats.length - 1]
247
+ snapshot.req_max = @req_lats[0]
248
+ snapshot.req_min = @req_lats[@req_lats.length - 1]
249
+ snapshot.delay_max = @delay_lats[0]
250
+ snapshot.delay_min = @delay_lats[@delay_lats.length - 1]
251
+ snapshot.res_max = @res_lats[0]
252
+ snapshot.res_min = @res_lats[@res_lats.length - 1]
253
+
254
+ status_code_dist = {}
255
+ snapshot.status_codes.each do |status_code|
256
+ status_code_dist[status_code] = status_code_dist.fetch(status_code, 0) + 1
257
+ end
258
+ snapshot.status_code_dist = status_code_dist
259
+
260
+ snapshot
261
+ end
262
+
263
+ def latencies
264
+ pctls = [10, 25, 50, 75, 90, 95, 99]
265
+ data = Array.new(pctls.length, 0.0)
266
+ j = 0
267
+ i = 0
268
+ while i < @lats.length && j < pctls.length
269
+ current = i * 100 / @lats.length
270
+ if current >= pctls[j]
271
+ data[j] = @lats[i]
272
+ j += 1
273
+ end
274
+ i += 1
275
+ end
276
+ res = Array.new(pctls.length) { LatencyDistribution.new(0, 0.0) }
277
+ pctls.each_index do |k|
278
+ res[k] = LatencyDistribution.new(pctls[k], data[k]) if data[k] > 0
279
+ end
280
+ res
281
+ end
282
+
283
+ def histogram
284
+ bc = 10
285
+ buckets = Array.new(bc + 1, 0.0)
286
+ counts = Array.new(bc + 1, 0)
287
+ bs = (@slowest - @fastest) / bc
288
+ bc.times do |i|
289
+ buckets[i] = @fastest + bs * i
290
+ end
291
+ buckets[bc] = @slowest
292
+ bi = 0
293
+ max = 0
294
+ i = 0
295
+ while i < @lats.length
296
+ if @lats[i] <= buckets[bi]
297
+ i += 1
298
+ counts[bi] += 1
299
+ max = counts[bi] if max < counts[bi]
300
+ elsif bi < buckets.length - 1
301
+ bi += 1
302
+ end
303
+ end
304
+ res = Array.new(buckets.length)
305
+ buckets.each_index do |k|
306
+ res[k] = Bucket.new(buckets[k], counts[k], counts[k].to_f / @lats.length)
307
+ end
308
+ res
309
+ end
310
+ end
311
+ end
312
+ end