├── .npmignore
├── LICENSE.md
├── README.md
├── package.json
└── wperf.js
/.npmignore:
--------------------------------------------------------------------------------
1 | .gitignore
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # License
2 |
3 | The MIT License (MIT)
4 |
5 | Copyright (c) 2019 Joseph Huckaby
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8 |
9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Table of Contents
2 |
3 |
4 | - [Overview](#overview)
5 | * [Progress Display](#progress-display)
6 | * [Completed Output](#completed-output)
7 | - [Usage](#usage)
8 | * [Configuration Options](#configuration-options)
9 | + [url](#url)
10 | + [params](#params)
11 | + [max](#max)
12 | + [threads](#threads)
13 | + [keepalive](#keepalive)
14 | + [throttle](#throttle)
15 | + [timeout](#timeout)
16 | + [warn](#warn)
17 | + [warnings](#warnings)
18 | + [log](#log)
19 | + [fatal](#fatal)
20 | + [verbose](#verbose)
21 | + [cache_dns](#cache_dns)
22 | + [auth](#auth)
23 | + [compress](#compress)
24 | + [useragent](#useragent)
25 | + [follow](#follow)
26 | + [retries](#retries)
27 | + [insecure](#insecure)
28 | + [headers](#headers)
29 | + [method](#method)
30 | + [data](#data)
31 | + [multipart](#multipart)
32 | + [files](#files)
33 | + [success_match](#success_match)
34 | + [error_match](#error_match)
35 | + [histo](#histo)
36 | + [histo_ranges](#histo_ranges)
37 | + [stats](#stats)
38 | + [quiet](#quiet)
39 | + [color](#color)
40 | + [wrapper (Advanced)](#wrapper-advanced)
41 | - [Wrapper Constructor](#wrapper-constructor)
42 | - [Wrapper Method Hook](#wrapper-method-hook)
43 | - [Related](#related)
44 | - [License (MIT)](#license-mit)
45 |
46 |
47 |
48 | # Overview
49 |
50 | **WebPerf** (`wperf`) is a simple command-line HTTP load tester utility, which can send repeated HTTP requests to a target URL or set of URLs. It provides detailed performance metrics, including a breakdown of all the HTTP request phases, and a histogram showing the elapsed time spread.
51 |
52 | This is similar to the popular [ApacheBench](http://httpd.apache.org/docs/2.4/programs/ab.html) (`ab`) tool, but provides additional features like dynamic URL substitution, progress display with time remaining and live req/sec, a more detailed performance breakdown, and the ability to save configurations in JSON files.
53 |
54 | During a run, the script will display a graphical progress bar with estimated time remaining, and the current requests per second. You can also hit **Ctrl+Z** to output an in-progress report, and of course **Ctrl+C** to abort a run, which will render one final report before exiting.
55 |
56 | ## Progress Display
57 |
58 | 
59 |
60 | ## Completed Output
61 |
62 | 
63 |
64 | **Notes:**
65 |
66 | - The "Samples" numbers may differ between metrics for things like Keep-Alives, which will reuse sockets and therefore require fewer DNS lookups and TCP connects.
67 | - The "Decompress" metric is only applicable for encoded (compressed) server responses (i.e. Gzip, Deflate).
68 | - The "Peak Performance" is the best performing second, which is only shown if the total time covered a full second.
69 | - A "warning" is a request that took longer than a specified duration (default is 1 second).
70 | - An "error is a bad HTTP response (outside of the 2xx or 3xx range) or DNS lookup / TCP connect failure.
71 |
72 | # Usage
73 |
74 | Use [npm](https://www.npmjs.com/) to install the module as a command-line executable:
75 |
76 | ```sh
77 | npm install -g wperf
78 | ```
79 |
80 | Then call it using `wperf` and specify your URL and options on the command-line:
81 |
82 | ```sh
83 | wperf URL [--OPTION1 --OPTION2 ...]
84 | ```
85 |
86 | Example command:
87 |
88 | ```sh
89 | wperf https://myserver.com/some/path --max 100 --threads 2 --keepalive
90 | ```
91 |
92 | This would send an HTTP GET request to the specified URL 100 times using 2 threads, and utilizing [HTTP Keep-Alives](https://en.wikipedia.org/wiki/HTTP_persistent_connection) (i.e. reuse sockets for subsequent requests, if the target server supports it).
93 |
94 | Alternatively, you can store all your configuration settings in a JSON file, and specify it as the first argument (see below for details on how to format the JSON file):
95 |
96 | ```sh
97 | wperf my-load-test.json
98 | ```
99 |
100 | You can also include command-line arguments after the configuration file which acts as overrides:
101 |
102 | ```sh
103 | wperf my-load-test.json --verbose
104 | ```
105 |
106 | ## Configuration Options
107 |
108 | You can specify most configuration options on the command line (using the syntax `--key value`) or in a JSON configuration file as a top-level property. There are a few exceptions which are noted below.
109 |
110 | ### url
111 |
112 | Use the command-line `--url` or JSON `url` property to specify the URL to be requested. As a shortcut, the URL can be specified as the first argument to the command-line script, without the `--url` prefix. Example command-line:
113 |
114 | ```sh
115 | wperf https://myserver.com/some/path
116 | ```
117 |
118 | Example JSON configuration:
119 |
120 | ```json
121 | {
122 | "url": "https://myserver.com/some/path"
123 | }
124 | ```
125 |
126 | ### params
127 |
128 | If you use a JSON configuration file, you can insert `[placeholder]` variables into your URL. These are expanded by looking in a `params` object in the JSON file, if provided. Further, each parameter may be an array of values, of which one is picked randomly per request. Example:
129 |
130 | ```json
131 | {
132 | "url": "https://myserver.com/some/path?food=[food]",
133 | "params": {
134 | "food": ["apple", "orange", "banana"]
135 | }
136 | }
137 | ```
138 |
139 | This would produce three different URLs, picked randomly for each request:
140 |
141 | ```
142 | https://myserver.com/some/path?food=apple
143 | https://myserver.com/some/path?food=orange
144 | https://myserver.com/some/path?food=banana
145 | ```
146 |
147 | You can nest parameters, meaning the values can themselves contain `[placeholder]` variables, which are further expanded until all are satisfied (up to 32 levels deep). Example of this:
148 |
149 | ```json
150 | {
151 | "url": "https://myserver.com[uri]",
152 | "params": {
153 | "uri": [
154 | "/some/path?&action=eat&food=[food]",
155 | "/some/other/path?&action=drink&beverage=[beverage]"
156 | ],
157 | "food": ["apple", "orange", "banana"],
158 | "beverage": ["coke", "pepsi"]
159 | }
160 | }
161 | ```
162 |
163 | Here we have the full URI path substituted out as `[uri]`, which may pick one of two values, one with a `[food]` and one with a `[beverage]` variable. This particular configuration would result in 5 total unique URLs.
164 |
165 | If you just want to pick from a random list of URLs, simply set the `url` property to a string containing a single macro like `[url]`, then list all your URLs like this:
166 |
167 | ```json
168 | {
169 | "url": "[url]",
170 | "params": {
171 | "url": [
172 | "http://server1.com/path/one",
173 | "http://server2.com/path/two",
174 | "http://server3.com/path/three"
175 | ]
176 | }
177 | }
178 | ```
179 |
180 | If you simply want a random number on your URLs, you can use the `[#-#]` shortcut, which will pick a random integer within the specified range (inclusive). Example:
181 |
182 | ```json
183 | {
184 | "url": "https://myserver.com/some/path?&random=[0-99999]"
185 | }
186 | ```
187 |
188 | You can optionally store your parameters in their own JSON file. To use this feature, specify the file path using the `--params` command-line argument, or as a `params` string property in your configuration file. Example of the latter:
189 |
190 | ```json
191 | {
192 | "url": "https://myserver.com/some/path?food=[food]",
193 | "params": "my_params_file.json"
194 | }
195 | ```
196 |
197 | And then the contents of `my_params_file.json` would be:
198 |
199 | ```json
200 | {
201 | "food": ["apple", "orange", "banana"]
202 | }
203 | ```
204 |
205 | Here is an example of specifying the parameters file using the command line:
206 |
207 | ```sh
208 | wperf https://myserver.com/some/path --params my_params_file.json
209 | ```
210 |
211 | You can override individual parameters on the command line like this:
212 |
213 | ```sh
214 | wperf https://myserver.com/some/path --food orange
215 | ```
216 |
217 | Another use of this is to make the hostname portion of the URL configurable, but have the rest of the URL be hard-coded or based on parameters in the config file. Example:
218 |
219 | ```json
220 | {
221 | "url": "https://[host][uri]",
222 | "params": {
223 | "host": [
224 | "myserver.com"
225 | ],
226 | "uri": [
227 | "/some/path?&action=eat&food=[food]",
228 | "/some/other/path?&action=drink&beverage=[beverage]"
229 | ],
230 | "food": ["apple", "orange", "banana"],
231 | "beverage": ["coke", "pepsi"]
232 | }
233 | }
234 | ```
235 |
236 | Then you can customize just the hostname per test run like this:
237 |
238 | ```sh
239 | wperf my-config-file.json --host myOTHERserver.com
240 | ```
241 |
242 | ### max
243 |
244 | The `max` parameter specifies the total number of HTTP requests to send (regardless of threads). You can specify this on the command-line or in a configuration file. The default is `1`. Example:
245 |
246 | ```sh
247 | wperf https://myserver.com/some/path --max 100
248 | ```
249 |
250 | Example JSON configuration:
251 |
252 | ```json
253 | {
254 | "url": "https://myserver.com/some/path",
255 | "max": 100
256 | }
257 | ```
258 |
259 | ### threads
260 |
261 | The `threads` parameter specifies the number of "threads" (i.e. concurrent HTTP requests) to send. You can specify this on the command-line or in a configuration file. The default is `1`. Example:
262 |
263 | ```sh
264 | wperf https://myserver.com/some/path --max 100 --threads 4
265 | ```
266 |
267 | Example JSON configuration:
268 |
269 | ```json
270 | {
271 | "url": "https://myserver.com/some/path",
272 | "max": 100,
273 | "threads": 4
274 | }
275 | ```
276 |
277 | ### keepalive
278 |
279 | The `keepalive` parameter, when present on the command-line or set to `true` in your JSON configuration, enables [HTTP Keep-Alives](https://en.wikipedia.org/wiki/HTTP_persistent_connection) for all requests. This means that sockets will be reused whenever possible (if the target server supports it and doesn't close the socket itself). The default behavior is to disable Keep-Alives, and open a new socket for every request. Example use:
280 |
281 | ```sh
282 | wperf https://myserver.com/some/path --max 100 --keepalive
283 | ```
284 |
285 | Example JSON configuration:
286 |
287 | ```json
288 | {
289 | "url": "https://myserver.com/some/path",
290 | "max": 100,
291 | "keepalive": true
292 | }
293 | ```
294 |
295 | Of course, Keep-Alives only take effect if you send more than one request.
296 |
297 | ### throttle
298 |
299 | The `throttle` parameter allows you to set a maximum requests per second limit, which the script will always stay under, regardless of the number of threads. You can specify this on the command-line or in a configuration file. The default is *unlimited*. Example use:
300 |
301 | ```sh
302 | wperf https://myserver.com/some/path --max 100 --throttle 10
303 | ```
304 |
305 | Example JSON configuration:
306 |
307 | ```json
308 | {
309 | "url": "https://myserver.com/some/path",
310 | "max": 100,
311 | "throttle": 10
312 | }
313 | ```
314 |
315 | ### timeout
316 |
317 | The `timeout` parameter allows you to specify a maximum time for requests in seconds, before they are aborted and considered an error. This is measured as the [time to first byte](https://en.wikipedia.org/wiki/Time_to_first_byte), and is specified as seconds. You can set this on the command-line or in a configuration file. The default is `5` seconds. The value can be a floating point decimal (fractional seconds). Example use:
318 |
319 | ```sh
320 | wperf https://myserver.com/some/path --timeout 2.5
321 | ```
322 |
323 | Example JSON configuration:
324 |
325 | ```json
326 | {
327 | "url": "https://myserver.com/some/path",
328 | "timeout": 2.5
329 | }
330 | ```
331 |
332 | ### warn
333 |
334 | The `warn` parameter allows you to specify a maximum time for requests in seconds, before they are logged as a warning. You can set this on the command-line or in a configuration file. The default is `1` second. The value can be a floating point decimal (fractional seconds). Example use:
335 |
336 | ```sh
337 | wperf https://myserver.com/some/path --warn 0.5
338 | ```
339 |
340 | Example JSON configuration:
341 |
342 | ```json
343 | {
344 | "url": "https://myserver.com/some/path",
345 | "warn": 0.5
346 | }
347 | ```
348 |
349 | Warnings are printed to STDERR, and contain a date/time stamp (local time), the request sequence number, the HTTP response code, and a JSON object containing the raw performance metrics (measured in milliseconds), along with the bytes sent & received, and the URL that was requested. Example:
350 |
351 | ```
352 | [2019/08/31 16:46:20] Perf Warning: Req #1: HTTP 200 OK -- {"scale":1000,"perf":{"total":20.584,"dns":2.647,"send":0,"connect":0.477,"wait":13.68,"receive":1.102,"decompress":2.323},"counters":{"bytes_sent":151,"bytes_received":266},"url":"http://localhost:3012/rsleep?veg=celery"}
353 | ```
354 |
355 | ### warnings
356 |
357 | If you would prefer warnings in a more machine-readable format, you can have them logged to a file using [newline delimited JSON](http://ndjson.org/) format. To enable this, include the `--warnings` command-line argument followed by a log file path, or use the `warnings` configuration property. Example use:
358 |
359 | ```sh
360 | wperf https://myserver.com/some/path --warn 0.5 --warnings /var/log/my-warning-log.ndjson
361 | ```
362 |
363 | Example JSON configuration:
364 |
365 | ```json
366 | {
367 | "url": "https://myserver.com/some/path",
368 | "warn": 0.5,
369 | "warnings": "/var/log/my-warning-log.ndjson"
370 | }
371 | ```
372 |
373 | Here is an example warning log entry (the JSON has been pretty-printed for display purposes):
374 |
375 | ```json
376 | {
377 | "perf": {
378 | "total": 34.992,
379 | "dns": 2.324,
380 | "send": 0,
381 | "connect": 0.423,
382 | "wait": 29.014,
383 | "receive": 0.801,
384 | "decompress": 2.168
385 | },
386 | "counters": {
387 | "bytes_sent": 151,
388 | "bytes_received": 266
389 | },
390 | "url": "http://localhost:3012/rsleep?veg=celery",
391 | "code": 200,
392 | "status": "OK",
393 | "req_num": 1,
394 | "now": 1567295312.123,
395 | "date_time": "[2019/08/31 16:48:32]"
396 | }
397 | ```
398 |
399 | Here are descriptions of the properties:
400 |
401 | | Property | Description |
402 | |----------|-------------|
403 | | `perf` | This object contains the raw performance metrics for the request, which are all measured in milliseconds. See [Performance Metrics](https://github.com/jhuckaby/pixl-request#performance-metrics) for details. |
404 | | `counters` | This object contains raw counters, including `bytes_sent` and `bytes_received`, which count the number of bytes sent and received for the request, respectively. |
405 | | `url` | This is the URL that was requested. It is included because it may have been dynamically constructed with placeholder substitutions. |
406 | | `code` | This is the HTTP response code sent back from the server. |
407 | | `status` | This is the HTTP status line sent back from the server. |
408 | | `req_num` | This is the request sequence number (from one to [max](#max)). |
409 | | `now` | This is the current date/time expressed as [Epoch](https://en.wikipedia.org/wiki/Unix_time) seconds. |
410 | | `date_time` | This is the current date/time expressed as a string in the local timezone. |
411 |
412 | ### log
413 |
414 | Building upon [warnings](#warnings) discussed above, you can also tell wperf to log *every request* regardless if it is a warning or not. To do this, include the `--log` command-line argument followed by a log file path, or use the `log` configuration property. Example use:
415 |
416 | ```sh
417 | wperf https://myserver.com/some/path --warn 0.5 --log /var/log/my-req-log.ndjson
418 | ```
419 |
420 | Example JSON configuration:
421 |
422 | ```json
423 | {
424 | "url": "https://myserver.com/some/path",
425 | "warn": 0.5,
426 | "log": "/var/log/my-req-log.ndjson"
427 | }
428 | ```
429 |
430 | See [warnings](#warnings) above for details on the [NDJSON](http://ndjson.org/) logging format.
431 |
432 | ### fatal
433 |
434 | The `fatal` parameter, when present on the command-line or set to `true` in your JSON configuration, will cause the first HTTP error response to abort the entire run. By default this is disabled, and the script continues after encountering errors. Example use:
435 |
436 | ```sh
437 | wperf https://myserver.com/some/path --fatal
438 | ```
439 |
440 | Example JSON configuration:
441 |
442 | ```json
443 | {
444 | "url": "https://myserver.com/some/path",
445 | "fatal": true
446 | }
447 | ```
448 |
449 | ### verbose
450 |
451 | The `verbose` parameter, when present on the command-line or set to `true` in your JSON configuration, outputs information about every single request just as it completes. Example use:
452 |
453 | ```sh
454 | wperf https://myserver.com/some/path --verbose
455 | ```
456 |
457 | Example JSON configuration:
458 |
459 | ```json
460 | {
461 | "url": "https://myserver.com/some/path",
462 | "verbose": true
463 | }
464 | ```
465 |
466 | Verbose output looks like this:
467 |
468 | ```
469 | [2019/08/31 16:42:54] Req #1: HTTP 200 OK -- {"scale":1000,"perf":{"total":24.139,"dns":2.044,"send":0,"connect":0.43,"wait":17.426,"receive":1.104,"decompress":2.802},"counters":{"bytes_sent":151,"bytes_received":266},"url":"http://localhost:3012/rsleep?veg=celery"}
470 | [2019/08/31 16:42:54] Req #2: HTTP 200 OK -- {"scale":1000,"perf":{"total":25.454,"dns":1.872,"send":0,"connect":0.402,"wait":20.285,"receive":0.171,"decompress":2.683},"counters":{"bytes_sent":152,"bytes_received":266},"url":"http://localhost:3012/rsleep?color=green"}
471 | [2019/08/31 16:42:54] Req #3: HTTP 200 OK -- {"scale":1000,"perf":{"total":35.035,"dns":2.317,"send":0,"connect":0.378,"wait":31.838,"receive":0.093,"decompress":0.393},"counters":{"bytes_sent":151,"bytes_received":266},"url":"http://localhost:3012/rsleep?veg=celery"}
472 | [2019/08/31 16:42:54] Req #4: HTTP 200 OK -- {"scale":1000,"perf":{"total":40.736,"dns":6.997,"send":0,"connect":0.476,"wait":32.247,"receive":0.074,"decompress":0.943},"counters":{"bytes_sent":150,"bytes_received":266},"url":"http://localhost:3012/rsleep?color=red"}
473 | [2019/08/31 16:42:54] Req #5: HTTP 200 OK -- {"scale":1000,"perf":{"total":10.893,"send":0.39,"wait":9.735,"receive":0.103,"decompress":0.641},"counters":{"bytes_sent":151,"bytes_received":264},"url":"http://localhost:3012/rsleep?veg=celery"}
474 | ```
475 |
476 | Similar to the [warn](#warn) output, these lines contain a date/time stamp (local time), the request sequence number, the HTTP response code, and a JSON object containing the raw performance metrics (measured in milliseconds), along with the bytes sent & received, and the URL that was requested.
477 |
478 | ### cache_dns
479 |
480 | The `cache_dns` parameter, when present on the command-line or set to `true` in your JSON configuration, will cache the IP addresses from DNS lookups, so they only need to be requested once per unique domain name. Example use:
481 |
482 | ```sh
483 | wperf https://myserver.com/some/path --cache_dns
484 | ```
485 |
486 | Example JSON configuration:
487 |
488 | ```json
489 | {
490 | "url": "https://myserver.com/some/path",
491 | "cache_dns": true
492 | }
493 | ```
494 |
495 | ### auth
496 |
497 | The `auth` parameter allows you to include [HTTP Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) credentials (i.e. username and password). These should be delimited by a colon (`:`) character. You can set this on the command-line or in a configuration file. Example:
498 |
499 | ```sh
500 | wperf https://myserver.com/some/path --auth "jsmith:12345"
501 | ```
502 |
503 | Example JSON configuration:
504 |
505 | ```json
506 | {
507 | "url": "https://myserver.com/some/path",
508 | "auth": "jsmith:12345"
509 | }
510 | ```
511 |
512 | ### compress
513 |
514 | By default, the request library supports compressed server responses (i.e. Gzip or Deflate content encoding), and announces support for these via the `Accept-Encoding` header. The `compress` property allows you to *disable* compression support in the request library. Specifically, disabling compression means that an `Accept-Encoding: none` header is sent with every request, informing the target server that the client doesn't support a compressed response. You can set this property to `false` on the command-line or in a configuration file. Example:
515 |
516 | ```sh
517 | wperf https://myserver.com/some/path --compress false
518 | ```
519 |
520 | Example JSON configuration:
521 |
522 | ```json
523 | {
524 | "url": "https://myserver.com/some/path",
525 | "compress": false
526 | }
527 | ```
528 |
529 | **Note:** It is really up to the target server whether this header is followed or not.
530 |
531 | ### useragent
532 |
533 | The `useragent` parameter allows you to specify a custom `User-Agent` request header. By default, this is set to `Mozilla/5.0; wperf/1.0.0`. You can set this property on the command-line or in a configuration file. Example:
534 |
535 | ```sh
536 | wperf https://myserver.com/some/path --useragent "My Custom Agent v1.2.3"
537 | ```
538 |
539 | Example JSON configuration:
540 |
541 | ```json
542 | {
543 | "url": "https://myserver.com/some/path",
544 | "useragent": "My Custom Agent v1.2.3"
545 | }
546 | ```
547 |
548 | ### follow
549 |
550 | The `follow` parameter, when present on the command-line or set to `true` in your JSON configuration, will cause the HTTP request library to automatically follow redirects. That is, HTTP response codes in the `3xx` range, with an accompanying `Location` response header. You can set this property on the command-line or in a configuration file. Example:
551 |
552 | ```sh
553 | wperf https://myserver.com/some/path --follow
554 | ```
555 |
556 | Example JSON configuration:
557 |
558 | ```json
559 | {
560 | "url": "https://myserver.com/some/path",
561 | "follow": true
562 | }
563 | ```
564 |
565 | Alternatively, you can set this parameter to a number value, which represents the total amount of redirects to follow for a given request. This can help prevent infinite redirect loops.
566 |
567 | **Note:** The request library only tracks the performance metrics of the *final* request in the redirect chain.
568 |
569 | ### retries
570 |
571 | The `retries` parameter allows you to set a number of retries before an error is logged, and possibly [fatal](#fatal). The default is `0` retries. You can set this property on the command-line or in a configuration file. Example:
572 |
573 | ```sh
574 | wperf https://myserver.com/some/path --retries 5
575 | ```
576 |
577 | Example JSON configuration:
578 |
579 | ```json
580 | {
581 | "url": "https://myserver.com/some/path",
582 | "retries": 5
583 | }
584 | ```
585 |
586 | **Note:** If retries occur, the request library only tracks the performance metrics of the *final* request of each group.
587 |
588 | ### insecure
589 |
590 | By default, when HTTPS requests are made, the SSL certificate is verified using a Certificate Authority (CA). The `insecure` parameter, when present on the command-line or set to `true` in your JSON configuration, will cause the request library to bypass all SSL certificate verification. One potential use of this is for self-signed certificates. Example:
591 |
592 | ```sh
593 | wperf https://myserver.com/some/path --insecure
594 | ```
595 |
596 | Example JSON configuration:
597 |
598 | ```json
599 | {
600 | "url": "https://myserver.com/some/path",
601 | "insecure": true
602 | }
603 | ```
604 |
605 | ### headers
606 |
607 | The `headers` parameter allows you to add custom HTTP headers to every request. These can be specified on the command-line using a `h_` prefix followed by the header name, then the header value. You can repeat this for adding multiple headers. Example:
608 |
609 | ```sh
610 | wperf https://myserver.com/some/path --h_Cookie "sessionid=1234567890;"
611 | ```
612 |
613 | If using a JSON configuration file, you can instead use a `headers` object, with the header names specified as the keys within. Example:
614 |
615 | ```json
616 | {
617 | "url": "https://myserver.com/some/path",
618 | "headers": {
619 | "Cookie": "sessionid=1234567890;"
620 | }
621 | }
622 | ```
623 |
624 | You can also use placeholder macros in your header values, which will be expanded with matching [params](#params). Example:
625 |
626 | ```json
627 | {
628 | "url": "https://myserver.com/some/path",
629 | "headers": {
630 | "Cookie": "sessionid=[session_id];"
631 | },
632 | "params": {
633 | "session_id": ["1234567890", "abcdefghij", "qrstuvwxyz"]
634 | }
635 | }
636 | ```
637 |
638 | The underlying request library also adds a few basic headers of its own, including `Host`, `Connection`, `Accept-Encoding` and `User-Agent`.
639 |
640 | ### method
641 |
642 | The `method` parameter allows you to set the HTTP method for all requests sent. By default this is `GET`, but you can set it to any of the standard values, i.e. `GET`, `HEAD`, `POST`, `PUT` and `DELETE`. You can set this property on the command-line or in a configuration file. Example:
643 |
644 | ```sh
645 | wperf https://myserver.com/some/path --method HEAD
646 | ```
647 |
648 | Example JSON configuration:
649 |
650 | ```json
651 | {
652 | "url": "https://myserver.com/some/path",
653 | "method": "HEAD"
654 | }
655 | ```
656 |
657 | ### data
658 |
659 | If you select any of the HTTP methods that support a request body, e.g. `POST`, `PUT` or `DELETE`, you can specify the data in a number of different ways. By default, it is sent as `application/x-www-form-urlencoded`, and you can specify named parameters on the command-line using a `d_` prefix like this:
660 |
661 | ```sh
662 | wperf https://myserver.com/some/path --method POST --d_username jsmith --d_email jsmith@aol.com
663 | ```
664 |
665 | This would serialize the two post parameters using standard form encoding, resulting in `username=jsmith&email=jsmith%40aol.com`. You can also specify these parameters in your JSON configuration file using the `data` property:
666 |
667 | ```json
668 | {
669 | "url": "https://myserver.com/some/path",
670 | "method": "POST",
671 | "data": {
672 | "username": "jsmith",
673 | "email": "jsmith@aol.com"
674 | }
675 | }
676 | ```
677 |
678 | Alternatively, you can specify the request body in "raw" format using the `--data` command-line argument (or `data` configuration property) by setting it to a string. This can be used to send a pure JSON post, for example:
679 |
680 | ```sh
681 | wperf https://myserver.com/some/path --method POST --h_Content-Type application/json --data '{"username":"jsmith","email":"jsmith@aol.com"}'
682 | ```
683 |
684 | Or similarly via configuration file:
685 |
686 | ```json
687 | {
688 | "url": "https://myserver.com/some/path",
689 | "method": "POST",
690 | "headers": {
691 | "Content-Type": "application/json"
692 | },
693 | "data": "{\"username\":\"jsmith\",\"email\":\"jsmith@aol.com\"}"
694 | }
695 | ```
696 |
697 | You can also use placeholder macros in your data, which will be expanded with matching [params](#params). Example:
698 |
699 | ```json
700 | {
701 | "url": "https://myserver.com/some/path",
702 | "method": "POST",
703 | "headers": {
704 | "Content-Type": "application/json"
705 | },
706 | "data": "{\"username\":\"jsmith\",\"sessionid\":\"[session_id]\"}",
707 | "params": {
708 | "session_id": ["1234567890", "abcdefghij", "qrstuvwxyz"]
709 | }
710 | }
711 | ```
712 |
713 | ### multipart
714 |
715 | To send a "multipart" HTTP POST, which is designed more for larger parameters (also see [files](#files)), include the `--multipart` parameter on the command-line, or set the `multipart` JSON configuration property to `true`. Example of this:
716 |
717 | ```sh
718 | wperf https://myserver.com/some/path --method POST --multipart --d_username jsmith --d_email jsmith@aol.com
719 | ```
720 |
721 | And in JSON configuration format:
722 |
723 | ```json
724 | {
725 | "url": "https://myserver.com/some/path",
726 | "method": "POST",
727 | "multipart": true,
728 | "data": {
729 | "username": "jsmith",
730 | "email": "jsmith@aol.com"
731 | }
732 | }
733 | ```
734 |
735 | ### files
736 |
737 | If you are performing a multipart HTTP POST, you can upload actual files from the local filesystem. These can be specified on the command-line using the `f_` prefix, or in a configuration file using a `files` object. Here are examples of each:
738 |
739 | ```sh
740 | wperf https://myserver.com/some/path --method POST --multipart --f_file1 /path/to/my/file.jpg
741 | ```
742 |
743 | Example JSON configuration:
744 |
745 | ```json
746 | {
747 | "url": "https://myserver.com/some/path",
748 | "method": "POST",
749 | "multipart": true,
750 | "files": {
751 | "file1": "/path/to/my/file.jpg"
752 | }
753 | }
754 | ```
755 |
756 | In this case the file is identified by the parameter name `file1`. The filename and content type are set automatically by the request library, based on the specified file path.
757 |
758 | ### success_match
759 |
760 | The `success_match` parameter allows you to specify a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) that must match against the server response content body in order for the request to be considered a success. You can set this property on the command-line or in a configuration file. Example:
761 |
762 | ```sh
763 | wperf https://myserver.com/some/path --success_match "Operation was successful"
764 | ```
765 |
766 | Example JSON configuration:
767 |
768 | ```json
769 | {
770 | "url": "https://myserver.com/some/path",
771 | "success_match": "Operation was successful"
772 | }
773 | ```
774 |
775 | ### error_match
776 |
777 | The `error_match` parameter allows you to specify a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) that generates an error when it matches the server response content body. You can set this property on the command-line or in a configuration file. Example:
778 |
779 | ```sh
780 | wperf https://myserver.com/some/path --error_match "Database failure"
781 | ```
782 |
783 | Example JSON configuration:
784 |
785 | ```json
786 | {
787 | "url": "https://myserver.com/some/path",
788 | "error_match": "Database failure"
789 | }
790 | ```
791 |
792 | ### histo
793 |
794 | Normally `wperf` outputs a histogram of the "Total Time" metric, which is the full HTTP request from beginning to end. However, you can customize this, and set it to generate multiple histograms of different HTTP metrics, including:
795 |
796 | | Metric | Description |
797 | |--------|-------------|
798 | | `dns` | Time to resolve the hostname to an IP address via DNS. This is omitted if cached, or you specify an IP on the URL. |
799 | | `connect` | Time to connect to the remote socket (omitted if using Keep-Alives and reusing a host). |
800 | | `send` | Time to send the request data (typically for POST / PUT). Also includes SSL handshake time (if HTTPS). |
801 | | `wait` | Time spent waiting for the server response (after request is sent). |
802 | | `receive` | Time spent downloading data from the server (after headers received). |
803 | | `decompress` | Time taken to decompress the response (if encoded with Gzip or Deflate). |
804 | | `total` | Total time of the entire HTTP transaction. |
805 |
806 | To specify additional histograms, include the `--histo` argument on the command-line, set to a comma-separate list of any of the metrics defined above. Or you can use the special keyword `all` to generate histograms for *all* of them. Example:
807 |
808 | ```sh
809 | wperf https://myserver.com/some/path --histo "dns,connect,receive"
810 | ```
811 |
812 | If you are using a JSON configuration file, you can set the `histo` property to an array instead of a CSV string:
813 |
814 | ```json
815 | {
816 | "url": "https://myserver.com/some/path",
817 | "histo": ["dns", "connect", "receive"]
818 | }
819 | ```
820 |
821 | See this [screenshot](https://pixlcore.com/software/wperf/terminal.png) for an example histogram.
822 |
823 | ### histo_ranges
824 |
825 | By default, the histograms generated by `wperf` include metrics in the following time ranges:
826 |
827 | - 0-1 ms
828 | - 1-2 ms
829 | - 2-3 ms
830 | - 3-4 ms
831 | - 4-5 ms
832 | - 5-10 ms
833 | - 10-20 ms
834 | - 20-30 ms
835 | - 30-40 ms
836 | - 40-50 ms
837 | - 50-100 ms
838 | - 100-200 ms
839 | - 200-300 ms
840 | - 300-400 ms
841 | - 400-500 ms
842 | - 500-1000 ms
843 | - 1-2 sec
844 | - 2-3 sec
845 | - 3-4 sec
846 | - 4-5 sec
847 | - 5+ sec
848 |
849 | You can customize these lanes by specifying a `histo_ranges` key in your JSON configuration file. This should be set to an array of strings, and the format of the strings needs to match `#-# ms`, `#+ ms`, `#-# sec` or `#+ sec`. Example:
850 |
851 | ```json
852 | {
853 | "url": "https://myserver.com/some/path",
854 | "histo_ranges": [
855 | "0-1 ms",
856 | "1-2 ms",
857 | "2-3 ms",
858 | "3-4 ms",
859 | "4-5 ms",
860 | "5-10 ms",
861 | "10-20 ms",
862 | "20-30 ms",
863 | "30-40 ms",
864 | "40-50 ms",
865 | "50-100 ms",
866 | "100-200 ms",
867 | "200-300 ms",
868 | "300-400 ms",
869 | "400-500 ms",
870 | "500-1000 ms",
871 | "1-2 sec",
872 | "2-3 sec",
873 | "3-4 sec",
874 | "4-5 sec",
875 | "5+ sec"
876 | ]
877 | }
878 | ```
879 |
880 | ### stats
881 |
882 | If you would like `wperf` to output more machine-readable statistics, you can do so by adding the `--stats` command-line argument, or the `stats` configuration property. Example use:
883 |
884 | ```sh
885 | wperf https://myserver.com/some/path --stats
886 | ```
887 |
888 | Example JSON configuration:
889 |
890 | ```json
891 | {
892 | "url": "https://myserver.com/some/path",
893 | "stats": true
894 | }
895 | ```
896 |
897 | You can alternatively have the statistics appended to a file, instead of printed to the console. To do this, simply include the destination target filename after `--stats` on the command-line, or set the `stats` property to a filename in the JSON configuration file.
898 |
899 | Example output (pretty-printed for display purposes):
900 |
901 | ```json
902 | {
903 | "current_sec": 1567391879,
904 | "count_sec": 39,
905 | "peak_sec": 61,
906 | "total_reqs": 100,
907 | "total_warnings": 0,
908 | "total_errors": 0,
909 | "bytes_sent": 58300,
910 | "bytes_received": 28900,
911 | "time_start": 1567391878.592,
912 | "total": {
913 | "min": 4.689,
914 | "max": 28.387,
915 | "total": 633.4549999999998,
916 | "count": 100,
917 | "avg": 6.334549999999998
918 | },
919 | "dns": {
920 | "min": 0.59,
921 | "max": 16.118,
922 | "total": 98.68200000000003,
923 | "count": 100,
924 | "avg": 0.9868200000000003
925 | },
926 | "connect": {
927 | "min": 0.177,
928 | "max": 0.934,
929 | "total": 22.475,
930 | "count": 100,
931 | "avg": 0.22475
932 | },
933 | "send": {
934 | "min": 0.967,
935 | "max": 1.873,
936 | "total": 110.59200000000003,
937 | "count": 100,
938 | "avg": 1.1059200000000002
939 | },
940 | "wait": {
941 | "min": 2.491,
942 | "max": 8.302,
943 | "total": 361.1120000000002,
944 | "count": 100,
945 | "avg": 3.611120000000002
946 | },
947 | "receive": {
948 | "min": 0.054,
949 | "max": 1.07,
950 | "total": 7.630000000000001,
951 | "count": 100,
952 | "avg": 0.0763
953 | },
954 | "decompress": {
955 | "min": 0.192,
956 | "max": 2.288,
957 | "total": 30.935,
958 | "count": 100,
959 | "avg": 0.30935
960 | }
961 | }
962 | ```
963 |
964 | Here are descriptions of the properties:
965 |
966 | | Property | Description |
967 | |----------|-------------|
968 | | `current_sec` | Date/time of the current second, expressed as [Epoch](https://en.wikipedia.org/wiki/Unix_time) seconds. |
969 | | `count_sec` | The number of requests completed during the current second (used to compute req/sec). |
970 | | `peak_sec` | The requests per second during the peak (best performing) second. |
971 | | `total_reqs` | The total number of requests sent. |
972 | | `total_warnings` | The total number of warnings received. |
973 | | `total_errors` | The total number of errors received. |
974 | | `bytes_sent` | The total number of bytes sent. |
975 | | `bytes_received` | The total number of bytes received. |
976 | | `time_start` | The date/time of when the run started, expressed as [Epoch](https://en.wikipedia.org/wiki/Unix_time) seconds. |
977 | | `total` | Raw HTTP performance metrics for the total request time (see below). |
978 | | `dns` | Raw HTTP performance metrics for the DNS lookup time (see below).|
979 | | `connect` | Raw HTTP performance metrics for the TCP connect time (see below). |
980 | | `send` | Raw HTTP performance metrics for the data send time (see below). |
981 | | `wait` | Raw HTTP performance metrics for the request wait time (see below). |
982 | | `receive` | Raw HTTP performance metrics for the data receive time (see below). |
983 | | `decompress` | Raw HTTP performance metrics for the decompress time (see below). |
984 |
985 | Each of the HTTP performance objects contain the following properties:
986 |
987 | | Property | Description |
988 | |----------|-------------|
989 | | `min` | The minimum time recorded for the metric, in milliseconds. |
990 | | `max` | The maximum time recorded for the metric, in milliseconds. |
991 | | `total` | The total of all time measurements added together (used to compute the average), in milliseconds. |
992 | | `count` | The total number of samples recorded for the metric. |
993 | | `avg` | The average time for the metric (`total` divided by `count`), in milliseconds. |
994 |
995 | ### quiet
996 |
997 | To suppress all `wperf` script output (with some exceptions -- see below), include the `--quiet` argument on the command-line, or set the `quiet` JSON configuration property to `true`. Example:
998 |
999 | ```sh
1000 | wperf https://myserver.com/some/path --quiet
1001 | ```
1002 |
1003 | Example JSON configuration:
1004 |
1005 | ```json
1006 | {
1007 | "url": "https://myserver.com/some/path",
1008 | "quiet": true
1009 | }
1010 | ```
1011 |
1012 | This will cause all output to be suppressed, with these two exceptions:
1013 |
1014 | - If the script detects that it is running on an actual terminal, vs. being called from a script or pipe, then the progress bar is displayed.
1015 | - If [stats](#stats) mode is activated, this busts through quiet mode (the assumption is, if you asked for stats, you want stats).
1016 |
1017 | ### color
1018 |
1019 | By default, `wperf` outputs its reports and tables using ANSI colors in your terminal. However, if for some reason you don't want color output, you can disable it by either setting the `--color` command-line argument to `false`, or setting the `color` JSON configuration property to `false`. Example:
1020 |
1021 | ```sh
1022 | wperf https://myserver.com/some/path --color false
1023 | ```
1024 |
1025 | Example JSON configuration:
1026 |
1027 | ```json
1028 | {
1029 | "url": "https://myserver.com/some/path",
1030 | "color": false
1031 | }
1032 | ```
1033 |
1034 | ### wrapper (Advanced)
1035 |
1036 | **Note:** This is an advanced feature, which requires Node.js programming knowledge.
1037 |
1038 | `wperf` allows you to "wrap" (hook) the internal request cycle, to run your own Node.js function and perform actions before and/or after each request. Using this you can manipulate or generate your own URLs in code, and/or filter or change the HTTP responses as well. To do this, you will have to write your own Node.js script.
1039 |
1040 | The feature is activated by the `--wrapper` command-line argument, or the `wrapper` JSON configuration property. Either way, it should be set to a filesystem path pointing at your Node.js script. Example:
1041 |
1042 | ```sh
1043 | wperf https://myserver.com/some/path --wrapper /path/to/my-request-wrapper.js
1044 | ```
1045 |
1046 | Example JSON configuration:
1047 |
1048 | ```json
1049 | {
1050 | "url": "https://myserver.com/some/path",
1051 | "wrapper": "/path/to/my-request-wrapper.js"
1052 | }
1053 | ```
1054 |
1055 | Your Node.js script should export a class with the following definition:
1056 |
1057 | ```js
1058 | module.exports = class RequestWrapper {
1059 |
1060 | constructor(request, args) {
1061 | // class constructor, save request and args for later use
1062 | this.request = request;
1063 | this.args = args;
1064 | }
1065 |
1066 | get(url, opts, callback) {
1067 | // called for every `GET` request
1068 | // callback expects (err, resp, data, perf)
1069 | this.request.get( url, opts, callback );
1070 | }
1071 |
1072 | };
1073 | ```
1074 |
1075 | #### Wrapper Constructor
1076 |
1077 | Your class constructor is passed exactly two arguments:
1078 |
1079 | - A reference to the request library, which is an instance of [pixl-request](https://github.com/jhuckaby/pixl-request).
1080 | - The current configuration object (usually parsed command-line arguments, but may also include a parsed JSON configuration file).
1081 |
1082 | It is recommended that you store both of these in your class for later use.
1083 |
1084 | #### Wrapper Method Hook
1085 |
1086 | For each HTTP request, your wrapper class is invoked by calling a method named after the current HTTP method, converted to lower-case. For example, if the current configuration is set to send HTTP GET requests, then your wrapper will need to have a `get()` method defined. For HTTP POST, you will need a `post()` method, and so on. In all cases, the function is passed 3 arguments:
1087 |
1088 | - The current URL, which may have been dynamically generated using placeholder substitution variables.
1089 | - The current request options object (see [http.request](https://nodejs.org/api/http.html#http_http_request_options_callback) for all the properties you can set here).
1090 | - A callback function to fire when the request completes.
1091 |
1092 | Upon the completion of each request, success or fail, the callback function expects four arguments:
1093 |
1094 | - An Error object, if an error occurred (otherwise this should be falsey).
1095 | - The HTTP response object, which should be a [http.ServerResponse](https://nodejs.org/api/http.html#http_class_http_serverresponse) object from Node.js.
1096 | - The HTTP response body, as a Buffer object.
1097 | - The performance metrics, which should be an instance of [pixl-perf](https://github.com/jhuckaby/pixl-perf).
1098 |
1099 | # Related
1100 |
1101 | - [pixl-request](https://github.com/jhuckaby/pixl-request)
1102 | - Underlying HTTP library used to send all requests and track detailed performance metrics.
1103 | - [pixl-cli](https://github.com/jhuckaby/pixl-cli)
1104 | - Command-line library, used to generate the progress bar, time remaining and colored tables.
1105 |
1106 | # License (MIT)
1107 |
1108 | **The MIT License**
1109 |
1110 | *Copyright (c) 2019 - 2025 Joseph Huckaby.*
1111 |
1112 | Permission is hereby granted, free of charge, to any person obtaining a copy
1113 | of this software and associated documentation files (the "Software"), to deal
1114 | in the Software without restriction, including without limitation the rights
1115 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1116 | copies of the Software, and to permit persons to whom the Software is
1117 | furnished to do so, subject to the following conditions:
1118 |
1119 | The above copyright notice and this permission notice shall be included in
1120 | all copies or substantial portions of the Software.
1121 |
1122 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1123 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1124 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1125 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1126 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1127 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
1128 | THE SOFTWARE.
1129 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wperf",
3 | "version": "1.0.11",
4 | "description": "A simple HTTP load testing utility with detailed performance metrics.",
5 | "author": "Joseph Huckaby ",
6 | "homepage": "https://github.com/jhuckaby/wperf",
7 | "license": "MIT",
8 | "main": "wperf.js",
9 | "bin": "wperf.js",
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/jhuckaby/wperf"
13 | },
14 | "bugs": {
15 | "url": "https://github.com/jhuckaby/wperf/issues"
16 | },
17 | "keywords": [
18 | "http",
19 | "performance",
20 | "test"
21 | ],
22 | "dependencies": {
23 | "pixl-cli": "^1.0.0",
24 | "pixl-request": "^1.0.0"
25 | },
26 | "devDependencies": {}
27 | }
28 |
--------------------------------------------------------------------------------
/wperf.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | // Simple CLI HTTP Test Script
4 | // Requests a URL via HTTP GET continuously, up to N iterations using N threads
5 | // Usage: wperf URL --max 1000 --threads 1 --cache_dns 0 --keepalive 0 --timeout 5 --verbose 0 --warn 1.0
6 | // Copyright (c) 2016 - 2019 by Joseph Huckaby, MIT License
7 |
8 | /*
9 | Usage: wperf URL [OPTIONS...]
10 |
11 | --max 1000 Total number of requests to send.
12 | --threads 1 Number of concurrent threads to use.
13 | --keepalive 0 Use HTTP Keep-Alive sockets.
14 | --throttle 0 Limit request rate to N per sec.
15 | --timeout 5 Timeout for each request in seconds.
16 | --fatal 0 Abort on first HTTP error.
17 | --verbose 0 Print metrics for every request.
18 | --warn 1.0 Emit warnings at N seconds and longer.
19 | --cache_dns 0 Cache DNS for duration of run.
20 | --compress 1 Allow compressed responses.
21 | --follow Follow HTTP 3xx redirects.
22 | --retries 5 Retry errors N times.
23 | --auth "U:P" HTTP Basic Auth (username:password).
24 | --useragent "Foo" Custom User-Agent string.
25 | --h_X-Test "foo" Add any HTTP request headers.
26 | --method get Specify HTTP request method.
27 | --f_file1 "1.txt" Attach file to HTTP POST request.
28 | --data "foo=bar" Provide raw HTTP POST data.
29 |
30 | Hit Ctrl-Z during run to see progress reports.
31 | For more info: https://github.com/jhuckaby/wperf
32 | */
33 |
34 | // Capture stdin in raw mode for Ctrl-Z
35 | var readline = require('readline');
36 | if (process.stdin.isTTY) {
37 | readline.emitKeypressEvents(process.stdin);
38 | process.stdin.setRawMode(true);
39 | }
40 |
41 | var fs = require('fs');
42 | var Path = require('path');
43 | var querystring = require('querystring');
44 | var package = require('./package.json');
45 | var PixlRequest = require('pixl-request');
46 | var cli = require('pixl-cli');
47 | cli.global();
48 |
49 | var Tools = cli.Tools;
50 | var async = Tools.async;
51 |
52 | // Process CLI args
53 | var args = cli.args;
54 | if (!args.other || !args.other.length || args.help) {
55 | fs.readFileSync( process.argv[1], 'utf8' ).match(/([\S\s]+)<\/Help>/);
56 | print( RegExp.$1 + "\n" );
57 | process.exit(0);
58 | }
59 | var url = args.other.shift();
60 | var config_file = '';
61 |
62 | // first argument may be config file
63 | if (!url.match(/^\w+\:\/\//) && fs.existsSync(url)) {
64 | config_file = url;
65 | var config = null;
66 | try { config = JSON.parse( fs.readFileSync(url), 'utf8' ); }
67 | catch (err) {
68 | die("Failed to read configuration file: " + url + ": " + err + "\n");
69 | }
70 | if (!config.url) {
71 | die("Configuration file is missing required 'url' property: " + url + "\n");
72 | }
73 | url = args.url || config.url;
74 | for (var key in config) {
75 | if (!(key in args)) args[key] = config[key];
76 | }
77 | }
78 |
79 | if (args.params && (typeof(args.params) == 'string')) {
80 | // params may live in a separate file
81 | var params_file = args.params;
82 | try { args.params = JSON.parse( fs.readFileSync(params_file), 'utf8' ); }
83 | catch (err) {
84 | die("Failed to read parameters file: " + params_file + ": " + err + "\n");
85 | }
86 | }
87 |
88 | // support string "false" as boolean false in certain cases
89 | if (args.compress === "false") args.compress = false;
90 | if (args.color === "false") args.color = false;
91 |
92 | var max_iter = args.max || 1;
93 | var max_threads = args.threads || 1;
94 | var timeout_sec = ("timeout" in args) ? args.timeout : 5.0;
95 | var warn_sec = ("warn" in args) ? args.warn : 1.0;
96 | var warn_ms = warn_sec * 1000;
97 | var allow_compress = ("compress" in args) ? args.compress : 1;
98 | var keep_alive = args.keepalive || args.keepalives || false;
99 | var method = (args.method || 'get').toLowerCase();
100 |
101 | // optionally disable all ANSI color
102 | if (("color" in args) && !args.color) {
103 | cli.chalk.enabled = false;
104 | }
105 |
106 | print("\n");
107 | print( bold.magenta("WebPerf (wperf) v" + package.version) + "\n" );
108 | print( gray.bold("Date/Time: ") + gray((new Date()).toString() ) + "\n" );
109 |
110 | if (config_file) {
111 | print( gray.bold("Configuration: ") + gray(config_file) + "\n" );
112 | }
113 |
114 | if (args.params) print( gray.bold("Base "));
115 | print( gray.bold("URL: ") + gray(url + " (" + method.toUpperCase() + ")") + "\n" );
116 |
117 | // print( gray( bold("Method: ") + method.toUpperCase()) + "\n" );
118 | print( gray.bold("Keep-Alives: ") + gray(keep_alive ? 'Enabled' : 'Disabled') + "\n" );
119 | print( gray.bold("Threads: ") + gray(max_threads) + "\n" );
120 |
121 | // setup histogram system
122 | var histo = {};
123 |
124 | histo.cats = args.histo || ['total'];
125 | if (histo.cats === 'all') histo.cats = ['dns', 'connect', 'send', 'wait', 'receive', 'decompress', 'total'];
126 | if (typeof(histo.cats) == 'string') histo.cats = histo.cats.split(/\,\s*/);
127 |
128 | histo.counts = {};
129 | histo.cats.forEach( function(key) {
130 | histo.counts[key] = {};
131 | });
132 |
133 | histo.ranges = args.histo_ranges || [
134 | '0-1 ms',
135 | '1-2 ms',
136 | '2-3 ms',
137 | '3-4 ms',
138 | '4-5 ms',
139 | '5-10 ms',
140 | '10-20 ms',
141 | '20-30 ms',
142 | '30-40 ms',
143 | '40-50 ms',
144 | '50-100 ms',
145 | '100-200 ms',
146 | '200-300 ms',
147 | '300-400 ms',
148 | '400-500 ms',
149 | '500-1000 ms',
150 | '1-2 sec',
151 | '2-3 sec',
152 | '3-4 sec',
153 | '4-5 sec',
154 | '5+ sec'
155 | ];
156 |
157 | histo.groups = histo.ranges.map( function(label) {
158 | var low = 0, high = 0;
159 | if (label.match(/(\d+)\-(\d+)\s*(\w+)$/)) {
160 | low = parseInt( RegExp.$1 );
161 | high = parseInt( RegExp.$2 );
162 | if (RegExp.$3 == 'sec') { low *= 1000; high *= 1000; }
163 | }
164 | else if (label.match(/^(\d+)\+\s*(\w+)$/)) {
165 | low = parseInt( RegExp.$1 );
166 | high = 86400;
167 | if (RegExp.$2 == 'sec') { low *= 1000; high *= 1000; }
168 | }
169 | return { low, high, label };
170 | });
171 |
172 | // request options
173 | var opts = {
174 | timeout: timeout_sec * 1000,
175 | headers: args.headers || {}
176 | };
177 | if (args.auth) opts.auth = args.auth;
178 | if (args.multipart) opts.multipart = true;
179 |
180 | // Custom headers with h_ prefix, e.g. --h_Cookie "dtuid=1000000000000000001"
181 | for (var key in args) {
182 | if (key.match(/^h_(.+)$/)) {
183 | var header_name = RegExp.$1;
184 | opts.headers[ header_name ] = args[key];
185 | }
186 | }
187 |
188 | // Custom file uploads with f_ prefix, e.g. f_file1 "1.txt"
189 | if (args.files) {
190 | method = 'post';
191 | opts.multipart = true;
192 | opts.files = args.files;
193 | }
194 | for (var key in args) {
195 | if (key.match(/^f_(.+)$/)) {
196 | var param_name = RegExp.$1;
197 | method = 'post';
198 | opts.multipart = true;
199 | if (!opts.files) opts.files = {};
200 | opts.files[ param_name ] = args[key];
201 | }
202 | }
203 |
204 | // Custom raw HTTP POST data, or d_ prefix
205 | if (args.data) {
206 | opts.data = args.data;
207 | if (!opts.headers['Content-Type'] && !opts.headers['content-type'] && (typeof(opts.data) != 'object')) {
208 | // convert data to hash, for application/x-www-form-urlencoded mode
209 | opts.data = querystring.parse(opts.data);
210 | }
211 | }
212 | for (var key in args) {
213 | if (key.match(/^d_(.+)$/)) {
214 | var param_name = RegExp.$1;
215 | method = 'post';
216 | if (!opts.data) opts.data = {};
217 | opts.data[ param_name ] = args[key];
218 | }
219 | }
220 |
221 | // Custom param override using p_ prefix, or if a param already exists with the same name, no prefix is needed
222 | for (var key in args) {
223 | if (key.match(/^p_(.+)$/)) {
224 | var param_name = RegExp.$1;
225 | if (!args.params) args.params = {};
226 | args.params[ param_name ] = args[key];
227 | }
228 | else if (args.params && args.params[key]) {
229 | args.params[ key ] = args[key];
230 | }
231 | }
232 |
233 | // Set User-Agent string, allow full customization
234 | var request = new PixlRequest( args.useragent || ("Mozilla/5.0; wperf/" + package.version) );
235 | request.setAutoError( true );
236 |
237 | // Optionally use HTTP Keep-Alives
238 | if (keep_alive) {
239 | request.setKeepAlive( true );
240 | }
241 |
242 | // Optionally cache DNS lookups
243 | if (args.cache_dns) {
244 | request.setDNSCache( 86400 );
245 | }
246 |
247 | // Optionally disable compression support
248 | if (!allow_compress) {
249 | request.setHeader( 'Accept-Encoding', "none" );
250 | }
251 |
252 | // Optionally follow redirects
253 | if (args.follow) {
254 | request.setFollow( args.follow );
255 | }
256 |
257 | // Optionally retry errors
258 | if (args.retries) {
259 | if (args.retries === true) args.retries = 1;
260 | request.setRetries( args.retries );
261 | }
262 |
263 | if (args.insecure) {
264 | // Allow this to work with HTTPS when the SSL certificate cannot be verified
265 | // process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
266 | opts.rejectUnauthorized = false;
267 | }
268 |
269 | // Keep track of stats
270 | var num_warns = 0;
271 | var count = 0;
272 | var stats = {
273 | // Note: sharing namespace with pixl-request perf here
274 | current_sec: Tools.timeNow(true),
275 | count_sec: 0,
276 | peak_sec: 0,
277 | total_reqs: 0,
278 | total_warnings: 0,
279 | total_errors: 0,
280 | bytes_sent: 0,
281 | bytes_received: 0,
282 | time_start: Tools.timeNow()
283 | };
284 |
285 | print( "\n" );
286 |
287 | // Begin progress bar
288 | cli.progress.start({
289 | catchInt: true,
290 | catchTerm: true,
291 | catchCrash: false,
292 | exitOnSig: false
293 | });
294 |
295 | var floatCheckWarn = function(value) {
296 | // prepare float value (milliseconds) for display, using shortFloat
297 | // also highlight with bold + red if over the warning threshold
298 | var is_warning = !!(warn_ms && (value >= warn_ms));
299 | return is_warning ? bold.red(shortFloat(value) + ' ms') : (shortFloat(value) + ' ms');
300 | };
301 |
302 | var dateTimeStamp = function(epoch) {
303 | // return date/time stamp in [YYYY-MM-DD HH:MI:SS] format
304 | var dargs = Tools.getDateArgs( epoch || Tools.timeNow(true) );
305 | return "[" + dargs.yyyy_mm_dd + " " + dargs.hh_mi_ss + "] ";
306 | };
307 |
308 | var printReport = function() {
309 | // Show final stats in table
310 | if (cli.progress.running) cli.progress.erase();
311 |
312 | // general stats
313 | var now = Tools.timeNow();
314 | var elapsed = now - stats.time_start;
315 | var elapsed_disp = Tools.getTextFromSeconds(elapsed, false, false);
316 | if (elapsed < 1) elapsed_disp = Math.floor(elapsed * 1000) + " ms";
317 |
318 | if (args.verbose || num_warns) {
319 | print("\n");
320 | }
321 | print( bold.yellow("Total requests sent: ") + commify(stats.total_reqs) + "\n" );
322 | print( bold.yellow("Total time elapsed: ") + elapsed_disp + "\n" );
323 | print( bold.yellow("Total bytes sent: ") + Tools.getTextFromBytes(stats.bytes_sent) + " (" + Tools.getTextFromBytes(stats.bytes_sent / elapsed) + "/sec)\n" );
324 | print( bold.yellow("Total bytes received: ") + Tools.getTextFromBytes(stats.bytes_received) + " (" + Tools.getTextFromBytes(stats.bytes_received / elapsed) + "/sec)\n" );
325 |
326 | print( "\n" );
327 | print( bold.yellow("Average performance: ") + commify( Math.floor(stats.total_reqs / elapsed) ) + " req/sec\n" );
328 | if (stats.peak_sec && (elapsed >= 2.0)) {
329 | print( bold.yellow("Peak performance: ") + commify(stats.peak_sec) + " req/sec\n" );
330 | }
331 |
332 | print( "\n" );
333 | var err_color = stats.total_errors ? bold.red : bold.yellow;
334 | print( bold.yellow("Number of warnings: ") + commify(stats.total_warnings) + "\n" );
335 | print( err_color("Number of errors: ") + commify(stats.total_errors) + "\n" );
336 |
337 | var rows = [
338 | ["Metric", "Minimum", "Average", "Maximum", "Samples"]
339 | ];
340 | var labels = ['DNS', 'Connect', 'Send', 'Wait', 'Receive', 'Decompress', 'Total'];
341 |
342 | labels.forEach( function(label) {
343 | var key = label.toLowerCase();
344 | var is_total = (key == 'total');
345 | var color = is_total ? cyan : green;
346 | var stat = stats[key] || {};
347 | stat.avg = stat.total / (stat.count || 1);
348 |
349 | rows.push([
350 | color( bold( label ) ),
351 | color( floatCheckWarn( stat.min || 0 ) ),
352 | color( floatCheckWarn( stat.avg || 0 ) ),
353 | color( floatCheckWarn( stat.max || 0 ) ),
354 | color( commify( stat.count || 0 ) )
355 | ]);
356 | } ); // forEach
357 |
358 | print( "\n" );
359 | print( bold("Performance Metrics:") + "\n" );
360 | print( table(rows, { textStyles: ["green"] }) + "\n" );
361 |
362 | // histograms
363 | labels.forEach( function(label) {
364 | var key = label.toLowerCase();
365 | if (!(key in histo.counts)) return;
366 |
367 | var counts = histo.counts[key];
368 | var rows = [
369 | ["Range", "Count", "Visual"]
370 | ];
371 |
372 | // find highest count (for visual max)
373 | var highest = 0;
374 | for (var range in counts) {
375 | if (counts[range] > highest) highest = counts[range];
376 | }
377 |
378 | histo.ranges.forEach( function(range) {
379 | var value = counts[range] || 0;
380 | var bar = "";
381 | var width = Math.max(0, Math.min(value / highest, 1.0)) * (args.width || 40);
382 | var partial = width - Math.floor(width);
383 |
384 | bar += cli.repeat(cli.progress.defaults.filled, Math.floor(width));
385 | if (partial > 0) {
386 | bar += cli.progress.defaults.filling[ Math.floor(partial * cli.progress.defaults.filling.length) ];
387 | }
388 |
389 | rows.push([
390 | range,
391 | Tools.commify(value),
392 | bar
393 | ]);
394 | });
395 |
396 | print("\n");
397 | print( bold(label + " Time Histogram:") + "\n" );
398 | print( table(rows, {}) + "\n" );
399 | }); // histos
400 |
401 | print( "\n" );
402 |
403 | if (cli.progress.running) cli.progress.draw();
404 | };
405 |
406 | var nextIteration = function(err, callback) {
407 | // check for error, apply throttling, proceed to next iteration
408 | if (err) {
409 | stats.total_errors++;
410 |
411 | if (args.fatal) return callback(err);
412 | else {
413 | cli.progress.erase();
414 | if (args.verbose) print("\n");
415 | print( dateTimeStamp() + bold.red("ERROR: ") + err.message + "\n" );
416 | if (args.verbose && err.url) {
417 | print( dateTimeStamp() + bold.yellow("URL: ") + err.url + "\n" );
418 | }
419 | if (args.verbose && err.content) {
420 | print( dateTimeStamp() + bold.yellow("Content: ") + JSON.stringify(err.content) + "\n" );
421 | }
422 | if (args.verbose && err.headers) {
423 | print( dateTimeStamp() + bold.yellow("Headers: ") + JSON.stringify(err.headers) + "\n" );
424 | }
425 | if (args.verbose && err.perf) {
426 | var metrics = err.perf.metrics();
427 | print( dateTimeStamp() + bold.yellow("Perf: ") + JSON.stringify(metrics) + "\n" );
428 | }
429 | cli.progress.draw();
430 | }
431 | }
432 |
433 | if (args.throttle && (stats.count_sec >= args.throttle)) {
434 | // whoa there, slow down a bit
435 | var cur_sec = stats.current_sec;
436 | async.whilst(
437 | function() { return (Tools.timeNow(true) == cur_sec); },
438 | function(callback) { setTimeout( function() { callback(); }, 50 ); },
439 | callback
440 | );
441 | }
442 | else {
443 | callback();
444 | }
445 | };
446 |
447 | // Catch term, int, abort the run
448 | var emergency_abort = false;
449 |
450 | process.once('SIGINT', function() {
451 | if (cli.progress.running) cli.progress.end();
452 | emergency_abort = true;
453 | // process.stdin.end();
454 | } );
455 | process.once('SIGTERM', function() {
456 | if (cli.progress.running) cli.progress.end();
457 | emergency_abort = true;
458 | // process.stdin.end();
459 | } );
460 |
461 | // Capture Ctrl-Z to emit progress reports
462 | process.stdin.on('keypress', function (ch, key) {
463 | if (key && key.ctrl && key.name == 'z') {
464 | printReport();
465 | }
466 | if (key && key.ctrl && key.name == 'c') {
467 | emergency_abort = true;
468 | }
469 | });
470 |
471 | // Allow CLI to provide a module that wraps the request object.
472 | // The module must implement METHOD(url, opts, callback) and callback with (err, resp, data, perf)
473 | // Where METHOD is one of `get`, `post`, `head`, `put` or `delete`, depending on config.
474 | if (args.wrapper) {
475 | var reqWrapper = require( args.wrapper.match(/^\//) ? args.wrapper : Path.join(process.cwd(), args.wrapper) );
476 | request = new reqWrapper(request, args);
477 | }
478 |
479 | var req_per_sec = 0;
480 | var success_match = args.success_match ? (new RegExp(args.success_match)) : null;
481 | var error_match = args.error_match ? (new RegExp(args.error_match)) : null;
482 | var params = args.params || {};
483 |
484 | for (var key in params) {
485 | params[key] = Tools.alwaysArray( params[key] );
486 | }
487 |
488 | // Main request loop
489 | async.timesLimit( max_iter, max_threads,
490 | function(idx, callback) {
491 | var current_opts = Tools.copyHash( opts, true );
492 | var current_url = url;
493 | var ebrake = 0;
494 |
495 | // apply placeholder substitution on URL
496 | while (current_url.match(/\[(\w+|\d+\-\d+)\]/)) {
497 | current_url = current_url.replace( /\[(\d+)\-(\d+)\]/g, function(m_all, m_g1, m_g2) {
498 | var low = parseInt(m_g1);
499 | var high = parseInt(m_g2);
500 | return Math.round( low + ((high - low) * Math.random()) );
501 | });
502 | current_url = current_url.replace( /\[(\w+)\]/g, function(m_all, key) {
503 | return params[key] ? Tools.randArray(params[key]) : '';
504 | });
505 | if (++ebrake > 32) break;
506 | }
507 |
508 | // Allow URL to override headers for current request only
509 | // Example: "/ads?place=yahoo&size=160x600&chan=tst&cb=1234 [header:Cookie:dtuid=tor00355;]"
510 | current_url = current_url.replace(/\s*\[header\:\s*([\w\-]+)\:\s*([^\]]+)\]/ig, function(m_all, m_g1, m_g2) {
511 | current_opts.headers[ m_g1 ] = m_g2;
512 | return '';
513 | }).trim();
514 |
515 | // allow placeholder substitution inside header values as well
516 | for (var key in current_opts.headers) {
517 | current_opts.headers[key] = current_opts.headers[key].toString().replace( /\[(\w+)\]/g, function(m_all, key) {
518 | return params[key] ? Tools.randArray(params[key]) : '';
519 | });
520 | }
521 |
522 | // allow placeholder sub in post data, if raw string or hash
523 | if (current_opts.data) {
524 | if (typeof(current_opts.data) == 'string') {
525 | current_opts.data = current_opts.data.replace( /\[(\w+)\]/g, function(m_all, key) {
526 | return params[key] ? Tools.randArray(params[key]) : '';
527 | });
528 | }
529 | else if (Tools.isaHash(current_opts.data)) {
530 | for (var key in current_opts.data) {
531 | current_opts.data[key] = current_opts.data[key].toString().replace( /\[(\w+)\]/g, function(m_all, key) {
532 | return params[key] ? Tools.randArray(params[key]) : '';
533 | });
534 | }
535 | }
536 | }
537 |
538 | if (args.verbose) {
539 | print( dateTimeStamp() + bold.cyan("Request URL: ") + current_url + "\n" );
540 | if (current_opts.data) print( dateTimeStamp() + bold.cyan("POST Data: ") + JSON.stringify(current_opts.data) + "\n" );
541 | if (current_opts.headers && Tools.numKeys(current_opts.headers)) print( dateTimeStamp() + bold.cyan("Request Headers: ") + JSON.stringify(current_opts.headers) + "\n" );
542 | }
543 |
544 | // send HTTP request
545 | request[method]( current_url, current_opts, function(err, resp, data, perf) {
546 | if (err) err.url = current_url;
547 |
548 | // Track req/sec
549 | var now = Tools.timeNow();
550 | var now_sec = Math.floor(now);
551 | stats.count_sec++;
552 | stats.total_reqs++;
553 |
554 | if (now_sec != stats.current_sec) {
555 | stats.current_sec = now_sec;
556 | req_per_sec = stats.count_sec;
557 | if (req_per_sec > stats.peak_sec) stats.peak_sec = req_per_sec;
558 | stats.count_sec = 0;
559 | }
560 |
561 | // Update progress bar
562 | count++;
563 | cli.progress.update({
564 | amount: count / max_iter,
565 | text: ' [' + req_per_sec + " req/sec]"
566 | });
567 |
568 | if (!err && (success_match || error_match)) {
569 | var text = data.toString();
570 | if (success_match && !text.match(success_match)) {
571 | err = new Error("Response does not contain success match (" + args.success_match + ")");
572 | err.headers = resp.headers;
573 | err.content = text;
574 | }
575 | else if (error_match && text.match(error_match)) {
576 | err = new Error("Response contains error match (" + args.error_match + ")");
577 | err.headers = resp.headers;
578 | err.content = text;
579 | }
580 | }
581 |
582 | // process metrics
583 | var metrics = perf ? perf.metrics() : { perf: { total: 0 }, counters: {} };
584 | metrics.url = current_url;
585 |
586 | var is_warning = !!(warn_ms && (metrics.perf.total >= warn_ms));
587 | if (is_warning) {
588 | stats.total_warnings++;
589 | num_warns++;
590 | }
591 |
592 | if (resp && (args.verbose || is_warning)) {
593 | // In verbose mode, print every success and perf metrics
594 | cli.progress.erase();
595 | cli[is_warning ? 'warn' : 'verbose']( dateTimeStamp() + (is_warning ? bold.red("Perf Warning: ") : '') + 'Req #' + count + ": HTTP " + resp.statusCode + " " + resp.statusMessage + " -- " + JSON.stringify(metrics) + "\n" );
596 | cli.progress.draw();
597 | }
598 |
599 | if (resp && is_warning && args.warnings) {
600 | var warn_data = Tools.mergeHashes( metrics, {
601 | code: resp.statusCode,
602 | status: resp.statusMessage,
603 | req_num: count,
604 | now: now,
605 | date_time: dateTimeStamp().trim()
606 | });
607 | fs.appendFileSync( args.warnings, JSON.stringify(warn_data) + "\n" );
608 | }
609 | if (resp && args.log) {
610 | var log_data = Tools.mergeHashes( metrics, {
611 | code: resp.statusCode,
612 | status: resp.statusMessage,
613 | req_num: count,
614 | now: now,
615 | date_time: dateTimeStamp().trim()
616 | });
617 | fs.appendFileSync( args.log, JSON.stringify(log_data) + "\n" );
618 | }
619 |
620 | // Compute min/avg/max stats
621 | for (var key in metrics.perf) {
622 | var value = metrics.perf[key];
623 |
624 | if (!stats[key]) stats[key] = {};
625 | var stat = stats[key];
626 |
627 | if (!("min" in stat) || (value < stat.min)) stat.min = value;
628 | if (!("max" in stat) || (value > stat.max)) stat.max = value;
629 | if (!("total" in stat)) stat.total = 0;
630 | if (!("count" in stat)) stat.count = 0;
631 |
632 | stat.total += value;
633 | stat.count++;
634 | }
635 |
636 | // Increment total counters
637 | for (var key in metrics.counters) {
638 | if (!stats[key]) stats[key] = 0;
639 | stats[key] += metrics.counters[key];
640 | }
641 |
642 | // Compute historgram data
643 | histo.cats.forEach( function(cat) {
644 | var value = metrics.perf[cat] || 0;
645 | var group = null;
646 |
647 | for (var idx = 0, len = histo.groups.length; idx < len; idx++) {
648 | group = histo.groups[idx];
649 | if ((value >= group.low) && (value < group.high)) {
650 | if (!histo.counts[cat][group.label]) histo.counts[cat][group.label] = 0;
651 | histo.counts[cat][group.label]++;
652 | idx = len;
653 | }
654 | }
655 | }); // histo
656 |
657 | if (emergency_abort) {
658 | // User hit Ctrl-C or someone TERM'ed us
659 | return callback( new Error("User Abort") );
660 | }
661 | else if (err) {
662 | // Core error such as DNS failure, socket connect timeout, custom error, etc.
663 | err.perf = perf;
664 | return nextIteration(err, callback);
665 | }
666 | else {
667 | // URL request was a success
668 | if (args.verbose) {
669 | print( dateTimeStamp() + bold.green("Response Status: ") + resp.statusCode + " " + resp.statusMessage + "\n" );
670 | print( dateTimeStamp() + bold.green("Response Content: ") + JSON.stringify(data.toString()) + "\n" );
671 | print( dateTimeStamp() + bold.green("Response Headers: ") + JSON.stringify(resp.headers) + "\n" );
672 | }
673 |
674 | nextIteration(null, callback);
675 | }
676 | } );
677 | },
678 | function(err) {
679 | // All requests complete
680 | cli.progress.end();
681 |
682 | if (err) {
683 | if (args.verbose || num_warns) {
684 | print("\n");
685 | }
686 | print( dateTimeStamp() + bold.red("ERROR: ") + err.message + "\n" );
687 | if (args.verbose && err.url) {
688 | print( dateTimeStamp() + bold.yellow("URL: ") + err.url + "\n" );
689 | }
690 | if (args.verbose && err.content) {
691 | print( dateTimeStamp() + bold.yellow("Content: ") + JSON.stringify(err.content) + "\n" );
692 | }
693 | if (args.verbose && err.headers) {
694 | print( dateTimeStamp() + bold.yellow("Headers: ") + JSON.stringify(err.headers) + "\n" );
695 | }
696 | if (args.verbose && err.perf) {
697 | var metrics = err.perf.metrics();
698 | print( dateTimeStamp() + bold.yellow("Perf: ") + JSON.stringify(metrics) + "\n" );
699 | }
700 | print( dateTimeStamp() + "Stopped test prematurely: " + count + " of " + max_iter + " requests completed.\n" );
701 | num_warns++;
702 | }
703 |
704 | printReport();
705 |
706 | if (args.stats) {
707 | // emit final stats as JSON, bust through `quiet` mode too
708 | if (args.stats === true) process.stdout.write( JSON.stringify(stats) + "\n" );
709 | else fs.appendFileSync( args.stats, JSON.stringify(stats) + "\n" );
710 | }
711 |
712 | // process.stdin.end();
713 | process.exit(0);
714 |
715 | } // complete
716 | );
717 |
--------------------------------------------------------------------------------