Repository: mcollina/autocannon (JavaScript)

Issue:
Avg latency is wrong when using pipelining
This was apparently addressed in a very old issue #15 but I am still seeing something wrong with latency when pipelining is used, especially in AVG. This is unless I'm interpreting the number wrong. I assume latency is the time a request takes to complete, as described in the docs.

Here's my web app:

```
const http = require("http");

http
  .createServer(function (req, res) {
    setTimeout(function () {
      res.end("hello world");
    }, 500);
  })
  .listen(3000);
```

And here's autocannon's output without and then with pipelining:

```
# without pipelining

autocannon http://localhost:3000
Running 10s test @ http://localhost:3000
10 connections

┌─────────┬────────┬────────┬────────┬────────┬───────────┬─────────┬────────┐
│ Stat    │ 2.5%   │ 50%    │ 97.5%  │ 99%    │ Avg       │ Stdev   │ Max    │
├─────────┼────────┼────────┼────────┼────────┼───────────┼─────────┼────────┤
│ Latency │ 499 ms │ 513 ms │ 522 ms │ 523 ms │ 510.27 ms │ 6.47 ms │ 523 ms │
└─────────┴────────┴────────┴────────┴────────┴───────────┴─────────┴────────┘
```

```
# with pipelining

autocannon http://localhost:3000 -p 2
Running 10s test @ http://localhost:3000
10 connections with 2 pipelining factor

┌─────────┬──────┬──────┬────────┬────────┬───────────┬───────────┬────────┐
│ Stat    │ 2.5% │ 50%  │ 97.5%  │ 99%    │ Avg       │ Stdev     │ Max    │
├─────────┼──────┼──────┼────────┼────────┼───────────┼───────────┼────────┤
│ Latency │ 0 ms │ 0 ms │ 527 ms │ 528 ms │ 256.62 ms │ 256.65 ms │ 530 ms │
└─────────┴──────┴──────┴────────┴────────┴───────────┴───────────┴────────┘
```

As you can see, the AVG latency when using pipelining seems to be divided by the pipelining factor, which doesn't sound right to me, in the sense that the server is still responding in ~500ms, so the average latency should always be around that value. Possibly as a consequence of that, Stdev also becomes wrong.



Discussion:
Good spot, PR? I _never_ look at the avg latency, just at 99% percentile.



Patch:
diff --git a/lib/httpClient.js b/lib/httpClient.js
index 0250c77c..2f6e04e0 100644
--- a/lib/httpClient.js
+++ b/lib/httpClient.js
@@ -112,10 +112,14 @@ function Client (opts) {
 
     this.emit('response', resp.headers.statusCode, resp.bytes, resp.duration, this.rate)
 
+    // Separating rip ensures that after getting response for first request, we fire
+    // first one again because second is already running right now (with pipelining=2).
+    // See detailed explaination at https://github.com/mcollina/autocannon/pull/317#discussion_r543678831
+    const rpi = (this.cer + pipelining) % pipelining
     resp.body = ''
     resp.bytes = 0
-    this.cer = this.cer === pipelining - 1 ? 0 : this.cer++
-    this._doRequest(this.cer)
+    this.cer = (this.cer + 1) % pipelining
+    this._doRequest(rpi)
   }
 
   this._connect()


Test patch:
diff --git a/test/helper.js b/test/helper.js
index c6913506..37b7b541 100644
--- a/test/helper.js
+++ b/test/helper.js
@@ -36,7 +36,13 @@ function startServer (opts) {
     }
 
     res.statusCode = statusCode
-    res.end(typeof body === 'function' ? body(req) : body)
+    const reply = () => res.end(typeof body === 'function' ? body(req) : body)
+
+    if (opts.delayResponse) {
+      setTimeout(reply, opts.delayResponse)
+    } else {
+      reply()
+    }
   }
 
   server.unref()
diff --git a/test/httpClient.test.js b/test/httpClient.test.js
index 4bd6bb20..2c8923e4 100644
--- a/test/httpClient.test.js
+++ b/test/httpClient.test.js
@@ -63,6 +63,25 @@ test('client calls a https server twice', (t) => {
   })
 })
 
+test('client calculates correct duration when using pipelining', (t) => {
+  t.plan(4)
+
+  const lazyServer = helper.startServer({ delayResponse: 500 })
+  const opts = lazyServer.address()
+  opts.pipelining = 2
+  const client = new Client(opts)
+  let count = 0
+
+  client.on('response', (statusCode, length, duration) => {
+    t.equal(statusCode, 200, 'status code matches')
+    t.ok(duration > 500 && duration < 600)
+
+    if (++count === 2) {
+      client.destroy()
+    }
+  })
+})
+
 test('client calls a tls server without SNI servername twice', (t) => {
   t.plan(4)
 
@@ -891,17 +910,17 @@ test('client invokes appropriate onResponse when using pipelining', (t) => {
         t.deepEqual(responses, ['POST'])
         break
       case 2:
-        // 3rd was sent as 1st is finished, receiving 2st
+        // 3rd was sent as 1st is finished, receiving 2nd
         t.same(client.getRequestBuffer().toString(), makeResponseFromBody({ server, method: 'PUT' }), 'current should be third request')
         t.deepEqual(responses, ['POST', 'GET'])
         break
       case 3:
-        // 1st was resent, receiving 3st
+        // 1st was resent, receiving 3rd
         t.same(client.getRequestBuffer().toString(), makeResponseFromBody({ server, method: 'POST' }), 'current should be first request')
         t.deepEqual(responses, ['POST', 'GET', 'PUT'])
         break
       case 4:
-        // 2st was resent, receiving 1st
+        // 2nd was resent, receiving 1st
         t.same(client.getRequestBuffer().toString(), makeResponseFromBody({ server, method: 'GET' }), 'current should be second request')
         t.deepEqual(responses, ['POST', 'GET', 'PUT', 'POST'])
         client.destroy()