A bit about the stuff I've done


Friday 4 January 2013

Staging long loops in Javascript

So I've encounted this issue a number of times where you need to do something, probably in a loop, which is going to take some time and the browser will "hang" will the loop takes place.
Sometimes you may even get a message pop up asking if you want to terminate a stuck script.

Not only does this make for a bad user experience but it can cause scripts to terminate without warning.

 The solution?
Plenty of posts on forum sites such as StackOverflow will tell you to use setTimeout.
It can get kind of messy though - so I thought I'd write a wrapper function to take away the pain, and have something that *looks* mostly like the familiar simple loop.
Now obviously there is a performance hit by adding all these extra "unnecessary" delays. (approx. 30% for a tight loop, less for a complex one) BUT if the browser pops up a box and waits for the user to respond, this will add much more delay to the total execution time!
so - heres the code:
function StagedLoop(IterationsBetweenPauses, Delay, AtLeastOneDelay, Test, Run, Complete) { /// <summary>Stages a loop, inserting timer pauses every n Iterations ///Warning - this method is asyncronus and will exit before looping is completed! /// put code to be run only after looping has finished into the Complete inline function parameter ///</summary> ///<param name="Test">A method to run which will perform the "test" part of the loop to see if looping should continue. This test will be run at the start of the loop (i.e. the loop may run 0 or more times)</param> ///<param name="Run">A method which will be run on every iteration of the loop - don't forget to increment any counters!</param> ///<param name="IterationsBetweenPauses">How many loops to run before using a timer pause - suggestion: use a number between 10 and 10000, but if iteration counters will appear anywhere on screen don't use a round number (i.e. 1000)</param> ///<param name="Delay">Milliseconds to set the timer pause for between stages. Recommended value: 1 - this gives the browser ample time to do "stuff" but avoids unnecassary delay. N.B. the actual delay could be substantially longer than the one specified here due to the way browser events are handled</param> ///<param name="Complete">Function to run when the loop has exited</param> if (IterationsBetweenPauses.IterationsBetweenPauses) { Run = IterationsBetweenPauses.Run; Delay = IterationsBetweenPauses.Delay; Complete = IterationsBetweenPauses.Complete; AtLeastOneDelay = IterationsBetweenPauses.AtLeastOneDelay; Test = IterationsBetweenPauses.Test; IterationsBetweenPauses = IterationsBetweenPauses.IterationsBetweenPauses; } if (AtLeastOneDelay) { setTimeout(StagedLoop, Delay, { Test: Test, Run: Run, IterationsBetweenPauses: IterationsBetweenPauses, Delay: Delay, Complete: Complete, AtLeastOneDelay: false }); return; } var RemainingIterations = IterationsBetweenPauses; while (RemainingIterations > 0) { if (Test && !Test()) { Complete(); return; } Run(); RemainingIterations--; } setTimeout(StagedLoop, Delay, { Test: Test, Run: Run, IterationsBetweenPauses: IterationsBetweenPauses, Delay: Delay, AtLeastOneDelay: AtLeastOneDelay, Complete: Complete }); } And here is an example usage: var I = 0; StagedLoop(4567, 1, false,function () { return I < 10000; }, function () { DoSomethingWith(I); I++; //don't forget to increment! },function(){ //run the next bit of code here }); //N.B. code places here will be run *before* the loop exists (possibly before it runs even a single iteration) //Don't put any code here! This would replace a simple for loop such as this one: for (var I=0;I<10000;I++) { DoSomethingWith(I); } //run the next bit of code here
ok - so it's not quite as clean, but it gets the job done with minimal fuss! Complete test harness below showing the difference in times taken <div id="here">Please Wait... - there is a good chance the browser will "hang" at this point</div> <script type="text/javascript"> setTimeout(RunTest, 100); function RunTest() { var Iterations = 100000; var I; var Start = new Date().getTime(); for (I = 0; I < Iterations; I++) { document.getElementById('here').innerHTML = I; } var End = new Date().getTime(); var Normal = (End - Start); I = 0; Start = new Date().getTime(); StagedLoop(4567, 1, false,function () { return I < Iterations; }, function () { document.getElementById('here').innerHTML = I; I++; }, function () { document.getElementById('here').innerHTML = 'Please wait...'; End = new Date().getTime(); var Staged = (End - Start); console.log('Duration for staged loop: ' + Staged); console.log('Duration for "normal" loop: ' + Normal); document.getElementById('here').innerHTML = 'Duration for "normal" loop: ' + Normal + '<br/>' + 'Duration for staged loop: ' + Staged + '<br/>' + '% performance loss: ' + Math.round(((100.0 * (Staged - Normal)) / Normal)) + '%'; console.log(Staged - Normal); console.log(100.0 * (Staged - Normal)); console.log(((100.0 * (Staged - Normal)) / Normal)); //end inline function }); //N.B. code places here will be run *before* the loop exits (possibly before it runs even a single iteration) //Don't put any code here! } function StagedLoop(IterationsBetweenPauses, Delay, AtLeastOneDelay, Test, Run, Complete) {/// <summary>Stages a loop, inserting timer pauses every n Iterations ///Warning - this method is asyncronus and will exit before looping is completed! /// put code to be run only after looping has finished into the Complete inline function parameter ///</summary> ///<param name="Test">A method to run which will perform the "test" part of the loop to see if looping should continue. This test will be run at the start of the loop (i.e. the loop may run 0 or more times)</param> ///<param name="Run">A method which will be run on every iteration of the loop - don't forget to increment any counters!</param> ///<param name="IterationsBetweenPauses">How many loops to run before using a timer pause - suggestion: use a number between 10 and 10000, but if iteration counters will appear anywhere on screen don't use a round number (i.e. 1000)</param> ///<param name="Delay">Milliseconds to set the timer pause for between stages. Recommended value: 1 - this gives the browser ample time to do "stuff" but avoids unnecassary delay. N.B. the actual delay could be substantially longer than the one specified here due to the way browser events are handled</param> ///<param name="Complete">Function to run when the loop has exited</param> if (IterationsBetweenPauses.IterationsBetweenPauses) { Run = IterationsBetweenPauses.Run; Delay = IterationsBetweenPauses.Delay; Complete = IterationsBetweenPauses.Complete; AtLeastOneDelay = IterationsBetweenPauses.AtLeastOneDelay; Test = IterationsBetweenPauses.Test; IterationsBetweenPauses = IterationsBetweenPauses.IterationsBetweenPauses; } if (AtLeastOneDelay) { setTimeout(StagedLoop, Delay, { Test: Test, Run: Run, IterationsBetweenPauses: IterationsBetweenPauses, Delay: Delay, Complete: Complete, AtLeastOneDelay: false }); return; } var RemainingIterations = IterationsBetweenPauses; while (RemainingIterations > 0) { if (Test && !Test()) { Complete(); return; } Run(); RemainingIterations--; } setTimeout(StagedLoop, Delay, { Test: Test, Run: Run, IterationsBetweenPauses: IterationsBetweenPauses, Delay: Delay, AtLeastOneDelay: AtLeastOneDelay, Complete: Complete }); } </script>