1

Trying to determine how many even numbers in a large list. Following the naive approach: Iterating over the list and increment the count if an even number is found. Example code:

const list = [34, 1, 35, 3, 4, 8]; //This list may become really big. taking more than 3 seconds often

let evenCount = 0;

for (const elem of list) {
  if (elem % 2 === 0) {
    evenCount++;
  }
}
console.log(evenCount);

I understand this will block the event loop the whole time it executes. Tried calculating inside promise i.e.

const determineEvenCount = async list => {
  return new Promise((resolve, reject) => {
    let evenCount = 0;
    for (const elem of list) {
      if (elem % 2 === 0) {
        evenCount++;
      }
    }
    resolve(evenCount);
  });
};

Will the event loop still be blocked? If it is, how to make it unblocked?

Md. Arafat Al Mahmud
  • 3,124
  • 5
  • 35
  • 66
  • 1
    Yes, it will block. You could use webWorkers, but for something so trivial, I've a feeling the IPC messaging overhead would be just as long. For 3 seconds though, how big is this array, are you sure it's this that takes that long? – Keith Apr 17 '21 at 06:40
  • You could execute it by chunks and await for a simple task (e.g `await new Promise(res => setImmediate(res));`) every *x* elements or *ms*. – Kaiido Apr 17 '21 at 07:31

1 Answers1

0

Yes, it will block. Promise is not some kind of magic dust; sprinkling it over blocking code doesn't suddenly make it non-blocking.

In your code new Promise is actually redundant: as you already declared your function as async, it already returns a promise all the time. And yes, as your original Promise Executor is synchronous (you iterate over your list with plain for ... of), the rest of your code has to wait till the commands' queue there is exhausted:

const smallerList = Object.keys([...Array(1E4)].map(Number));
const largerList = Object.keys([...Array(1E5)].map(Number));

const determineEvenCount = async (list) => {
  console.time('Inside Loop: ' + list.length);
  let evenCount = 0;
  for (let elem of list) {
    if (elem % 2 === 0) {
      evenCount++;
    }
  }
  console.timeEnd('Inside Loop: ' + list.length);
  console.log(evenCount);
  return evenCount;
};

console.time('Timer execution');
setTimeout(() => {
  console.timeEnd('Timer execution');
}, 5);
Promise.resolve().then(() => { 
  console.log('Microtask execution');
});
console.time('Waiting for sync');
determineEvenCount(largerList);
determineEvenCount(smallerList);
console.timeEnd('Waiting for sync');

As you can see, lists were iterated in sequence (larger list before smaller, even though the latter clearly took less time), and both timers and promises were fired afterwards. Totally blocking.

Still, there's a really simple way to make this function less blocking:

const smallerList = Object.keys([...Array(1E4)].map(Number));
const largerList = Object.keys([...Array(1E5)].map(Number));

const determineEvenCount = async (list) => {
  console.time('Inside Loop: ' + list.length);
  let evenCount = 0;
  for await (let elem of list) {
    if (elem % 2 === 0) {
      evenCount++;
    }
  }
  console.timeEnd('Inside Loop: ' + list.length);
  console.log(evenCount);
  return evenCount;
};

console.time('Timer execution');
setTimeout(() => {
  console.timeEnd('Timer execution');
}, 5);
Promise.resolve('Microtask execution A').then(console.log);
console.time('Waiting for async');
determineEvenCount(largerList);
determineEvenCount(smallerList);
console.timeEnd('Waiting for async');
Promise.resolve('Microtask execution B').then(console.log);

... with results looking like this:

Waiting for async: 0.075ms
Microtask execution A
Microtask execution B
Inside Loop: 18.555ms
5000
50000
Timer execution: 46.400ms

As you can see, not only the wrapping timer executed instantly, but also both Promises have resolved before your array was processed. All's rosy, right?

Nope. We stayed in the same loop (kudos for @Kaiido for pointing that part out), which means not only the timers were blocked for the whole set, but also no other tasks (I/O processing in particular) were able to execute. Yet the processing time increased significantly, as fetching of each separate element of that list was delayed.

That's why what you most likely should look to is chunking the processing first, and using setImmediate for delaying each chunk. For example (using setTimeout here to emulate setImmediate, just to show the idea):

const smallerList = Object.keys([...Array(1E4)].map(Number));
const largerList = Object.keys([...Array(1E5)].map(Number));

const setImmediate = (fn) => {
  setTimeout(fn, 0);
};

const determineEvenCount = async (list) => {
  console.time('Inside Loop: ' + list.length);
  return new Promise((resolve) => {
    let evenCount = 0;

    function counter(elem) {
      if (elem % 2 === 0) {
        evenCount++;
      }
    }

    const CHUNK_SIZE = 100;
    ! function processChunk(start, end) {
      const boundary = Math.min(end, list.length);
      let i = start;
      while (i < boundary) {
        counter(list[i++]);
      }
      if (i === list.length) {
        console.timeEnd('Inside Loop: ' + list.length);
        console.log(evenCount);
        return resolve(evenCount);
      }
      setImmediate(() => processChunk(i, i + CHUNK_SIZE));
    }(0, CHUNK_SIZE);

  });
  console.timeEnd('Inside Loop: ' + list.length);
};

console.time('Timer execution');
setTimeout(() => {
  console.timeEnd('Timer execution');
}, 5);
Promise.resolve('Microtask execution A').then(console.log);
console.time('Waiting for async');
determineEvenCount(largerList);
determineEvenCount(smallerList);
console.timeEnd('Waiting for async');
Promise.resolve('Microtask execution B').then(console.log);

... if you don't want to go into workers territory and just chunk out this processing out of the main loop (which usually is the best way to handle this).

Finally, some food for thought; this article has a lot of helpful links inside.

raina77ow
  • 103,633
  • 15
  • 192
  • 229
  • Your "non blockable" version is still very blocking. It will just delay the blocking to the next microtask, but it will still block the event loop just the same way. Wrap all the code between `console.time("nextTask");` and `setTimeout(() => { console.timeEnd( "nextTask" ); } );`. Our next task won't get called before this function is done. – Kaiido Apr 17 '21 at 07:19
  • @Kaiido You're right; only microtasks do not get blocked here. Updated the answer to point that out, added chunk-based non-blocking implementation. – raina77ow Apr 17 '21 at 08:18