Saturday, September 13, 2014

Unresponsive Scripts and Javascript's Lack of Sleep



Working on a single-page app using a MV* library? You've probably had this wonderful dialog popup at some point. If your Javascript is running without taking a breadth, the browser will get chippy and start alerting the user that something has gone awry. While its most likely true, popping up messages the user doesn't understand really doesn't help. Even worse are the solutions to turn off Javascript because many sites still function even if scripting is disabled. Unfortunate advice in today's Internet.

In many cases, its a loop with a significant number of iterations that is probably triggering any number of more complex operations. Without yielding to the browser from time-to-time, it will create an unresponsive experience for your user (dialog or not). If you've programmed in other languages, you're immediate go to solution might be to sprinkle in time kind of sleep calls in those process intensive loops. Unfortunately, there is no sleep function in Javascript. The only mechanism to break out of a call stack is the setTimeout function. However, it does not work like sleep in other languages where the current execution point blocks while something else runs and then returns back to the next line after the sleep timeout. Instead, it just keeps on running:


array.forEach( function( element, index, array ) {
   
   // Do something ...
   
   // This won't block this from continuing to 
   // run so there will be no pause between iterations
   setTimeout( ... );
   
});

// Code run by setTimeout may or may not be
// done running so now what?



The best solution I've devised so far that maintains the readability of the above syntax is to extend Array with a forEachSleep function which looks the same as forEach but adds a parameter interval for how long to wait between each iteration on the array. Additionally, since I can't add any finalization code in right after the loop, it returns a promise so that code can be run after the last iteration over the array:


array.forEachSleep( function( element, index, array ) {
   
   //- Do something useful ...
   
}, interval, scope ).done(function( array ) {
   
   //- 
});



Here's the code to implement that functionality. The forEachSleep function sets up the promise using a jQuery Deferred object and then calls an internal function to handle the "looping". That function calls the passed in block with the appropriate arguments and then either uses setTimeout to call itself or resolves the promise:


/*!
 * Copyright (c) 2014 Ben Olson
 * http://benknowscode.com/2014/09/unresponsive-script-and-javascript-sleep.html
 * MIT License (http://opensource.org/licenses/MIT)
*/
(function() {
   
   function _next_timer( a, b, t, s, i, p ) {
      
      b.call( s, a[i], i, a );
      
      if ( i + 1 >= a.length )
         p.resolveWith( s, [a] );
      else
         setTimeout( function() { _next_timer( a, b, t, s, i + 1, p ) }, t );
   }
   
   Object.defineProperty(Array.prototype, 'forEachSleep', {
      // not enumerable
      // not configurable
      // not writable
      value: function( blk, intv, scope ) {

         var ary = this,
             dfd = $.Deferred();

         _next_timer(          
            ary,
            blk,
            intv || 5,
            scope || this,
            0,
            dfd
         );

         return dfd.promise();
      }
   });

})();



I used the EcmaScript 5.1 Object.defineProperty when adding the function to the built-in Array object to avoid any issues with it becoming enumerable which could (and most likely) break other code. Its not supported in old browsers, but neither is forEach so I figured I'd look forward rather than backwards.

As a quick test, this code should print out to the console once a second. There are 10 iteration log messages and one finalization log message each with a timestamp and all the arguments available to each function:


function testScope() { }

function testForEach() {
   
   var scope = new testScope();
  
   _.range(100,200,10).forEachSleep( function( e, i, a ) {
      console.log( 'run', ( new Date() ).toISOString(), e, i, a, this );
   }, 1000, scope ).done(function( a ) {
      console.log( 'done', ( new Date() ).toISOString(), a, this );
   });

}



If you open your debugging console, you can run testForEach and you should see output similar to this:


run 2014-09-13T11:52:03.231Z 100 0 [100, 110, 120, 130, 140, 150, 160, 170, 180, 190] testScope {}
run 2014-09-13T11:52:04.291Z 110 1 [100, 110, 120, 130, 140, 150, 160, 170, 180, 190] testScope {}
run 2014-09-13T11:52:05.307Z 120 2 [100, 110, 120, 130, 140, 150, 160, 170, 180, 190] testScope {}
run 2014-09-13T11:52:06.319Z 130 3 [100, 110, 120, 130, 140, 150, 160, 170, 180, 190] testScope {}
run 2014-09-13T11:52:07.331Z 140 4 [100, 110, 120, 130, 140, 150, 160, 170, 180, 190] testScope {}
run 2014-09-13T11:52:08.342Z 150 5 [100, 110, 120, 130, 140, 150, 160, 170, 180, 190] testScope {}
run 2014-09-13T11:52:09.381Z 160 6 [100, 110, 120, 130, 140, 150, 160, 170, 180, 190] testScope {}
run 2014-09-13T11:52:10.391Z 170 7 [100, 110, 120, 130, 140, 150, 160, 170, 180, 190] testScope {}
run 2014-09-13T11:52:11.461Z 180 8 [100, 110, 120, 130, 140, 150, 160, 170, 180, 190] testScope {}
run 2014-09-13T11:52:12.490Z 190 9 [100, 110, 120, 130, 140, 150, 160, 170, 180, 190] testScope {}
done 2014-09-13T11:52:12.546Z [100, 110, 120, 130, 140, 150, 160, 170, 180, 190] testScope {}



I used Lodash in the testing, but its not a dependency - only jQuery is needed for the promise. The interval and scope arguments are optional and will default to 5 milliseconds and the array being iterated over, respectively. While not a universal solution to Javascript's lack of a sleep function, it provides a reasonable drop-in replacement to forEach in places where you know there may be longer running code over a large set of array elements which should result in a more responsive application and happier users.