Bug 64937 - [Progress] Using IProgressService.busyCursorWhile() from modal dialogs
Summary: [Progress] Using IProgressService.busyCursorWhile() from modal dialogs
Status: VERIFIED FIXED
Alias: None
Product: Platform
Classification: Eclipse Project
Component: UI (show other bugs)
Version: 3.0   Edit
Hardware: PC Windows XP
: P2 critical (vote)
Target Milestone: 3.0 RC2   Edit
Assignee: Tod Creasey CLA
QA Contact:
URL:
Whiteboard:
Keywords: polish
: 64925 65024 (view as bug list)
Depends on:
Blocks: 65182 65504
  Show dependency tree
 
Reported: 2004-06-01 09:32 EDT by Darin Wright CLA
Modified: 2004-06-10 16:10 EDT (History)
7 users (show)

See Also:


Attachments
Patch for busyCursorWhile (11.26 KB, patch)
2004-06-04 09:42 EDT, Tod Creasey CLA
no flags Details | Diff
Test with the properties dialog (2.44 KB, patch)
2004-06-04 14:12 EDT, Tod Creasey CLA
no flags Details | Diff
Updated patch to open on blocked (11.32 KB, patch)
2004-06-04 14:13 EDT, Tod Creasey CLA
no flags Details | Diff
plug-in with my test setup (14.20 KB, application/octet-stream)
2004-06-04 17:19 EDT, Erich Gamma CLA
no flags Details
Updated patch (12.62 KB, patch)
2004-06-07 09:53 EDT, Tod Creasey CLA
no flags Details | Diff
Updated patch (13.60 KB, patch)
2004-06-07 10:58 EDT, Tod Creasey CLA
no flags Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Darin Wright CLA 2004-06-01 09:32:11 EDT
RC1

- lock workspace in background operation
- open installed JREs
- add a JRE
- press OK

observe: nothing happens. 

There should be a blocking dialog informing me about the conflict with the 
background operation. What is dangerous in this scenario is the fact that I 
can cancel the dialog and starting another operation which basically stacks 
operations.
Comment 1 Erich Gamma CLA 2004-06-01 10:52:59 EDT
The code is calling PlatformUI.getWorkbench().getProgressService
().busyCursorWhile() which is the correct thing to do.

Need to track down why busyCursorWhile() doesn't show the progress dialog.

Don't change your code until we understand the issue with busyCursorWhile().
Comment 2 Erich Gamma CLA 2004-06-01 11:01:10 EDT
The problem is that the ProgressDialog doesn't show up. 
IProgressService.busyCursorWhile() defers the appearance of the progress 
dialog until no more modal dialog is visible. However, in this scenario 
busyCursorWhile() is called from a modal dialog. This looks like a similar 
problem that existed in runInUI when called from a modal dialog. 

Moving to platform-ui and increasing the severity since you can easily 
generate exceptions once you are in this state.
Comment 3 Tod Creasey CLA 2004-06-01 11:12:26 EDT
IProgressService.busyCursorWhile() is speced to not open another dialog if 
there is one already open - we would break API if we made any change like that.

This was a basic design goal of this support as we do not want to block a 
request made from a running job. I don't think you should call busyCursorWhile 
from a modal dialog as we can't make the right decisions about disabling 
buttons etc.
Comment 4 Tod Creasey CLA 2004-06-01 11:12:44 EDT
Adding Erich for comment.
Comment 5 Tod Creasey CLA 2004-06-01 11:13:19 EDT
I think we need to be more explicit in the javadoc about the issues with 
running this from a modal dialog.
Comment 6 Christof Marti CLA 2004-06-01 12:12:41 EDT
*** Bug 64925 has been marked as a duplicate of this bug. ***
Comment 7 Christof Marti CLA 2004-06-01 12:13:51 EDT
*** Bug 65024 has been marked as a duplicate of this bug. ***
Comment 8 Erich Gamma CLA 2004-06-01 13:01:18 EDT
Making the javadoc more explicit doesn't fully address this issue.

1) The "Showing Modal Progress" document is wrong. It recommends to use 
IProgressService.busyCursorWhile() when the operation can run in the 
forked=true mode. The document can be fixed as well, but the progress 
migration story is getting more complex.

2) IProgressService.run() which implements IRunnableContext.run() has the same 
constraint when fork=true. This violates the API contract from 
IRunnableContext.run() (it makes the precondition stronger).

So when it comes to use IProgressService from a Modal Dialog then our story is:
run() - no, runInUI() - yes, busyCursorWhile() - no. 

This isn't consistent. Is there a way to make this more consistent. For 
example, by distinguishing whether:
1) busyCursorWhile() was called from a modal dialog
2) a modal dialog is up when the ProgressDialog is about to be shown after 
IProgressService.getLongOperationTime().
Comment 9 Tod Creasey CLA 2004-06-01 13:36:44 EDT
Whatever we do we should do it by RC2 so I am marking as such.

We have the following issues

1) If called from a modal dialog we cannot open if someone closes that dialog 
in the meantime - the parent would be null. 

As we cannot reparent a dialog without an API change at this stage we would 
have to just continue with the busy cursor and not show the dialog.

2) We have to know if the current modal dialog is the one that was open when 
we were called. This is just a case of caching the modal shell.

I am reluctant to go back allowing these dialogs to open on a modal shell as 
this is a challenge to get right - it also breaks our rule of trying to avoid 
multiple modal dialogs opened.

However I think what might be better is to allow them to open a blocked dialog 
in this case like in runInUI - for instance if I did:

Platform.getJobManager().beginRule(null,				
			new EventLoopProgressMonitor(new NullProgressMonitor
())));

Opinions? I think the lack of blocked notification is the bigger problem here.
Comment 10 Tod Creasey CLA 2004-06-01 15:29:23 EDT
I just had a look at the code and in JREPreferencePage and it calls the code 
below. Are you doing this somewhere else?

BusyIndicator.showWhile(null, new Runnable() {
			public void run() {
				IVMInstall defaultVM = getCurrentDefaultVM();
				IVMInstall[] vms = fJREBlock.getJREs();
				JREsUpdater updater = new JREsUpdater(getShell
());
				if (!updater.updateJRESettings(vms, 
defaultVM)) {
					canceled[0] = true;
				}
			}
		});
Comment 11 Darin Wright CLA 2004-06-01 15:36:55 EDT
This code is executed from anywhere the JREs pref page can be accessed - i.e. 
the standard pref pages, from "Installed JREs..." button on the JRE tab of a 
launch config, and the "define system library" quick fix (there may be 
others). However, it all uses the same code.
Comment 12 Tod Creasey CLA 2004-06-01 16:13:07 EDT
Adding a JRE does not use this API in my install of RC1. Search does (but it 
doesn't have this problem).

My steps are

 lock workspace in background operation
- open installed JREs preference page
- add a JRE
- press OK

and the code I pasted from JREPreferencePage gets called. I really need a 
better pointer and better steps to be sure this is the same code because right 
now this looks unrelated. Has it changed in HEAD since RC1?

Comment 13 Darin Wright CLA 2004-06-01 16:21:17 EDT
Debug has not changed since RC1 (i.e. no changes in HEAD). Dirk originally 
filed this problem (thru me, and bug 64928), so perhaps he can enlighten us on 
the test case.
Comment 14 Dirk Baeumer CLA 2004-06-02 05:46:29 EDT
YOu have two press OK to times in the end. First on the dialog adding the JRE 
and then on the preference dialog. When pressing OK on the preference dialog 
the dialog doesn't disappear and the cursor turns into a busy cursor. However 
the dialog is still enabled and I can easily press other controls.
Comment 15 Erich Gamma CLA 2004-06-02 06:36:50 EDT
The test case is correct, Tod is correct, my initial analysis wasn't 
correct... The problem isn't caused by IProgressService.busyCursorWhile() 
(I'll work on a test case for this scenario). The issue is that the blocked 
jobs dialog doesn't appear after pressing OK for the first time, that is, the 
user shouldn't be able to press OK a second time. 

I've attached the stack trace below, the operation is executed with a 
NullProgressMonitor which is then wrappered into an EventLoopProgressMonitor.

So let's continue from comment#9. A solution similar to the one used in 
runInUI would be desirable.

Thread [main] (Suspended)
	Object.wait(long) line: not available [native method]
	ThreadJob.joinRun(IProgressMonitor) line: 159
	ImplicitJobs.begin(ISchedulingRule, IProgressMonitor, boolean) line: 87
	JobManager.beginRule(ISchedulingRule, IProgressMonitor) line: 170
	WorkManager.checkIn(ISchedulingRule, IProgressMonitor) line: 95
	Workspace.prepareOperation(ISchedulingRule, IProgressMonitor) line: 
1628
	Workspace.run(IWorkspaceRunnable, ISchedulingRule, int, 
IProgressMonitor) line: 1668
	SetClasspathOperation(JavaModelOperation).runOperation
(IProgressMonitor) line: 744
	JavaProject.setRawClasspath(IClasspathEntry[], IPath, 
IProgressMonitor, boolean, IClasspathEntry[], boolean, boolean) line: 2655
	JavaCore$3.run(IProgressMonitor) line: 3433
	BatchOperation.executeOperation() line: 34
	BatchOperation(JavaModelOperation).run(IProgressMonitor) line: 700
	Workspace.run(IWorkspaceRunnable, ISchedulingRule, int, 
IProgressMonitor) line: 1673
	JavaCore.run(IWorkspaceRunnable, ISchedulingRule, IProgressMonitor) 
line: 3246
	JavaCore.setClasspathContainer(IPath, IJavaProject[], 
IClasspathContainer[], IProgressMonitor) line: 3416
	JREContainerInitializer.initialize(IPath, IJavaProject) line: 51
	LaunchingPlugin$VMChanges.rebind() line: 250
	LaunchingPlugin$VMChanges.access$0(LaunchingPlugin$VMChanges) line: 214
	LaunchingPlugin$1.run(IProgressMonitor) line: 204
	BatchOperation.executeOperation() line: 34
	BatchOperation(JavaModelOperation).run(IProgressMonitor) line: 700
	Workspace.run(IWorkspaceRunnable, ISchedulingRule, int, 
IProgressMonitor) line: 1673
	JavaCore.run(IWorkspaceRunnable, ISchedulingRule, IProgressMonitor) 
line: 3246
	LaunchingPlugin$VMChanges.process() line: 207
	LaunchingPlugin.processVMPrefsChanged(String, String) line: 586
	LaunchingPlugin.propertyChange(Preferences$PropertyChangeEvent) line: 
493

Comment 16 Tod Creasey CLA 2004-06-02 08:04:27 EDT
I have logged Bug 65277 about the preference page so that we can concentrate 
on the busyCursorWhile issue in this discussion.

There is still one thing that would be an issue if we used the same solution 
as runInUI - namely that you could try and open the progress dialog on a 
disposed shell.

It is somewhat more acute in this case as delay the opening of the dialog for 
250ms which is ample time for the parent to be destroyed.

I think the only safe solution is to open on the shell we start with and only 
block on different shells. The issue is what to do if that shell is disposed 
in the meantime. 

If the API freeze hadn't happened we could just reparent (Dialogs don't allow 
reparenting currently). As we can't do that I could

a) Cache the starting shell and change getParentShell() to make the right 
decision - i.e. not return the parent if it is disposed. This is a little 
risky but I think it can be done without serious breakage. I would add a check 
to see if the modal shell open was the parent and not block if so.

b) Never open the progress dialog if the parent is disposed - leave it as the 
busy cursor.

I think a) is likely the only acceptable solution. Opinions?
Comment 17 Erich Gamma CLA 2004-06-02 09:15:28 EDT
Just to summarize where we are at. There are two issues:

1) when executing an operation from a dialog with a null progress monitor then 
the blocked jobs dialog doesn't show up (this is the original problem of this 
bug report). This one is now tracked in bug# 65277

2) when calling IProgressService.busyCursorWhile() from a modal dialog 
(PreferenceDialog, PropertiesDialog, or a custom dialog) then the progress 
dialog doesn't show up. I could verify this behaviour in a test case. This 
issue is tracked in this bug report. I've changed the bug title to match the 
discussion.

Continuing with problem 2). The API freeze is real, but the PMC can approve 
API additions. What would the API addition be and how would it simplify the 
solution. I agree that b) isn't an acceptable solution, but if an API addition 
makes the implementation of a) simpler and more robust then we should pursue 
this path.
Comment 18 Tod Creasey CLA 2004-06-02 09:28:55 EDT
1) The case in Bug 65277 is unrelated to the progress support. It is just a 
safety check for the preference dialog. The oroblem we have now is that we do 
not ever open when something is not using the progress API and a modal dialog 
is open. I think we won't want to change that.

2) I'm not sure that we need to ask for an API change - I can infer the open 
modal dialog as well as have it passed in. The issue with the Dialog class is 
not a general problem so I could always copy code down and refactor post 3.0.
Comment 19 Tod Creasey CLA 2004-06-02 11:03:32 EDT
Stefan has come up with a much better solution.

The issue we have is that we don't know if the modal dialog that is up is 
something that is blocking the progress or not. Inferring something from the 
dialog is hard to do as there are no pointers to our code.

If we infer instead based on ticks from the progress we can know if we are 
blocked or not - that is if we get a worked() call at any point in that 800 ms 
we know that progress is not blocked and we can open the progress dialog. 
Likewise if showBlocked is called on the dialog we know that it can't do 
anything yet and can open it.

If not then it is blocked on user input and we should wait. This relies on 
users of this API to update thier ticks when possible but that it a good idea 
anyways.
Comment 20 Erich Gamma CLA 2004-06-03 05:17:44 EDT
>1) The case in Bug 65277 is unrelated to the progress support. It is just a 
>safety check for the preference dialog. The oroblem we have now is that we do 
>not ever open when something is not using the progress API and a modal dialog 
>is open. I think we won't want to change that.
I don't like that we cannot provide more support to make this better, but I 
have to agree with you. The real problem here is that clients are using null 
progress monitors when they shouldn't. We the new responsiveness work almost 
any operation can be long running since it can block. Filed a separate bug for 
the original issue bug# 65504.
Comment 21 Erich Gamma CLA 2004-06-03 17:46:02 EDT
Here is another idea for how to solve this problem...

Make the ProgressMonitorJobsDialog API. This would allow the client to pass in 
the parent shell. 

+ no new code in a sensible area
- an API addition (I would bring this forward to the rest of the PMC)
- you don't get the delayed dialog appearance (which I would tolerate given 
where we are at).
Comment 22 Tod Creasey CLA 2004-06-04 07:39:06 EDT
But then that defeats the purpose of busyCursorWhile - that is that you can a 
busy cursor if your operation is less than 800ms. If we open imemdiately it is 
no different that ProgressMonitorDialog.run() except that you will get the 
ProgressMonitorJobsDialog instead.

I think I would like to take Stefans idea and see how it plays - I think the 
code change is hopefully low risk.
Comment 23 Erich Gamma CLA 2004-06-04 08:58:11 EDT
If you consider the code change low risk then I also prefer to go for the full 
solution and supporting a delayed appearance. 
Comment 24 Tod Creasey CLA 2004-06-04 09:41:55 EDT
I will attach a suggested patch. I have tested it against the JRE preference 
page Search button.

Now if you open a progress monitor jobs dialog and ask it if it ticking I set 
a flag where I track it and try again in 800 ms. If it gets a tick in that 
time (i.e. anything sent to the progress monitor) I will then open the dialog.

There was also a bug where the dialog could be closed by the end of the job 
and then re-opened. I have included a fix for that by setting a flag on close 
that the rescheduling algorithm checks.

I tested using the JUnitPlugin.createAllPackagesDialog method using the JUnit 
Preference Page.

I added in a series of delays 

- no delay is fast enough that the reschedule check never happens
- 50ms delay creates a reschedule check but the runnable is done. I check the 
closed dialog case with this
- 100 ms delay will bring up the progress dialog.

One thing I am tempted to do is to reschedule the opening sooner (maybe short 
operation rather than long operation time). Opinions?

Erich if you could apply this patch and let me know if it solves your problem 
that would be very much appreciated.
Comment 25 Tod Creasey CLA 2004-06-04 09:42:30 EDT
Created attachment 11591 [details]
Patch for busyCursorWhile
Comment 26 Erich Gamma CLA 2004-06-04 12:08:07 EDT
Here is what I'm observing when running with the patch.
The testcase is to execute a runnable with busyCursorWhile() from the 
PropertiesDialog.performOK() method.

1) workspace isn't locked
- press OK
- the jobs progress dialog appears after a delay
->this is the expected behaviour

2) lock the workspace 
- press OK
->only the busy cursor shows up, no progress dialog is shown and I'm not 
informed about the blockage.

Case 2) is not what I would have expected/wished. I would have wished that the 
ProgressDialog appears (immediatly) with the details area expanded and I can 
cancel the blocking job consistent with our "user is in control" goal.

This means a) we now get the delayed dialog appearance but b) we don't get the 
blockage information. 

To illustrate the behaviour I would like to get replace busyCursorWhile(r) 
with run(true, false, r) (which is a kludge, to get the progress dialog to 
appear immediatly).
Comment 27 Tod Creasey CLA 2004-06-04 12:44:53 EDT
Good idea Erich. I'll keep working on that case.
Comment 28 Tod Creasey CLA 2004-06-04 14:09:02 EDT
Erich there is no class PropertiesDialog and PropertyDialog does not have a 
performOK method. Is this a JDT thing?

Either way could you attach a patch with your test case? I think I have what 
you need but I am not sure how to test it.
Comment 29 Tod Creasey CLA 2004-06-04 14:12:19 EDT
Created attachment 11606 [details]
Test with the properties dialog

I think this is the test condition you were talking about
(PropertiesDialog#okPressed). When I applied the updated patch and tested this
worked.
Comment 30 Tod Creasey CLA 2004-06-04 14:13:18 EDT
Created attachment 11607 [details]
Updated patch to open on blocked
Comment 31 Tod Creasey CLA 2004-06-04 14:14:20 EDT
I have updated the patch.

The test case I used was

1) Apply the patch
2) Start a job that locks the workbench
3) Open a properties dialog on a project
4) Modify the referenced projects
5) Hit save
Comment 32 Erich Gamma CLA 2004-06-04 17:19:56 EDT
Created attachment 11620 [details]
plug-in with my test setup

Winzip with test plug-in
Comment 33 Erich Gamma CLA 2004-06-04 17:21:59 EDT
I've attached my test setup in a plug-in above.
The plugin contributes a property page "Progress Sample". Pressing OK start a 
long running operation.

I'll verify the behaviour with your updated patch as well.
Comment 34 Erich Gamma CLA 2004-06-04 17:35:06 EDT
Tested with the updated patch, but I still see no progress dialog in the 
blocked case. This confuses me since our test cases are very similar.

Here are my steps:
1) start a job that is locking the workspace from the JobFactory view
2) select a project 
3) open Properties>Sample Progress
4) press OK
->busy cursor only
Comment 35 Tod Creasey CLA 2004-06-07 09:52:51 EDT
I have tested with your test plug-in and your steps are fine for me.

However I had a problem applying this patch against HEAD so I suspect you 
might have been looking at a different state.

I am updating the patch.
Comment 36 Tod Creasey CLA 2004-06-07 09:53:26 EDT
Created attachment 11657 [details]
Updated patch
Comment 37 Tod Creasey CLA 2004-06-07 10:57:40 EDT
Erichs test case uncovered had a timing issue which I will update with a 
patch. If you setBlocked before the first tick was checked there would never 
be another one.

The updated patch sets watching of ticks to true if you are blocked thereby 
always opening when blocked.
Comment 38 Tod Creasey CLA 2004-06-07 10:58:52 EDT
Created attachment 11663 [details]
Updated patch
Comment 39 Tod Creasey CLA 2004-06-07 11:17:53 EDT
Patch submitted.

John and I talked and there is still one concern about a possible problem case.

If the following occurs:

1) Tick happens
2) Question Dialog is opened
3) Check the tick again and open progress dialog

The progress dialog will cover the question dialog and you will have a 
deadlock. This is a pretty dangerous case and we should decide how to handle 
this i.e. do we document it or try some greater protection?
Comment 40 Stefan Xenos CLA 2004-06-07 12:30:15 EDT
Comment 19 doesn't describe the algorithm I suggested. This was the suggestion:

1. Job starts. Take a timestamp.
2. Inside the handler for worked(), do this:
{
  if (currentTime - initialTimestamp > 800ms) {
    open progress dialog
  }
}

This doesn't use any background jobs, timers, retries, or have any memory of
when past ticks have occurred. The progress dialog is opened inside the worked()
handler -- not in a background thread that checks if worked() has been called.
Since worked() is called from the thread we're monitoring progress from, there
are no concurrency issues.
Comment 41 John Arthorne CLA 2004-06-07 13:38:26 EDT
Stefan's approach in comment #40 certainly sounds quite a lot simpler than the
existing code. It avoids the possibility of multiple modal shell deadlock
because it can't possibly be in a syncExec asking the user a question at the
instant that it is reporting work. It would also fix bug 65951.
Comment 42 Tod Creasey CLA 2004-06-07 14:28:01 EDT
Stefan and I have gone through the deadlock case and have reworked the 
algorithm somewhat (and released it).

Now we record the time of the first tick and open the dialog when a tick 
happens after 800 ms. This way a dialog opening in the meantime will block the 
tick and hence not open the dialog.

By doing this via a callback from a tick rather than a polling from a tick we 
save ourselves having to keep state to check.

I have also modified Erichs test case to open dialogs at some inconvenient 
moments. I will attach it for verfication.
Comment 43 Tod Creasey CLA 2004-06-09 11:40:40 EDT
Verified in 20040609 using the task dialog from an editor
Comment 44 John Arthorne CLA 2004-06-10 15:20:02 EDT
I noticed that ProgressManager.busyCursorWhile is still scheduling a job to open
the progress dialog. Is this needed in light of Stefan's fix to open the dialog
from the worked callback? I am concerned because in some cases thousands of
instances of this job were being created and flooding the job manager.
Comment 45 Tod Creasey CLA 2004-06-10 15:29:16 EDT
Yes it is - otherwise we are relying even more heavily on the user of the API 
to use the IProgressMonitor API.

How is it flooding the job manager? If we are too aggressive we could look 
into that.
Comment 46 John Arthorne CLA 2004-06-10 16:10:27 EDT
After retrying the scenario in bug 65415, it is no longer as bad. You have taken
out the code that rescheduled the "Open Progress Monitor" job if a modal shell
was open. So, there are are still 1500 instances of this job being created in
this scenario, but they no longer reschedule themselves for the duration of the
operation. It would be more optimal to use a singleton to avoid the overhead of
all these jobs, but it is not as critical as before.