Copyright © 2016 Stefan Xenos and others
 Eclipse Corner Article

 

Using Progress Monitors

Summary
In this article I'll explain how to report progress in Eclipse. I'll discuss the contract on IProgressMonitor, demonstrate some common patterns using SubMonitor, and explain how to migrate legacy code to take advantage of the API introduced in Eclipse 4.6.

By Stefan Xenos, Google
April 19, 2016


1.0 Introduction

So you're writing a long-running method. It takes an IProgressMonitor as a parameter. You want it to report smooth progress, be cancellable from the UI thread and accomplish this with minimal boilerplate. Unfortunately, most of the examples you've seen either omit progress reporting entirely or contain more boilerplate than code.

Is it possible to get the smooth progress reporting without all that boilerplate? If so, what are the responsibilities of your method and what are the responsibilities of its caller? This article should help.

2.0 Using SubMonitor

Use SubMonitor. If you remember nothing else from this article, remember that.

SubMonitor is an implementation of IProgressMonitor that simplifies everything related to progress reporting. Whenever you write a method that accepts an IProgressMonitor, the first thing you should do is convert it to a SubMonitor using SubMonitor.convert(...).

2.1 Allocating Ticks

There are several overloads for convert and most of them accept a number of ticks as an argument. These are said to "allocate ticks". A tick is a unit of work to be performed. Allocating ticks distributes those ticks across the remaining space of the progress monitor but doesn't actually report any progress. It basically sets the denominator that will be used for subsequent progress reporting.

How many ticks do you need? Take a look at all the slow things your method does and assign them a number of ticks based on how long you think they will take. Any method you call that takes a progress monitor should be considered a "slow thing". If you think one thing will take longer than another, give it more ticks. If you think it will take twice as long, give it twice as many. When you're done, add up all the ticks. That's how many you should allocate. Ticks only have meaning relative to other ticks on the same monitor instance: their absolute value doesn't mean anything.

There are several methods which allocate ticks. Normally you'll allocate them at construction-time using SubMonitor.convert(...) but this only works if you're creating a new SubMonitor instance.

Sometimes you'll want to allocate (or reallocate) the ticks on an existing monitor in which case you'll want SubMonitor.setWorkRemaining. You can call this as often as you like on a SubMonitor instance. When you do, any remaining unconsumed space on the monitor is divided into the given number of ticks and any previously-allocated ticks are forgotten.

The last method that allocates ticks is called beginTask. It's used as part of the progress reporting framework and you rarely need to call it directly. You'll see this used a lot in older code and we'll get more into it later. For now, it's best to avoid it in new code unless you're implementing your own IProgressMonitor.

2.2 Consuming Ticks

Once you've allocated the ticks you can consume them. Consuming ticks is what reports progress. Let's say you allocate 50 ticks and then consume 3 of them. That means your progress bar will be 3/50 of the way across, or 6%. Note that you must allocate ticks before you can consume them on any given progress monitor instance. Attempting to consume ticks without allocating them is an error.

There are several methods on SubMonitor which consume ticks. Namely, split(...), newChild(...), and worked(...). Practically speaking the only one you need is split(...).

I'll be using split(...) for most of the code examples in this article, but it is new in Eclipse 4.6. If your code is meant to work on earlier versions of Eclipse you should use newChild(...) instead of split(...). The two do pretty much the same thing except that split(...) also performs cancellation checks.

2.3 Split

split(...) doesn't immediately consume the ticks. It uses the ticks to create a new child monitor but first it fully consumes any leftover ticks in any previous children of the same parent.

I'll demonstrate with an example:

    void myMethod(IProgressMonitor monitor) {
        // No ticks have been allocated yet, so we can't consume them.
        SubMonitor subMonitor = SubMonitor.convert(monitor, 100);

        // monitor is now being managed by subMonitor. We shouldn't use it directly again.
        // subMonitor has 100 ticks allocated for it and we can start consuming them.

        SubMonitor child1 = subMonitor.split(10);

        // subMonitor now has 90 ticks allocated. 10 of the original 100 were used to build child1.
        // child1 has no ticks allocated for it yet so we can't consume ticks from it yet.

        child1.setWorkRemaining(100);

        // child1 now has 100 ticks allocated for it. Consuming 1 tick from child1 now would
        // advance the root monitor by 0.1%.

        SubMonitor grandchild1 = child1.split(50);

        // child1 now has 50 ticks allocated.

        SubMonitor grandchild2 = child1.split(50);

        // Allocating a new grandchild from child1 has caused grandchild1 to be consumed.
        // Our root progress monitor now shows 5% progress.

        SubMonitor child2 = subMonitor.split(40);

        // Allocating a new child from subMonitor has caused child1 and grandchild2 to be
        // consumed. Our root progress monitor now shows 10% progress.

        SubMonitor child3 = subMonitor.split(10);

        // Child2 was consumed. The root progress monitor now shows 50% progress.

        SubMonitor child4 = subMonitor.split(40);

        // Child3 was consumed. The root progress monitor now shows 60% progress.
    }

2.4 Cancellation

Long-running methods should check the value of IProgressMonitor.isCanceled() periodically. If it returns true, they should terminate cleanly and throw OperationCanceledException. In Eclipse 4.5 and earlier, this was done with explicit cancellation checks like this:
    if (monitor.isCanceled()) {
        throw new OperationCanceledException();
    }
Unfortunately, these sorts of cancellation checks are cumbersome and can become a performance bottleneck if performed too frequently.

In Eclipse 4.6 and on, cancellation checks are be performed implicitly by SubMonitor.split(...). Code should be migrated to use split wherever possible and explicit cancellation checks should be deleted.

So how does split work and how does it replace explicit cancellation checks? It's basically just a helper that does the same thing as newChild but additionally includes a cancellation check. Internally, split does something like this (pseudocode):

    SubMonitor split(int ticks) {
        if (checking_cancelation_now_wouldnt_cause_a_performance_problem()) {
            if (isCanceled()) {
                throw new OperationCanceledException();
            }
        }
        return newChild(ticks);
    }

3.0 Examples

Lets look at some examples of common progress reporting patterns.

3.1 Calling child methods

Most long-running operation will need to call out to other long-running operations.
     void doSomething(IProgressMonitor monitor) {
        // Convert the given monitor into a SubMonitor instance. We shouldn't use the original
        // monitor object again since subMonitor will consume the entire monitor.
         SubMonitor subMonitor = SubMonitor.convert(monitor, 100);

         // Use 30% of the progress to do some work
         someChildTask(subMonitor.split(30));

         // Use the remaining 70% of the progress to do some more work
         someChildTask(subMonitor.split(70));
     }

3.2 Branches

Sometimes some long-running piece of code is optional. If the optional piece is skipped, you still want the total work to add up to 100%, but you also don't want to waste a portion of the progress monitor just to make the ticks line up. One approach is to do the optional part first and then use setWorkRemaining to redistribute the remaining ticks.
     void doSomething(IProgressMonitor monitor) {
         SubMonitor subMonitor = SubMonitor.convert(monitor, 100);

         if (condition1) {
         	doSomeWork(subMonitor.split(20));
         }

         // Don't report any work, but ensure that we have 80 ticks remaining on the progress monitor.
         // If we already consumed ticks in the above branch, this is a no-op. Otherwise, the remaining
         // space in the monitor is redistributed.
         subMonitor.setWorkRemaining(80);

         if (condition2) {
         	doMoreWork(subMonitor.split(40));
         }

         subMonitor.setWorkRemaining(40)

         doSomeMoreWork(subMonitor.split(40));
     }
This approach works well enough in most cases and requires minimal boilerplate but the progress it reports can sometimes be uneven if the method ends with a bunch of conditionals that are often skipped. Another approach is to count the number of ticks in advance:
     void doSomething(IProgressMonitor monitor) {
         int totalTicks = 0;
         if (condition1) {
             totalTicks += OPERATION_ONE_TICKS;
         }
         if (condition2) {
             totalTicks += OPERATION_TWO_TICKS;
         }
         if (condition3) {
             totalTicks += OPERATION_THREE_TICKS;
         }
         // Allocate a different number of ticks based on which branches we expect to execute.
         SubMonitor subMonitor = SubMonitor.convert(monitor, totalTicks);

         if (condition1) {
             doSomeWork(subMonitor.split(OPERATION_ONE_TICKS));
         }
         if (condition2) {
             doSomeWork(subMonitor.split(OPERATION_TWO_TICKS));
         }
         if (condition3) {
             doSomeWork(subMonitor.split(OPERATION_THREE_TICKS));
         }
     }
This will usually report smoother progress, but due to the extra complexity it's usually best to only use this pattern if otherwise jerkiness of reported progress would be annoying to the user.

3.3 Loops

This example demonstrates how to report progress in a loop where every iteration takes roughly the same amount of time.

     void doSomething(IProgressMonitor monitor, Collection someCollection) {
         SubMonitor subMonitor = SubMonitor.convert(monitor, 100);

         // Create a new progress monitor for the loop.
         SubMonitor loopMonitor = subMonitor.split(70).setWorkRemaining(someCollection.size());

         for (Object next: someCollection) {
             // Create a progress monitor for each loop iteration.
             SubMonitor iterationMonitor = loopMonitor.split(1);

             doWorkOnElement(next, iterationMonitor);
         }

         // The original progress monitor can be used for further work after the loop terminates.
         doSomeWork(subMonitor.split(30));
     }

3.4 Skipping elements in a loop

Sometimes some elements from a loop will be skipped. In that case, it's better to use the space in the progress monitor to report the work done for the long-running elements rather than the fast elements.
    void doSomething(IProgressMonitor monitor, Collection someCollection) {
        SubMonitor loopMonitor = SubMonitor.convert(monitor, someCollection.size());

        int remaining = someCollection.size();
        for (Object next : someCollection) {
            loopMonitor.setWorkRemaining(remaining--);

            if (shouldSkip(next)) {
                continue;
            }

            // Create a progress monitor for each loop iteration.
            SubMonitor iterationMonitor = loopMonitor.split(1);
            doWorkOnElement(next, iterationMonitor);
        }
    }
This works well enough if skipped elements are rare but if most of the elements are skipped, this pattern will tend to make poor use of the end of the monitor. If many elements might be skipped, it's better to pre-filter the list like this:
    void doSomething(IProgressMonitor monitor, Collection someCollection) {
        List filteredElements = someCollection
            .stream()
            .filter(next -> !shouldSkip(next))
            .collect(Collectors.toList());

        SubMonitor loopMonitor = SubMonitor.convert(monitor, filteredElements.size());
        for (Object next : filteredElements) {
            doWorkOnElement(next, loopMonitor.split(1));
        }
    }

3.5 Queues

What if you're performing a depth-first search or some other algorithm where more work is continually discovered as you proceed? Try putting the work-discovered-so-far in a queue and using the size of the queue to allocate ticks on your monitor. When using this pattern, always make sure you never allocate less than some minimum number of ticks or you'll consume the entire progress monitor on the first few iterations of your algorithm.
    void depthFirstSearch(IProgressMonitor monitor, Object root) {
        SubMonitor subMonitor = SubMonitor.convert(monitor);
        ArrayList queue = new ArrayList();
        queue.add(root);

        while (!queue.isEmpty()) {
            // Allocate a number of ticks equal to the size of the queue or some constant,
            // whatever is larger. This constant prevents the entire monitor from being consumed
            // at the start when the queue is very small. 
            subMonitor.setWorkRemaining(Math.max(queue.size(), 20));
            Object next = queue.remove(queue.size() - 1);
            processElement(next, subMonitor.split(1));
            queue.addAll(getChildrenFor(next));
        }
    }

3.6 Unknown progress

What about those situations where you really have no idea how much work to report or how long it will take? Try allocating a small percentage of the remaining space on each iteration.
    void unknownProgress(IProgressMonitor monitor) {
        SubMonitor subMonitor = SubMonitor.convert(monitor);
        while (hasMore()) {
            // Use 1% of the remaining space for each iteration
            processNext(subMonitor.setWorkRemaining(100).split(1));
        }
    }
Notice the idiom setWorkRemaining(denominator).split(numerator). This can be used at any point to consume numerator/denominator of the remaining space in a monitor.

3.7 Naming conventions

In these examples we've used the same naming convention that has been used within the Eclipse platform. You may wish to use the same convention to help convey the purpose of your progress monitors:

4.0 Responsibilities of callers and callees

Imagine you need to invoke another method that accepts an IProgressMonitor. What are the responsibilities of the caller and what are the responsibilities of the callee?

The caller:

The callee: In practice, callers will be using SubMonitor wherever possible and method implementations will not be calling done(). This means that the only calls to done() will occur in root-level methods (methods which obtain their own IProgressMonitor via some mechanism other than having it passed it as a method parameter).

5.0 Different versions of Eclipse

In Eclipse 4.5 (Mars) and earlier, it was standard practice for method implementations to invoke done() on any monitor passed in as an argument and for the caller to rely upon this fact.

In Eclipse 4.6 (Neon), method implementations should still invoke done() if they did so previously. Callers are also required to either invoke done() or select a monitor implementation like SubMonitor which doesn't require the use of done().

In Eclipse 4.7 (Oxygen) and higher, method implementations are not required to invoke done(). Callers must either invoke done() or select a monitor implementation like SubMonitor which doesn't require the use of done().

5.1 Changes in Eclipse 4.6

The following changes were made in Eclipse 4.6: As of Eclipse 4.6 (Neon), any client code that obtains a root monitor (any monitor that isn't passed in as a method argument) is responsible for invoking done() on that monitor. It must not rely on the methods it calls to invoke done(). Please see the Migration guide for more information on how to locate such code.

Method implementations that previously invoked done() should continue to do so, since the root monitors need to be updated first.

5.2 Migrating from SubProgressMonitor to SubMonitor

Eclipse 3.2 and earlier used SubProgressMonitor for nesting progress monitors. This class is deprecated as of Eclipse 4.6 (Neon). It has been replaced by SubMonitor.

The process for converting code which used SubProgressMonitor into SubMonitor is:

Note that SubMonitor.convert(monitor, ticks) is not a direct replacement for new SubProgressMonitor(monitor, ticks). The former fully consumes a monitor which hasn't had ticks allocated on it yet and creates a new monitor with the given number of ticks allocated. The latter consumes only the given number of ticks from an input monitor which has already had ticks allocated and produces a monitor with no ticks allocated. If you attempt to do a search-and-replace of one to the other, your progress reporting won't work.

5.3 Changes in Eclipse 4.7

In Eclipse 4.7 (Oxygen) and higher, method implementations that receive a progress monitor are not required to invoke done() on it. Such calls may be removed if they were present in earlier versions.

Methods in plugins that are also intended for use with earlier Eclipse versions should continue calling done() as long as those earlier Eclipse versions are still being supported by the plugin.

6.0 The protocol of IProgressMonitor

This section is for those of you doing something advanced. What if you want to implement your own IProgressMonitor or can't use the SubMonitor helper class? In that case you'll need to work with the IProgressMonitor contracts directly.

An IProgressMonitor instance can be in one of three states. Any given implementation may or may not track state changes and may or may not do anything about them.

UNALLOCATED

This is the initial state of a newly created IProgressMonitor instance, before beginTask() has been called to allocate ticks. Attempting to call worked() or otherwise consume ticks from a monitor in this state is an error. From here the monitor can enter the ALLOCATED state as a result of a beginTask() call or the FINISHED done() state as a result of a call to done().

ALLOCATED

The monitor enters this state after the first and only call to beginTask() has allocated ticks for the monitor. Attempting to call beginTask() in this state is an error. From here the monitor can enter the FINISHED state as a result of a done() call.

FINISHED

The monitor enters this state after the first call to done(). Unlike beginTask(), done() may be called any number of times. However, only the first call to done() has any effect. Reporting work or calling beginTask() on a FINISHED monitor is a programming error and will have no effect. Unless the implementation says otherwise, done() must always be called on a monitor once beginTask() has been called. It is not necessary to call done() on a monitor in the UNALLOCATED state.

6.1 Cancellation

A monitor can become cancelled at any time due to activity on another thread. The monitor indicates that it has been cancelled by returning true from isCanceled(). When a long-running operation detects that it has been cancelled, it should abort its operation cleanly.

It is technically possible to cancel a monitor by invoking setCanceled() but you should never do this. A long-running method that wishes to cancel itself it should throw OperationCanceledException rather than invoking any particular method on its monitor.

6.2 Threading

All implementations of IProgressMonitor are single-threaded by default. Unless the JavaDoc for the specific monitor implementation says otherwise, all methods must be invoked on the same thread that instantiated the monitor. If the concrete type of the IProgressMonitor is unknown, it must be assumed to be single-threaded.

6.3 Allocating ticks

When using beginTask to allocate ticks on an IProgressMonitor, the caller should always allocate at least 1000 ticks. Many root monitors use this value to set the resolution for all subsequent progress reporting, even if the ticks are later subdivided using a SubMonitor.

SubMonitor users don't need to worry about this since SubMonitor.convert does this internally when converting an unknown monitor.

6.4 Passing strings to beginTask

At the lowest level, any method that needs to allocate ticks ends up calling beginTask. Usually this is done indirectly by a call to SubMonitor.convert(...). Unfortunately, beginTask also takes a string argument which root monitors use to set the task name.

Every long-running operation needs to allocate ticks but most don't want to modify the task name. For this reason, most callers of beginTask call it with the empty string or a temporary string that isn't intended to be seen by the end user. Similarly, most implementations of beginTask are expected to ignore the string argument.

The exception is root monitors. Many root monitors display the string argument somewhere. For this reason, any code that obtains a root monitor is expected to convert it to a form that will filter out the string argument before passing it to another method.

The default expectation is that progress monitors passed as parameters will do this filtering. Any method that receives a progress monitor and does not want the beginTask(...) argument filtered must say so clearly in its JavaDoc.

6.5 IProgressMonitor.UNKNOWN

IProgressMonitor.UNKNOWN is only supported by root monitors and should never be used by method implementations which receive a progress monitor as a parameter.

6.6 Task names and performance concerns

As of the time of this writing (Eclipse 4.6), several of the root monitors in Eclipse have expensive implementations of setTaskName(...) and subTask(...). Plugin authors are advised not to call these methods more than 3 times per second. Doing so may introduce performance problems. See Eclipse bug 445802 for more information.