11

Having a set of async operations on db to do, I'm wondering what's the difference performance-wise of doing a "blocking" await loop versus a Promise.all.

let insert = (id,value) => {
    return new Promise(function (resolve, reject) {
        connnection.query(`insert into items (id,value) VALUES (${id},"${value}")`, function (err, result) {
            if (err) return reject(err)
                return resolve(result);
        });
    });
};

Promise.all solution (it needs a for loop to builds the array of promises..)

let inserts = [];
for (let i = 0; i < SIZE; i++) inserts.push(insert(i,"..string.."))
Promise.all(inserts).then(values => { 
    console.log("promise all ends");
});

await loop solution

let inserts = [];
(async function loop() {
    for (let i = 0; i < SIZE; i++) {
        await insert(i, "..string..")
    }
    console.log("await loop ends");
})

Edit: thanks for the anwsers, but I would dig into this a little more. await is not really blocking, we all know that, it's blocking in its own code block. An await loop sequentially fire requests, so if in the middle 1 requests takes longer, the other ones waits for it. Well this is similar to Promise.all: if a 1 req takes longer, the callback is not executed until ALL the responses are returned.

alfredopacino
  • 2,979
  • 9
  • 42
  • 68

3 Answers3

27

Your example of using Promise.all will create all promises first before waiting for them to resolve. This means that your requests will fire concurrently and the callback given to Promise.all(...).then(thisCallback) will only fire if all requests were successful.

Note: promise returned from Promise.all will reject as soon as one of the promises in the given array rejects.

const SIZE = 5;
const insert = i => new Promise(resolve => {
  console.log(`started inserting ${i}`);
  setTimeout(() => {
    console.log(`inserted ${i}`);
    resolve();
  }, 300);
});

// your code
let inserts = [];
for (let i = 0; i < SIZE; i++) inserts.push(insert(i, "..string.."))
Promise.all(inserts).then(values => {
  console.log("promise all ends");
});

// requests are made concurrently

// output
// started inserting 0
// started inserting 1
// started inserting 2
// ...
// started inserting 4
// inserted 0
// inserted 1
// ...
// promise all ends

Note: It might be cleaner to use .map instead of a loop for this scenario:

Promise.all(
  Array.from(Array(SIZE)).map((_, i) => insert(i,"..string.."))
).then(values => { 
    console.log("promise all ends");
});

Your example of using await on the other hand, waits for each promise to resolve before continuing and firing of the next one:

const SIZE = 5;
const insert = i => new Promise(resolve => {
  console.log(`started inserting ${i}`);
  setTimeout(() => {
    console.log(`inserted ${i}`);
    resolve();
  }, 300);
});

let inserts = [];
(async function loop() {
  for (let i = 0; i < SIZE; i++) {
    await insert(i, "..string..")
  }
  console.log("await loop ends");
})()

// no request is made until the previous one is finished

// output
// started inserting 0
// inserted 0
// started inserting 1
// ...
// started inserting 4
// inserted 4
// await loop ends

The implications for performance in the above cases are directly correlated to their different behavior.

If "efficient" for your use case means to finish up the requests as soon as possible, then the first example wins because the requests will be happening around the same time, independently, whereas in the second example they will happen in a serial fashion.

In terms of complexity, the time complexity for your first example is equal to O(longestRequestTime) because the requests will happen essentially in parallel and thus the request taking the longest will drive the worst-case scenario.

On the other hand, the await example has O(sumOfAllRequestTimes) because no matter how long individual requests take, each one has to wait for the previous one to finish and thus the total time will always include all of them.

To put things in numbers, ignoring all other potential delays due to the environment and application in which the code is ran, for 1000 requests, each taking 1s, the Promise.all example would still take ~1s while the await example would take ~1000s.

Maybe a picture would help:

time comparison chart

Note: Promise.all won't actually run the requests exactly in parallel and the performance in general will greatly depend on the exact environment in which the code is running and the state of it (for instance the event loop) but this is a good approximation.

nem035
  • 34,790
  • 6
  • 87
  • 99
  • 1
    thanks for the explanation. It confirm what I knew. But you can't indeed think Promise.all runs concurrently, that's why I was in doubt about any real differences between those 2 solutions..actually I'm still in doubt – alfredopacino Dec 16 '18 at 13:37
  • please refer to my edit – alfredopacino Dec 16 '18 at 13:53
  • Happy to help. `Promise.all` does run concurrently. The requests are running almost in parallel. I read your edit and I'm not sure where you're in doubt. A long request is the max run-time of `Promise.all` while it's just a portion of run time of the `await` loop, thus `await` loop is inherently slower. In both examples, all requests are happening, they are just happening more efficiently using the `Promise.all` example. – nem035 Dec 16 '18 at 17:38
6

The major difference between the two approaches is that

  1. The await version issues server requests sequentially in the loop. If one of them errors without being caught, no more requests are issued. If request errors are trapped using try/catch blocks, you can identify which request failed and perhaps code in some some form of recovery or even retry the operation.

  2. The Promise.all version will make server requests in or near parallel fashion, limited by browser restrictions on the maximum number of concurrent requests permitted. If one of the requests fails the Promise.all returned promise fails immediately. If any requests were successful and returned data, you lose the data returned. In addition if any request fails, no outstanding requests are cancelled - they were initiated in user code (the insert function) when creating the array of promises.

As mentioned in another answer, await is non blocking and returns to the event loop until its operand promise is settled. Both the Promise.all and await while looping versions allow responding to other events while requests are in progress.

traktor
  • 17,588
  • 4
  • 32
  • 53
  • 1
    What do you mean by "If any requests were successful and returned data, you lose the data returned"? You mean the requests that were successful **after** the first failed one? – nem035 Dec 16 '18 at 06:52
  • If _any_ promise provided to 'Promise.all' rejects, the promise returned by the call to `Promise.all' is rejected with the rejection reason of the first one to do so. Hence you could lose data for promises succeeding both before **and** after rection of an input promise. – traktor Dec 16 '18 at 20:29
  • Sure thing, just wanted to make sure that's what you meant. Your intended meaning was clearer to me after a second reading :) – nem035 Dec 16 '18 at 20:31
2

Each has different advantages, it's up to us which one we need to solve our problem.

await loop

for(let i = 0;i < SIZE; i++){
    await promiseCall();
}

It will call all promises in parallel if any promise rejected it won't have any effect on other promises.

In ES2018 it has simplified for certain situation like if you want to call the second iteration only if the first iteration got finished, refer the following ex.

async function printFiles () {
  const files = await getFilePaths()

  for await (const file of fs.readFile(file, 'utf8')) {
    console.log(contents)
  }
}

Promise.all()

var p1 = Promise.resolve(32);
var p2 = 123;
var p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("foo");
  }, 100);
});
Promise.all([p1, p2, p3]).then(values => { 
  console.log(values); // [32, 123, "foo"]
});

This will execute every promise sequentially and finally return combined revolved values array.

If any one of these promise get rejected it will return value of that rejected promise only. follow following ex,

var p1 = Promise.resolve(32);
var p2 = Promise.resolve(123);
var p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("foo");
  }, 100);
});
Promise.all([p1, p2, p3]).then(values => { 
  console.log(values); // 123
});
Ashish Mukarne
  • 813
  • 8
  • 12