git: reduce blinking when loading annotations

* avoid unnecessary update in `invokeLater` if file history can be computed fast enough
This commit is contained in:
Aleksey Pivovarov
2016-09-06 15:21:41 +03:00
parent 7bef6c6596
commit c7c745c197
2 changed files with 136 additions and 54 deletions
@@ -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 <possibly> 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 <i>possibly</i> long operation on pooled thread.
* If computation was performed within given time frame,
* the computed callback will be executed synchronously (avoiding unnecessary <tt>invokeLater()</tt>).
* In this case, {@code onSlowAction} will not be executed at all.
* <ul>
* <li> If the computation is fast, execute callback synchronously.
* <li> If the computation is slow, execute <tt>onSlowAction</tt> synchronously. When the computation is completed, execute callback in EDT.
* </ul><p>
* It can be used to reduce blinking when background task might be completed fast.<br>
* A Simple approach:
* <pre>
* 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
* });
* </pre>
* 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<Runnable> helper = new Helper<>();
Pair<Runnable, ProgressIndicator> 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.
* <ul>
* <li> If the computation is fast, return computed value.
* <li> If the computation is slow, abort computation (cancel ProgressIndicator).
* </ul>
*/
@Nullable
@CalledInAwt
public static <T> T tryComputeFast(@NotNull Function<ProgressIndicator, T> backgroundTask,
int waitMillis) {
Ref<T> resultRef = new Ref<>();
ProgressIndicator indicator = executeAndTryWait(indicator1 -> {
T result = backgroundTask.fun(indicator1);
return () -> resultRef.set(result);
}, null, waitMillis, false);
indicator.cancel();
Pair<T, ProgressIndicator> 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> T computeInBackgroundAndTryWait(@NotNull Computable<T> computable,
@NotNull Consumer<T> asyncCallback,
int waitMillis) {
Pair<T, ProgressIndicator> 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.
* <ul>
* <li> If the computation is fast, return computed value synchronously. Callback will not be called in this case.
* <li> If the computation is slow, return <tt>null</tt>. When the computation is completed, pass the value to the callback.
* </ul>
* Callback will be executed on the same thread as the background task.
*/
@NotNull
@CalledInAny
public static <T> Pair<T, ProgressIndicator> computeInBackgroundAndTryWait(@NotNull Function<ProgressIndicator, T> task,
@NotNull PairConsumer<T, ProgressIndicator> asyncCallback,
@NotNull ModalityState modality,
int waitMillis) {
ProgressIndicator indicator = new EmptyProgressIndicator(modality);
Helper<T> 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);
}
@@ -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<VcsFileRevision> fileRevisions = BackgroundTaskUtil.computeInBackgroundAndTryWait(
() -> {
try {
VirtualFile file = fileAnnotation.getFile();
FilePath filePath = VcsUtil.getFilePath(file);
VcsRevisionNumber currentRevision = fileAnnotation.getCurrentRevision();
List<VcsFileRevision> 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