Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Slowdown due to non-parallel awaiting of promises in async generators

Tags:

I'm writing code using generators and Bluebird and I have the following:

var async = Promise.coroutine; function Client(request){     this.request = request; }   Client.prototype.fetchCommentData = async(function* (user){     var country = yield countryService.countryFor(user.ip);     var data = yield api.getCommentDataFor(user.id);     var notBanned = yield authServer.authenticate(user.id);     if (!notBanned) throw new AuthenticationError(user.id);     return {         country: country,         comments: data,         notBanned: true     }; }); 

However, this is kind of slow, I feel like my application is waiting too much for I/O and it's not in parallel. How can I improve the performance of my application?

The total response time is 800 for countryFor + 400 for getCommentDataFor + 600 for authenticate so in total 1800ms which is a lot.

like image 618
Benjamin Gruenbaum Avatar asked Jun 12 '14 20:06

Benjamin Gruenbaum


People also ask

What happens if you await a non promise?

This rule applies when the await operator is used on a non-Promise value. await operator pauses the execution of the current async function until the operand Promise is resolved. When the Promise is resolved, the execution is resumed and the resolved value is used as the result of the await .

Does async await run in parallel?

In order to run multiple async/await calls in parallel, all we need to do is add the calls to an array, and then pass that array as an argument to Promise. all() .

How is async await related to promises?

async and awaitInside an async function, you can use the await keyword before a call to a function that returns a promise. This makes the code wait at that point until the promise is settled, at which point the fulfilled value of the promise is treated as a return value, or the rejected value is thrown.

Why is async await better than promises?

Promises are one of the best additions to JavaScript because they make handling async code so much easier. Going from callbacks to promises feels like a massive upgrade, but there is something even better than promises and that is async/await.


2 Answers

You are spending too much time waiting for I/O from different sources.

In normal promise code, you'd use Promise.all for this, however - people have a tendency to write code that waits for requests with generators. Your code does the following:

<-client     service-> countryFor..            ''--..               ''--..                  ''--.. country server sends response                ..--''           ..--''      ..--'' getCommentDataFor      ''--..            ''--..                ''--..                      ''--.. comment service returns response                 ..--''           ..--''       ..--'' authenticate        ''--..             ''--..                   ''--.. authentication service returns              ..--''        ..--''  ..--''  Generator done. 

Instead, it should be doing:

<-client     service-> countryFor.. commentsFor..''--.. authenticate..''--..''--..                  ''--..''--..''--.. country server sends response                         ''--..--''..  comment service returns response                    ..--''..--''..     authentication service returns response           ..--''..--''..  ..--''..--''..--''  ..--''..--''  ..--''  Generator done 

Simply put, all your I/O should be done in parallel here.

To fix this, I'd use Promise.props. Promise.props takes an objects and waits for all its properties to resolve (if they are promises).

Remember - generators and promises mix and match really well, you simply yield promises:

Client.prototype.fetchCommentData = async(function* (user){     var country = countryService.countryFor(user.ip);     var data = api.getCommentDataFor(user.id);     var notBanned = authServer.authenticate(user.id).then(function(val){           if(!val) throw new AuthenticationError(user.id);     });     return Promise.props({ // wait for all promises to resolve         country : country,         comments : data,         notBanned: notBanned     }); }); 

This is a very common mistake people make when using generators for the first time.

ascii art shamelessly taken from Q-Connection by Kris Kowal

like image 61
Benjamin Gruenbaum Avatar answered Sep 20 '22 15:09

Benjamin Gruenbaum


As it is mentioned in the Bluebird docs for Promise.coroutine, you need to watch out not to yield in a series.

var county = yield countryService.countryFor(user.ip); var data = yield api.getCommentDataFor(user.id); var notBanned = yield authServer.authenticate(user.id); 

This code has 3 yield expressions, each of them stopping execution until the particular promise is settled. The code will create and execute each of the async tasks consecutively.

To wait for multiple tasks in parallel, you should yield an array of promises. This will wait until all of them are settled, and then return an array of result values. Using ES6 destructuring assignments leads to concise code for that:

Client.prototype.fetchCommentData = async(function* (user){     var [county, data, notBanned] = yield [ //             a single yield only: ^^^^^         countryService.countryFor(user.ip),         api.getCommentDataFor(user.id),         authServer.authenticate(user.id)     ];     if (!notBanned)         throw new AuthenticationError(user.id);     return {         country: country,         comments: data,         notBanned: true     }; }); 
like image 42
Bergi Avatar answered Sep 20 '22 15:09

Bergi