Before you go, check out these stories!

0
Hackernoon logoHow to make the fastest Promise library by@suguru.motegi

How to make the fastest Promise library

Author profile picture

@suguru.motegiSuguru Motegi

I have developed Aigle which is a fast Promise library. It is inspired byBluebird. 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.

What are Promises?

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.

Promise states (quote: MDN)

Besides, Promises must always resolve asynchronously, this is one of the most important things.

new Promise(resolve => resolve(1)) // synchronously
.then(num => {
// called asynchronously
});

Best practices for high-performance libraries

When I develop libraries, I always follow these three principles.

  • Avoid creating unnecessary variables, functions and instances
  • Avoid executing unnecessary functions
  • Deal with asynchronous function smartly

Avoid creating unnecessary variables, functions and instances

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.

Avoid executing unnecessary functions

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.

Deal with asynchronous function smartly

I would like to explain this concept with Bluebird examples.

What makes 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.

If the state is pending:

new Bluebird(function executor(resolve) { 
// called synchronously
setTimeout(function timer() {
resolve(1); // called asynchronously
}, 10);
})
.then(function onFulfilled(value) {
// called asynchronously
});

This execution order is,

  1. Make a parent instance of Bluebird when new Bluebird is called
  2. Execute executor
  3. Execute then.then makes a child instance of Bluebird
  4. Execute resolve
  5. Execute 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 is called asynchronously by setTimeout. And then, onFulfilled 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.

If state is not pending:

new Bluebird(function executor(resolve) {
resolve(); // called synchronously
})
.then(function onFulfilled(value) {
// ensured asynchronously
});

The execution order is

  1. Make a parent instance of Bluebird when new Bluebird is called
  2. Execute executor
  3. Execute resolve
  4. Execute then. then makes a child instance of Bluebird
  5. Execute 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 and this schedule function is called. The schedule is setImmediate on Node.js. And then setImmediate 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.

How to handle the asynchronous function smartly

I would like to explain it with this example.

Bluebird.resolve(1) // synchronously
.then(num => console.log(num)); // asynchronously
Bluebird.resolve(2) // synchronously
.then(num => console.log(num)); // asynchronously
Bluebird.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.

What makes Aigle fast?

I have just followed these important principles,

  • Avoid creating unnecessary variables, functions and instances
  • Avoid executing unnecessary functions
  • Deal with asynchronous function smartly

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.

  • Node v6.9.1
  • Aigle v0.5.0
  • Bluebird v3.5.0
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.

Intended for production environment

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 list
aigle-benchmark@0.0.0
├─┬ aigle@0.5.0 <- aigle@0.5.0 has aigle-core@0.2.0
│ └── aigle-core@0.2.0
├─┬ benchmark@2.1.3
│ └── platform@1.3.3
├── bluebird@3.5.0
├── lodash@4.17.4
├── minimist@1.2.0
└─┬ promise-libraries@0.3.0
├── aigle@0.4.0 <- aigle@0.4.0 has aigle-core@0.2.0 too
└── bluebird@3.4.6

As you can see, different aigle dependencies have same aigle-core sub-dependencies. When aigle@0.4.0 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.

Conclusion

If you follow the three important principles, you will make a fast library. 
Also Aigle has many functions inspired by Async and 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.

Reference

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.