I have developed [Aigle](https://github.com/suguru03/aigle)
which is a fast Promise library. It is inspired by[Bluebir](https://github.com/petkaantonov/bluebird)d
. The library is not only a benchmark exercise but a production-ready library that implements the Promise A+ standard, and does so faster than Bluebird
.
Before explaining it, I would like to give basic information about Promises. A Promise can have three states: pending
, fulfilled
and rejected
. Once the state goes to another state from pending
, the state cannot change again.
Besides, Promises must always resolve asynchronously, this is one of the most important things.
new Promise(resolve => resolve(1)) // synchronously.then(num => {// called asynchronously});
When I develop libraries, I always follow these three principles.
Following this principle helps avoiding unnecessary memory allocations. For example,
function sum(array) {return array.reduce(function iterator(result, num) {return result + num;});}
sum([1, 2, 3]); // 6
When sum
is called, the iterator
function is always created. It is one of unnecessary memory allocations. The code is rewritten to follow next example.
function iterator(result, num) {return result + num;}
function sum(array) {return array.reduce(iterator);}
sum([1, 2, 3]); // 6
The code avoids making unnecessary functions.
Next is another extra example of memory allocation.
function get(type) {const array = [];const object = {};const number = 0;const string = '';switch (type) {case 'array':return array;case 'object':return object;case 'number':return number;case 'string':return string;}}get('string');
In this case, string
is the only required variable. array
, object
and number
are unnecessary. The code is rewritten as below,
function get(type) {switch (type) {case 'array':return [];case 'object':return {};case 'number':return 0;case 'string':return '';}}get('string');
There is not a big difference between the examples, but if instances or functions are created in the function, it would make big difference.
For example, when creating APIs, it is necessary to check the request parameters.
function api(req, res) {const { id } = req.body;if (**!**isNumber(id)) {return res.sendStatus(400);}innerFunc(id).then(...).catch(...)}
function isNumber(id) {return typeof id === 'number';}
function innerFunc(id) {if (**!**isNumber(id)) {return Promise.reject(new Error('error'));}...}
When implementing APIs, it is preferable to check the arguments in inner functions because the function might be called from other functions. However, when implementing libraries, the function doesn’t need to check the arguments. Before executing inner functions, the arguments are already checked, so it isn’t necessary to check them again in inner functions.
I would like to explain this concept with Bluebird
examples.
Bluebird fast?
If you open Bluebird
library code, you will see bitField
parameters. But the bitField
is not so important. Bluebird
has roughly two states: pending
or not. The big difference in handling is between the two states.
new Bluebird(function executor(resolve) {// called synchronouslysetTimeout(function timer() {resolve(1); // called asynchronously}, 10);}).then(function onFulfilled(value) {// called asynchronously});
This execution order is,
Bluebird
when new Bluebird
is calledexecutor
then
.then
makes a child instance of Bluebird
resolve
onFulfilled
When then
is called, a child instance is created. At that time, resolve
is not called yet, so the parent’s state is pending
. When the state is pending
, the child instance is linked to the parent instance as a child. After that, [resolve](https://github.com/petkaantonov/bluebird/blob/v3.4.7/src/promise.js#L513)
is called asynchronously by setTimeout
. And then, [onFulfilled](https://github.com/petkaantonov/bluebird/blob/v3.4.7/src/promise.js#L703-L710)
is called with the result.When state is pending
, a child instance is linked to parent instance. After resolve
is called, onFulfilled
is called. It is very simple.
new Bluebird(function executor(resolve) {resolve(); // called synchronously}).then(function onFulfilled(value) {// ensured asynchronously});
The execution order is
Bluebird
when new Bluebird
is calledexecutor
resolve
then
. then
makes a child instance of Bluebird
onFulfilled
When then
is called, the parent’s state is already not pending
. In this case, if onFulfilled
is called without anything, the function is executed synchronously. For that reason, the library needs to call an asynchronous function. Before calling the function, onFulfilled
is set to a [queue](https://github.com/petkaantonov/bluebird/blob/v3.4.7/src/async.js#L88)
and this [schedule](https://github.com/petkaantonov/bluebird/blob/v3.4.7/src/async.js#L159)
function is called. The schedule
is setImmediate
on Node.js. And then [setImmediate](https://github.com/petkaantonov/bluebird/blob/v3.4.7/src/async.js#L144)
calls all queued functions asynchronously.When a state is not pending
, onFulfilled
is set to a queue
, and then queued functions are executed by asynchronous functions.
You might be wondering why onFulfilled
is set to a queue
. This is the smartest idea in Bluebird
.
I would like to explain it with this example.
Bluebird.resolve(1) // synchronously.then(num => console.log(num)); // asynchronouslyBluebird.resolve(2) // synchronously.then(num => console.log(num)); // asynchronouslyBluebird.resolve(3) // synchronously.then(num => console.log(num)); // asynchronously
If a queue
is not used, an asynchronous function is called three times. But if a queue
is used, the function executes all queued functions at once.I’m not sure if the bitField
gives a big benefit or not. But I think why Bluebird
is fast is because of simplicity.
I have just followed these important principles,
If you follow them, you will be able to make good libraries. I made a benchmark to check performance between Aigle
and Bluebird
. The benchmark result is here.
Aigle vs Bluebird
Aigle
has very simple implementation, therefore it is faster than Bluebird
. If you are interested in Aigle
, I would like you to contribute to it.
The most important part isaigle-core
dependency. In Bluebird
, every promise instance has to be an instance of Bluebird
. When the instance is checked, instanceof
function is called. But Bluebird
only checks if the instance is made by the current **Bluebird**
class or not. So if many versions are used, Bluebird
will be slowed down.The key to avoiding losing performance is to have same dependency. Aigle
has aigle-core
dependency, therefore every Aigle
instance is extended by same AigleCore
class. Aigle
will keep high performance.
I would like to show the benchmark example.
$ npm [email protected]├─┬ [email protected] <- [email protected] has [email protected]│ └── [email protected]├─┬ [email protected]│ └── [email protected]├── [email protected]├── [email protected]├── [email protected]└─┬ [email protected]├── [email protected] <- [email protected] has [email protected] too└── [email protected]
As you can see, different aigle
dependencies have same aigle-core
sub-dependencies. When [email protected]
is used, the aigle-core
is shared.
Node v6.9.1
Aigle
v0.4.0, v0.5.0
Bluebird
v3.4.6, v3.5.0
$ node --expose_gc . -t then======================================[Aigle] v0.5.0[Bluebird] v3.5.0======================================[promise:then:same] Preparing...--------------------------------------[promise:then:same] Executing...[1] "aigle" 180μs[1.00][1.00][2] "bluebird" 341μs[0.526][1.90]======================================[promise:then:diff] Preparing...--------------------------------------[promise:then:diff] Executing...[1] "aigle" 178μs[1.00][1.00][2] "bluebird" 506μs[0.352][2.84]
promise:then:same
was using same versions, and promise:then:diff
used child instances of different versions. Aigle
never slows down even if the versions are mixed.
If you follow the three important principles, you will make a fast library. Also Aigle
has many functions inspired by [Async](https://github.com/caolan/async)
and [Neo-Async](https://github.com/suguru03/neo-async)
. If you still use callback style, I would like to encourage you using Aigle
. If I contribute to Node.js
and JavaScript
communities, I would be happy as a contributor.
[Aigle](https://github.com/suguru03/aigle)
[Bluebird](https://github.com/petkaantonov/bluebird)