Thread.Sleep

This paper is not about insomnia (though my audience of over-caffeinated programmers may occasionally suffer from that).  No, its about the wonderful world of JScript.  A world that seems to have many limitations at first glance, but where we continually find elegant work-arounds to these limitations. 

One of the bigger limitations is thread-control, and particularly the absence of Thread.Sleep.  Turns out, setTimeout and setInterval are really quite versatile functions.  And here I'll demonstrate - with some real life examples - how they can replace Thread.Sleep to achieve the same ends.

 

Waiting on Another Task to Complete

Lets start with an advanced chat program.  This example is certainly interesting because it does not have a Send button.  Instead, all keystrokes are automatically packaged up into messages sent at 2 sec intervals.  The time between keystrokes is recorded so that it can be played back on the other person's screen the same way, just like it was typed in.  I conceived of this rather simple idea while romancing a girl long-distance (naturally a courtier wants to know every detail - if she hurriedly types something down, or is pausing over a certain choice word, and most especially you want to know if she types something and then changes her mind and erases it!).

So here setTimeout will be needed, to play back events on the other person's screen in the same time intervals with which they were originally typed.  But there are two variables that have to be dealt with: network latency, and slight variations in CPU computation even over similar tasks.  Obviously the more important goal is that keystrokes are played back in the right sequence (otherwise it would look like the other person is occasionally misspelling words).  So getting keystrokes played in the original timing, that's a secondary goal.  We can try to do that as much as those two variables allow us, but we must also put in a safeguard to ensure that unexpected delays don't switch the order of their execution.  If its time for keystroke #2 to execute, but keystroke #1 hasn't finished, then keystroke #2 must wait.

Normally this waiting would be a while-loop with a very small Sleep inside of it: every 5 millisecs, we check to see if keystroke #1 has completed yet.  But in JScript we don't have Sleep, so we have to get a little fancy with setInterval...

chat.htm     (Show Example - Download Source)

function Bus()

{

      this.url = "/chat.aspx";

      this.httpGetObj = XmlHttp();

}

 

...

 

Bus.prototype.Start = function()

{

    this._missedChecks = 0;

 

    var _this = this;

 

    clearInterval(this.timerID);

    this.timerID = setInterval(function() { _this.checkServer(); }, 2 * 1000); // every 2 secs

};

 

... 

 

Bus.prototype.checkServer = function()

{

    // If the last "checkServer" is still waiting for a response, then do not send another one.

    if (this.httpGetObj.readyState != 4 && this.httpGetObj.readyState != 0)

    {

        this._missedChecks++;

 

        // If over 5 had to be skipped because one request is hung-up, then cancel

        // that request, and start a new one.

        // Cancel the current request (its taking too long)

        if ( this._missedChecks > 5 )

              this.Start();

 

        return;

    }

 

    this.httpGetObj.open('POST', this.url, true);

 

    var _this = this;

    this.httpGetObj.onreadystatechange = function()

    {

        if (_this.httpGetObj.readyState === 4)

        {

            if ( _this.httpGetObj.status != 200 && _this.httpGetObj.status != 304 )

                  alert( 'POST request error: ' + _this.httpGetObj.statusText ) ;

 

            // Update Client-State with info from the Server

            _this.onReceive( _this.httpGetObj.responseText );

        }

    }

 

    this.httpGetObj.send( this.onSend() );

};

 

 

Bus.prototype.onSend = function()

{

    Bus.lastSendDate = new Date();

 

    var msg = ...

 

    return msg;

}

 

Bus.prototype.onReceive = function( responseText )

{

    if (responseText.length==0)

        return;

 

    var messageId = _nextMessageId++;

 

    ...parse response into list of actions (each action is a keystroke)...

 

    var catchUp = false;

    if ( actions.length > 1)

    {

        var timeSpan = actions[actions.length-1].millisec - actions[0].millisec;

       

        // If more then 6 secs worth of activity needs to be played, then either

        // there was some network lag, or we just connected and there's a backlog

        // of activety from the other user.  In either case, we need to catch-up

        // and play out all this activety quickly (immediate/sequential).

        if ( timeSpan > 6 * 1000 )

            catchUp = true;

    }

 

    PlayMessage_WhenItsYourTurn( messageId, actions, (catchUp) ? PlayMessage_Immediately : PlayMessage_RealTime );

}

 

 

function PlayMessage_WhenItsYourTurn( messageId, actions, func )

{

    // Messages have to be played synchronosly.  We cannot start

    // a message while another is still executing its actions.

    // Wait until its you're turn. (this is the best way I can think of to implement sleep in jscript)

    var timerId = setInterval(

            function()

            {

                if ( _completedMessage+1 == messageId )

                {

                    clearInterval( timerId );

                    func( messageId, actions  );

                }

            }

            ,40

        );

}

    var _nextMessageId = 0;

    var _completedMessage = -1;

 

 

function PlayMessage_Immediately( messageId, actions )

{

    for (var inx = 0; inx < actions.length; inx++)

    {

        var a = actions[inx];

        a.Play( false );

    }

 

    _completedMessage = messageId;

}

 

 

function PlayMessage_RealTime( messageId, actions  )

{

    // Sequence our actions.  (We should have exclusive access to _nextActionID).

    for (var inx = 0; inx < actions.length; inx++)

    {

        var a = actions[inx];

        a.actionID = _nextActionID++;

    }

 

    // Start this message.

    var pendingActionsFromCurrentMessage = actions.length;

 

    for (var inx = 0; inx < actions.length; inx++)

    {

        var a = actions[inx];

        setTimeout(

            function(a)

            {

                return function()

                {

                    // Wait until its you're turn.

                    var timerId = setInterval(

                            function()

                            {

                                if ( _completedAction+1 == a.actionID )

                                {

                                    clearInterval( timerId );

 

                                    a.Play( false );

                                    _completedAction = a.actionID;

                                   

                                    pendingActionsFromCurrentMessage--;

                                    if (pendingActionsFromCurrentMessage == 0)

                                        _completedMessage = messageId;

                                }

                            }

                            ,10

                        );

                }

            }(a)

            ,a.millisec

        );

    }

}

    var _nextActionID = 0;

    var _completedAction = -1;

 

So instead of sleep, we use setInterval.  setInterval let's us create a loop that periodically checks a condition, and once the condition is true, the loop is terminated.  We have to do this in two places:

 

Execute a Set of Tasks Slowly

Here's another example which screams for Thread.Sleep, but its a different situation altogether.  We want to traverse a folder and everything inside of it, and we want to do this without freezing the browser.

For this technique, we use setTimeout...

Walk Folder Hierarchy.htm

<html>

<body>

 

<button onclick="walker.stop();">Stop Walk</button>

 

<table>

<tr>

<td valign=top>

      <table id=dirList>

      </table>

</td>

<td valign=top>

      <table id=fileList>

      </table>

</td>

</tr>

</table>

 

 

<script>

 

var walker;

window.attachEvent("onload", function()

{

      walker = Directory.Walk({

            path:"D:\\VSProjects" ,

            onFile: function(filePath)

            {

                  var tr = fileList.insertRow();

                  tr.insertCell().innerHTML = filePath;

            },

            onDirectory: function(dirPath)

            {

                  var tr = dirList.insertRow();

                  tr.insertCell().innerHTML = dirPath;

            }

      });

});

 

 

function Directory() {}

 

 

Directory.GetFiles = function ( path )

{

    _FSO_Init();

 

    var folder = _fso.GetFolder( path );

    var list = new Array();

    var inx = 0;

    var files = new Enumerator( folder.files );

    for(; !files.atEnd(); files.moveNext() )

    {

        var filename = files.item().Path;

        list[inx++] = filename;

    }

    return list;

}

Directory.GetDirectories = function ( path )

{

    _FSO_Init();

 

    var folder = _fso.GetFolder( path );

    var files = new Enumerator( folder.SubFolders );

    var list = new Array();

    var inx = 0;

    for(; !files.atEnd(); files.moveNext() )

        list[inx++] = files.item();

    return list;

}

 

    _FSO_Init = function ()

    {

        if (!_fso)

            _fso = new ActiveXObject("Scripting.FileSystemObject");

    }

    var _fso;

   

   

 

/* This is an interesting function.  The logic for walking a directory structure is simple recursion:

        WalkDirectory( dir )

            for each sub-directory in dir

                WalkDirectory( sub-directory )

 

   However, a larger folder can take awhile to walk, so we must consider two other issues:

        - repainting the screen (i.e. don't let this function consume all CPU in the browser)

        - allow the user to stop the walk prematurely.

 

   In a WinForm application, these two issues are easily handled if a background-worker thread

   is used.  The solution in a browser is not quite so simple.  We do not have a low-level thread

   interface, but we do have a pair of functions that can do the trick: setTimeout and clearTimeout.

 

   Now while the absence of Thread.Sleep() is dearly missed, we can get it around it.

   And the fact that this is all in script makes it a cinch to debug.  In fact, I think this

   is easier to work with then a WinForms app that has native threads.

 

 */

 

Directory.Walk = function ( args )

{

    var walkerObj = { _threads: [],

                      stop: function()

                            {

                                for (var inx = 0; inx < this._threads.length; inx++)

                                    clearTimeout(this._threads[inx]);

                            }

        };

    walkerObj._threads.push( setTimeout( function()

        {

            Directory._Walk( walkerObj,

                            args.path,

                            (args.onFile)?args.onFile:null,

                            (args.onDirectory)?args.onDirectory:function(){} ,

                            (args.onCompleted)?args.onCompleted:function(){}

                           );

        },1)

    );

    return walkerObj;

}

    Directory._Walk = function ( walkerObj, path, onFile, onDirectory, onCompleted )

    {

        if (onFile)

        {

            var list = Directory.GetFiles( path );

            for (var inx = 0; inx < list.length; inx++)

                onFile( list[inx] );

        }

 

        var list = Directory.GetDirectories( path );

 

        /* This won't do.  We need a pause after each directory.  But there is no thread.sleep() command.

        for (var inx = 0; inx < list.length; inx++)

        {

            var dir = list[inx];

            onDirectory( dir );

        }

        */

 

        // So instead, we have a function that calls itself, once for

        // each directory, and only until there are no more directories.

        // At first, this sounds like recursion: "a function calling itself",

        // and anytime where a loop is replaced by recursion, you incur

        // the overhead of a bigger stack which is waiting to unwind.

        //

        // However, this is not really recursion, we are not incrementing the

        // stack.  Though the function does call itself, the call happens on

        // a new thread (which gets a new stack).  The caller's thread can

        // terminate itself after the new thread is spawned, so we don't

        // grow the stack or the thread-pool on each iteration, we just transfer

        // control to a new thread, to get a slight pause in its start-up.

        //

        // In short, we've replaced the loop with a function that transfers

        // to a new thread each iteration. 

        var dnx = 0;

        var nextFunc = function()

        {

            if ( dnx == list.length )

            {

                onCompleted();

                return;

            }

 

            var dir = list[dnx++];

            onDirectory( dir );

 

            walkerObj._threads.push( setTimeout( function()

                {

                    Directory._Walk(

                            walkerObj,

                            dir,

                            onFile,

                            onDirectory,

                            function ()

                            {

                                walkerObj._threads.pop();

                                nextFunc();

                            }

                        );

                }

                ,20)

            );

        };

 

        nextFunc();

    }

 

</script>