performance benchmark of async-design-patterns - recursive-callbacks vs. promises vs. async/await
Michael J. Ryan
tracker1 at gmail.com
Sun Apr 29 20:24:58 UTC 2018
Nice... And not really surprising. I am slightly surprised async/await is
so close to promises. Which means that improving promises performance
should probably be a priority. I still feel the easier to reason with code
is well worth it, given many apps now scale horizontally.
On Sun, Apr 29, 2018, 10:31 kai zhu <kaizhu256 at gmail.com> wrote:
> fyi, here are some benchmark results of nodejs' client-based http-request
> throughput, employing various async-design-patterns (on a 4gb linode box).
> overall, recursive-callbacks seem to ~15% faster than both async/await and
> promises (~3000 vs ~2600 client-http-request/s).
>
> ```shell
> $ REQUESTS_PER_TICK=10 node example.js
>
> state 1 - node (v9.11.1)
> state 2 - http-server listening on port 3000
> ...
> state 3 - clientHttpRequestWithRecursiveCallback - flooding http-server
> with request "http://localhost:3000"
> state 5 - clientHttpRequestWithRecursiveCallback - testRun #99
> state 5 - clientHttpRequestWithRecursiveCallback - requestsTotal = 14690
> (in 5009 ms)
> state 5 - clientHttpRequestWithRecursiveCallback - requestsPassed = 7349
> state 5 - clientHttpRequestWithRecursiveCallback - requestsFailed = 7341 ({
> "statusCode - 500": true
> })
> state 5 - clientHttpRequestWithRecursiveCallback - 2933 requests / second
> state 5 - mean requests / second = {
> "clientHttpRequestWithRecursiveCallback": "3059 (156 sigma)",
> "clientHttpRequestWithPromise": "2615 (106 sigma)",
> "clientHttpRequestWithAsyncAwait": "2591 (71 sigma)"
> }
> ```
>
>
> you can reproduce the benchmark-results by running this
> zero-dependency/zero-config, standalone nodejs script below:
>
>
> ```js
> /*
> * example.js
> *
> * this zero-dependency example will benchmark nodejs' client-based
> http-requests throughput,
> * using recursive-callback/promise/async-await design-patterns.
> *
> * the program will make 100 test-runs (randomly picking a design-pattern
> per test-run),
> * measuring client-based http-requests/seconde over a 5000 ms interval.
> * it will save the 16 most recent test-runs for each design-pattern,
> * and print the mean and standard deviation.
> * any test-run with unusual errors (timeouts, econnreset, etc),
> * will be discarded and not used in calculations
> *
> * the script accepts one env variable $REQUESTS_PER_TICK, which defaults
> to 10
> * (you can try increasing it if you have a high-performance machine)
> *
> *
> *
> * example usage:
> * $ REQUESTS_PER_TICK=10 node example.js
> *
> * example output:
> *
> * state 1 - node (v9.11.1)
> * state 2 - http-server listening on port 3000
> * ...
> * state 3 - clientHttpRequestWithRecursiveCallback - flooding http-server
> with request "http://localhost:3000"
> * state 5 - clientHttpRequestWithRecursiveCallback - testRun #99
> * state 5 - clientHttpRequestWithRecursiveCallback - requestsTotal =
> 14690 (in 5009 ms)
> * state 5 - clientHttpRequestWithRecursiveCallback - requestsPassed = 7349
> * state 5 - clientHttpRequestWithRecursiveCallback - requestsFailed =
> 7341 ({
> * "statusCode - 500": true
> * })
> * state 5 - clientHttpRequestWithRecursiveCallback - 2933 requests /
> second
> * state 5 - mean requests / second = {
> * "clientHttpRequestWithRecursiveCallback": "3059 (156 sigma)",
> * "clientHttpRequestWithPromise": "2615 (106 sigma)",
> * "clientHttpRequestWithAsyncAwait": "2591 (71 sigma)"
> * }
> *
> * state 6 - process.exit(0)
> */
>
> /*jslint
> bitwise: true,
> browser: true,
> maxerr: 4,
> maxlen: 100,
> node: true,
> nomen: true,
> regexp: true,
> stupid: true
> */
>
> (function () {
> 'use strict';
> var local;
> local = {};
>
> // require modules
> local.http = require('http');
> local.url = require('url');
>
> /* jslint-ignore-begin */
> local.clientHttpRequestWithAsyncAwait = async function (url, onError) {
> /*
> * this function will make an http-request using async/await
> design-pattern
> */
> var request, response, timerTimeout;
> try {
> response = await new Promise(function (resolve, reject) {
> // init timeout
> timerTimeout = setTimeout(function () {
> reject(new Error('timeout - 2000 ms'));
> }, 2000);
> request = local.http.request(local.url.parse(url),
> resolve);
> request.on('error', reject);
> request.end();
> });
> await new Promise(function (resolve, reject) {
> // ignore stream-data
> response.on('data', local.nop);
> if (response.statusCode >= 400) {
> reject(new Error('statusCode - ' +
> response.statusCode));
> return;
> }
> response.on('end', resolve);
> response.on('error', reject);
> });
> } catch (error) {
> // cleanup timerTimeout
> clearTimeout(timerTimeout);
> // cleanup request and response
> if (request) {
> request.destroy();
> }
> if (response) {
> response.destroy();
> }
> onError(error);
> return;
> }
> onError();
> };
> /* jslint-ignore-end */
>
> local.clientHttpRequestWithPromise = function (url, onError) {
> /*
> * this function will make an http-request using promise design-pattern
> */
> var request, response, timerTimeout;
> new Promise(function (resolve, reject) {
> // init timeout
> timerTimeout = setTimeout(function () {
> reject(new Error('timeout - 2000 ms'));
> }, 2000);
> request = local.http.request(local.url.parse(url), resolve);
> request.on('error', reject);
> request.end();
> }).then(function (result) {
> return new Promise(function (resolve, reject) {
> response = result;
> // ignore stream-data
> response.on('data', local.nop);
> if (response.statusCode >= 400) {
> reject(new Error('statusCode - ' +
> response.statusCode));
> return;
> }
> response.on('end', resolve);
> response.on('error', reject);
> });
> }).then(onError).catch(function (error) {
> // cleanup timerTimeout
> clearTimeout(timerTimeout);
> // cleanup request and response
> if (request) {
> request.destroy();
> }
> if (response) {
> response.destroy();
> }
> onError(error);
> });
> };
>
> local.clientHttpRequestWithRecursiveCallback = function (url, onError)
> {
> /*
> * this function will make an http-request using recursive-callback
> design-pattern
> */
> var isDone, modeNext, request, response, onNext, timerTimeout;
> onNext = function (error) {
> modeNext += error instanceof Error
> ? Infinity
> : 1;
> switch (modeNext) {
> case 1:
> // init timeout
> timerTimeout = setTimeout(function () {
> onNext(new Error('timeout - 2000 ms'));
> }, 2000);
> request = local.http.request(local.url.parse(url), onNext);
> request.on('error', onNext);
> request.end();
> break;
> case 2:
> response = error;
> // ignore stream-data
> response.on('data', local.nop);
> if (response.statusCode >= 400) {
> onNext(new Error('statusCode - ' +
> response.statusCode));
> }
> response.on('end', onNext);
> response.on('error', onNext);
> break;
> default:
> if (isDone) {
> return;
> }
> // cleanup timerTimeout
> clearTimeout(timerTimeout);
> // cleanup request and response
> if (request) {
> request.destroy();
> }
> if (response) {
> response.destroy();
> }
> isDone = true;
> onError(error);
> }
> };
> modeNext = 0;
> onNext();
> };
>
> local.clientHttpRequestOnError = function (error) {
> /*
> * this function is the callback for clientHttpRequest
> */
> if (error) {
> local.errorDict[error.message] = true;
> local.requestsFailed += 1;
> } else {
> local.requestsPassed += 1;
> }
> if (local.timeElapsed >= 5000 &&
> (local.requestsFailed + local.requestsPassed) ===
> local.requestsTotal) {
> local.main();
> }
> };
>
> local.nop = function () {
> /*
> * this function will do nothing
> */
> return;
> };
>
> local.templateRenderAndPrint = function (template) {
> /*
> * this function render simple double-mustache templates with the
> local dict,
> * and print to stderr
> */
> console.error(template.replace((/\{\{.*?\}\}/g), function (match0)
> {
> return local[match0.slice(2, -2)];
> }));
> };
>
> local.main = function (error) {
> /*
> * this function will fun the main-loop
> */
> local.state += error
> ? Infinity
> : 1;
> switch (local.state) {
> case 1:
> // init local var
> local.clientHttpRequestUrl = 'http://localhost:3000';
> local.version = process.version;
> local.templateRenderAndPrint('state {{state}} - node
> ({{version}})');
> // create simple http-server that responds with random 200 or
> 500 statusCode
> local.http.createServer(function (request, response) {
> request
> // ignore stream-data
> .on('data', local.nop)
> .on('error', console.error);
> // respond randomly with either 200 or 500 statusCode
> response.statusCode = Math.random() < 0.5
> ? 200
> : 500;
> response
> .on('error', console.error)
> .end();
> // listen on port 3000
> }).listen(3000, local.main);
> break;
> case 2:
> local.templateRenderAndPrint('state {{state}} - http-server
> listening on port 3000');
> local.main();
> break;
> case 3:
> local.clientHttpRequestState = local.clientHttpRequestState ||
> 0;
> local.clientHttpRequestState += 1;
> if (local.clientHttpRequestState < 100) {
> switch (Math.floor(Math.random() * 3)) {
> case 0:
> local.clientHttpRequest =
> 'clientHttpRequestWithAsyncAwait';
> break;
> case 1:
> local.clientHttpRequest =
> 'clientHttpRequestWithPromise';
> break;
> case 2:
> local.clientHttpRequest =
> 'clientHttpRequestWithRecursiveCallback';
> break;
> }
> } else {
> local.state += 2;
> local.main();
> return;
> }
> local.templateRenderAndPrint('\nstate {{state}} -
> {{clientHttpRequest}} - ' +
> 'flooding http-server with request
> "{{clientHttpRequestUrl}}"');
> local.errorDict = {};
> local.requestsFailed = 0;
> local.requestsPassed = 0;
> local.requestsTotal = 0;
> local.timeElapsed = 0;
> local.timeStart = Date.now();
> local.main();
> break;
> case 4:
> setTimeout(function () {
> for (local.ii = 0;
> // configurable REQUESTS_PER_TICK
> local.ii < (Number(process.env.REQUESTS_PER_TICK)
> || 10);
> local.ii += 1) {
> local.requestsTotal += 1;
> local[local.clientHttpRequest](
> local.clientHttpRequestUrl,
> local.clientHttpRequestOnError
> );
> }
> // recurse / repeat this step for 5000 ms
> local.timeElapsed = Date.now() - local.timeStart;
> if (local.timeElapsed < 5000) {
> local.state -= 1;
> local.main();
> }
> });
> break;
> case 5:
> local.timeElapsed = Date.now() - local.timeStart;
> local.requestsPerSecond = Math.round(1000 *
> local.requestsTotal / local.timeElapsed);
> local.errorDictJson = JSON.stringify(local.errorDict, null, 4);
> local.resultList = local.resultList || {};
> local.resultMean = local.resultMean || {};
> // only save result if no unusual errors occurred
> if (Object.keys(local.errorDict).length <= 1) {
> local.resultList[local.clientHttpRequest] =
> local.resultList[local.clientHttpRequest] || [];
>
> local.resultList[local.clientHttpRequest].push(local.requestsPerSecond);
> // remove old data
> if (local.resultList[local.clientHttpRequest].length > 16)
> {
> local.resultList[local.clientHttpRequest].shift();
> }
> // calculate mean
> local.resultMean[local.clientHttpRequest] = Math.round(
>
> local.resultList[local.clientHttpRequest].reduce(function (aa, bb) {
> return aa + (bb || 0);
> }, 0) /
> local.resultList[local.clientHttpRequest].length
> );
> // calculate sigma
> local.resultMean[local.clientHttpRequest] += ' (' +
> Math.round(Math.sqrt(
>
> local.resultList[local.clientHttpRequest].reduce(function (aa, bb) {
> return aa + Math.pow(
> (bb || 0) -
> local.resultMean[local.clientHttpRequest],
> 2
> );
> }, 0) /
> (local.resultList[local.clientHttpRequest].length - 1)
> )) + ' sigma)';
> }
> local.resultJson = JSON.stringify(local.resultMean, null, 4);
> local.templateRenderAndPrint(
> /* jslint-ignore-begin */
> '\
> state {{state}} - {{clientHttpRequest}} - testRun
> #{{clientHttpRequestState}}\n\
> state {{state}} - {{clientHttpRequest}} - requestsTotal =
> {{requestsTotal}} (in {{timeElapsed}} ms)\n\
> state {{state}} - {{clientHttpRequest}} - requestsPassed =
> {{requestsPassed}}\n\
> state {{state}} - {{clientHttpRequest}} - requestsFailed =
> {{requestsFailed}} ({{errorDictJson}})\n\
> state {{state}} - {{clientHttpRequest}} - {{requestsPerSecond}} requests /
> second\n\
> state {{state}} - mean requests / second = {{resultJson}}\n\
> ',
> /* jslint-ignore-end */
> );
> // repeat test with other design-patterns
> local.state -= 3;
> local.main();
> break;
> default:
> if (error) {
> console.error(error);
> }
> local.exitCode = Number(!!error);
> local.templateRenderAndPrint('state {{state}} -
> process.exit({{exitCode}})');
> process.exit(local.exitCode);
> }
> };
> // run main-loop
> local.state = 0;
> local.main();
> }());
> ```
>
> kai zhu
> kaizhu256 at gmail.com
>
>
>
> _______________________________________________
> es-discuss mailing list
> es-discuss at mozilla.org
> https://mail.mozilla.org/listinfo/es-discuss
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20180429/26ebbbdd/attachment-0001.html>
More information about the es-discuss
mailing list