What if, you have a task for which it is completely impossible to gauge how long the task will take?
What if, you have a task for which you have a rough estimate for how long it will take but, you have no way to increment the progress bar because the library/process/whatever doesn't provide any feedback. For example, maybe you're running a third-party process and there's no way for you to get any feedback about what that process is doing. Now would you do?
What if there's a bomb on a bus. Once the bus goes 50 miles an hour, the bomb is armed. If it drops below 50, it blows up. What do you do? What do you do?
I have answers to these questions. Well, all questions except the last one. But first: task estimation.
In the comments of my last article, a friend of mine pointed out that I never actually mentioned how you come up with the numbers you pass to compositeProgressBar.buildSubProgressBar(). Typically, all you need to do is run your program a few times and time how long each sub task takes. You can then figure out how long the task is spending in each one of the subtasks. You then use that to figure out what to send to compositeProgressBar.buildSubProgressBar().
For example:
sub task | time in seconds | value to send to compositeProgressBar.buildSubProgressBar()
1 | 10 | 10 / 45 = 0.222
2 | 20 | 20 / 45 = 0.444
3 | 15 | 15 / 45 = 0.333
Total time: 45 seconds
And there you go. I would suggest doing more than one timing with many sets of data.
Sometimes the ratios don't stay fixed and instead vary depending on what sort of machine you're running on or what set of data you have.
The programmers I've talked to are always concerned about giving inaccurate information in a progress bar. They've all experienced the frustration of an inaccurate progress bar. It's important to not fall into the trap of thinking that the progress bar needs to be super accurate. At the same time, you need to be careful you don't make the classic error of having a progress bar that jumps to 90% done right away and stays there forever. It can be a delicate balancing act but in my experience, programmers often underestimate the value and accuracy of the information they have.
Consider the following: as a programmer, you almost always know approximately how long the task will take to complete to complete. Put yourself in the user's shoes. The user has absolutely no clue whatsoever how long the task will take to complete. As far as the user is concerned it can take a second, an hour, a day or a millions years. If you know the task will take, at most, one minute, then you already know far more than your user. Communicate this very valuable information to them.
Before we continue remember that there is no solution to determining how long a task will take in the general case. To solve that you would need to solve the the halting problem. That's not going to happen. Instead I can show you a few tricks I've learned about displaying a progress bar in circumstances where you wouldn't think it would be possible.
First question: what is the probability that the task will finish at any given time?
If the probability graph looks like this:
That's good. It's an easy candidate for progress bar because the progress bar tends to finish in one range of values of time.
If you have a task made up of subtasks, it's also makes things easy if the time taken by each sub task stays in a fixed ratio. That is, if one sub task takes 1 second then you know that the next sub task will take approximately 1.5 seconds and the sub task after that will always be a little shorter. In this case, let's say half a second.
If each of the subtasks doesn't stay in a fixed ratio than that's a bummer. When estimating the time a task will take, it's good to consider whether it's likely your task is network bound, disk bound, CPU bound or other. This is important because if you have a large task that consists of, say, a network bound task and a CPU bound task then it's going to be difficult to give an accurate (at least with respect to time) progress bar. On one computer, you may have a very fast CPU with a very slow network connection and on another computer the situation may be reversed. This will cause the progress bar to zip through one section of the meta- task and then crawl through the other section.
The situation above are fairly rare. Most tasks tend to use some combination of the resources of a machine and as a result the differences in the speed of various components tend to cancel out so that the ratios between the different subtasks tend to remain constant (to some reasonable approximation).
If you have run your progress bar a few times and are not happy with its accuracy you have two choices. 1) Run a calibration loop or otherwise figure out how to compensate for these things on that particular machine or 2) live with the fact your progress bar isn't going to be super accurate.
There's something of a black art to writing calibration loops. You have to be careful of things like caches and you have to make sure that you add the right fudge factors to make the progress bar behave correctly. It's surprisingly time-consuming to do this right and most times, in my experience anyway, it's not worth the effort. If you find yourself having to do one of these then it is going to take you a while.
A more common problem is that a program might have no way of getting any feedback as to the progress of one of its subtasks. This can happen if you have to hand off a task to a library.
If you actually do know, approximately, how long that task will take to complete in real time then you still have a chance. You may know that it will take approximately 2 seconds for every item that it's processing. Or you may be able to guess at how long the task will take given the time it took to complete some previous processing step. For example, if it took 5 seconds to complete the first step, it should only take about 10 seconds to complete the second step.
In these situations I like to use a technique that I call Zeno's progress bar. It's a way of providing a progress bar when you have no idea what the real progress is. It's terrifyingly convincing.
Before I continue I would just like to say that I am serious when I say it's terrifyingly convincing. I've actually been duped by my own Zeno's progress bar at least once.
While using the product I work on (Inteleviewer) one day, I noticed that it was providing a progress bar. However, I was under the impression that this was impossible because the task it was using a library that had no way of providing any feedback. I was curious to see if someone on the team had fixed the library or had found some other clever trick to get the library to provide feedback. Someone had found a trick. It was me! It was my own Zeno's progress bar! I had made this error knowing that there were these sorts of progress bars lurking in the code. It was then I knew I had to share this idea with the world.
I came up with the idea based on watching how people would provide their time estimates for tasks that they weren't being informed on the progress of but had some idea of how long it should take.
An example would be an online merchant that knows that something is supposed to ship within two weeks but since it's being shipped by the manufacturer of the product, they really have no good idea of the order's real status. They will tell you it will ship in two weeks and if it doesn't then they will tell you it will ship next week and if it doesn't then they will tell you will ship in the next few days. It still hasn't shipped they will always keep repeating that it will ship in the next few days. Only at this point you start to realize that it may never ship at all.
If you think about it you can probably come up with other examples of this sort.
The technique works really well when your estimate is off slightly. It works even better when your estimate is dead on. This is what it looks like in a progress bar:
Let's say you have a task that could take 10 seconds. You start off with the progress bar moving at a speed that is consistent with the task taking 10 seconds. At the 5 second mark you slow the speed of the progress bar by half. So that by the 10 second mark the progress bar will only be at 75% complete. If all goes well, and your estimate of 10 seconds is correct, then the progress bar will just jump to 100% at this point. If it doesn't and your task is going to take longer then you slow the speed of the progress bar by half again. By the 15 second mark the progress bar will now be at 87.5% complete. This goes on until your task completes.
Obviously, you should try to avoid giving overly optimistic estimates. Being completely off results of one of those annoying 99% complete progress bar that we all love to hate. Being off by only a factor of two, however, actually still gives a half decent effect.
Okay, so how do you code this puppy?
Well, the first step is to create an object to update the progress bar without any help from the task itself. You see, usually the task would tell the progress bar to update from its own thread. In this case it won't so we'll have to reproduce the effect using a timer. It looks like this:
/**
* Makes a progress bar that can show progress if all you know is approximately
* how long the task will take.
*/
public class MagicProgressBar {
private static double BASE = 0.5;
private final Progress progress;
private final int estimatedTime;
private final Timer timer;
private long startTime;
public MagicProgressBar(Progress progressBar, int estimatedTimeOfTask) {
this.progress = progressBar;
this.estimatedTime = estimatedTimeOfTask;
timer = new Timer(30, new ActionListener() {
public void actionPerformed(ActionEvent e) {
//progress.advance( magic??? );
}
});
}
public void start() {
startTime = System.currentTimeMillis();
timer.start();
}
public void stop() {
timer.stop();
}
}
You would use it like this:
private static void doMagic(Progress masterProgress) {
MagicProgressBar magicBar = new MagicProgressBar(masterProgress, TIME_ESTIMATE);
magicBar.start();
doStuff(); // doesn't provide progress
magicBar.stop();
masterProgress.advance(1);
}
Where "doStuff()" is the task were waiting to complete. Note that TIME_ESTIMATE is not usually a constant. In many cases it would be calculated by using a weighting number, like we sent to compositeProgressBar.buildSubProgressBar(), and the time it's taken to do some earlier step in the task.
Well, we still need to add the implementation of the timer in the MagicProgressBar class. Let's do that now.
timer = new Timer(30, new ActionListener() {
public void actionPerformed(ActionEvent e) {
long timeSinceStart = System.currentTimeMillis() - startTime;
int unitLengthInMillis = estimatedTime / 2;
int units = (int) (timeSinceStart / unitLengthInMillis);
double percent = ( timeSinceStart % unitLengthInMillis ) / (double) unitLengthInMillis;
progress.advance(findSum(units) + percent * Math.pow(BASE, units + 1) );
}
});
private static double findSum(int n) {
return (Math.pow(BASE, n + 1) - BASE) / (BASE - 1);
}
"findSum()" is calculating how far complete the progress bar should be given at a given time unit index. In our example, where we expect the task to take 10 seconds, unit index 1 would be the first 5 seconds, unit 2 would be the next 5 seconds, unit 3 would be the 5 seconds after that etc... a unit's length is equal to one half of the estimated time of the task.
The rest of the code is either trying to figure out which time unit we're at or how far through the current time unit we are.
Let's see what the object looks like when we put it all together:
/**
* Makes a progress bar that can show progress if all you know is approximately
* how long the task will take.
*/
public class MagicProgressBar {
private static double BASE = 0.5;
private final Progress progress;
private final int estimatedTime;
private final Timer timer;
private long startTime;
public MagicProgressBar(Progress progressBar, int estimatedTimeOfTask) {
this.progress = progressBar;
this.estimatedTime = estimatedTimeOfTask;
timer = new Timer(30, new ActionListener() {
public void actionPerformed(ActionEvent e) {
long timeSinceStart = System.currentTimeMillis() - startTime;
int unitLengthInMillis = estimatedTime / 2;
int units = (int) (timeSinceStart / unitLengthInMillis);
double percent = ( timeSinceStart % unitLengthInMillis ) / (double) unitLengthInMillis;
progress.advance(findSum(units) + percent * Math.pow(BASE, units + 1) );
}
});
}
public void start() {
startTime = System.currentTimeMillis();
timer.start();
}
public void stop() {
timer.stop();
}
private static double findSu5m(int n) {
return (Math.pow(BASE, n + 1) - BASE) / (BASE - 1);
}
}
Now you have the power of Zeno's progress bar. Use it wisely, young grasshopper. Zeno's progress bar has no mind. Isn't it wiser to seek proper progress reporting then to desire the swift completion of the programming exercise? Oops, I'm channeling David Carradine again. All I'm trying to say is don't use it for the hell of it. That's all.
Okay, on to the next part which is all about indeterminate progress bars.
This is what an indeterminate progress bar looks like:
The one of the left is used on the Macintosh. The one on the right is used by Windows.
Indeterminate progress bars are used to signal that we have absolutely no idea how long a task is going to take. Both the Macintosh and Windows version of this type of progress bar use animation to signal that, yes, there really is something happening. The computer has not fallen asleep. It has not started daydreaming. It hasn't quit and joined a hippie commune where it's not asked to do tasks that are difficult to estimate tasks all day.
If you really, really don't know how long the task is going to take then this is the progress bar to use.
The decision to use an indeterminate progress bar is a hard one. Most of the time you'll want to avoid using it. Users don't like being in the dark about how long their computer might be occupied. Going back to my online merchant example above, this would be the equivalent of an indeterminate progress bar:
Customer: How long is it going to take the ship?
Merchant: I don't know.
Customer: Is it going to take about two weeks? Or two days? Or two years?
Merchant: I don't know.
Customer: Are you saying it may take two years for me to get my package?
Merchant: It could. I don't think that's very likely though. Well, I don't know, it could take two years. Yes. Maybe.
Despite the fact that indeterminate progress bars are annoyingly, I can think of two common uses for them..
The first use is for the relatively short-lived progress bar that shows up because the computer is taking a little longer than it should to do something and the programmer thought that he should provide some obvious feedback that the computer hasn't crashed. These indeterminate progress bars are often short-lived. Most the time the programmers didn't even think it would be shown at all. I haven't seen one of these types of progress bars in a while. In this case the indeterminate progress bar means "hang on a second".
Another common use for the indeterminate progress bar when it's used at the beginning of a long task when the computer is still calculating how long it will take for it to do the task. For example, if you're running a calibration loop at the beginning of your process, you might show an indeterminate progress. Another example would be the OS might show an indeterminate progress when it's calculating the number of files involved in a file copy operation. It's also often used at the beginning of a task when trying to connect to a remote server. Connecting to a remote server is one of these things that will either be instantaneous or take a while due to the server being down or a network error.
In these examples you go from an indeterminate progress bar to a regular progress bar. This sort of transition is okay. You're essentially saying to the user "I have no idea how long this is going to take" because you're trying to figure out how long the task is going to take. So long as this step is relatively short it's ok.
Going from a regular progress bar to an indeterminate progress bar is less ok. It's like saying you know how long a task is going to take then saying you have no idea then claiming you know again.
I have seen this behavior when it's used to mean "hang on a second, I've got an unexpected delay". A task might display this when it tries to connect to a server that's taking a while to respond. I've also seen it during a compression task that would occasionally start an optimization step. If the optimization step was taking too long the progress bar would become indeterminate for a while.
Progress bars that go from a determinant progress to an indeterminate and back are very rare. It's probably not a pattern you'll ever need to use.
Well, that's all I have to say. If you want to read more about progress bars (and who doesn't.. pfff..) then I'd suggest visiting Jeff Atwood's blog post on the subject. It's a great starting off point. He also goes into how to make your task look like it ran faster without actually making the task run any faster.
You can also check out the miscellaneous human interface guidelines documents by Microsoft and Apple. Perhaps you want to expand your horizons into other kinds of progress indicators.
Oh, and here's the source code for all the examples I've given.
Until next time bye.
3 comments:
I have the answer to the question about the bus!
Drive on some snow or gravel. Apply the emergency brake, and floor the gas.
The bus won't go anywhere, but the front wheels will spin, making the speedometer think you are going fast.
PS. I like the zeno progress bar, and I think I will use it.
I'm fairly certain that buses are rear wheel drive so you couldn't just apply the handbrake.. unless buses have a handbrake that works on the front wheels. It would also depend on whether the bomb is hooked up to the speedometer. Maybe they're using a GPS? Even if they were using a speedometer, is it connected to the drive wheels?
The real solution to the bus problem is simply to not go 50 miles an hour in the first place. That way the bomb isn't armed and you can avoid the whole sticky mess. I think that movie would be a bit boring, though.
Post a Comment