Jump to: navigation, search

E4/EAS/Progress Service

< E4‎ | EAS

Based on a user's interaction with an application, the application may wish to schedule work to be done by other worker threads. The work that is done on these other worker threads may or may not be presented to the user. If background work is being invoked automatically based on some conditions, it may not necessarily be useful to present this information to the user. However, if the user explicitly invokes an action that causes a long-running operation to begin, they may wish to know about this so that they can respond accordingly when the operation has completed or just cancel the operation outright.

Eclipse 3.x API

In Eclipse 3.x, there are a variety of classes and interfaces that exposes progress reporting and work scheduling features.

  • org.eclipse.jface.operation.IRunnableContext
  • org.eclipse.ui.progress.IProgressService
  • org.eclipse.ui.progress.IWorkbenchSiteProgressService
  • org.eclipse.jface.operation.IRunnableWithProgress
  • org.eclipse.core.runtime.jobs.Job
  • org.eclipse.core.runtime.jobs.ProgressProvider

Suppose an application wants to do some work, reporting some progress and errors. The essential application code (what is being done) is

monitor.beginTask("foo", 100);
try {
   doFirstHalfFoo();
   monitor.worked(50);
   doSecondHalfFoo();
} catch (FooException e) {
   status = new Status(IStatus.ERROR, "id", "foo failure message", e);
} finally {
   monitor.done();
}

The code structure depends on how the code is run. For scheduling work to be completed by a worker thread, the Jobs framework is used.

Job aJob = new Job("My long-running operation...") {
  protected IStatus run(IProgressMonitor monitor) {
  IStatus status = Status.OK_STATUS;
    monitor.beginTask("foo", 100);
    try {
       doFirstHalfFoo();
       monitor.worked(50);
       doSecondHalfFoo();
    } catch (FooException e) {
       status = new Status(IStatus.ERROR, "id", "foo failure message", e);
    } finally {
       monitor.done();
    }
    return status;  }
};
// true to indicate that this job was initiated by a UI end user
aJob.setUser(true);

To show progress for the same work in a wizard, IRunnableWithProgress is used.

IRunnableWithProgress runnable = new IRunnableWithProgress() {
  public void run(IProgressMonitor monitor) {
    monitor.beginTask("foo", 100);
    try {
       doFirstHalfFoo();
       monitor.worked(50);
       doSecondHalfFoo();
    } catch (FooException e) {
       throw new InvocationTargetException(e);
    } finally {
       monitor.done();
    }
   }
};
 
try {
   getContainer().run((true, true, runnable);
} catch (InvocationTargetException e) {
   Status status = new Status(IStatus.ERROR, "id", "foo failure message", e.getCause());
   StatusManager.getManager().handle(status, StatusManager.SHOW);
} catch (InterruptedException e) {
   /* user canceled the operation, reset wizard state / do something about it */
}

Things get more complicated as soon as you deviate from the expected usage pattern. Suppose you want to report the progress of your job somewhere in addition to the progress view, and report errors locally. This has to happen in the client code.

Job aJob = new Job("My long-running operation...") {
  protected IStatus run(IProgressMonitor monitor) {
    IStatus status = Status.OK_STATUS;
    IProgressMonitor wrappedMonitor = new ProgressMonitorWrapper(monitor) {
       // override everything so that you can report it locally, too
        public void beginTask(String name, int totalWork) {
		super.beginTask(name, totalWork);
                someLocalMonitor.beginTask(name, totalWork);
	}
    };
    wrappedMonitor.beginTask("foo", 100);
    try {
       doFirstHalfFoo();
       wrappedMonitor.worked(50);
       doSecondHalfFoo();
    } catch (FooException e) {
       status = new Status(IStatus.ERROR, "id", "foo failure message", e);
    } finally {
       wrappedMonitor.done();
    }
    return status;  }
};
// true to indicate that this job was initiated by a UI end user
aJob.setUser(true);
// don't report errors in a separate dialog
aJob.setProperty(IProgressConstants.NO_IMMEDIATE_ERROR_PROMPT_PROPERTY, Boolean.TRUE);
// report errors the way I want to
aJob.addJobListener(new JobChangeAdapter() {
  public void done(JobChangeEvent event) {
    if (!status.isOK()) {
      updateStatus(event.getResult());
    }
  }
});

Critique

  • More code for how than what.
  • Code duplication since it's not easy to define what independently from how. See bug 293098.
  • Jobs and wizards/dialogs don't play well together
    • Need to wrap the monitor if you run a job or user can't see what's going on.
    • You can get unexpected blocking dialogs (see bug 276904)
    • Some constructs use jobs and provide no way to redirect progress. (DeferredTreeContentManager is IWorkbenchSiteProgressService savvy but can't show progress elsewhere and there's no easy way to wrap the monitor).
  • If you run some operation synchronously and want to report failure, you have to decide in-line whether its logged, shown, or both, but that decision is not always known on the calling side.
  • You can get broken if you call code that forks a job deep in the bowels of its implementation and your monitor wasn't expecting it. See bug 282333 and bug 244246.

Comments from --JohnA 15:30, 30 October 2009 (UTC) :

  • It's clear we need some common abstraction of the thing to run (Runnable, Job, IRunnableWithProgress, etc). Many components have worked around this with various bridging utilities. It needs to live low down so all bundles can use it
  • I think some decisions about the how will always need to be made at or very near the code that does the what. This isn't to say we can't have an abstraction of the mechanism used to execute the code, but there needs to be a way to provide various kinds of inputs to that mechanism. Some things that the what provider might want to tell the how provider about:
    • In same thread or different thread (IRunnableContext's notion of fork)
    • Synchronous vs Asynchronous
    • Run now, run later, run repeatedly
    • Should the end user be unaware (system job), have background awareness (regular job), or in-your-face awareness (user job)?
  • A big advantage of Jobs was to decouple the role of progress provider from the client scheduling the job. This prevented having to thread the progress monitor deep into the internals where the fact that it's scheduling anything at all is a hidden implementation detail. This made client code easier to write, but in some cases that connection between progress provider and the scheduled job is important (for example if you wanted to report job progress in a dialog).
  • We've been doing battle with singletons in e4, and this case is no exception. A big source of problems for jobs is the singleton ProgressProvider, which is global and has no way of accessing or knowing about the local context in which the job was scheduled. Perhaps if the ProgressProvider wasn't a singleton, and we had some contextual information on the job/runnable being scheduled, we'd be able to redirect progress to a different provider on a case by case basis.
  • On the other hand, a single global knowledge of all the work units currently running can be very useful. For example we can say the job you are attempting to execute in this context cannot proceed because it's blocked by some other work executed in some other context. Do we really want different progress providers, or do we just need a way to hook into that mechanism to redirect progress elsewhere (and not have to perform that redirection down near the what code. For example a dialog that opens can declare interest in jobs scheduled while it is open and be able to access/listen to the progress information)?
  • See the org.eclipse.equinox.concurrent.future package where an attempt has been made to provide some of the common abstractions: IProgressRunnable, IExecutor, etc. I think some of the details of this package are wrong but it's a very good starting point. Equinox is probably the right place for this abstraction stuff to ultimately live so it can be used by all.
  • Open question: When work is scheduled from within a different context such as a dialog, is it only presentation of progress that we want to redirect into the dialog, or the entire task of executing the work? What happens to the work that may still be running when the dialog is closed? Does the work get canceled, do we lose any presentation of progress for that work, or does the progress get redirected to a different context?

e4 (Java)

Idea 1: Status returning runnable with progress (like Job)

@Inject Provider<RunnableContext> runnableContext;
 
IRunnable runnable = new Runnable("My long-running operation...") {
  protected IStatus run(IProgressMonitor monitor) {
    IStatus status = Status.OK_STATUS;
    monitor.beginTask("foo", 100);
    try {
       doFirstHalfFoo();
       monitor.worked(50);
       doSecondHalfFoo();
    } catch (FooException e) {
       status = new Status(IStatus.ERROR, "id", "foo failure message", e);
    } finally {
       monitor.done();
    }
    return status;
  }
};
runnableContext.get().run(runnable);

This pattern requires a RunnableContext who knows how to run the runnable (fork, cancelable, UI-thread, etc), pass the appropriate monitor to the runnable, and report the returned status.

Idea 2: Simple runnable that obtains progress and status from injected services.

@Inject Provider<StatusHandler> statusHandler;
@Inject Provider<RunnableContext> runnableContext;
IRunnable runnable = new Runnable("My long-running operation...") {
  protected IStatus run() {
    IProgressMonitor monitor = runnableContext.get().getProgressMonitor();
    monitor.beginTask("foo", 100);
    try {
       doFirstHalfFoo();
       monitor.worked(50);
       doSecondHalfFoo();
    } catch (FooException e) {
       statusHandler.get().handle(e, "foo failure message", runnableContext.get().getStatusReportingService()); 
    } finally {
       monitor.done();
    }
  }
};
runnableContext.get().run(runnable);

This pattern still requires a RunnableContext for controlling how the code is run (fork, cancelable, UI-thread, modal, etc.), but the client code is injected with appropriate status and progress services.

The main win is that the runnable code (what) is the same no matter how progress and errors are reported. Complex scenarios involving progress wrapping are handled outside of the client.

The runnable context defines

  • how something is run.
    • Synchronous (in current thread)
    • Modal (forked but blocking on result)
    • Asynchronously (in the background)
  • how to get progress and status reporting
  • deal with UI-thread safety issues
  • Locks and scheduling rules (ISchedulingRule)
  • Batching of operations/notifications (IWorkspaceOperation)

Example contexts include:

  • wizard or dialog modal context (local progress and error reporting)
  • parts (adornment when busy or local progress, centralized error reporting via dialog?)
  • operations on a shared set of resources
  • jobs (not tied to any visible part, centralized error and progress reporting)
  • futures (same as above with post-processing)

Need to review the AC's meeting minutes prior to the creation of this e4 API.