diff --git a/platform/platform-impl/src/com/intellij/internal/TestWriteActionUnderProgress.java b/platform/platform-impl/src/com/intellij/internal/TestWriteActionUnderProgress.java index fd8348f17f62..706b56924ad4 100644 --- a/platform/platform-impl/src/com/intellij/internal/TestWriteActionUnderProgress.java +++ b/platform/platform-impl/src/com/intellij/internal/TestWriteActionUnderProgress.java @@ -30,16 +30,22 @@ public class TestWriteActionUnderProgress extends DumbAwareAction { @Override public void actionPerformed(AnActionEvent e) { ApplicationImpl app = (ApplicationImpl)ApplicationManager.getApplication(); - app.runWriteActionWithProgress("Progress", null, null, null, TestWriteActionUnderProgress::runIndeterminateProgress); - app.runWriteActionWithProgress("Cancellable Progress", null, null, "Stop", TestWriteActionUnderProgress::runDeterminateProgress); + + boolean success = app.runWriteActionWithProgressInDispatchThread( + "Progress", null, null, null, + TestWriteActionUnderProgress::runIndeterminateProgress); + assert success; + + app.runWriteActionWithProgressInBackgroundThread("Cancellable Progress", null, null, "Stop", TestWriteActionUnderProgress::runDeterminateProgress); } private static void runDeterminateProgress(ProgressIndicator indicator) { indicator.setIndeterminate(false); int iterations = 3000; - indicator.setText(""); + indicator.setText("In background thread"); for (int i = 0; i < iterations; i++) { TimeoutUtil.sleep(1); + ApplicationManager.getApplication().assertWriteAccessAllowed(); indicator.setFraction(((double)i + 1) / ((double)iterations)); indicator.setText2(String.valueOf(i)); ProgressManager.checkCanceled(); @@ -48,9 +54,10 @@ public class TestWriteActionUnderProgress extends DumbAwareAction { private static void runIndeterminateProgress(ProgressIndicator indicator) { indicator.setIndeterminate(true); - indicator.setText("Indeterminate"); + indicator.setText("In Event Dispatch thread"); for (int i = 0; i < 1000; i++) { TimeoutUtil.sleep(5); + ApplicationManager.getApplication().assertWriteAccessAllowed(); indicator.checkCanceled(); } } diff --git a/platform/platform-impl/src/com/intellij/openapi/application/impl/ApplicationImpl.java b/platform/platform-impl/src/com/intellij/openapi/application/impl/ApplicationImpl.java index 7ca46059c120..bdc6a3b01f09 100644 --- a/platform/platform-impl/src/com/intellij/openapi/application/impl/ApplicationImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/application/impl/ApplicationImpl.java @@ -72,6 +72,7 @@ import com.intellij.util.concurrency.Semaphore; import com.intellij.util.containers.Stack; import com.intellij.util.io.storage.HeavyProcessLatch; import com.intellij.util.ui.UIUtil; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -109,6 +110,7 @@ public class ApplicationImpl extends PlatformComponentManagerImpl implements App private final Stack myWriteActionsStack = new Stack<>(); // accessed from EDT only, no need to sync private int myWriteStackBase = 0; + private volatile Thread myWriteActionThread; private int myInEditorPaintCounter; // EDT only private final long myStartTime; @@ -690,6 +692,10 @@ public class ApplicationImpl extends PlatformComponentManagerImpl implements App @Override @NotNull public ModalityState getCurrentModalityState() { + if (Thread.currentThread() == myWriteActionThread) { + return getDefaultModalityState(); + } + return LaterInvocator.getCurrentModalityState(); } @@ -942,22 +948,41 @@ public class ApplicationImpl extends PlatformComponentManagerImpl implements App myLock.readUnlock(); } - public boolean runWriteActionWithProgress(@NotNull String title, - @Nullable Project project, - @Nullable JComponent parentComponent, - @Nullable String cancelText, - @NotNull Consumer action) { + @ApiStatus.Experimental + public boolean runWriteActionWithProgressInDispatchThread( + @NotNull String title, @Nullable Project project, @Nullable JComponent parentComponent, @Nullable String cancelText, + @NotNull Consumer action + ) { Class clazz = action.getClass(); startWrite(clazz); try { PotemkinProgress indicator = new PotemkinProgress(title, project, parentComponent, cancelText); - try { - ProgressManager.getInstance().runProcess(() -> action.consume(indicator), indicator); - } - catch (ProcessCanceledException ignore) { } - finally { - indicator.progressFinished(); - } + indicator.runInSwingThread(() -> action.consume(indicator)); + return !indicator.isCanceled(); + } + finally { + endWrite(clazz); + } + } + + @ApiStatus.Experimental + public boolean runWriteActionWithProgressInBackgroundThread( + @NotNull String title, @Nullable Project project, @Nullable JComponent parentComponent, @Nullable String cancelText, + @NotNull Consumer action + ) { + Class clazz = action.getClass(); + startWrite(clazz); + try { + PotemkinProgress indicator = new PotemkinProgress(title, project, parentComponent, cancelText); + indicator.runInBackground(() -> { + assert myWriteActionThread == null; + myWriteActionThread = Thread.currentThread(); + try { + action.consume(indicator); + } finally { + myWriteActionThread = null; + } + }); return !indicator.isCanceled(); } finally { @@ -1035,7 +1060,10 @@ public class ApplicationImpl extends PlatformComponentManagerImpl implements App @Override public boolean isReadAccessAllowed() { - return isDispatchThread() || myLock.isReadLockedByThisThread(); + if (isDispatchThread()) { + return myWriteActionThread == null; // no reading from EDT during background write action + } + return myLock.isReadLockedByThisThread() || myWriteActionThread == Thread.currentThread(); } @Override @@ -1140,7 +1168,9 @@ public class ApplicationImpl extends PlatformComponentManagerImpl implements App } private void startWrite(@NotNull Class clazz) { - assertIsDispatchThread("Write access is allowed from event dispatch thread only"); + if (!isWriteAccessAllowed()) { + assertIsDispatchThread("Write access is allowed from event dispatch thread only"); + } HeavyProcessLatch.INSTANCE.stopThreadPrioritizing(); // let non-cancellable read actions complete faster, if present boolean writeActionPending = myWriteActionPending; if (gatherStatistics && myWriteActionsStack.isEmpty() && !writeActionPending) { @@ -1283,7 +1313,7 @@ public class ApplicationImpl extends PlatformComponentManagerImpl implements App @Override public boolean isWriteAccessAllowed() { - return isDispatchThread() && myLock.isWriteLocked(); + return isDispatchThread() && myLock.isWriteLocked() || myWriteActionThread == Thread.currentThread(); } @Override diff --git a/platform/platform-impl/src/com/intellij/openapi/progress/util/PotemkinProgress.java b/platform/platform-impl/src/com/intellij/openapi/progress/util/PotemkinProgress.java index 863e4770d52f..a0b32f6051e6 100644 --- a/platform/platform-impl/src/com/intellij/openapi/progress/util/PotemkinProgress.java +++ b/platform/platform-impl/src/com/intellij/openapi/progress/util/PotemkinProgress.java @@ -15,11 +15,12 @@ */ package com.intellij.openapi.progress.util; -import com.intellij.concurrency.JobScheduler; import com.intellij.ide.IdeEventQueue; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.Disposer; +import com.intellij.util.concurrency.Semaphore; import com.intellij.util.io.storage.HeavyProcessLatch; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -29,30 +30,27 @@ import javax.swing.*; import java.awt.*; import java.awt.event.InputEvent; import java.util.Objects; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; /** - * A progress indicator for processes running in EDT. Paints itself in checkCanceled calls. + * A progress indicator for write actions. Paints itself explicitly, without resorting to normal Swing's delayed repaint API. + * Doesn't dispatch Swing events, except for handling manually those that can cancel it or affect the visual presentation. * * @author peter */ public class PotemkinProgress extends ProgressWindow { private long myLastUiUpdate = System.currentTimeMillis(); - private final Queue myEventQueue = new ConcurrentLinkedQueue<>(); + private final LinkedBlockingQueue myEventQueue = new LinkedBlockingQueue<>(); public PotemkinProgress(@NotNull String title, @Nullable Project project, @Nullable JComponent parentComponent, @Nullable String cancelText) { super(cancelText != null,false, project, parentComponent, cancelText); setTitle(title); - installCheckCanceledPaintingHook(); + ApplicationManager.getApplication().assertIsDispatchThread(); startStealingInputEvents(); } private void startStealingInputEvents() { - checkNativeEventsRegularly(); - IdeEventQueue.getInstance().addPostEventListener(event -> { if (event instanceof InputEvent) { myEventQueue.offer((InputEvent)event); @@ -62,12 +60,6 @@ public class PotemkinProgress extends ProgressWindow { }, this); } - private void checkNativeEventsRegularly() { - ScheduledFuture future = JobScheduler.getScheduler().scheduleWithFixedDelay( - () -> SunToolkit.flushPendingEvents(), 3, 3, TimeUnit.MILLISECONDS); - Disposer.register(this, () -> future.cancel(false)); - } - @NotNull @Override protected ProgressDialog getDialog() { @@ -83,7 +75,7 @@ public class PotemkinProgress extends ProgressWindow { @Override public boolean isCanceled() { if (ApplicationManager.getApplication().isDispatchThread()) { - dispatchAwtEventsWithoutModelAccess(); + dispatchAwtEventsWithoutModelAccess(0); updateUI(); } return super.isCanceled(); @@ -91,12 +83,18 @@ public class PotemkinProgress extends ProgressWindow { }); } - private void dispatchAwtEventsWithoutModelAccess() { - while (true) { - InputEvent event = myEventQueue.poll(); - if (event == null) return; + private void dispatchAwtEventsWithoutModelAccess(int timeoutMs) { + SunToolkit.flushPendingEvents(); + try { + while (true) { + InputEvent event = myEventQueue.poll(timeoutMs, TimeUnit.MILLISECONDS); + if (event == null) return; - dispatchInputEvent(event); + dispatchInputEvent(event); + } + } + catch (InterruptedException e) { + throw new RuntimeException(e); } } @@ -147,7 +145,7 @@ public class PotemkinProgress extends ProgressWindow { return true; } - public void progressFinished() { + private void progressFinished() { getDialog().hideImmediately(); } @@ -163,4 +161,46 @@ public class PotemkinProgress extends ProgressWindow { dialogPanel.paintImmediately(dialogPanel.getBounds()); } + /** Executes the action in EDT, paints itself inside checkCanceled calls. */ + public void runInSwingThread(@NotNull Runnable action) { + ApplicationManager.getApplication().assertIsDispatchThread(); + installCheckCanceledPaintingHook(); + try { + ProgressManager.getInstance().runProcess(action, this); + } + catch (ProcessCanceledException ignore) { } + finally { + progressFinished(); + } + } + + /** Executes the action in a background thread, block Swing thread, handles selected input events and paints itself periodically. */ + public void runInBackground(@NotNull Runnable action) { + ApplicationManager.getApplication().assertIsDispatchThread(); + enterModality(); + + try { + ensureBackgroundThreadStarted(action); + + while (isRunning()) { + dispatchAwtEventsWithoutModelAccess(10); + updateUI(); + } + } + finally { + exitModality(); + progressFinished(); + } + } + + private void ensureBackgroundThreadStarted(@NotNull Runnable action) { + Semaphore started = new Semaphore(); + started.down(); + ApplicationManager.getApplication().executeOnPooledThread(() -> ProgressManager.getInstance().runProcess(() -> { + started.up(); + action.run(); + }, this)); + + started.waitFor(); + } }