Eclipse Corner Article |
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
October 19, 2016
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.
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(...)
.
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.
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.
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. }
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); }
In some rare cases, you really need to perform an explicit cancellation check
at a specific time and can't rely on the sparse cancellation checks done
by split
. In such cases, you can use the SubMonitor.checkCanceled
utility introduced in Eclipse 4.7.
For example, this code converts an IProgressMonitor to a SubMonitor while performing a guaranteed cancellation check:
SubMonitor subMonitor = SubMonitor.convert(monitor).checkCanceled();
org.eclipse.equinox.common/debug=true org.eclipse.equinox.common/progress_monitors=true
Once enabled, you will see a warning written to the log whenever your code violates the API contracts on a SubMonitor. The tracing options can also be enabled in the tracing tab of any launch configuration. If you maintain any code that reports progress, it's generally a good idea to leave these options enabled at all times.
If you see nothing, it either means that your code is working perfectly or that the diagnostic tool isn't running. You can confirm that the diagnostic tool is running by using your debugger to confirm that the following variable is true:
org.eclipse.core.internal.runtime.TracingOptions.debugProgressMonitors
More information about enabling Eclipse tracing options can be found here.
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)); }
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.
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)); }
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)); } }
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)); } }
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.
OperationCanceledException
while running the very
code that reacts to OperationCanceledException
. For this reason, you should use the
SUPPRESS_ISCANCELED
flag whenever creating a child monitor within a catch or finally
block.
void tryFinallyBlockExample(IProgressMonitor monitor) { SubMonitor subMonitor = SubMonitor.convert(monitor, 2); try { doOperation(subMonitor.split(1)); } catch (SomeException e) { handleException(subMonitor.setWorkRemaining(2) .split(1, SubMonitor.SUPPRESS_ISCANCELED | SubMonitor.SUPPRESS_BEGINTASK)); } finally { doFinallyBlock(subMonitor .split(1, SubMonitor.SUPPRESS_ISCANCELED | SubMonitor.SUPPRESS_BEGINTASK)); } }Notice that this example also uses the
SUPPRESS_BEGINTASK
flag. When passing flags to
split
or newChild
, you should always include the
SUPPRESS_BEGINTASK
flag unless you have a specific reason not to.
The caller:
beginTask
invoked on it yet
(or an implementation such as SubMonitor which permits beginTask to be invoked multiple times).done()
on the monitor. The
caller must either use an SubMonitor (or a similar implementation which does not require done()
to be invoked), or it must take responsibility for calling done()
on the monitor after the
callee has finished.beginTask
unless the JavaDoc
for the callee says that it requires otherwise.beginTask
0 or 1 times on the monitor, at its option.done()
on the monitor, although it is allowed to do so.setCanceled
on the monitor.beginTask
unless
its JavaDoc says otherwise.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).
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()
.
done(IProgressMonitor)
method that can be used to
call done()
on a possibly-null IProgressMonitor instance.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.
The process for converting code which used SubProgressMonitor into SubMonitor is:
IProgressMonitor.beginTask
on the root monitor should be replaced by a call
to SubMonitor.convert
. Keep the returned SubMonitor around as a local variable and refer
to it instead of the root monitor for the remainder of the method.new SubProgressMonitor(IProgressMonitor, int)
should be replaced by calls to
SubMonitor.split(int)
.SubMonitor.split(int, int)
using SubMonitor.SUPPRESS_SUBTASK
as the second argument.SubMonitor
.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.
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.
Eclipse 4.7 also introduced the SubMonitor.checkCanceled
utility for convenient
explicit cancellation checks.
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.
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.
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.
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.
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.