Skip to main content

Notice: this Wiki will be going read only early in 2024 and edits will no longer be possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.

Jump to: navigation, search

Difference between revisions of "E4/EAS/Progress Service"

< E4‎ | EAS
(Eclipse 3.x API)
(Critique)
 
(25 intermediate revisions by 3 users not shown)
Line 1: Line 1:
 +
__TOC__
 +
 
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.
 
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.
  
Line 8: Line 10:
 
*org.eclipse.jface.operation.IRunnableWithProgress
 
*org.eclipse.jface.operation.IRunnableWithProgress
 
*org.eclipse.core.runtime.jobs.Job
 
*org.eclipse.core.runtime.jobs.Job
 +
*org.eclipse.core.runtime.jobs.ProgressProvider
  
Typically, for scheduling work to be completed by a worker thread, the Jobs framework is used.
+
Suppose an application wants to do some work, reporting some progress and errors.  The essential application code ('''what''' is being done) is
 +
<source lang="java">
 +
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();
 +
}
 +
</source>
 +
 
 +
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.
  
 
<source lang="java">
 
<source lang="java">
 
Job aJob = new Job("My long-running operation...") {
 
Job aJob = new Job("My long-running operation...") {
 
   protected IStatus run(IProgressMonitor monitor) {
 
   protected IStatus run(IProgressMonitor monitor) {
     // do stuff and report progress via the IPM
+
  IStatus status = Status.OK_STATUS;
     return 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
 
// true to indicate that this job was initiated by a UI end user
Line 22: Line 48:
 
</source>
 
</source>
  
In the IProgressService and IWorkbenchSiteProgressService case, these are queried from the local service locator.
+
To show progress for the same work in a wizard, IRunnableWithProgress is used.
  
Critique:
+
<source lang="java">
* You need to know how your code is going to be used (in a worker thread) at the time you create it (I need a Job).
+
IRunnableWithProgress runnable = new IRunnableWithProgress() {
* You need to decide in the client code whether the user sees the job
+
  public void run(IProgressMonitor monitor) {
* We have no headless IRunnableWithProgress(Today we have JFace IRunnableWithProgress, Job.run(IProgressMonitor), IDE's WorkspaceModifyOperation, Equinox future's IProgressRunnable). If we had a core-level IRunnableWithProgress then applications could define runnables that could be run in different runnable contexts (UI modal context, user job, future, etc.) Today the application has to define its own operations framework to handle these differences.
+
    monitor.beginTask("foo", 100);
* Likewise, we need a way to ensure that the runnables can be run in different runnable contexts (ie, a user job) with progress monitors and error reporting that is appropriate for where they are triggered. See [https://bugs.eclipse.org/bugs/show_bug.cgi?id=293098 Bug 293098].
+
    try {
* Examples of 3.x limitations
+
      doFirstHalfFoo();
** Jobs and wizards/dialogs don't play well together
+
      monitor.worked(50);
***If you a run a job in a modal dialog, there is no jobs progress dialog and your only progress indicator is the status bar unless you happen to have the progress view open.
+
      doSecondHalfFoo();
***You can get weird blocking dialogs (See [https://bugs.eclipse.org/bugs/show_bug.cgi?id=276904 Bug 276904])
+
    } catch (FooException e) {
** Some constructs (DeferredTreeContentManager) use jobs and only know about certain progress contextsIf I run a deferred fetch in a site, the site is adorned and the job progress is reported normally.  If I run the same viewer in a dialog, there is no local progress.
+
      throw new InvocationTargetException(e);
** 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.
+
    } finally {
** If you want to show progress in multiple places, you have to wrap the monitor.  If you are using jobs this means defining your own framework so you can intercept the monitor on the run method and wrap it.
+
      monitor.done();
* 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 [https://bugs.eclipse.org/bugs/show_bug.cgi?id=282333 Bug 282333] and [https://bugs.eclipse.org/bugs/show_bug.cgi?id=244246 Bug 244246].
+
    }
 +
  }
 +
};
 +
 
 +
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 */
 +
}
 +
 
 +
</source>
 +
 
 +
Things get more complicated as soon as you deviate from the expected usage patternSuppose 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.
 +
 
 +
<source lang="java">
 +
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());
 +
    }
 +
  }
 +
});
 +
</source>
 +
 
 +
===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 --[[User:John arthorne.ca.ibm.com|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)==
 
==e4 (Java)==
It would be nice to create a generic runnable (with progress.  is it injected?)
+
Idea 1:  Status returning runnable with progress (like Job)
 
<source lang="java">
 
<source lang="java">
 +
@Inject Provider<RunnableContext> runnableContext;
 +
 
IRunnable runnable = new Runnable("My long-running operation...") {
 
IRunnable runnable = new Runnable("My long-running operation...") {
 
   protected IStatus run(IProgressMonitor monitor) {
 
   protected IStatus run(IProgressMonitor monitor) {
Line 57: Line 160:
 
   }
 
   }
 
};
 
};
 +
runnableContext.get().run(runnable);
 
</source>
 
</source>
  
Another alternative is that status and progress are both injected, so that there is no return status from the runnable itself.  There is likely some relationship between the runnable context and how/where status is reportedNot sure who knows about who.
+
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.
  
 
<source lang="java">
 
<source lang="java">
Line 79: Line 185:
 
   }
 
   }
 
};
 
};
 +
runnableContext.get().run(runnable);
 
</source>
 
</source>
 +
 +
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  
 
The runnable context defines  
Line 86: Line 197:
 
** Modal (forked but blocking on result)  
 
** Modal (forked but blocking on result)  
 
** Asynchronously (in the background)
 
** Asynchronously (in the background)
* How progress is reported
+
* how to get progress and status reporting
* How status is reported
+
* deal with UI-thread safety issues
 
* Locks and scheduling rules (ISchedulingRule)
 
* Locks and scheduling rules (ISchedulingRule)
 
* Batching of operations/notifications (IWorkspaceOperation)
 
* Batching of operations/notifications (IWorkspaceOperation)
Line 97: Line 208:
 
* futures (same as above with post-processing)
 
* futures (same as above with post-processing)
  
 +
Need to review the AC's [[Architecture Council/Minutes May 15 2008#Becoming More Asynchronous|meeting]] [[Architecture Council/Minutes May 15 2008#Jobs and Scheduling_Rules|minutes]] prior to the creation of this e4 API.
  
The main idea here is that how the code is organized (the runnable) is separate from how progress and errors are reported, and you shouldn't have to know in advance when writing the code how your code will be run or where things get reported.
 
  
'''Questions:'''
+
[[Category:E4]]
* Is there really a runnable context (implemented by various UI elements such as parts, wizards, etc.)  Who is told to run the runnable and how? 
+
* Is a localized status and progress reporting service inferred by a particular kind of runnable context or is it injected, or is it obtained from the IEclipseContext?
+
 
+
 
+
Need to review the AC's [[Architecture Council/Minutes May 15 2008#Becoming More Asynchronous|meeting]] [[Architecture Council/Minutes May 15 2008#Jobs and Scheduling_Rules|minutes]] prior to the creation of this e4 API.
+

Latest revision as of 15:06, 19 March 2010

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.

Back to the top