UltrasoundJelly UltrasoundJelly - 4 months ago 47
Node.js Question

DOM redraw being blocked by childProcess.spawnSync

I have an electron/node.js app that I'm having trouble with. Specifically, I need to give the user some feedback on the progress of a long loop of synchronous spawns by updating an element in the DOM. However, what is actually displayed does not change during the loop. Once the entire loop is complete, the element does update with the final

max
index value.

function dosomething(index,callback) {
childProcess.spawnSync('app1',... //takes 1 second
childProcess.spawnSync('app2',... //takes 6 seconds, depends on app1 running 1st
console.log('working on: ' + index);
callback(index);
}

function dosomethingelse(index) {
$('#somediv').html(index); //update progress bar
console.log('displaying: ' + $('#somediv').html());
}

for(var i=0; i<max; i++){ //max is usually 10-100, loop takes 1 to 10 minutes
dosomething(i,dosomethingelse);
}


When I dump the progress html to the console, it does increment with index as a callback:

CONSOLE:
working on: 0
displaying: 0
working on: 1
displaying: 1
working on: 2
displaying: 2
...


Also, I have tried to force redraw the div by running the following code to no avail:

function dosomethingelse(index) {
$('#somediv').html(index); //update progress bar
console.log($('#somediv').html());
//REDRAW
document.getElementById("somediv").innerHTML =num;
document.getElementById("somediv").style.display = 'none';
document.getElementById("somediv").offsetHeight;
document.getElementById("somediv").style.display = 'block';
}


From what I have read, using spawnSync makes my binary app run in blocking mode on nodejs. This is directly against the nonblocking core of node, but is absolutely necessary in my situation since my command line call runs for 6 seconds and takes nearly 100% CPU. If I instead use the standard async spawn I end up with 50x 100% processes running simultaneously, finishing after a few minutes around the same time. Again, no progress feedback provided to the user. I don't understand why my callback is not being completed. If I switch around the functions so that
dosomethingelse
is called first with a
dosomething
callback, I still get no DOM updates.

Two other things I have tried: npm sleep:

dosomething(i);
var sleep = require('sleep');
sleep.usleep(100);
dosomethingelse(i);


And deasync:

var deasync = require('deasync');
deasync(dosomething(i));
dosomethingelse(i);


Same result. Also, if I take out my long running
dosomething
function and replace it with a
sleep.sleep(3)
I get the same result. Node is just going from one blocking task to the next without updating the UI.

Answer

You seem to be only worried about too many async processes running at the same time. But in fact, you can control how many run at the same time.

For example like so:

function runTask(tasks, index, callback) {
  if (index >= tasks.length) {
    callback();
  }
  tasks[index](() => runTask(tasks, index + 1, callback));
}

function queue(tasks, callback) {
  runTask(tasks, 0, callback);
}

with this you'd have a simple way to queue your async spawns up:

const spawn = require('child_process').spawn;

function customSpawn(command, args) {
  return callback => {
    const child = spawn(command, args);
    child.on('close', callback);
  }
} 

queue([customSpawn('app1', []), customSpawn('app2', [])], dosomethingelse);

I never tested any of the above code and therefore I can not guarantee for its correctness.

Also, have a look at promises and generators if you want to get rid of all those callbacks.

For example with promises it might look like this:

function queue(tasks) {
  let index = 0;
  const runTask = arg => {
    if (index >= tasks.length) {
      return Promise.resolve(arg);
    }
    return new Promise((resolve, reject) => {
      tasks[index++](arg).then(arg => resolve(runTask(arg))).catch(reject);
    });
  };
  return runTask();
}

const spawn = require('child_process').spawn;

function customSpawn(command, args) {
  return () => new Promise((resolve, reject) => {
    const child = spawn(command, args);
    child.on('close', code => {
      if (code === 0) {
        resolve();
      } else {
        reject();
      }
    });
  });
}

queue([customSpawn('app1', []), customSpawn('app2', [])])
  .then(dosomethingelse)
  .catch(err => console.error(err));

Try it!

A note regarding the for loop: Build the queue with what you want to do up first using closures as demonstrated in customSpawn and then pass it to queue.