PDA

View Full Version : Progress Bars : A New Approach to an old problem



mikecar
15 Nov 2009, 6:45 PM
I am posting this solution to a common problem, which I have seen others asking
questions about, in the hope it helps somebody. I also would like some feedback as to
the solution, and your opinion on whether this post should be converted to a FAQ
or something...

USE CASE

You want to execute a server-side task which may take a long time to finish.
During this time, you don't want the user to be able to start another request,
(ie you want the UI to block), and also you would like to provide visual
feedback as to what state the server-side process is in, i.e. some kind of
progress indicator.

POSSIBLE SOLUTIONS

I have seen folks advocating using synchronous AJAX calls (not supported by Ext),
and actually using this use case as a good example of the need for synchronous
AJAX requests. While I don't want to re-open this issue here, IMHO sync AJAX is
something that can be useful, and should be supported. Nevertheless, this
post shows a solution which does not require sync calls, so the good people at
Ext will be happy :)

Current wisdom in Ext forums says that you solve this particular problem by
using a client-side polling task, which asks the server for the progress status,
and updates the UI. This works, but I do not like the approach. I consider polling
a very bad design, and always avoid it if at all possible. Further, this approach
requires that the server-side code has some way of inter-thread communication.
This is because, when you start the actual task on the server, it gets executed
in a particular context (a thread in most topologies). However, the polling
request is executed in a different context (thread), and does not have access to
the data of the original request. So, you need to create some kind of mechanism
to communicate between the two threads. Using a data base table, or a file on
disk has been suggested. Another idea would be to use an application-wide cache,
like memCache in PHP. Still, this complicates the server-side, and it is wasteful
of resources, for something that is very commonplace,and should be easy to
accomplish.

MY SOLUTION

The basic concept behind my approach is that the server starts the task,
and then periodically emits progress information to the client, which displays
a meaningful progress bar or something. The status updates are part of a single
HTTP transaction, so are very efficient.

The problem with implementing this in Ext is that the only "official" mechanism
of executing server-side code is via the Ajax.request method, which cannot be
called synchronously (a requirement in this approach), and also does not provide
events while loading content, only at the end, therefore a progress bar that contains server-side information is impossible.

An alternative approach (which is widely used by non-Ext developers), is to use
a client IFRAME to make the request, and handle the progress updates.
This works because content loaded in the IFRAME is incrementally rendered by the
browser during loading, and not just at the end of the document. It is this feature
of most modern browsers that allows us to use this approach for a progress indicator.

While the concept is simple, the mechanics of putting everything together in a
working solution can be intimidating. The purpose of this post is to show a full
working solution, and provide some reusable code. The server-side is in PHP 5.x, but
I imagine that it can be coded in any environment that supports FLUSHING of the
output to the client under program control.

DESIGN DETAILS

At each status update point,the server-side code emits "scriptlets",
whose purpose is to trigger a client-side function that updates the progress UI.
These scriptlets have the following format:


<script type='text/javascript>
updateProgress(status);
</script>
where "updateProgress" is a function defined by the client, and accessible at that
execution context, and "status" is the server-side status information. Don't worry,
it will become clear later on...

The client browser will "see" these scriptlets at the time they are emitted
by the server, and will execute them immediately, thus updating the UI.
Issues to consider here are :


how and where is this "callback" function defined,so that it can be accessible
by the IFRAME that is doing the script execution ?
How are memory leaks avoided ?
how is the whole process initiated, and shutdown ?


Details follow.

SERVER SIDE CODE

First, you need a reusable class to encapsulate the process. here is the code:



<?php
class AJAX_PROGRESS
{
private $template;

function __construct()
{
$this->template="
<script type='text/javascript'>
window.top.mk_update_progress(%s,'%s','%s');
</script>";
$this->write(str_pad('<HTML><BODY>',4096)); //force browser to render
}

private function write($a)
{
echo $a;
ob_flush();
flush();
}

function advance($perc,$msg1='',$msg2='')
{
$this->write(sprintf($this->template,$perc,$msg1,$msg2));
}

function __destruct()
{
$this->advance(-1);
$this->write('</BODY></HTML>');
}
} // AJAX_PROGRESS
?>
Copy-paste this class to a file, and save it as AJAX_PROGRESS.class.php
or whatever naming conventions you have. It has no external requirements.

What this class does is to handle the incremental generation which will be
the "content" loaded by the client IFRAME.

Next, you need a server-side file that executes a long task,
and uses this class to handle progress indication.

Here is an example, which simulates a long running-task:



<?php

require_once 'AJAX_PROGRESS.class.php';
//pick up any parameters for the task.
//in this example, n=how may times to execute a loop, and delay=the loop delay

$n=$_REQUEST['n'];
$delay=$_REQUEST['delay'];

$pb=new AJAX_PROGRESS();

for ($i=0; $i<$n; $i++)
{
//here you add the code to actually do the work
//call "advance" periodically to update the client
$pb->advance(($i+1)/$n,'Loading...',"Step $i");
usleep($delay*1000);
}

//this destroys the progress object, and ends the output to the client
//it also emits a special call to the client-side function, with a progress value
//of -1, which tells the client that the task is completed

$pb=null;

?>
Copy-paste this to a file called "progress.php" in the same directory as the
class file above, so that it "finds" the class. alternatively, use whatever
server-side mechanism you are accustomed to, as far as placement of code is
concerned, and adjust the "require" statement.

What you need to know at this point is the function "advance", which is called
with three arguments:

$perc=progress percent, in the range 0..1
$msg1=Some text that must be displayed by the client
$msg2=Another piece of text that should be displayed.

In this example, the design assumes that the client side uses an Ext.Msg.Progress
dialog, which takes exactly these arguments. if not using Msg.Progress, adjust the
function arguments and implementation accordingly, the principle is the same.

Notice that the scriptlet emitted by the server assumes that there exists a
function called mk_update_progress, defined at the top window level, so that it
can be "found" by the IFRAME. The client-side code takes care of defining this function.

CLIENT-SIDE CODE

First, you need a reusable,self-contained javascript function, which sets up the
environment, and handles the details. This function does not have dependencies other
than Ext, and can be added to your client-side library, or saved as a single file,
and included in your html via a <script> tag.



function execWithProgress(title,url,params)
{
var iframe,pbw;

window.top.mk_update_progress=function(value,msg1,msg2)
{
if (value<0)
{
pbw.hide();
iframe.remove();
window.top.mk_update_progress=null;
return;
}
pbw.updateProgress(value,msg1,msg2);
}

pbw=Ext.Msg.progress(title,'','');
if (params) url+='?'+Ext.urlEncode(params);
iframe=Ext.getBody().createChild({tag:'IFRAME',style:'display:none',src:url});
}
You call this function with three parameters:

title: the title for the progress dialog

url: the url of the server file that initiates the long task, and supplies status information

params: An object hash that contains key/value pairs that are passed as parameters of the GET request.


Notice that, because of the use of javascript closures, this function does not
pollute the global namespace, and cleans up after itself, so you can place it
virtually anywhere. it will create the hidden IFRAME, the progress dialog,
and clean up when the server-side task is done. You only need to call it
to execute the whole process, it is that simple!

Example code that uses this function,and uses the file "progress.php" we
defined earlier :




var content=new Ext.Panel(
{
border:false,
tbar:[
{text:'Call server',handler:function(){
execWithProgress('Calling Server','progress.php',{n:10,delay:200});
}}
],
items:[
]
}
);
This is it. I hope this post helps somebody, and will provide a "clean"
solution to this very common use case, as a fully worked-out example.

Enjoy!

Mike Cariotoglou
Ext Newbie

P.S Excuse my English, I am Greek :)

agfk
16 Nov 2009, 11:24 AM
It sounds like you have more or less reinvented Comet (http://en.wikipedia.org/wiki/Comet_(programming)).

You might want to take a look at this thread (http://www.extjs.com/forum/showthread.php?t=4935).

mikecar
16 Nov 2009, 7:21 PM
Indeed, you are right! I did not know of Comet, and had not seen that thread. Thanks for pointing this out, I think I can now create a much simpler implementation, thanks to the info in that thread. I did not know that one can sample the responseText even before the response is complete !

Troy Wolf
26 Feb 2010, 8:55 PM
mikecar,

I appreciated your detailed, well-written post. Just today, I wrapped up a project today that does exactly what you describe. I am VERY pleased with the outcome. ~o)

I develop web applications that do a LOT of frequent data updating--both public applications and Intranet. My team has worked hard to minimize the data structures and take advantage of caching, etc, to get the communications as efficient as possible. Until this most recent app, we've been using traditional AJAX style calls. We've had great success even with apps that need to request an update every 3 seconds.

However, for the app I just completed, the user clicks a button that kicks off a server-side process that updates a few thousand items. It was desired for the user interface to show "live" progress of the update including outputting each item name along with a status (SUCCESS/FAIL) and even an animated progress bar.

The server process handles 10 to 20 items per second. I really enjoyed the challenge, and learned a few things along the way. I was pleasantly surprised at how easily the browser handles hundreds of script tags flooding in, processing each one as soon as it hits. What turned out to be a bigger challenge was the sheer number of lines I needed to display. I was able to get the HTML structure down to a bare minimum using single character elements (b,i,u) to define the lines and class names with single character names. I think people take naming for granted, but when you are talking about thousands of items, those bytes add up.

Likewise, I created local javascript variables in the iframe with single character names as aliases to the longer real object names in the parent document. This allowed me to output very minified script lines--again, those bytes add up.

The UI is relatively rich using an Ext ViewPort with multiple regions and panels. Each update must, in addition to creating and inserting the item line in the displayed log, update at least 3 counters displayed in the UI as well as a progress bar. I also have a client-side duration timer running using 1 second setTimouts to update the seconds elapsed. So there is a lot going on. To my surprise, I was able to push a rate of 50 updates per second!

I tested in IE8, FF3.6, and Chrome something. At 30 updates per second, all 3 handled the load just fine. At 50 per second, they handled the load "OK", but things started to get over-loaded. For example, my duration timer would stop updating until everything caught up.

Something I found surprising was that IE8 seemed to handle the load "easier". I noticed the CPU load was smoother and lower than with FF and Chrome. Probably more surprising is I found Chrome struggled when the number of displayed lines reached 600+. IE and FF had zero noticeable change in performance all the way up to 3,000 lines--the max lines in my testing. Of course my results are very specific to my exact HTML and CSS, but for this particular situation Chrome struggled hard where the other 2 did not sweat.

Where your explanation differs from mine is that you dynamically write and tear down the iframe. I really like that and will incorporate that into my system unless I find a good reason not to.

While I knew this approach would work, I guess I'm astonished out how performant it is. In my testing, I'd run multiple tests with hundreds or thousands of items back to back---without refreshing my iframe. I guess I expected that iframe "buffer" to get really full and start causing issues somehow.

Maybe you are doing this, too, but I added an onunload event handler to my iframe that detects connection loss or the successful completion of the request. In my testing, I could kill my webserver or pull the network cable in the middle of processing, and the browser correctly ran my unload event handler every time. This allows me to do any cleanup, report the failure to the user, open a new connection, etc.

I'm going to do more testing and build more of a "framework", but I may convert more of my Intranet apps that rely on frequent data updates to this methodology. ~o)