Providing Feedback on Long-running Scripts in Older Browsers

By Rob Gravelle

Providing Feedback on Long-running Scripts in Older Browsers

In the Show Progress Report for Long-running PHP Scripts article, we learned how to display a progress bar to update the user on a long-running server-side process. It's hard to believe that only a short time ago, users had to make due with a spinning hour glass while waiting for long-running tasks such as the uploading of image and video files to complete. The two objects responsible for making progress bars more prevalent are the JavaScript EventSource and the HTML5 Progress Bar element. If your browser does not support these two recent additions to the HTML spec, do not despair, as there are ways to emulate their functionality. In today's article, we'll explore some of those in detail.

Your Choices

Being a stateless protocol, TCP/IP is not terribly conducive to providing partial data for an ongoing process. It tends to send a chunk of data to the client and close the connection until another client request comes in. That really limits the number of possibilities for providing updates. If Server-sent Events (SSEs) are not an option for you, or you'd like to support clients who can't use SSEs for whatever reason, there are only a handful of viable workarounds. The three that we will be exploring today are:

  • Websockets: HTML5's websocket API allows to open a "permanent communication channel" between the browser and the server. The communication channel is bidirectional, thus enabling the browser to query the server for status updates, or the server to send data and progression information to the client.
  • Script/Iframe streaming: This technique involves the use of an IFrame to stream HTML JavaScript snippets from the long running script. The browser then interprets the scripts and carries out the commands contained therein, such as updating a status bar.
  • Ajax polling: A server-side script stores progress information and another, triggered by Ajax calls, fetches the progress data at regular intervals.

The next few sections will cover each of these technologies in more detail.

WebSockets

Out of the three, WebSockets are the least optimal workaround. There are a couple of good reasons why. For starters, chances are that if your browser doesn't support the HTML5 Progress Bar, it likely won't support the WebSockets API either. Another potential deal breaker is that the API requires a permanent server socket, which is not always permitted by hosts. The socket will either not connect at all or your persistent connection will be automatically shut down by the server after a very short period.

I covered WebSockets in my Making HTML5 WebSockets Work article, in case you do want to pursue this avenue further.

Script/Iframe Streaming

I have personal experience with Iframe Streaming because I wrote about it in an article entitled Comet Programming: the Hidden IFrame Technique. It began as a page that would inform the user how many CDs were left in stock after each purchase. To make it more dynamic, I added a loop to the PHP code to purchase some CDs at random intervals and update the totals accordingly. If you'd like to try out the demo, here is the complete code for the IFrameLongPolling.php file. To set up the demo, first, create a file called "CdCount.txt" and save it with the contents "100" to your Web server's root directory. That will be the starting number of CD stock. Next, insert the code below into a file called IFrameLongPolling.php and save it to the root directory. Then call IFrameLongPolling.php from the browser to see the CD stock tick down by random amounts.

<?php
set_time_limit(0);

header("Cache-Control: no-cache, must-revalidate");
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
flush();
?>

<!DOCTYPE HTML>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>CD Store</title>
        <script type="text/javascript">
            function updateCount(c) {
               document.getElementById('CD Count').innerHTML = c;
            }
        </script>
    </head>
    <body bgcolor="#FFFFFF" id=body>
        <h2 align="center">CD Sales Page</h2>
          <form id="frmCdSales" name="frmCdSales" action="IFrameLongPolling.php" METHOD="GET" target="hidden">
          <div align="left">
            <p>Quantity:</p>
            </div>
          </form>
          <iframe name="hidden" id="hidden" src="IFrameLongPolling.php" frameborder="0" height="0" width="100%"></iframe>
          <p>There are <span id="CD Count">100</span> CDs left.</p>
        <div align="left"><br>
          <br>
        </div>
         <div id="errors" align="center"> </div>
    </body>
<?php

$cd_stock = ("CdCount.txt");

function updateStock($num)
{
    global $cd_stock;
    $count = file($cd_stock);
    $count = (int)$count[0];
    $count = $count - $num;
    if ($count < 0) $count = 0;
    $fp = fopen($cd_stock , "w");
    fputs($fp , "$count");
    fclose($fp);

    echo "<script type='text/javascript'>parent.updateCount('" . $count . "')</script>\n";
    flush();

    return $count;
}

$num = $_GET['num'];
if ( $num == "")
{
    //start the update service
    srand();
    do
    {
        $newOrder  = rand(1, 3);
        $sleeptime = rand(2, 10);
        sleep($sleeptime);
    } while(updateStock($newOrder) > 0);
}
else
{
    updateStock((int)$num);
}

?>
</html>

Ajax Polling

In Ajax Polling, the server-side process stores progress information either in a session variable or file and a second script, triggered by Ajax calls, fetches the progress data at regular intervals. The client can't know the rate of the status changes so updates must happen at predefined intervals either by means of a setInterval() or recursive setTimeout().

In the following demo, the first of two PHP scripts, called long_process.php, emulates the long-running task using a loop. Session_write_close() must be called after each iteration since session data is usually stored after your script terminates. Until then, session data is locked to prevent concurrent writes so only one script may operate on a session at any time.

<?php 
    for($i=1;$i<=10;$i++){ 
        session_start(); 
        $_SESSION["progress"]=$i; 
        session_write_close(); 
        sleep(1); 
    } 
?>

The second PHP script, named get_progress.php, reads the session variable and sends it to the client.

<?php 
    session_start(); 
    echo $_SESSION["progress"]; 
?>

The JavaScript is made up of two parts: the first kicks off the long-running script, the second is an inline function that calls get_progress.php to get the progress information. Once completed, the poll() function calls itself recursively every three seconds.

//kick off the process
$.Ajax({         
    url: 'long_process.php',         
    success: function(data) {}     
}); 
//start polling
(function poll(){
   setTimeout(function(){
      $.Ajax({ 
         url: "get_progress.php", 
         success: function(data){
             //Update the progress bar
             setProgress(data.value); 

             //Setup the next poll recursively
             poll();
         }, 
         dataType: "json"
     });
  }, 3000);
})();

Conclusion

While long polling has been been extremely beneficial to a myriad of applications over the years, there are perhaps too many moving parts for something that should be as simple as providing updates on a long-running task. Using a trick or two, it is in fact possible to dispense with everything but one PHP script and accomplish the same thing that these techniques do, but with a lot less overhead and complexity. We'll save that solution for next time.


Rob Gravelle resides in Ottawa, Canada, and is the founder of GravelleWebDesign.com. Rob has built systems for Intelligence-related organizations such as Canada Border Services, CSIS as well as for numerous commercial businesses.

In his spare time, Rob has become an accomplished guitar player, and has released several CDs. His band, Ivory Knight, was rated as one Canada's top hard rock and metal groups by Brave Words magazine (issue #92).



Make a Comment

Loading Comments...

  • Web Development Newsletter Signup

    Invalid email
    You have successfuly registered to our newsletter.
  •  
  •