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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +33 -0
- data/LICENSE +202 -0
- data/NOTICE +18 -0
- data/README.md +165 -0
- data/exe/hey +405 -0
- data/lib/hey/requester/now_other.rb +29 -0
- data/lib/hey/requester/now_windows.rb +46 -0
- data/lib/hey/requester/print.rb +151 -0
- data/lib/hey/requester/report.rb +312 -0
- data/lib/hey/requester.rb +533 -0
- data/lib/hey/version.rb +5 -0
- data/lib/hey.rb +8 -0
- metadata +162 -0
data/exe/hey
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Copyright 2014 Google Inc. All Rights Reserved.
|
|
5
|
+
#
|
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
# you may not use this file except in compliance with the License.
|
|
8
|
+
# You may obtain a copy of the License at
|
|
9
|
+
#
|
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
#
|
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
# See the License for the specific language governing permissions and
|
|
16
|
+
# limitations under the License.
|
|
17
|
+
|
|
18
|
+
# Translated from hey.go. Command hey is an HTTP load generator.
|
|
19
|
+
|
|
20
|
+
require "etc"
|
|
21
|
+
require "uri"
|
|
22
|
+
|
|
23
|
+
require "hey"
|
|
24
|
+
|
|
25
|
+
module Hey
|
|
26
|
+
module CLI
|
|
27
|
+
HEADER_REGEXP = '^([\w-]+):\s*(.+)'
|
|
28
|
+
AUTH_REGEXP = '^(.+):([^\s].+)'
|
|
29
|
+
HEY_UA = "hey/0.0.1"
|
|
30
|
+
|
|
31
|
+
# Go's math.MaxInt32.
|
|
32
|
+
MAX_INT32 = 2_147_483_647
|
|
33
|
+
|
|
34
|
+
# A stand-in for Go's flag package, covering the forms it accepts for the
|
|
35
|
+
# flags declared below: -name value, -name=value, --name value, and a bare
|
|
36
|
+
# -name for booleans. Parsing stops at the first non-flag argument.
|
|
37
|
+
FLAGS = {
|
|
38
|
+
"m" => [:string, "GET"],
|
|
39
|
+
"h" => [:string, ""],
|
|
40
|
+
"d" => [:string, ""],
|
|
41
|
+
"D" => [:string, ""],
|
|
42
|
+
"A" => [:string, ""],
|
|
43
|
+
"T" => [:string, "text/html"],
|
|
44
|
+
"a" => [:string, ""],
|
|
45
|
+
"host" => [:string, ""],
|
|
46
|
+
"U" => [:string, ""],
|
|
47
|
+
|
|
48
|
+
"o" => [:string, ""],
|
|
49
|
+
|
|
50
|
+
"c" => [:int, 50],
|
|
51
|
+
"n" => [:int, 200],
|
|
52
|
+
"q" => [:float, 0.0],
|
|
53
|
+
"t" => [:int, 20],
|
|
54
|
+
"z" => [:duration, 0.0],
|
|
55
|
+
|
|
56
|
+
"h2" => [:bool, false],
|
|
57
|
+
"cpus" => [:int, Etc.nprocessors],
|
|
58
|
+
|
|
59
|
+
"disable-compression" => [:bool, false],
|
|
60
|
+
"disable-keepalive" => [:bool, false],
|
|
61
|
+
"disable-redirects" => [:bool, false],
|
|
62
|
+
"x" => [:string, ""],
|
|
63
|
+
|
|
64
|
+
# Go: flag.Var(&hs, "H", "") -- a repeatable flag.
|
|
65
|
+
"H" => [:strings, []]
|
|
66
|
+
}.freeze
|
|
67
|
+
|
|
68
|
+
USAGE = <<~'TEXT'
|
|
69
|
+
Usage: hey [options...] <url>
|
|
70
|
+
|
|
71
|
+
Options:
|
|
72
|
+
-n Number of requests to run. Default is 200.
|
|
73
|
+
-c Number of workers to run concurrently. Total number of requests cannot
|
|
74
|
+
be smaller than the concurrency level. Default is 50.
|
|
75
|
+
-q Rate limit, in queries per second (QPS) per worker. Default is no rate limit.
|
|
76
|
+
-z Duration of application to send requests. When duration is reached,
|
|
77
|
+
application stops and exits. If duration is specified, n is ignored.
|
|
78
|
+
Examples: -z 10s -z 3m.
|
|
79
|
+
-o Output type. If none provided, a summary is printed.
|
|
80
|
+
"csv" is the only supported alternative. Dumps the response
|
|
81
|
+
metrics in comma-separated values format.
|
|
82
|
+
|
|
83
|
+
-m HTTP method, one of GET, POST, PUT, DELETE, HEAD, OPTIONS.
|
|
84
|
+
-H Custom HTTP header. You can specify as many as needed by repeating the flag.
|
|
85
|
+
For example, -H "Accept: text/html" -H "Content-Type: application/xml" .
|
|
86
|
+
-t Timeout for each request in seconds. Default is 20, use 0 for infinite.
|
|
87
|
+
-A HTTP Accept header.
|
|
88
|
+
-d HTTP request body.
|
|
89
|
+
-D HTTP request body from file. For example, /home/user/file.txt or ./file.txt.
|
|
90
|
+
-T Content-type, defaults to "text/html".
|
|
91
|
+
-U User-Agent, defaults to version "hey/0.0.1".
|
|
92
|
+
-a Basic authentication, username:password.
|
|
93
|
+
-x HTTP Proxy address as host:port.
|
|
94
|
+
-h2 Enable HTTP/2.
|
|
95
|
+
|
|
96
|
+
-host HTTP Host header.
|
|
97
|
+
|
|
98
|
+
-disable-compression Disable compression.
|
|
99
|
+
-disable-keepalive Disable keep-alive, prevents re-use of TCP
|
|
100
|
+
connections between different HTTP requests.
|
|
101
|
+
-disable-redirects Disable following of HTTP redirects
|
|
102
|
+
-cpus Number of used cpu cores.
|
|
103
|
+
(default for current machine is %d cores)
|
|
104
|
+
TEXT
|
|
105
|
+
|
|
106
|
+
# Units accepted by Go's time.ParseDuration.
|
|
107
|
+
DURATION_UNITS = {
|
|
108
|
+
"ns" => 1e-9,
|
|
109
|
+
"us" => 1e-6,
|
|
110
|
+
"µs" => 1e-6,
|
|
111
|
+
"ms" => 1e-3,
|
|
112
|
+
"s" => 1.0,
|
|
113
|
+
"m" => 60.0,
|
|
114
|
+
"h" => 3600.0
|
|
115
|
+
}.freeze
|
|
116
|
+
|
|
117
|
+
class << self
|
|
118
|
+
def usage_text
|
|
119
|
+
format(USAGE, Etc.nprocessors)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Go: flag.Usage
|
|
123
|
+
def print_usage
|
|
124
|
+
$stderr.print(usage_text)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def err_and_exit(msg)
|
|
128
|
+
$stderr.printf("%s", msg)
|
|
129
|
+
$stderr.printf("\n")
|
|
130
|
+
exit 1
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def usage_and_exit(msg)
|
|
134
|
+
unless msg.nil? || msg.empty?
|
|
135
|
+
$stderr.printf("%s", msg)
|
|
136
|
+
$stderr.printf("\n\n")
|
|
137
|
+
end
|
|
138
|
+
print_usage
|
|
139
|
+
$stderr.printf("\n")
|
|
140
|
+
exit 1
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def parse_input_with_regexp(input, regx)
|
|
144
|
+
re = Regexp.new(regx)
|
|
145
|
+
matches = re.match(input)
|
|
146
|
+
# Go returns the full match followed by the groups; the array is
|
|
147
|
+
# shaped the same way here so the indices line up.
|
|
148
|
+
raise ArgumentError, "could not parse the provided input; input = #{input}" if matches.nil?
|
|
149
|
+
|
|
150
|
+
[matches[0], matches[1], matches[2]]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Go: time.ParseDuration
|
|
154
|
+
def parse_duration(value)
|
|
155
|
+
raise ArgumentError, "time: invalid duration #{value.inspect}" if value.nil? || value.empty?
|
|
156
|
+
|
|
157
|
+
rest = value
|
|
158
|
+
sign = 1.0
|
|
159
|
+
if rest.start_with?("-", "+")
|
|
160
|
+
sign = -1.0 if rest.start_with?("-")
|
|
161
|
+
rest = rest[1..]
|
|
162
|
+
end
|
|
163
|
+
return 0.0 if rest == "0"
|
|
164
|
+
raise ArgumentError, "time: invalid duration #{value.inspect}" if rest.empty?
|
|
165
|
+
|
|
166
|
+
total = 0.0
|
|
167
|
+
until rest.empty?
|
|
168
|
+
m = /\A(\d+(?:\.\d*)?|\.\d+)(ns|us|µs|ms|s|m|h)/.match(rest)
|
|
169
|
+
raise ArgumentError, "time: invalid duration #{value.inspect}" if m.nil?
|
|
170
|
+
|
|
171
|
+
total += m[1].to_f * DURATION_UNITS.fetch(m[2])
|
|
172
|
+
rest = m.post_match
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
sign * total
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Go's flag package accepts these spellings for a bool flag's value.
|
|
179
|
+
def parse_bool(value)
|
|
180
|
+
case value
|
|
181
|
+
when "1", "t", "T", "true", "TRUE", "True" then true
|
|
182
|
+
when "0", "f", "F", "false", "FALSE", "False" then false
|
|
183
|
+
else raise ArgumentError, "invalid boolean value #{value.inspect}"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def coerce(name, type, value)
|
|
188
|
+
case type
|
|
189
|
+
when :string then value
|
|
190
|
+
when :int then Integer(value, 10)
|
|
191
|
+
when :float then Float(value)
|
|
192
|
+
when :duration then parse_duration(value)
|
|
193
|
+
when :bool then parse_bool(value)
|
|
194
|
+
end
|
|
195
|
+
rescue ArgumentError, TypeError => e
|
|
196
|
+
usage_and_exit("invalid value #{value.inspect} for flag -#{name}: #{e.message}")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Go: flag.Parse(). Returns [values, remaining args].
|
|
200
|
+
def parse_flags(argv)
|
|
201
|
+
values = {}
|
|
202
|
+
# dup so the repeatable -H array is not shared between parses.
|
|
203
|
+
FLAGS.each { |name, (_type, default)| values[name] = default.dup }
|
|
204
|
+
|
|
205
|
+
args = argv.dup
|
|
206
|
+
until args.empty?
|
|
207
|
+
arg = args.first
|
|
208
|
+
break unless arg.start_with?("-") && arg.length > 1
|
|
209
|
+
|
|
210
|
+
args.shift
|
|
211
|
+
break if arg == "--"
|
|
212
|
+
|
|
213
|
+
body = arg.sub(/\A--?/, "")
|
|
214
|
+
name, eq, inline = body.partition("=")
|
|
215
|
+
|
|
216
|
+
spec = FLAGS[name]
|
|
217
|
+
usage_and_exit("flag provided but not defined: -#{name}") if spec.nil?
|
|
218
|
+
|
|
219
|
+
type, = spec
|
|
220
|
+
|
|
221
|
+
if type == :bool
|
|
222
|
+
# Go only accepts -name=false, never -name false.
|
|
223
|
+
value = eq.empty? ? "true" : inline
|
|
224
|
+
elsif eq.empty?
|
|
225
|
+
usage_and_exit("flag needs an argument: -#{name}") if args.empty?
|
|
226
|
+
value = args.shift
|
|
227
|
+
else
|
|
228
|
+
value = inline
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
if type == :strings
|
|
232
|
+
values[name] << value
|
|
233
|
+
else
|
|
234
|
+
values[name] = coerce(name, type, value)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
[values, args]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def main(argv = ARGV)
|
|
242
|
+
flags, args = parse_flags(argv)
|
|
243
|
+
usage_and_exit("") if args.length < 1
|
|
244
|
+
|
|
245
|
+
hs = flags["H"]
|
|
246
|
+
|
|
247
|
+
# Go: runtime.GOMAXPROCS(*cpus). Ruby's async runs on a single thread
|
|
248
|
+
# per process, so the flag is accepted and has no effect.
|
|
249
|
+
num = flags["n"]
|
|
250
|
+
conc = flags["c"]
|
|
251
|
+
q = flags["q"]
|
|
252
|
+
dur = flags["z"]
|
|
253
|
+
|
|
254
|
+
if dur > 0
|
|
255
|
+
num = MAX_INT32
|
|
256
|
+
usage_and_exit("-c cannot be smaller than 1.") if conc <= 0
|
|
257
|
+
else
|
|
258
|
+
usage_and_exit("-n and -c cannot be smaller than 1.") if num <= 0 || conc <= 0
|
|
259
|
+
|
|
260
|
+
usage_and_exit("-n cannot be less than -c.") if num < conc
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
url = args[0]
|
|
264
|
+
method = flags["m"].upcase
|
|
265
|
+
|
|
266
|
+
# set content-type
|
|
267
|
+
header = ::Protocol::HTTP::Headers.new
|
|
268
|
+
header.set("content-type", flags["T"])
|
|
269
|
+
# set any other additional headers
|
|
270
|
+
usage_and_exit("Flag '-h' is deprecated, please use '-H' instead.") unless flags["h"].empty?
|
|
271
|
+
# set any other additional repeatable headers
|
|
272
|
+
hs.each do |h|
|
|
273
|
+
begin
|
|
274
|
+
match = parse_input_with_regexp(h, HEADER_REGEXP)
|
|
275
|
+
rescue ArgumentError => e
|
|
276
|
+
usage_and_exit(e.message)
|
|
277
|
+
end
|
|
278
|
+
header.set(match[1], match[2])
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
header.set("accept", flags["A"]) unless flags["A"].empty?
|
|
282
|
+
|
|
283
|
+
# set basic auth if set
|
|
284
|
+
username = ""
|
|
285
|
+
password = ""
|
|
286
|
+
unless flags["a"].empty?
|
|
287
|
+
begin
|
|
288
|
+
match = parse_input_with_regexp(flags["a"], AUTH_REGEXP)
|
|
289
|
+
rescue ArgumentError => e
|
|
290
|
+
usage_and_exit(e.message)
|
|
291
|
+
end
|
|
292
|
+
username = match[1]
|
|
293
|
+
password = match[2]
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
body_all = nil
|
|
297
|
+
body_all = flags["d"].b unless flags["d"].empty?
|
|
298
|
+
unless flags["D"].empty?
|
|
299
|
+
begin
|
|
300
|
+
slurp = File.binread(flags["D"])
|
|
301
|
+
rescue SystemCallError => e
|
|
302
|
+
err_and_exit(e.message)
|
|
303
|
+
end
|
|
304
|
+
body_all = slurp
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
proxy_url = nil
|
|
308
|
+
unless flags["x"].empty?
|
|
309
|
+
begin
|
|
310
|
+
proxy_url = ::URI.parse(flags["x"])
|
|
311
|
+
rescue ::URI::Error => e
|
|
312
|
+
usage_and_exit(e.message)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
begin
|
|
317
|
+
req = Requester::Request.new(method, url)
|
|
318
|
+
rescue ArgumentError => e
|
|
319
|
+
usage_and_exit(e.message)
|
|
320
|
+
end
|
|
321
|
+
req.content_length = body_all ? body_all.bytesize : 0
|
|
322
|
+
req.set_basic_auth(username, password) if !username.empty? || !password.empty?
|
|
323
|
+
|
|
324
|
+
# set host header if set
|
|
325
|
+
req.host = flags["host"] unless flags["host"].empty?
|
|
326
|
+
|
|
327
|
+
ua = header["user-agent"]
|
|
328
|
+
if ua.nil? || ua.empty?
|
|
329
|
+
ua = HEY_UA
|
|
330
|
+
else
|
|
331
|
+
ua += " #{HEY_UA}"
|
|
332
|
+
end
|
|
333
|
+
header.set("user-agent", ua)
|
|
334
|
+
|
|
335
|
+
# set userAgent header if set
|
|
336
|
+
unless flags["U"].empty?
|
|
337
|
+
ua = "#{flags['U']} #{HEY_UA}"
|
|
338
|
+
header.set("user-agent", ua)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# NOTE: this mirrors hey.go exactly, including an upstream bug: the
|
|
342
|
+
# assignment replaces the header set built above, discarding the
|
|
343
|
+
# Authorization header that SetBasicAuth wrote a few lines earlier. So
|
|
344
|
+
# -a has no effect, in Go and here alike.
|
|
345
|
+
req.headers = header
|
|
346
|
+
|
|
347
|
+
w = Requester::Work.new(
|
|
348
|
+
request: req,
|
|
349
|
+
request_body: body_all,
|
|
350
|
+
n: num,
|
|
351
|
+
c: conc,
|
|
352
|
+
qps: q,
|
|
353
|
+
timeout: flags["t"],
|
|
354
|
+
disable_compression: flags["disable-compression"],
|
|
355
|
+
disable_keep_alives: flags["disable-keepalive"],
|
|
356
|
+
disable_redirects: flags["disable-redirects"],
|
|
357
|
+
h2: flags["h2"],
|
|
358
|
+
proxy_addr: proxy_url,
|
|
359
|
+
output: flags["o"]
|
|
360
|
+
)
|
|
361
|
+
w.init
|
|
362
|
+
|
|
363
|
+
# Go: signal.Notify(c, os.Interrupt) plus a goroutine per stop source.
|
|
364
|
+
# A self-pipe carries the signal into the reactor, because a Ruby trap
|
|
365
|
+
# handler cannot touch async primitives directly.
|
|
366
|
+
interrupt_r, interrupt_w = IO.pipe
|
|
367
|
+
previous = Signal.trap("INT") do
|
|
368
|
+
begin
|
|
369
|
+
interrupt_w.write_nonblock("x")
|
|
370
|
+
rescue StandardError
|
|
371
|
+
nil
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
Async do |task|
|
|
376
|
+
stopper = task.async do
|
|
377
|
+
interrupt_r.wait_readable
|
|
378
|
+
w.stop
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
timer =
|
|
382
|
+
if dur > 0
|
|
383
|
+
task.async do
|
|
384
|
+
sleep(dur)
|
|
385
|
+
w.stop
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
begin
|
|
390
|
+
w.perform
|
|
391
|
+
ensure
|
|
392
|
+
stopper.stop
|
|
393
|
+
timer&.stop
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
ensure
|
|
397
|
+
Signal.trap("INT", previous) if previous
|
|
398
|
+
interrupt_r&.close
|
|
399
|
+
interrupt_w&.close
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
Hey::CLI.main if $PROGRAM_NAME == __FILE__
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Copyright 2018 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/now_other.go (build tag: !windows).
|
|
16
|
+
|
|
17
|
+
module Hey
|
|
18
|
+
module Requester
|
|
19
|
+
# Process start, captured once, against which every measurement is taken.
|
|
20
|
+
START_TIME = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
21
|
+
|
|
22
|
+
# now returns the elapsed duration, in seconds, using the stdlib monotonic
|
|
23
|
+
# clock. The Go original returns a time.Duration; durations are plain
|
|
24
|
+
# Floats of seconds throughout this port.
|
|
25
|
+
def self.now
|
|
26
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - START_TIME
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Copyright 2018 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/now_windows.go.
|
|
16
|
+
#
|
|
17
|
+
# The Go original reaches into kernel32.dll for QueryPerformanceCounter and
|
|
18
|
+
# QueryPerformanceFrequency, because Go's time.Now() on Windows used to have
|
|
19
|
+
# only millisecond resolution -- far too coarse for per-request latencies.
|
|
20
|
+
#
|
|
21
|
+
# CRuby does not have that problem: on Windows, Process::CLOCK_MONOTONIC is
|
|
22
|
+
# itself implemented on top of QueryPerformanceCounter, scaled by
|
|
23
|
+
# QueryPerformanceFrequency. So the correct translation of the Go file is to
|
|
24
|
+
# use the monotonic clock directly rather than to re-bind the DLL through
|
|
25
|
+
# Fiddle. The frequency is read here only so the resolution is reportable.
|
|
26
|
+
|
|
27
|
+
module Hey
|
|
28
|
+
module Requester
|
|
29
|
+
# Ticks per second of the underlying performance counter.
|
|
30
|
+
QPC_FREQUENCY = begin
|
|
31
|
+
resolution = Process.clock_getres(Process::CLOCK_MONOTONIC)
|
|
32
|
+
resolution.positive? ? (1.0 / resolution).round : 0
|
|
33
|
+
rescue StandardError
|
|
34
|
+
0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Process start, captured once, against which every measurement is taken.
|
|
38
|
+
START_TIME = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
39
|
+
|
|
40
|
+
# now returns the elapsed duration, in seconds, using the
|
|
41
|
+
# QueryPerformanceCounter-backed monotonic clock.
|
|
42
|
+
def self.now
|
|
43
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - START_TIME
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,151 @@
|
|
|
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/print.go.
|
|
16
|
+
#
|
|
17
|
+
# Hey supports two output formats: summary and CSV
|
|
18
|
+
#
|
|
19
|
+
# The summary output presents a number of statistics about the requests in a
|
|
20
|
+
# human-readable format, including:
|
|
21
|
+
# - general statistics: requests/second, total runtime, and average, fastest,
|
|
22
|
+
# and slowest requests.
|
|
23
|
+
# - a response time histogram.
|
|
24
|
+
# - a percentile latency distribution.
|
|
25
|
+
# - statistics (average, fastest, slowest) on the stages of the requests.
|
|
26
|
+
#
|
|
27
|
+
# The comma-separated CSV format is proceeded by a header, and consists of the
|
|
28
|
+
# following columns:
|
|
29
|
+
# 1. response-time: Total time taken for request (in seconds)
|
|
30
|
+
# 2. DNS+dialup: Time taken to establish the TCP connection (in seconds)
|
|
31
|
+
# 3. DNS: Time taken to do the DNS lookup (in seconds)
|
|
32
|
+
# 4. Request-write: Time taken to write full request (in seconds)
|
|
33
|
+
# 5. Response-delay: Time taken to first byte received (in seconds)
|
|
34
|
+
# 6. Response-read: Time taken to read full response (in seconds)
|
|
35
|
+
# 7. status-code: HTTP status code of the response (e.g. 200)
|
|
36
|
+
# 8. offset: The time since the start of the benchmark when the
|
|
37
|
+
# request was started. (in seconds)
|
|
38
|
+
|
|
39
|
+
require "erb"
|
|
40
|
+
require "json"
|
|
41
|
+
|
|
42
|
+
module Hey
|
|
43
|
+
module Requester
|
|
44
|
+
# Go's text/template becomes ERB here. The two built-in templates below are
|
|
45
|
+
# transliterated from the Go originals, whitespace included, so the output
|
|
46
|
+
# is byte-identical; ERB happens to treat the newline after a `<% %>` tag
|
|
47
|
+
# the same way Go templates do for this layout.
|
|
48
|
+
#
|
|
49
|
+
# As in Go, an -o value other than "" or "csv" is itself treated as a
|
|
50
|
+
# user-supplied template.
|
|
51
|
+
def self.new_template(output)
|
|
52
|
+
output_tmpl =
|
|
53
|
+
case output
|
|
54
|
+
when nil, "" then DEFAULT_TMPL
|
|
55
|
+
when "csv" then CSV_TMPL
|
|
56
|
+
else output
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
ERB.new(output_tmpl)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The helper functions exposed to templates; the Go original registers
|
|
63
|
+
# these in a template.FuncMap.
|
|
64
|
+
module TemplateFunctions
|
|
65
|
+
def jsonify(value)
|
|
66
|
+
JSON.generate(value)
|
|
67
|
+
rescue JSON::GeneratorError
|
|
68
|
+
value.to_json
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def format_number(duration)
|
|
72
|
+
format("%4.4f", duration)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def format_number_int(duration)
|
|
76
|
+
format("%d", duration)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def histogram(buckets)
|
|
80
|
+
max = 0
|
|
81
|
+
buckets.each do |b|
|
|
82
|
+
v = b.count
|
|
83
|
+
max = v if v > max
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
res = +""
|
|
87
|
+
buckets.each_index do |i|
|
|
88
|
+
# Normalize bar lengths.
|
|
89
|
+
bar_len = 0
|
|
90
|
+
bar_len = (buckets[i].count * 40 + max / 2) / max if max > 0
|
|
91
|
+
res << format(" %4.3f [%s]\t|%s\n", buckets[i].mark, buckets[i].count,
|
|
92
|
+
BAR_CHAR * bar_len)
|
|
93
|
+
end
|
|
94
|
+
res
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The object a template is evaluated against. It exposes the report
|
|
99
|
+
# snapshot as `r` (Go's dot) alongside the helper functions.
|
|
100
|
+
class TemplateContext
|
|
101
|
+
include TemplateFunctions
|
|
102
|
+
|
|
103
|
+
attr_reader :r
|
|
104
|
+
|
|
105
|
+
def initialize(report)
|
|
106
|
+
@r = report
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def render(template)
|
|
110
|
+
template.result(binding)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
DEFAULT_TMPL = <<~'TMPL'
|
|
115
|
+
|
|
116
|
+
Summary:
|
|
117
|
+
Total: <%= format_number(r.total) %> secs
|
|
118
|
+
Slowest: <%= format_number(r.slowest) %> secs
|
|
119
|
+
Fastest: <%= format_number(r.fastest) %> secs
|
|
120
|
+
Average: <%= format_number(r.average) %> secs
|
|
121
|
+
Requests/sec: <%= format_number(r.rps) %>
|
|
122
|
+
<% if r.size_total > 0 %>
|
|
123
|
+
Total data: <%= r.size_total %> bytes
|
|
124
|
+
Size/request: <%= r.size_req %> bytes<% end %>
|
|
125
|
+
|
|
126
|
+
Response time histogram:
|
|
127
|
+
<%= histogram(r.histogram) %>
|
|
128
|
+
|
|
129
|
+
Latency distribution:<% r.latency_distribution.each do |d| %>
|
|
130
|
+
<%= d.percentage %>%% in <%= format_number(d.latency) %> secs<% end %>
|
|
131
|
+
|
|
132
|
+
Details (average, fastest, slowest):
|
|
133
|
+
DNS+dialup: <%= format_number(r.avg_conn) %> secs, <%= format_number(r.conn_max) %> secs, <%= format_number(r.conn_min) %> secs
|
|
134
|
+
DNS-lookup: <%= format_number(r.avg_dns) %> secs, <%= format_number(r.dns_max) %> secs, <%= format_number(r.dns_min) %> secs
|
|
135
|
+
req write: <%= format_number(r.avg_req) %> secs, <%= format_number(r.req_max) %> secs, <%= format_number(r.req_min) %> secs
|
|
136
|
+
resp wait: <%= format_number(r.avg_delay) %> secs, <%= format_number(r.delay_max) %> secs, <%= format_number(r.delay_min) %> secs
|
|
137
|
+
resp read: <%= format_number(r.avg_res) %> secs, <%= format_number(r.res_max) %> secs, <%= format_number(r.res_min) %> secs
|
|
138
|
+
|
|
139
|
+
Status code distribution:<% r.status_code_dist.sort.each do |code, num| %>
|
|
140
|
+
[<%= code %>] <%= num %> responses<% end %>
|
|
141
|
+
|
|
142
|
+
<% if r.error_dist.length > 0 %>Error distribution:<% r.error_dist.sort.each do |err, num| %>
|
|
143
|
+
[<%= num %>] <%= err %><% end %><% end %>
|
|
144
|
+
TMPL
|
|
145
|
+
|
|
146
|
+
CSV_TMPL = <<~'TMPL'.chomp
|
|
147
|
+
response-time,DNS+dialup,DNS,Request-write,Response-delay,Response-read,status-code,offset<% r.lats.each_index do |i| %>
|
|
148
|
+
<%= format_number(r.lats[i]) %>,<%= format_number(r.conn_lats[i]) %>,<%= format_number(r.dns_lats[i]) %>,<%= format_number(r.req_lats[i]) %>,<%= format_number(r.delay_lats[i]) %>,<%= format_number(r.res_lats[i]) %>,<%= format_number_int(r.status_codes[i]) %>,<%= format_number(r.offsets[i]) %><% end %>
|
|
149
|
+
TMPL
|
|
150
|
+
end
|
|
151
|
+
end
|