1

Currently, I have a function which is called to do a large task using many setTimeout's in order to not block page interaction.

Example code follows

function runAsync(maxMs, throttle, func){
  var begin = window.performance.now();
  var cont = func(throttle);
  var end = window.performance.now();
  if (cont){
    var newThrottle = Math.ceil((throttle/(end-begin))*maxMs);
    setTimeout(function(){
      runAsync(maxMs, newThrottle, func);
    }, 0);
  }
}

a = 0;

function makeHuge(addNum){
  var target = 1000000000;
  for (var x = 0; x < Math.min(addNum, target-a); x++){
    a ++;
  }
  $('#result').text(a);
  return a < target;
}

$('#run').on('click', function(){
  a = 0;
  runAsync(16, 100, makeHuge);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<span id="result"></span><br/>
<button id="run">Run</button>

As you can see above, setTimeout is used to do a long-running task without blocking user interaction. The called function, func, must return a boolean which says whether or not to continue doing the action. The throttle value is used to keep the execution time of func below maxMs, which, if set to 16, represents something like 60fps.

However, using this setup, I cannot figure out how to incorporate the new Promises to let me know when the runAsync task is finished, or if it failed.

user24984
  • 185
  • 1
  • 12
  • It should probably be noted that isn't truly async at all, it's just deferred until the thread is free, and then it blocks. – adeneo Mar 11 '16 at 16:25
  • 1
    You are probably looking for something like web Workers http://www.w3schools.com/html/html5_webworkers.asp – Lukas Goßmann Mar 11 '16 at 16:28
  • @adeneo You are absolutely right. I just meant to use the word "async" in the function title to try to say that it is trying to be as async as possible in browser Javascript. – user24984 Mar 11 '16 at 16:29
  • Well, no, going about this route isn't *completely* without merits. It's not async in the same sense, but at the very least, it will allow you to perform a complex operation without web workers, while still allowing frequent interactions with the UI. Web workers would probably be the best solution, but I haven't looked into how reliably consistent they are cross-browser. Also be aware that some libraries may have resolved this issue. – Katana314 Mar 11 '16 at 16:32
  • @Katana314 The problem is that I can't rely on HTML5, since I might be supporting old browsers – user24984 Mar 11 '16 at 16:35
  • @user24984 Okay, then you're going about it pretty well. But, you will need to structure any function like `makeHuge` differently. You have to have it only do a very small "unit" of work, and save a function state for its next run. I could imagine having it receive an object from which it sets `x` to `1`, then next run sees x is 1 and increments to 2. You could probably also run it multiple times until you see that a certain amount of time has passed. – Katana314 Mar 11 '16 at 17:00

1 Answers1

1

I'll give two solutions, as there's a trade-off between speed and being idiomatic in your case.

Best performing solution

setTimeout has terrible error-handling characteristics, in that errors always end in web console, so use try/catch around everything to pass errors to reject manually. I also had to refactor a bit:

function runAsync(maxMs, throttle, func){
  return new Promise((resolve, reject) => {
    setTimeout(function again(){
      try {
        var begin = window.performance.now();
        if (!func(throttle)) return resolve();
        var end = window.performance.now();
        throttle = Math.ceil((throttle/(end-begin))*maxMs);
        setTimeout(again, 0);
      } catch (e) {
        reject(e);
      }
    }, 0);
  });
}

a = 0;

function makeHuge(addNum){
  var target = 1000000000;
  for (var x = 0; x < Math.min(addNum, target-a); x++){
    a ++;
  }
  result.innerHTML = a;
  return a < target;
}

run.onclick = function(){
  a = 0;
  runAsync(16, 100, makeHuge)
  .then(() => result.innerHTML = "Done!")
  .catch(e => console.error(e));
};
<span id="result"></span><br/>
<button id="run">Run</button>

Quite ugly, with a Promise constructor and try/catch muck, but there's a clean alternative.

Most idiomatic solution

With a promise-returning wait helper to tame setTimeout, use promises throughout:

var wait = ms => new Promise(resolve => setTimeout(resolve, ms));

function runAsync(maxMs, throttle, func){
  var begin = window.performance.now();
  var cont = func(throttle);
  var end = window.performance.now();
  return (!cont)? Promise.resolve() : wait(0).then(() => {
    var newThrottle = Math.ceil((throttle/(end-begin))*maxMs);
    return runAsync(maxMs, newThrottle, func);
  });
}

a = 0;

function makeHuge(addNum){
  var target = 1000000000;
  for (var x = 0; x < Math.min(addNum, target-a); x++){
    a ++;
  }
  result.innerHTML = a;
  return a < target;
}

run.onclick = function(){
  a = 0;
  runAsync(16, 100, makeHuge)
  .then(() => result.innerHTML = "Done!")
  .catch(e => console.error(e));
};
<span id="result"></span><br/>
<button id="run">Run</button>

Easy to read, and structured like your original code. Normally, this would be the clear winner.

However, be aware that there's a potential performance problem here if this is to run for a long time or an open-ended amount of time. That's because it builds up a long resolve chain.

In other words, as long there's more to do, every time wait resolves, it's with a new promise from a subsequent call to wait, and even though each promise gets resolved almost immediately, they all get fulfilled together with the same value (undefined in this case) only at the very end! The longer things run, the bigger the one-time jank may be when the last promise finally resolves with undefined.

While browsers could optimize this away, none do today AFAIK. See long discussion here.

Community
  • 1
  • 1
jib
  • 40,579
  • 17
  • 100
  • 158