From c7c745c1977b4a3fad005733b923d3522a41273d Mon Sep 17 00:00:00 2001 From: Aleksey Pivovarov Date: Mon, 5 Sep 2016 12:16:20 +0300 Subject: [PATCH] git: reduce blinking when loading annotations * avoid unnecessary update in `invokeLater` if file history can be computed fast enough --- .../progress/util/BackgroundTaskUtil.java | 148 +++++++++++++----- .../annotate/GitAnnotationProvider.java | 42 +++-- 2 files changed, 136 insertions(+), 54 deletions(-) diff --git a/platform/platform-impl/src/com/intellij/openapi/progress/util/BackgroundTaskUtil.java b/platform/platform-impl/src/com/intellij/openapi/progress/util/BackgroundTaskUtil.java index a427e61e72d1..47a6fe3a0c6f 100644 --- a/platform/platform-impl/src/com/intellij/openapi/progress/util/BackgroundTaskUtil.java +++ b/platform/platform-impl/src/com/intellij/openapi/progress/util/BackgroundTaskUtil.java @@ -23,10 +23,12 @@ import com.intellij.openapi.progress.EmptyProgressIndicator; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.Disposer; -import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.Pair; import com.intellij.util.Consumer; import com.intellij.util.Function; +import com.intellij.util.PairConsumer; import org.jetbrains.annotations.CalledInAny; import org.jetbrains.annotations.CalledInAwt; import org.jetbrains.annotations.NotNull; @@ -39,18 +41,24 @@ import java.util.concurrent.atomic.AtomicReference; public class BackgroundTaskUtil { private static final Logger LOG = Logger.getInstance(BackgroundTaskUtil.class); - /* - * Executor to perform long operations on pooled thread - * It can be used to reduce blinking if background task completed fast. In this case callback will be called without invokeLater(). - * - * Simple approach: - * + /** + * Executor to perform possibly long operation on pooled thread. + * If computation was performed within given time frame, + * the computed callback will be executed synchronously (avoiding unnecessary invokeLater()). + * In this case, {@code onSlowAction} will not be executed at all. + *

+ * It can be used to reduce blinking when background task might be completed fast.
+ * A Simple approach: + *

    * onSlowAction.run() // show "Loading..."
    * executeOnPooledThread({
-   *     Runnable callback = backgroundTask(); // some background computations
-   *     invokeLater(callback); // apply changes
-   *   });
-   *
+   *   Runnable callback = backgroundTask(); // some background computations
+   *   invokeLater(callback); // apply changes
+   * });
+   * 
* will lead to "Loading..." visible between current moment and execution of invokeLater() event. * This period can be very short and looks like 'jumping' if background operation is fast. */ @@ -69,9 +77,9 @@ public class BackgroundTaskUtil { int waitMillis, boolean forceEDT) { ModalityState modality = ModalityState.current(); - ProgressIndicator indicator = new EmptyProgressIndicator(modality); if (forceEDT) { + ProgressIndicator indicator = new EmptyProgressIndicator(modality); try { Runnable callback = backgroundTask.fun(indicator); finish(callback, indicator); @@ -81,52 +89,116 @@ public class BackgroundTaskUtil { catch (Throwable t) { LOG.error(t); } + return indicator; } else { - Helper helper = new Helper<>(); + Pair pair = computeInBackgroundAndTryWait( + backgroundTask, + (callback, indicator) -> { + ApplicationManager.getApplication().invokeLater(() -> { + finish(callback, indicator); + }, modality); + }, + modality, + waitMillis); - ApplicationManager.getApplication().executeOnPooledThread(() -> { - ProgressManager.getInstance().executeProcessUnderProgress(() -> { - Runnable callback = backgroundTask.fun(indicator); + Runnable callback = pair.first; + ProgressIndicator indicator = pair.second; - if (!helper.setResult(callback)) { - ApplicationManager.getApplication().invokeLater(() -> { - finish(callback, indicator); - }, modality); - } - }, indicator); - }); - - if (helper.await(waitMillis)) { - finish(helper.getResult(), indicator); + if (callback != null) { + finish(callback, indicator); } else { if (onSlowAction != null) onSlowAction.run(); } - } - return indicator; + return indicator; + } } @CalledInAwt private static void finish(@NotNull Runnable result, @NotNull ProgressIndicator indicator) { - if (indicator.isCanceled()) return; - result.run(); - indicator.stop(); + if (!indicator.isCanceled()) result.run(); } + /** + * Try to compute value in background and abort computation if it takes too long. + *
    + *
  • If the computation is fast, return computed value. + *
  • If the computation is slow, abort computation (cancel ProgressIndicator). + *
+ */ @Nullable @CalledInAwt public static T tryComputeFast(@NotNull Function backgroundTask, int waitMillis) { - Ref resultRef = new Ref<>(); - ProgressIndicator indicator = executeAndTryWait(indicator1 -> { - T result = backgroundTask.fun(indicator1); - return () -> resultRef.set(result); - }, null, waitMillis, false); - indicator.cancel(); + Pair pair = computeInBackgroundAndTryWait( + backgroundTask, + (result, indicator) -> { + }, + ModalityState.defaultModalityState(), + waitMillis); - return resultRef.get(); + T result = pair.first; + ProgressIndicator indicator = pair.second; + + indicator.cancel(); + return result; + } + + @Nullable + @CalledInAny + public static T computeInBackgroundAndTryWait(@NotNull Computable computable, + @NotNull Consumer asyncCallback, + int waitMillis) { + Pair pair = computeInBackgroundAndTryWait( + indicator -> computable.compute(), + (result, indicator) -> asyncCallback.consume(result), + ModalityState.defaultModalityState(), + waitMillis + ); + return pair.first; + } + + /** + * Compute value in background and try wait for its completion. + *
    + *
  • If the computation is fast, return computed value synchronously. Callback will not be called in this case. + *
  • If the computation is slow, return null. When the computation is completed, pass the value to the callback. + *
+ * Callback will be executed on the same thread as the background task. + */ + @NotNull + @CalledInAny + public static Pair computeInBackgroundAndTryWait(@NotNull Function task, + @NotNull PairConsumer asyncCallback, + @NotNull ModalityState modality, + int waitMillis) { + ProgressIndicator indicator = new EmptyProgressIndicator(modality); + + Helper helper = new Helper<>(); + + indicator.start(); + ApplicationManager.getApplication().executeOnPooledThread(() -> { + ProgressManager.getInstance().executeProcessUnderProgress(() -> { + try { + T result = task.fun(indicator); + if (!helper.setResult(result)) { + asyncCallback.consume(result, indicator); + } + } + finally { + indicator.stop(); + } + }, indicator); + }); + + T result = null; + if (helper.await(waitMillis)) { + result = helper.getResult(); + } + + return Pair.create(result, indicator); } diff --git a/plugins/git4idea/src/git4idea/annotate/GitAnnotationProvider.java b/plugins/git4idea/src/git4idea/annotate/GitAnnotationProvider.java index e3ea1dc6003c..6ee4137e0a5e 100644 --- a/plugins/git4idea/src/git4idea/annotate/GitAnnotationProvider.java +++ b/plugins/git4idea/src/git4idea/annotate/GitAnnotationProvider.java @@ -21,6 +21,8 @@ import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.util.BackgroundTaskUtil; +import com.intellij.openapi.progress.util.ProgressWindow; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.ProjectLevelVcsManager; @@ -173,30 +175,38 @@ public class GitAnnotationProvider implements AnnotationProviderEx { } private void loadFileHistoryInBackground(@NotNull GitFileAnnotation fileAnnotation) { - ApplicationManager.getApplication().executeOnPooledThread(() -> { - try { - VirtualFile file = fileAnnotation.getFile(); - FilePath filePath = VcsUtil.getFilePath(file); - VcsRevisionNumber currentRevision = fileAnnotation.getCurrentRevision(); + List fileRevisions = BackgroundTaskUtil.computeInBackgroundAndTryWait( + () -> { + try { + VirtualFile file = fileAnnotation.getFile(); + FilePath filePath = VcsUtil.getFilePath(file); + VcsRevisionNumber currentRevision = fileAnnotation.getCurrentRevision(); - List revisions; - if (file.isInLocalFileSystem() || currentRevision == null) { - revisions = loadFileHistory(filePath); + if (file.isInLocalFileSystem() || currentRevision == null) { + return loadFileHistory(filePath); + } + else { + return GitHistoryUtils.history(myProject, filePath, null, currentRevision); + } } - else { - revisions = GitHistoryUtils.history(myProject, filePath, null, currentRevision); + catch (VcsException e) { + LOG.error(e); + return null; } - + }, + (revisions) -> { if (revisions == null) return; ApplicationManager.getApplication().invokeLater(() -> { fileAnnotation.setRevisions(revisions); fileAnnotation.reload(); }); - } - catch (VcsException e) { - LOG.error(e); - } - }); + }, + ProgressWindow.DEFAULT_PROGRESS_DIALOG_POSTPONE_TIME_MILLIS + ); + + if (fileRevisions != null) { + fileAnnotation.setRevisions(fileRevisions); + } } @Nullable