performance benchmark of async-design-patterns - recursive-callbacks vs. promises vs. async/await
kai zhu
kaizhu256 at gmail.com
Sun Apr 29 17:31:13 UTC 2018
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
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20180430/7f519ae4/attachment-0001.html>
More information about the es-discuss
mailing list