From 7aedda9dcdd01d6172a187a57e8ad74fda71d4b0 Mon Sep 17 00:00:00 2001
From: Andrey Pechkurov <apechkurov@gmail.com>
Date: Wed, 23 Dec 2020 16:52:44 +0300
Subject: [PATCH] benchmark: add simple https benchmark

Adds a simple benchmark for https server based on the http simple
benchmark. Updates benchmarker integration for autocannon and wrk,
so that they support https scheme.

Also adds a new HTTPS section and improves HTTP/2 section in
the benchmark guide.

PR-URL: https://github.com/nodejs/node/pull/36612
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
---
 benchmark/_http-benchmarkers.js              | 14 ++--
 benchmark/_test-double-benchmarker.js        | 15 +++-
 benchmark/fixtures/simple-https-server.js    | 72 ++++++++++++++++++++
 benchmark/https/simple.js                    | 29 ++++++++
 doc/guides/writing-and-running-benchmarks.md | 11 ++-
 5 files changed, 132 insertions(+), 9 deletions(-)
 create mode 100644 benchmark/fixtures/simple-https-server.js
 create mode 100644 benchmark/https/simple.js

diff --git a/benchmark/_http-benchmarkers.js b/benchmark/_http-benchmarkers.js
index d0f192e75948b6..f25bcd319882f6 100644
--- a/benchmark/_http-benchmarkers.js
+++ b/benchmark/_http-benchmarkers.js
@@ -29,7 +29,8 @@ class AutocannonBenchmarker {
     for (const field in options.headers) {
       args.push('-H', `${field}=${options.headers[field]}`);
     }
-    args.push(`http://127.0.0.1:${options.port}${options.path}`);
+    const scheme = options.scheme || 'http';
+    args.push(`${scheme}://127.0.0.1:${options.port}${options.path}`);
     const child = child_process.spawn(this.executable, args);
     return child;
   }
@@ -60,11 +61,12 @@ class WrkBenchmarker {
     const duration = typeof options.duration === 'number' ?
       Math.max(options.duration, 1) :
       options.duration;
+    const scheme = options.scheme || 'http';
     const args = [
       '-d', duration,
       '-c', options.connections,
       '-t', Math.min(options.connections, require('os').cpus().length || 8),
-      `http://127.0.0.1:${options.port}${options.path}`,
+      `${scheme}://127.0.0.1:${options.port}${options.path}`,
     ];
     for (const field in options.headers) {
       args.push('-H', `${field}: ${options.headers[field]}`);
@@ -90,8 +92,8 @@ class WrkBenchmarker {
  */
 class TestDoubleBenchmarker {
   constructor(type) {
-    // `type` is the type of benchmarker. Possible values are 'http' and
-    // 'http2'.
+    // `type` is the type of benchmarker. Possible values are 'http', 'https',
+    // and 'http2'.
     this.name = `test-double-${type}`;
     this.executable = path.resolve(__dirname, '_test-double-benchmarker.js');
     this.present = fs.existsSync(this.executable);
@@ -101,8 +103,9 @@ class TestDoubleBenchmarker {
   create(options) {
     process.env.duration = process.env.duration || options.duration || 5;
 
+    const scheme = options.scheme || 'http';
     const env = {
-      test_url: `http://127.0.0.1:${options.port}${options.path}`,
+      test_url: `${scheme}://127.0.0.1:${options.port}${options.path}`,
       ...process.env
     };
 
@@ -179,6 +182,7 @@ const http_benchmarkers = [
   new WrkBenchmarker(),
   new AutocannonBenchmarker(),
   new TestDoubleBenchmarker('http'),
+  new TestDoubleBenchmarker('https'),
   new TestDoubleBenchmarker('http2'),
   new H2LoadBenchmarker(),
 ];
diff --git a/benchmark/_test-double-benchmarker.js b/benchmark/_test-double-benchmarker.js
index 60264dfd46a606..89843d4616cc50 100644
--- a/benchmark/_test-double-benchmarker.js
+++ b/benchmark/_test-double-benchmarker.js
@@ -1,10 +1,15 @@
 'use strict';
 
 const myModule = process.argv[2];
-if (!['http', 'http2'].includes(myModule)) {
+if (!['http', 'https', 'http2'].includes(myModule)) {
   throw new Error(`Invalid module for benchmark test double: ${myModule}`);
 }
 
+let options;
+if (myModule === 'https') {
+  options = { rejectUnauthorized: false };
+}
+
 const http = require(myModule);
 
 const duration = +process.env.duration;
@@ -33,8 +38,12 @@ function request(res, client) {
 }
 
 function run() {
-  if (http.get) { // HTTP
-    http.get(url, request);
+  if (http.get) { // HTTP or HTTPS
+    if (options) {
+      http.get(url, options, request);
+    } else {
+      http.get(url, request);
+    }
   } else { // HTTP/2
     const client = http.connect(url);
     client.on('error', (e) => { throw e; });
diff --git a/benchmark/fixtures/simple-https-server.js b/benchmark/fixtures/simple-https-server.js
new file mode 100644
index 00000000000000..d3b07a110113d7
--- /dev/null
+++ b/benchmark/fixtures/simple-https-server.js
@@ -0,0 +1,72 @@
+'use strict';
+
+const fixtures = require('../../test/common/fixtures');
+const https = require('https');
+
+const options = {
+  key: fixtures.readKey('rsa_private.pem'),
+  cert: fixtures.readKey('rsa_cert.crt')
+};
+
+const storedBytes = Object.create(null);
+const storedBuffer = Object.create(null);
+
+module.exports = https.createServer(options, (req, res) => {
+  // URL format: /<type>/<length>/<chunks>/chunkedEnc
+  const params = req.url.split('/');
+  const command = params[1];
+  let body = '';
+  const arg = params[2];
+  const n_chunks = parseInt(params[3], 10);
+  const chunkedEnc = params.length >= 5 && params[4] === '0' ? false : true;
+  let status = 200;
+
+  let n, i;
+  if (command === 'bytes') {
+    n = ~~arg;
+    if (n <= 0)
+      throw new Error('bytes called with n <= 0');
+    if (storedBytes[n] === undefined) {
+      storedBytes[n] = 'C'.repeat(n);
+    }
+    body = storedBytes[n];
+  } else if (command === 'buffer') {
+    n = ~~arg;
+    if (n <= 0)
+      throw new Error('buffer called with n <= 0');
+    if (storedBuffer[n] === undefined) {
+      storedBuffer[n] = Buffer.allocUnsafe(n);
+      for (i = 0; i < n; i++) {
+        storedBuffer[n][i] = 'C'.charCodeAt(0);
+      }
+    }
+    body = storedBuffer[n];
+  } else {
+    status = 404;
+    body = 'not found\n';
+  }
+
+  // example: https://localhost:port/bytes/512/4
+  // sends a 512 byte body in 4 chunks of 128 bytes
+  const len = body.length;
+  if (chunkedEnc) {
+    res.writeHead(status, {
+      'Content-Type': 'text/plain',
+      'Transfer-Encoding': 'chunked'
+    });
+  } else {
+    res.writeHead(status, {
+      'Content-Type': 'text/plain',
+      'Content-Length': len.toString()
+    });
+  }
+  // send body in chunks
+  if (n_chunks > 1) {
+    const step = Math.floor(len / n_chunks) || 1;
+    for (i = 0, n = (n_chunks - 1); i < n; ++i)
+      res.write(body.slice(i * step, i * step + step));
+    res.end(body.slice((n_chunks - 1) * step));
+  } else {
+    res.end(body);
+  }
+});
diff --git a/benchmark/https/simple.js b/benchmark/https/simple.js
new file mode 100644
index 00000000000000..243546c346f484
--- /dev/null
+++ b/benchmark/https/simple.js
@@ -0,0 +1,29 @@
+'use strict';
+const common = require('../common.js');
+
+const bench = common.createBenchmark(main, {
+  type: ['bytes', 'buffer'],
+  len: [4, 1024, 102400],
+  chunks: [1, 4],
+  c: [50, 500],
+  chunkedEnc: [1, 0],
+  benchmarker: ['test-double-https'],
+  duration: 5
+});
+
+function main({ type, len, chunks, c, chunkedEnc, duration }) {
+  const server = require('../fixtures/simple-https-server.js')
+  .listen(common.PORT)
+  .on('listening', () => {
+    const path = `/${type}/${len}/${chunks}/${chunkedEnc}`;
+
+    bench.http({
+      path,
+      connections: c,
+      scheme: 'https',
+      duration
+    }, () => {
+      server.close();
+    });
+  });
+}
diff --git a/doc/guides/writing-and-running-benchmarks.md b/doc/guides/writing-and-running-benchmarks.md
index b6b22d75c17e2c..ef93714899e534 100644
--- a/doc/guides/writing-and-running-benchmarks.md
+++ b/doc/guides/writing-and-running-benchmarks.md
@@ -4,6 +4,8 @@
 
 * [Prerequisites](#prerequisites)
   * [HTTP Benchmark Requirements](#http-benchmark-requirements)
+  * [HTTPS Benchmark Requirements](#https-benchmark-requirements)
+  * [HTTP/2 Benchmark Requirements](#http2-benchmark-requirements)
   * [Benchmark Analysis Requirements](#benchmark-analysis-requirements)
 * [Running benchmarks](#running-benchmarks)
   * [Running individual benchmarks](#running-individual-benchmarks)
@@ -43,13 +45,20 @@ benchmarker to be used should be specified by providing it as an argument:
 
 `node benchmark/http/simple.js benchmarker=autocannon`
 
+#### HTTPS Benchmark Requirements
+
+To run the `https` benchmarks, one of `autocannon` or `wrk` benchmarkers must
+be used.
+
+`node benchmark/https/simple.js benchmarker=autocannon`
+
 #### HTTP/2 Benchmark Requirements
 
 To run the `http2` benchmarks, the `h2load` benchmarker must be used. The
 `h2load` tool is a component of the `nghttp2` project and may be installed
 from [nghttp2.org][] or built from source.
 
-`node benchmark/http2/simple.js benchmarker=autocannon`
+`node benchmark/http2/simple.js benchmarker=h2load`
 
 ### Benchmark Analysis Requirements