From 35282651ee9b8d4947fd4edd511178fe98c4121a Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Mon, 22 Jun 2026 10:45:18 +0200 Subject: [PATCH] Defer short-running jobs from the progress view Non-system jobs were shown in the progress view the moment they were scheduled and removed when they finished, so jobs that complete within a few hundred milliseconds flashed in and out and could not be read. Add a per-job grace period before a job is announced to the progress listeners. Jobs that finish, sleep or are cancelled before the grace period elapses are never shown, which removes the flicker for refresh, validation and decoration bursts. The grace period defaults to half of getLongOperationTime() with a lower bound of 200ms and can be overridden through the org.eclipse.ui.progress.viewGracePeriod system property. Jobs kept after completion (KEEP_PROPERTY or an error result) are still captured. Fixes https://github.com/eclipse-platform/eclipse.platform.ui/issues/4126 --- .../ui/internal/progress/ProgressManager.java | 102 +++++++++++++++++- .../ui/tests/progress/ProgressViewTests.java | 48 +++++++++ 2 files changed, 148 insertions(+), 2 deletions(-) diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/progress/ProgressManager.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/progress/ProgressManager.java index e9b35fe2010..233951109fd 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/progress/ProgressManager.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/progress/ProgressManager.java @@ -38,6 +38,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IProgressMonitor; @@ -176,6 +177,23 @@ public class ProgressManager extends ProgressProvider implements IProgressServic private final Throttler uiRefreshThrottler; + /** + * System property overriding, in milliseconds, the grace period a job must run + * before it is shown in the progress view; {@code 0} disables it and restores + * the immediate display of jobs. + */ + public static final String GRACE_PERIOD_PROPERTY = "org.eclipse.ui.progress.viewGracePeriod"; //$NON-NLS-1$ + + private static final int MINIMUM_GRACE_PERIOD = 200; + + /** + * Scheduled jobs not yet announced, mapped to the {@link System#nanoTime()} at + * which their grace period elapses. Jobs that finish, sleep or are cancelled + * before then are removed here and never appear, which avoids flicker from very + * short running jobs. + */ + private final Map pendingJobAdditions = new ConcurrentHashMap<>(); + /** * Returns the progress manager currently in use. * @@ -360,6 +378,7 @@ public void setBlocked(IStatus reason) { * Send pending notifications to listeners. */ /* Visible for testing */ public void notifyListeners() { + promotePendingJobs(); Set localPendingGroupUpdates, localPendingGroupRemoval; Map> localPendingJobUpdates, localPendingJobAddition, localPendingJobRemoval; @@ -395,6 +414,33 @@ public void setBlocked(IStatus reason) { localPendingGroupRemoval.forEach(group -> { listeners.forEach(listener -> listener.removeGroup(group)); }); + + // Keep ticking so pending jobs are announced even when nothing else updates. + if (!pendingJobAdditions.isEmpty()) { + uiRefreshThrottler.throttledAsyncExec(); + } + } + + /** + * Announces the jobs whose grace period has elapsed. Jobs removed from + * {@link #pendingJobAdditions} meanwhile are never announced. + */ + private void promotePendingJobs() { + if (pendingJobAdditions.isEmpty()) { + return; + } + long now = System.nanoTime(); + for (Iterator> it = pendingJobAdditions.entrySet().iterator(); it.hasNext();) { + Entry entry = it.next(); + if (now - entry.getValue() < 0) { + continue; // still within its grace period + } + it.remove(); + Job job = entry.getKey(); + if (managedJobs.contains(job)) { + rememberJobAddition(progressFor(job).getJobInfo()); + } + } } private void setUpImages() { @@ -508,6 +554,10 @@ public void awake(IJobChangeEvent event) { @Override public void sleeping(IJobChangeEvent event) { + if (cancelPendingAdd(event.getJob())) { + // Slept before its grace period elapsed; never announced, nothing to sleep. + return; + } if (managedJobs.contains(event.getJob())) { // Are we showing this? sleepJobInfo(progressFor(event.getJob()).getJobInfo()); } @@ -629,6 +679,10 @@ void removeListener(IJobProgressManagerListener listener) { */ public void refreshJobInfo(JobInfo info) { checkForStaleness(info.getJob()); + if (pendingJobAdditions.containsKey(info.getJob())) { + // Still within its grace period; state is read when it is announced. + return; + } synchronized (pendingUpdatesMutex) { Predicate predicate = listener -> !isNeverDisplaying(info.getJob(), listener.showsDebug()); rememberListenersForJob(info, pendingJobUpdates, predicate); @@ -658,6 +712,9 @@ public JobInfo removeJob(Job job) { JobInfo info; synchronized (runnableMonitors) { info = progressFor(job).getJobInfo(); + // Drop a pending add so the job is never shown; the removal below still + // captures jobs that must be kept (KEEP_PROPERTY or an error result). + cancelPendingAdd(job); managedJobs.remove(job); synchronized (pendingUpdatesMutex) { Predicate predicate = listener -> !isNeverDisplaying(info.getJob(), listener.showsDebug()); @@ -704,12 +761,53 @@ public void addJobInfo(JobInfo info) { refreshGroup(group); } - managedJobs.add(info.getJob()); + Job job = info.getJob(); + managedJobs.add(job); + + int gracePeriod = getViewGracePeriod(); + if (gracePeriod <= 0) { + rememberJobAddition(info); + } else { + // Defer the announcement; promotePendingJobs picks it up once the grace period elapses. + pendingJobAdditions.put(job, System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(gracePeriod)); + } + uiRefreshThrottler.throttledExec(); + } + + /** + * Remembers a job addition for the registered listeners. + */ + private void rememberJobAddition(JobInfo info) { synchronized (pendingUpdatesMutex) { Predicate predicate = listener -> !isCurrentDisplaying(info.getJob(), listener.showsDebug()); rememberListenersForJob(info, pendingJobAddition, predicate); } - uiRefreshThrottler.throttledExec(); + } + + /** + * Cancels a still pending grace-period add so the job is never announced. + * + * @return {@code true} if an add was pending and got cancelled + */ + private boolean cancelPendingAdd(Job job) { + return pendingJobAdditions.remove(job) != null; + } + + /** + * Returns the grace period in milliseconds a job must run before it is shown, + * overridable through the {@link #GRACE_PERIOD_PROPERTY} system property. + * {@code 0} disables it. + */ + private int getViewGracePeriod() { + String override = System.getProperty(GRACE_PERIOD_PROPERTY); + if (override != null) { + try { + return Math.max(0, Integer.parseInt(override)); + } catch (NumberFormatException e) { + // Ignore and fall back to the computed default. + } + } + return Math.max(MINIMUM_GRACE_PERIOD, getLongOperationTime() / 2); } private void rememberListenersForJob(JobInfo info, Map> listenersMap, Predicate predicate) { diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/ProgressViewTests.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/ProgressViewTests.java index b192a4d489e..7665e2ee5b3 100644 --- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/ProgressViewTests.java +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/progress/ProgressViewTests.java @@ -34,6 +34,7 @@ import org.eclipse.ui.internal.progress.JobInfo; import org.eclipse.ui.internal.progress.JobTreeElement; import org.eclipse.ui.internal.progress.ProgressInfoItem; +import org.eclipse.ui.internal.progress.ProgressManager; import org.eclipse.ui.internal.progress.TaskInfo; import org.eclipse.ui.progress.IProgressConstants; import org.junit.After; @@ -206,6 +207,53 @@ public void testItemOrder() throws Exception { } } + @Test + public void testShortJobIsSuppressedFromView() throws Exception { + // A grace period far longer than the job's lifetime: the job must never + // be shown as running and therefore never flickers in the view. + System.setProperty(ProgressManager.GRACE_PERIOD_PROPERTY, Long.toString(TimeUnit.MINUTES.toMillis(1))); + DummyJob job = new DummyJob("Short job", Status.OK_STATUS); + try { + openProgressView(); + job.schedule(); + job.join(); + // Flush any pending throttled updates. + processEventsUntil(() -> false, 500); + assertEquals("Short job should not appear in the progress view", 0, countJobs(job)); + } finally { + System.clearProperty(ProgressManager.GRACE_PERIOD_PROPERTY); + processEvents(); + } + } + + @Test + public void testLongRunningJobAppearsAfterGracePeriod() throws Exception { + long grace = TimeUnit.SECONDS.toMillis(2); + System.setProperty(ProgressManager.GRACE_PERIOD_PROPERTY, Long.toString(grace)); + DummyJob job = new DummyJob("Grace period job", Status.OK_STATUS); + job.shouldFinish = false; + try { + openProgressView(); + long start = System.currentTimeMillis(); + job.schedule(); + // Wait until the job is actually running. + processEventsUntil(() -> job.inProgress, TimeUnit.SECONDS.toMillis(3)); + // Within the grace period the job must not be shown yet. + if (System.currentTimeMillis() - start < grace) { + assertEquals("Job appeared before its grace period elapsed", 0, countJobs(job)); + } + // Once the grace period elapses, the job appears. + processEventsUntil(() -> countJobs(job) == 1, grace + TimeUnit.SECONDS.toMillis(5)); + assertEquals("Job did not appear after its grace period", 1, countJobs(job)); + } finally { + job.shouldFinish = true; + job.cancel(); + job.join(); + processEvents(); + System.clearProperty(ProgressManager.GRACE_PERIOD_PROPERTY); + } + } + private int countJobs(Job job) { int count = 0; ProgressInfoItem[] progressInfoItems = progressView.getViewer().getProgressInfoItems();