Bug 500332 - Store JobInfo in the JobMonitor

I prevents accessing a concurrent hash map when it's not needed
(monitor's methods are only invoked from the job's thread).

Change-Id: Icaeb468daf4ac55cff861413fd1718d7ed257264
Signed-off-by: Mikael Barbero <mikael@eclipse.org>
diff --git a/bundles/org.eclipse.ui.workbench/Eclipse UI/org/eclipse/ui/internal/progress/ProgressManager.java b/bundles/org.eclipse.ui.workbench/Eclipse UI/org/eclipse/ui/internal/progress/ProgressManager.java
index c0ff8a1..78dd550 100644
--- a/bundles/org.eclipse.ui.workbench/Eclipse UI/org/eclipse/ui/internal/progress/ProgressManager.java
+++ b/bundles/org.eclipse.ui.workbench/Eclipse UI/org/eclipse/ui/internal/progress/ProgressManager.java
@@ -96,7 +96,7 @@
 
 	private static ProgressManager singleton;
 
-	final private ConcurrentMap<Job, JobInfo> jobs = new ConcurrentHashMap<>();
+	final private Set<Job> managedJobs = ConcurrentHashMap.newKeySet();
 
 	final private Map<Object, Collection<IJobBusyListener>> familyListeners = Collections
 			.synchronizedMap(new LinkedHashMap<>());
@@ -207,6 +207,7 @@
 	 */
 	public class JobMonitor implements IProgressMonitorWithBlocking {
 		Job job;
+		JobInfo info;
 		String currentTaskName;
 		Set<IProgressMonitorWithBlocking> monitors = Collections.emptySet();
 
@@ -215,8 +216,13 @@
 		 *
 		 * @param newJob
 		 */
-		JobMonitor(Job newJob) {
+		JobMonitor(Job newJob, JobInfo jobInfo) {
 			job = newJob;
+			info = jobInfo;
+		}
+
+		public JobInfo getJobInfo() {
+			return info;
 		}
 
 		/**
@@ -229,7 +235,6 @@
 			Set<IProgressMonitorWithBlocking> newSet = new LinkedHashSet<>(monitors);
 			newSet.add(monitor);
 			this.monitors = Collections.unmodifiableSet(newSet);
-			JobInfo info = getJobInfo(job);
 			TaskInfo currentTask = info.getTaskInfo();
 			if (currentTask != null) {
 				monitor.beginTask(currentTaskName, currentTask.totalWork);
@@ -245,7 +250,6 @@
 
 		@Override
 		public void beginTask(String taskName, int totalWork) {
-			JobInfo info = getJobInfo(job);
 			info.beginTask(taskName, totalWork);
 			refreshJobInfo(info);
 			currentTaskName = taskName;
@@ -254,16 +258,13 @@
 
 		@Override
 		public void done() {
-			JobInfo info = getJobInfo(job);
 			info.clearTaskInfo();
 			info.clearChildren();
-			runnableMonitors.remove(job);
 			monitors.stream().forEach(IProgressMonitorWithBlocking::done);
 		}
 
 		@Override
 		public void internalWorked(double work) {
-			JobInfo info = getJobInfo(job);
 			if (info.hasTaskInfo()) {
 				info.addWork(work);
 				refreshJobInfo(info);
@@ -273,17 +274,11 @@
 
 		@Override
 		public boolean isCanceled() {
-			// Use the internal get so we don't create a Job Info for
-			// a job that is not running (see bug 149857).
-			JobInfo info = internalGetJobInfo(job);
-			if (info == null)
-				return false;
 			return info.isCanceled();
 		}
 
 		@Override
 		public void setCanceled(boolean value) {
-			JobInfo info = getJobInfo(job);
 			// Don't bother canceling twice.
 			if (value && !info.isCanceled()) {
 				info.cancel();
@@ -294,7 +289,6 @@
 
 		@Override
 		public void setTaskName(String taskName) {
-			JobInfo info = getJobInfo(job);
 			if (info.hasTaskInfo()) {
 				info.setTaskName(taskName);
 			} else {
@@ -312,7 +306,6 @@
 			if (name == null) {
 				return;
 			}
-			JobInfo info = getJobInfo(job);
 			info.clearChildren();
 			info.addSubTask(name);
 			refreshJobInfo(info);
@@ -326,7 +319,6 @@
 
 		@Override
 		public void clearBlocked() {
-			JobInfo info = getJobInfo(job);
 			info.setBlockedStatus(null);
 			refreshJobInfo(info);
 			monitors.stream().forEach(IProgressMonitorWithBlocking::clearBlocked);
@@ -334,7 +326,6 @@
 
 		@Override
 		public void setBlocked(IStatus reason) {
-			JobInfo info = getJobInfo(job);
 			info.setBlockedStatus(reason);
 			refreshJobInfo(info);
 			monitors.stream().forEach(listener -> listener.setBlocked(reason));
@@ -357,44 +348,44 @@
 		Job.getJobManager().addJobChangeListener(this.changeListener);
 		StatusManager.getManager().addListener(notificationListener);
 
-		uiRefreshThrottler = new Throttler(Display.getDefault(), Duration.ofMillis(100), () -> {
-			Set<GroupInfo> localPendingGroupUpdates, localPendingGroupRemoval;
-			Map<JobInfo, Set<IJobProgressManagerListener>> localPendingJobUpdates, localPendingJobAddition,
-					localPendingJobRemoval;
-			synchronized (pendingUpdatesMutex) {
-				localPendingJobUpdates = pendingJobUpdates;
-				pendingJobUpdates = new LinkedHashMap<>();
-				localPendingGroupUpdates = pendingGroupUpdates;
-				pendingGroupUpdates = new LinkedHashSet<>();
-				localPendingJobRemoval = pendingJobRemoval;
-				pendingJobRemoval = new LinkedHashMap<>();
-				localPendingGroupRemoval = pendingGroupRemoval;
-				pendingGroupRemoval = new LinkedHashSet<>();
-				localPendingJobAddition = pendingJobAddition;
-				pendingJobAddition = new LinkedHashMap<>();
-			}
+		uiRefreshThrottler = new Throttler(Display.getDefault(), Duration.ofMillis(100), this::notifyListeners);
+	}
 
-			localPendingJobAddition.entrySet()
-					.forEach(e -> e.getValue().forEach(listener -> listener.addJob(e.getKey())));
+	/* Visible for testing */ public void notifyListeners() {
+		Set<GroupInfo> localPendingGroupUpdates, localPendingGroupRemoval;
+		Map<JobInfo, Set<IJobProgressManagerListener>> localPendingJobUpdates, localPendingJobAddition,
+				localPendingJobRemoval;
+		synchronized (pendingUpdatesMutex) {
+			localPendingJobUpdates = pendingJobUpdates;
+			pendingJobUpdates = new LinkedHashMap<>();
+			localPendingGroupUpdates = pendingGroupUpdates;
+			pendingGroupUpdates = new LinkedHashSet<>();
+			localPendingJobRemoval = pendingJobRemoval;
+			pendingJobRemoval = new LinkedHashMap<>();
+			localPendingGroupRemoval = pendingGroupRemoval;
+			pendingGroupRemoval = new LinkedHashSet<>();
+			localPendingJobAddition = pendingJobAddition;
+			pendingJobAddition = new LinkedHashMap<>();
+		}
 
-			// Adds all non null JobInfo#getGroupInfo to the list of groups to
-			// be refreshed
-			localPendingJobUpdates.entrySet().stream().map(e -> e.getKey().getGroupInfo()).filter(Objects::nonNull)
-					.forEach(localPendingGroupUpdates::add);
+		localPendingJobAddition.entrySet().forEach(e -> e.getValue().forEach(listener -> listener.addJob(e.getKey())));
 
-			localPendingJobUpdates.entrySet()
-					.forEach(e -> e.getValue().forEach(listener -> listener.refreshJobInfo(e.getKey())));
+		// Adds all non null JobInfo#getGroupInfo to the list of groups to
+		// be refreshed
+		localPendingJobUpdates.entrySet().stream().map(e -> e.getKey().getGroupInfo()).filter(Objects::nonNull)
+				.forEach(localPendingGroupUpdates::add);
 
-			// refresh groups
-			localPendingGroupUpdates
-					.forEach(groupInfo -> listeners.forEach(listener -> listener.refreshGroup(groupInfo)));
+		localPendingJobUpdates.entrySet()
+				.forEach(e -> e.getValue().forEach(listener -> listener.refreshJobInfo(e.getKey())));
 
-			localPendingJobRemoval.entrySet()
-					.forEach(e -> e.getValue().forEach(listener -> listener.removeJob(e.getKey())));
+		// refresh groups
+		localPendingGroupUpdates.forEach(groupInfo -> listeners.forEach(listener -> listener.refreshGroup(groupInfo)));
 
-			localPendingGroupRemoval.forEach(group -> {
-				listeners.forEach(listener -> listener.removeGroup(group));
-			});
+		localPendingJobRemoval.entrySet()
+				.forEach(e -> e.getValue().forEach(listener -> listener.removeJob(e.getKey())));
+
+		localPendingGroupRemoval.forEach(group -> {
+			listeners.forEach(listener -> listener.removeGroup(group));
 		});
 	}
 
@@ -432,7 +423,7 @@
 		return new JobChangeAdapter() {
 			@Override
 			public void aboutToRun(IJobChangeEvent event) {
-				JobInfo info = getJobInfo(event.getJob());
+				JobInfo info = progressFor(event.getJob()).getJobInfo();
 				refreshJobInfo(info);
 				Iterator<IJobBusyListener> startListeners = busyListenersForJob(event.getJob()).iterator();
 				while (startListeners.hasNext()) {
@@ -452,7 +443,7 @@
 					next.decrementBusy(event.getJob());
 				}
 
-				final JobInfo info = getJobInfo(event.getJob());
+				final JobInfo info = progressFor(event.getJob()).getJobInfo();
 				removeJobInfo(info);
 
 				/*
@@ -510,10 +501,10 @@
 			 * @param event
 			 */
 			private void updateFor(IJobChangeEvent event) {
-				if (jobs.containsKey(event.getJob())) {
-					refreshJobInfo(getJobInfo(event.getJob()));
+				if (managedJobs.contains(event.getJob())) {
+					refreshJobInfo(progressFor(event.getJob()).getJobInfo());
 				} else {
-					addJobInfo(new JobInfo(event.getJob()));
+					addJobInfo(progressFor(event.getJob()).getJobInfo());
 				}
 			}
 
@@ -524,9 +515,8 @@
 
 			@Override
 			public void sleeping(IJobChangeEvent event) {
-
-				if (jobs.containsKey(event.getJob()))// Are we showing this?
-					sleepJobInfo(getJobInfo(event.getJob()));
+				if (managedJobs.contains(event.getJob()))// Are we showing this?
+					sleepJobInfo(progressFor(event.getJob()).getJobInfo());
 			}
 		};
 	}
@@ -614,7 +604,9 @@
 	 * @return IProgressMonitor
 	 */
 	public JobMonitor progressFor(Job job) {
-		return runnableMonitors.computeIfAbsent(job, JobMonitor::new);
+		return runnableMonitors.computeIfAbsent(job, (j) -> {
+			return new JobMonitor(j, new JobInfo(j));
+		});
 	}
 
 	/**
@@ -637,27 +629,6 @@
 	}
 
 	/**
-	 * Returns the JobInfo for the job. If it does not exist create it.
-	 *
-	 * @param job
-	 * @return JobInfo
-	 */
-	JobInfo getJobInfo(Job job) {
-		return jobs.computeIfAbsent(job, JobInfo::new);
-	}
-
-	/**
-	 * Returns an existing job info for the given Job or <code>null</code> if
-	 * there isn't one.
-	 *
-	 * @param job
-	 * @return JobInfo
-	 */
-	JobInfo internalGetJobInfo(Job job) {
-		return jobs.get(job);
-	}
-
-	/**
 	 * Refreshes the IJobProgressManagerListeners as a result of a change in
 	 * info.
 	 *
@@ -691,7 +662,7 @@
 	 */
 	public void removeJobInfo(JobInfo info) {
 		Job job = info.getJob();
-		jobs.remove(job);
+		managedJobs.remove(job);
 		synchronized (pendingUpdatesMutex) {
 			rememberListenersForJob(info, pendingJobRemoval);
 		}
@@ -723,7 +694,7 @@
 			refreshGroup(group);
 		}
 
-		jobs.put(info.getJob(), info);
+		managedJobs.add(info.getJob());
 		synchronized (pendingUpdatesMutex) {
 			rememberListenersForJob(info, pendingJobAddition);
 		}
@@ -772,7 +743,8 @@
 	 * @return JobInfo[]
 	 */
 	public JobInfo[] getJobInfos(boolean debug) {
-		return jobs.entrySet().stream().filter(entry -> !isCurrentDisplaying(entry.getKey(), debug))
+		return managedJobs.stream().filter(job -> !isCurrentDisplaying(job, debug))
+				.map(job -> progressFor(job).getJobInfo())
 				.toArray(JobInfo[]::new);
 	}
 
@@ -783,8 +755,8 @@
 	 * @return JobTreeElement[]
 	 */
 	public JobTreeElement[] getRootElements(boolean debug) {
-		return jobs.entrySet().stream().filter(entry -> !isCurrentDisplaying(entry.getKey(), debug)).map(entry -> {
-			JobInfo jobInfo = entry.getValue();
+		return managedJobs.stream().filter(job -> !isCurrentDisplaying(job, debug)).map(job -> {
+			JobInfo jobInfo = progressFor(job).getJobInfo();
 			GroupInfo group = jobInfo.getGroupInfo();
 			if (group == null) {
 				return jobInfo;
@@ -799,7 +771,7 @@
 	 * @return boolean
 	 */
 	public boolean hasJobInfos() {
-		return !jobs.isEmpty();
+		return !managedJobs.isEmpty();
 	}
 
 	/**
@@ -924,7 +896,7 @@
 		JobMonitor monitor = progressFor(job);
 		if (group instanceof GroupInfo) {
 			GroupInfo groupInfo = (GroupInfo) group;
-			JobInfo jobInfo = getJobInfo(job);
+			JobInfo jobInfo = monitor.getJobInfo();
 			jobInfo.setGroupInfo(groupInfo);
 			jobInfo.setTicks(ticks);
 			groupInfo.addJobInfo(jobInfo);
@@ -1105,7 +1077,7 @@
 	 */
 	boolean checkForStaleness(Job job) {
 		if (job.getState() == Job.NONE) {
-			removeJobInfo(getJobInfo(job));
+			removeJobInfo(progressFor(job).getJobInfo());
 			return true;
 		}
 		return false;
diff --git a/bundles/org.eclipse.ui.workbench/Eclipse UI/org/eclipse/ui/internal/progress/ProgressMonitorFocusJobDialog.java b/bundles/org.eclipse.ui.workbench/Eclipse UI/org/eclipse/ui/internal/progress/ProgressMonitorFocusJobDialog.java
index f6fbedb..af251c1 100644
--- a/bundles/org.eclipse.ui.workbench/Eclipse UI/org/eclipse/ui/internal/progress/ProgressMonitorFocusJobDialog.java
+++ b/bundles/org.eclipse.ui.workbench/Eclipse UI/org/eclipse/ui/internal/progress/ProgressMonitorFocusJobDialog.java
@@ -255,7 +255,7 @@
 		Control area = super.createDialogArea(parent);
 		// Give the job info as the initial details
 		getProgressMonitor().setTaskName(
-				ProgressManager.getInstance().getJobInfo(this.job)
+				ProgressManager.getInstance().progressFor(this.job).getJobInfo()
 						.getDisplayString());
 		return area;
 	}
diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/DummyJob.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/DummyJob.java
index 8e0a042..d5253c0 100644
--- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/DummyJob.java
+++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/DummyJob.java
@@ -30,7 +30,7 @@
 
 	public boolean inProgress = false;
 	/** if false, infinite until changed or job is cancelled */
-	public boolean shouldFinish = true;
+	public volatile boolean shouldFinish = true;
 
 	public DummyJob(String name, IStatus status) {
 		super(name);
diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/ProgressAnimationItemTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/ProgressAnimationItemTest.java
index e17068c..385431c 100644
--- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/ProgressAnimationItemTest.java
+++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/ProgressAnimationItemTest.java
@@ -61,8 +61,7 @@
 	public void testSingleJobRefreshOnce() throws Exception {
 		createAndScheduleJob();
 
-		waitAndReadAndDispatch();
-
+		ProgressManager.getInstance().notifyListeners();
 		refresh();
 
 		assertSingleAccessibleListener();
@@ -73,8 +72,7 @@
 		createAndScheduleJob();
 		createAndScheduleJob();
 
-		waitAndReadAndDispatch();
-
+		ProgressManager.getInstance().notifyListeners();
 		refresh();
 
 		assertSingleAccessibleListener();
@@ -84,19 +82,13 @@
 	public void testSingleJobRefreshTwice() throws Exception {
 		createAndScheduleJob();
 
-		waitAndReadAndDispatch();
-
+		ProgressManager.getInstance().notifyListeners();
 		refresh();
 		refresh();
 
 		assertSingleAccessibleListener();
 	}
 
-	private void waitAndReadAndDispatch() throws InterruptedException {
-		while (PlatformUI.getWorkbench().getDisplay().readAndDispatch()) {
-		}
-	}
-
 	private ProgressAnimationItem createProgressAnimationItem(Composite composite) {
 		ProgressRegion progressRegion = new ProgressRegion();
 		progressRegion.createContents(composite);
@@ -106,8 +98,6 @@
 	private static void createAndScheduleJob() throws InterruptedException {
 		DummyJob job = new DummyJob("Keep me", Status.OK_STATUS);
 		job.setProperty(IProgressConstants.KEEP_PROPERTY, true);
-		ExtendedJobInfo info = new ExtendedJobInfo(job);
-		ProgressManager.getInstance().addJobInfo(info);
 		job.schedule();
 		job.join();
 	}