How do you report progress in your software ??


In order to improve usability of my RCP application I decided to manage all the “long” operations  using Jobs. This post will introduce several solutions to handle “composite” tasks progress reporting across a concrete example I faced this morning.

Image just to bring your eyes to my post and encourage you to read it, is it working ??

Image just to bring your eyes to my post and encourage you to read it, is it working ?? ;-)

Having already used the Job API I felt confident on the time needed to implement this. I was right about the time needed to use the Job’s API but I was wrong about the way to organize several related operations into several (or one ??) job(s).

Lets describe the context. The user is performing a “File->Open” action and want report on the progress of this action. Progress report is needed because opened files are big and a lot of computation has to be done on these files. This “File->Open” action is composed of 3 sequential “subtasks”: parsing, analyse and display. I am able to easily report accurate progress for each one of these tasks, but unfortunately I am not able then to have an accurate estimation of each task time in the global process composed of the 3 tasks.

Today I have 2 solutions in order to report what is happening behind the scene how many time is happening behind the scene (this IS really what the user cares !!!).

First solution: Use 3 distincts Jobs.

This solution can be easily implemented using IJobChangeListeners. A first Job is created for the parsing tasks. This Job is scheduled and thanks to Job listeners I am able to be notified when it’s completed in order to create and schedule the analyze Job. The same process is apply between the analyze Job and the display Job. This solution present the 3 tasks to the user with an accurate estimation time for EACH ONE of these tasks. As I mentioned before, this is the best I can do because I am not able to estimate each task time in the global process.  Here the new user may think that the global “File Open” action will be completed at the end of the first Job ….??!!!  Another drawback of this solution is that the user is prompted with 3 UI progress dialogs (all my Jobs are user Jobs so there is successively: “Parsing File dialog”, “Analyze File dialog” and “Display Dialog” ) for only one “File->Open” action.

Second solution: Use one main Job and 3 Submonitors inside this Job.

I came to this solution in order to try to fix the second drawback of the first one (3 UI dialogs for oen action). I’ll not step into implementation detail here but lets analyze the result. Why ? We have a unique UI progress dialog NOT accurate: we can clearly distinct the 3 stages stepping each one at a different speed. What is better: only one UI dialog not accurate or 2 “surprise” dialogs that appear after the first accurate one ?

An other solution would be (I didn’t find anyway to implement it for now):

Third solution (not sure this can be done .. any ideas ??): Have one Job with 3 Submonitors inside this Job but reporting the 3 Submonitors as 3 X 100 %. What I mean here is to have only one UI progress dialog called “Opening File” that will be “filled in from 0% to 100%” three times (one for each sub-task). This solution is the same as the first one but it will fix the problem of having 3 separated UI dialogs.

In all of these solution the end user “will” have on the first usage a “wrong” first estimation time …. Thus I would be interested to know how you handle such situations, so feel free to leave comments on this topic.

Advertisements

7 thoughts on “How do you report progress in your software ??

  1. The solution is to give each submonitor a fraction of the main job and update the progress of the main job accordingly. This is easier than it sounds:

    – Run each sub job from 0..100%
    – in the parent job, multiply the progress of the sub job with the amount allocated for this job:

       double amount; int index = 0;
       for( Job subJob : subJobs ) {
           amount += ( subJob.getProgress() / 100. ) 
               * jobPortion[index++];
       }
    

    Just make sure that the sum of all items in jobPortion == 100.

    This pattern can be applied recursively.

    • If the first part usually takes 10% of the time, and the last part 15%, use this:

         double[] jobPortion = { 10., 100-10-15., 15. };
      

      That way, your jobs always run from 0 to 100% and the parent jobs calculate their progress recursively based on the progress of the sub jobs.

  2. I would go with your second solution, but instead of allocating one third of the total progress for each submonitor, I would use empirical values that make it appear to the end user that the rate of progress *does not slow down* as progress approaches 100%. You say that you cannot predict how long the subtasks of parsing, analyse and display will take, but isn’t there some kind of correlation between the three durations, at least roughly, or in the common case? For example, if analyzing typically takes three times as long as parsing, and displaying typically takes the same amount of time as parsing, you would allocate 20% for parsing, 60% for analyzing, and 20% for displaying.

    When in doubt, make your estimates for later phases more generous (e.g. 15%/60%/25%) because users usually don’t mind when progress is not deterministic as long as it is not stuck for long times in later phases. The worst case is when you spend a really long time at 99%.

  3. I would go with your first solution. Here is how you can show progress across jobs:

    Using the JobManager create a new Progress Group:

    IProgressMonitor groupMonitor = JobManager.createProgressGroup();

    Set the progress group to this monitor on all of your jobs:

    Job director = new Job(“Director”) {

    // implementation goes here

    protected IStatus run(IProgressMonitor monitor) {

    final Object parserFamily = new Object();
    final Object analyzerFamily = new Object();
    final Object directorFamily = new Object[] { parserFamily, analyzerFamily };

    Job parseJob = new Job(“Parser”) {

    protected IStatus run(IProgressMonitor monitor) {

    // do some work here
    }

    // only want the jobs created by this instance of the “Director” job
    // to indicate they belong together, the “Director” job should not
    // belong to the same family, it is “grouped” with it’s spawned “child”
    // jobs via the progressGroup
    public boolean belongsTo(Object family) {

    if (null != family){
    if( family.equals(parserFamily) ) {
    return true;
    } else if( family.equals(directorFamily) ) {
    return true;
    }
    }

    return false;
    }
    };

    Job analyzeJob = new Job(“Analyzer”) {

    protected IStatus run(IProgressMonitor monitor) {

    // notice that when this job is run it will wait for the
    // jobs that are members of the parserFamily to finish before
    // continuing execution
    try {

    getJobManager().join(parserFamily, null);

    // won’t get here until all jobs belonging to the parserFamily complete

    // now do some work here specific to the analyzer

    } catch (OperationCanceledException ex) {
    getJobManager().cancel(analyzerFamily);
    return Status.CANCEL_STATUS;
    } catch (InterruptedException ex) {
    return new Status(IStatus.ERROR, PLUGIN_ID,
    “Exception caught running job ‘” + getName() + “‘”, ex);
    } finally {
    // add any clean up necessary here
    }
    }

    // only want the jobs created by this instance of the “Director” job
    // to indicate they belong together, the “Director” job should not
    // belong to the same family, it is “grouped” with it’s spawned “child”
    // jobs via the progressGroup
    public boolean belongsTo(Object family) {

    if (null != family){
    if( family.equals(analyzerFamily) ) {
    return true;
    } else if( family.equals(directorFamily) ) {
    return true;
    }
    }

    return false;
    }

    };

    parseJob.setProgressGroup(groupMonitor, 1);
    analyzeJob.setProgressGroup(groupMonitor, 1);

    parseJob.schedule();
    analyzeJob.schedule();

    // notice that when this job is run it will wait for the
    // jobs that are members of the directorFamily to finish
    try {

    getJobManager().join(directorFamily, null);

    // won’t get here until all jobs belonging to the directorFamily complete

    } catch (OperationCanceledException ex) {
    getJobManager().cancel(directorFamily);
    return Status.CANCEL_STATUS;
    } catch (InterruptedException ex) {
    return new Status(IStatus.ERROR, PLUGIN_ID,
    “Exception caught running job ‘” + getName() + “‘”, ex);
    } finally {
    // add any clean up necessary here
    }
    }

    };

    director.setProgressGroup(groupMonitor, 1);
    director.schedule();

    All of the run(IProgressMonitor monitor) methods for the jobs belonging to the same progress group will affect a single IProgressMonitor instance. The subdivision of work is defined by calling setProgressGroup(IProgressMonitor group, int ticks) on each job that will be reporting combined progress across jobs. The Progress Group also informs the Progress View that these jobs should be shown together in the UI as a single Process.

    This should solve your problem.

  4. Pingback: Sequential Jobs « Manuel Selva’s Eclipse blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s