6

I have a long running function. Which iterates through a large array and performs a function within each loop.

longFunction : function(){
       var self = this;
       var data = self.data;

       for(var i=0; len = data.length; i<len; i++){
              self.smallFunction(i);
       }
},
smallFunction : function(index){

// Do Stuff!

}

For the most part this is fine but when I am dealing with arrays above around 1500 or so we get to the point of recieving a javascript execution alert message.

So I need to break this up. My first attempt is like so:

longFunction : function(index){
       var self = this;
       var data = self.data;


      self.smallFunction(index);

      if(data.slides[index+1){
         setTimeout(function(){
            self.longFunction(index+1);
         },0);
      }
      else {
               //WORK FINISHED
      }

},
smallFunction : function(index){

// Do Stuff!

}

So here I am removing the loop and introducing a self calling function which increases its index each iteration. To return control to the main UI thread in order to prevent the javascript execution warning method I have added a setTimeout to allow it time to update after each iteration. The problem is that with this method getting the actual work done takes quite literally 10 times longer. What appears to be happening is although the setTimeout is set to 0, it is actually waiting more like 10ms. which on large arrays builds up very quickly. Removing the setTimeout and letting longFunction call itself gives performance comparable to the original loop method.

I need another solution, one which has comparable performance to the loop but which does not cause a javascript execution warning. Unfortunately webWorkers cannot be used in this instance.

It is important to note that I do not need a fully responsive UI during this process. Just enough to update a progress bar every few seconds.

Would breaking it up into chunks of loops be an option? I.e. perform 500 iterations at a time, stop, timeout, update progress bar, perform next 500 etc.. etc..

Is there anything better?

ANSWER:

The only solution seems to be chunking the work.

By adding the following to my self calling function I am allowing the UI to update every 250 iterations:

 longFunction : function(index){
           var self = this;
           var data = self.data;


          self.smallFunction(index);

          var nextindex = i+1;

          if(data.slides[nextindex){
            if(nextindex % 250 === 0){
             setTimeout(function(){               
                self.longFunction(nextindex);
             },0);
            }
            else {
                self.longFunction(nextindex);
            }
          }
          else {
                   //WORK FINISHED
          }

    },
    smallFunction : function(index){

    // Do Stuff!

    }

All I am doing here is checking if the next index is divisble by 250, if it is then we use a timeout to allow the main UI thread to update. If not we call it again directly. Problem solved!

gordyr
  • 6,078
  • 14
  • 65
  • 123
  • Is it possible to run it asynchronously in the background? If you can't do that, yes, break it up into chunks. – Patashu May 03 '13 at 06:53
  • What exactly your function is doing for each item? Maybe you can parallel them by executing multiple setTimeout at once. – exebook May 03 '13 at 06:55
  • 1
    @Patashu - web workers are not available yet in all browsers... So breaking into reasonable chunk looks like safe approach. – Alexei Levenkov May 03 '13 at 06:55
  • As for the timeout, please see this: http://stackoverflow.com/a/14840584/1026459 – Travis J May 03 '13 at 06:56
  • I think you keep your setTimout method, but call it only if MAXTIME elapsed, not every 1500 time. So 1400 times it will be self-calling function and 100 times - setTimeout will work. – exebook May 03 '13 at 06:57
  • 3
    `setTimeout` has a minimal value of 4ms. And yes, breaking up the loops in chunks of 500 is the way to go. – Bergi May 03 '13 at 06:58
  • Thanks guys so it looks like chunking is the only way to go. Whether I do this with a timer or at a specific index interval I am not sure yet, i will have to do some testing. Regardless if one of you wants to write that up as the answer i'll mark it as correct. Thanks all! – gordyr May 03 '13 at 07:05
  • @Bergi - What if he created iframes (one per "small function") to run this code in? – Travis J May 03 '13 at 07:12
  • You may be interested in https://github.com/adambom/parallel.js – Prinzhorn May 03 '13 at 07:17
  • @Prinzhorn Thanks, looks like a nice library. Unfortunately I cannot use webworkers in this project but this should be handy in the future. cheers. – gordyr May 03 '13 at 07:18
  • @TravisJ Hmmmm interesting. Could you elaborate on the iframes idea? Would creating hundreds/thousands of iframes not take a significant overhead in itself? – gordyr May 03 '13 at 07:19
  • @exebook it's impossible to "execute multiple setTimeout at once" - by definition each elapsed timer will run _consecutively_. – Alnitak May 03 '13 at 07:21
  • @gordyr - That is a good question. It is hard for me to test because I do not have anything which would require such processing. However, I know that you can psuedo implement a web socket with an iframe without locking the UI. I would assume that just doing something very intensive would mock the situation though. – Travis J May 03 '13 at 07:22
  • @TravisJ Thanks mate. I'm actually going to look into this across the weekend, if for nothing else it could be a fun experiment. Thanks! – gordyr May 03 '13 at 07:26
  • 1
    @gordyr - The iframes do take up a decent amount of overhead, it would seem that it takes up to 12 seconds to make 1000 of them. – Travis J May 03 '13 at 07:27
  • @gordyr - Here is a sample code to play with in a fiddle (saving it broke jsfiddle because of document.write): `` – Travis J May 03 '13 at 07:34
  • @TravisJ Awesome, thanks for that. This looks extremely interesting :) Although it probably isn't the best solution for this use case... It's a very good technique to know and there are definitely times where I could see this being useful. – gordyr May 03 '13 at 07:53
  • Hmmm thinking about it... I might be able to execute each 250 piece chunk in a separate iframe each, I know at least Chrome runs iframes in a seperate thread from the main browser, it wouldn't suprise me if other browsers have followed suit recently. – gordyr May 03 '13 at 07:59

4 Answers4

3

Actually 1500 timeouts is nothing, so you can simply do this:

var i1 = 0
for (var i = 0; i < 1500; i++) setTimeout(function() { doSomething(i1++) }, 0)

System will queue the timeout events for you and they will be called immediately one after another. And if users click anything during execution, they will not notice any lag. And no "script is running too long" thing.

From my experiments V8 can create 500,000 timeouts per second.

UPDATE

If you need i1 passed in order to your worker function, just pass an object with it, and increment the counter inside of your function.

function doSomething(obj) {
   obj.count++
   ...put actual code here
}
var obj = {count: 0}
for (var i = 0; i < 1500; i++) setTimeout(function() { doSomething(obj) }, 0)

Under Node.js you can aslo use setImmediate(...).

exebook
  • 32,014
  • 33
  • 141
  • 226
  • 1
    While thats correct it slows everything down massively since we are using a timeout for each iteration. In most browsers a 0 timeout actually equals around 10ms meaning that for a 2000 long set of processes we are adding 20 seconds on to the load time that is not needed. The solutions we have come up with only runs the timeout at a specified interval allowing the others to fire instantly. – gordyr May 03 '13 at 08:17
  • 1
    I am quite sure you do not understand this solution. setTimeouts will start all together at same time. Few ms will elapse before the first of them start executing. But others will follow it without any delay. You will not get 20 second extra. – exebook May 03 '13 at 09:07
  • 1
    when I said 1500 timeouts is nothing, I did not mean that 1500*10ms is not that much. I mean V8 can easily hold a queue of pending thousands of timouts, and their time has already come! so they will execute immediately one after one. – exebook May 03 '13 at 09:11
  • 1
    Oh I see!! Sorry I misundersood completely. Hmmm this might actually be the best solution of all. I will do some testing and get back to you. If it is better than what I am currenly using I will reaward you the answer. Thanks! – gordyr May 03 '13 at 11:12
  • 1
    Right, having tested your suggestion its perfect except for the fact that order is not guaranteed. I am enclosing them in an anonymous function and passing in the index but over large arrays I am finding variations in order. Presumably where one of the timeouts has executed before another. I'm not sure why this is happening. I would have expected all browsers to queue them all but it appears that is not the case. I'll do some further testing in case I have made a mistake somewhere. – gordyr May 03 '13 at 13:47
2

Here's some batching code modified from an earlier answer I had written:

var n = 0,
    max = data.length;
    batch = 100;

(function nextBatch() {
    for (var i = 0; i < batch && n < max; ++i, ++n) {
        myFunc(n);
    }
    if (n < max) {
        setTimeout(nextBatch, 0);
    }
})();
Alnitak
  • 334,560
  • 70
  • 407
  • 495
  • Both your method and the one I just added into my question work perfectly. I would expect yours to be slightly faster as it is avoiding function overhead. Thanks alnitak. – gordyr May 03 '13 at 07:17
1

You might want to use requestAnimationFrame to break up your execution. Here is an example from an developers.google.com article Optimize JavaScript Execution where they even do a couple iterations a time if the task were quicker than X ms.

var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);

function processTaskList(taskStartTime) {
  var taskFinishTime;

  do {
    // Assume the next task is pushed onto a stack.
    var nextTask = taskList.pop();

    // Process nextTask.
    processTask(nextTask);

    // Go again if there’s enough time to do the next task.
    taskFinishTime = window.performance.now();
  } while (taskFinishTime - taskStartTime < 3);

  if (taskList.length > 0)
    requestAnimationFrame(processTaskList);

}
Jonas Äppelgran
  • 2,617
  • 26
  • 30
0

Is there anything better?

If you’re OK with it working only in modern browsers – then you should look into “Web Workers”, that let you execute JS in the background.

https://developer.mozilla.org/en-US/docs/DOM/Using_web_workers

CBroe
  • 91,630
  • 14
  • 92
  • 150
  • I have a lot of experience with webworkers but unfortunately they are not applicable to this situation due to DOM access limitations therefore cannot be used. – gordyr May 03 '13 at 11:16