Using setTimeout to update progress bar when looping over multiple variables

11,264

Solution 1

If you want to use setTimeout you could capture the x, y, z and count variables into a closure:

function run() {
    var x = 100,
        y = 100,
        z = 10,
        count = 0;
    for (var i=0; i<x; i++) {
        for (var j=0; j<y; j++) {
            for (var k=0; k<z; k++) {
                (function(x, y, z, count) {
                    window.setTimeout(function() {
                        $('#progressbar').reportprogress((100*count)/(x*y*z));
                    }, 100);
                })(x, y, z, ++count);
            }
        }
    }
}

Live demo.

Solution 2

TL;DR: Use CPS: http://jsfiddle.net/christophercurrie/DHqeR/

The problem with the code in the accepted answer (as of Jun 26 '12) is that it creates a queue of timeout events that don't fire until the triple loop has already exited. You're not actually seeing the progress bar update in real-time, but seeing a late report of what the values of the variables were at the time they were captured in the inner closure.

I'd expect that your 'recursive' solution looks a bit like using continuation-passing style to ensure that your loop doesn't continue until after you've yielded control via setTimeout. You might not know you were using CPS, but if you're using setTimeout to implement a loop, you're probably pretty close to it.

I've spelled out this approach for future reference, because it's useful to know, and the resulting demo performs better than the ones presented. With triple nested loops it looks a bit convoluted, so it may be overkill for your use case, but can be useful in other applications.

(function($){
    function run() {
        var x = 100,
            y = 100,
            z = 10,
            count = 0;

        /*
        This helper function implements a for loop using CPS. 'c' is
        the continuation that the loop runs after completion. Each
        'body' function must take a continuation parameter that it
        runs after doing its work; failure to run the continuation
        will prevent the loop from completing.
        */
        function foreach(init, max, body, c) {
            doLoop(init);
            function doLoop(i) {
                if (i < max) {
                    body(function(){doLoop(i+1);});
                }
                else {
                    c();
                }
            }
        }

        /*
        Note that each loop body has is own continuation parameter (named 'cx',
        'cy', and 'cz', for clarity). Each loop passes the continuation of the
        outer loop as the termination continuation for the inner loop.
        */
        foreach(0, x, function(cx) {
            foreach(0, y, function(cy) {
                foreach(0, z, function(cz) {
                    count += 1;
                    $('#progressbar').reportprogress((100*(count))/(x*y*z));
                    if (count * 100 % (x*y*z) === 0) {
                        /*
                        This is where the magic happens. It yields
                        control to the javascript event loop, which calls
                        the "next step of the foreach" continuation after
                        allowing UI updates. This is only done every 100
                        iterations because setTimeout can actually take a lot
                        longer than the specified 1 ms. Tune the iterations
                        for your specific use case.                   
                        */
                        setTimeout(cz, 1);
                    } else {
                        cz();
                    }
                }, cy);
            }, cx);
        }, function () {});    
    }

    $('#start').click(run);
})(jQuery);

You can see on jsFiddle that this version updates quite smoothly.

Solution 3

Probably a jquery function in reportprogress plugin uses a setTimeout. For example if you use setTimeout and make it run after 0 milliseconds it doesn't mean that this will be run immediately. The script will be executed when no other javascript is executed.

Here you can see that i try to log count when its equal to 0. If i do it in setTimeout callback function then that is executed after all cycles and you will get 100000 no 0. This explains why progress-bar shows only 100%. js Fiddle link to this script

function run() {
    x = 100;
    y = 100;
    z = 10;
    count = 0;
    for (i=0; i<x; i++) {
        //some code
        for (j=0; j<y; j++) {
            // some code
            for (k=0; k<z; k++) {
                //some code
                if(count===0) {
                     console.log('log emidiatelly ' + count);
                    setTimeout(function(){
                        console.log('log delayed ' + count);
                    },0);
                }
                count++;
            }
        }
    }
}
console.log('started');
run();
console.log('finished');

wrapping everything after for(i) in setTimeout callback function made the progress-bar work. js Fiddle link

Edit: Just checked that style setting code for item is actually executed all the time. I think that it might be a browser priority to execute javascript first and then display CSS changes.

I wrote a another example where i replaced first for loop with a setInterval function. It's a bit wrong to use it like this but maybe you can solve this with this hack.

var i=0;
var interval_i = setInterval(function (){

    for (j=0; j<y; j++) {
        for (k=0; k<z; k++) {
            $("#progressbar").reportprogress(100*++count/(x*y*z));
        }
    }

  i++;
  if((i<x)===false) {
    clearInterval(interval_i);
  }
},0);

JS Fiddle

Share:
11,264
Josiah
Author by

Josiah

Updated on June 29, 2022

Comments

  • Josiah
    Josiah almost 2 years

    Suppose you have 3 arrays you want to loop over, with lengths x, y, and z, and for each loop, you want to update a progress bar. For example:

    function run() {
        x = 100;
        y = 100;
        z = 10;
        count = 0;
        for (i=0; i<x; i++) {
            //some code
            for (j=0; j<y; j++) {
                // some code
                for (k=0; k<z; k++) {
                    //some code
                    $("#progressbar").reportprogress(100*++count/(x*y*z));
                }
            }
        }
    }
    

    However, in this example, the progress bar doesn't update until the function completes. Therefore, I believe I need to use setTimeout to make the progress bar update while the function runs, although I'm not sure how to do that when you have nested for loops.

    Do I need to break each loop up into its own function, or can I leave them as nested for loops?

    I created a jsfiddle page in case you'd like to run the current function: http://jsfiddle.net/jrenfree/6V4Xp/

    Thanks!

  • Josiah
    Josiah over 12 years
    The progress bar updates don't seem to be very smooth. It will jump to a few percent (2 or 3%), then stop, then suddenly be at 100%. Any idea why it's doing this?
  • Josiah
    Josiah over 12 years
    I've tried implementing this in my code, but something isn't working. I'm guessing it's due to the other processing lines, many of which access the index variables i, j, and k
  • Josiah
    Josiah over 12 years
    I indeed did need to use setTimeout, but instead of using a closure, I made the main calculation function recursive. I began to realize that setTimeouts do not pause the rest of code from processing, but you can seemingly force the setTimeouts to run by using recursive functions.
  • Chin
    Chin over 9 years
    this works fine, but why is it so slow? For x = 10000, y = 1, z = 1 (so 10000 iterations), it takes ~50 seconds to run. Is there any way to speed it up?
  • Christopher Currie
    Christopher Currie over 9 years
    Sure, you can yield less frequently; I've seen delays as much as 150 ms even for a 1 ms setTimeout request, which adds up over 10000 iterations. Just decide how often your UI actually needs to update. In this case, you may only care about updating when the percentage changes, so the last line of the innermost loop could be replaced with: if (count * 100 % (x*y*z) === 0) { setTimeout(cz, 1); } else cz();
  • Christopher Currie
    Christopher Currie almost 9 years
    I've updated the jsFiddle sample as indicated in the above comment, and the performance much better with 10000 iterations.