package com.intellij.vcs.log.data;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.PerformInBackgroundOption;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.Consumer;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.ui.UIUtil;
import com.intellij.vcs.log.CommitId;
import com.intellij.vcs.log.VcsLogProvider;
import com.intellij.vcs.log.VcsShortCommitDetails;
import com.intellij.vcs.log.data.index.IndexedDetails;
import com.intellij.vcs.log.data.index.VcsLogIndex;
import com.intellij.vcs.log.util.SequentialLimitedLifoExecutor;
import gnu.trove.TIntHashSet;
import gnu.trove.TIntIntHashMap;
import gnu.trove.TIntObjectHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* The DataGetter realizes the following pattern of getting some data (parametrized by {@code T}) from the VCS:
* <ul>
* <li>it tries to get it from the cache;</li>
* <li>if it fails, it tries to get it from the VCS, and additionally loads several commits around the requested one,
* to avoid querying the VCS if user investigates details of nearby commits.</li>
* <li>The loading happens asynchronously: a fake {@link LoadingDetails} object is returned </li>
* </ul>
*
* @author Kirill Likhodedov
*/
abstract class AbstractDataGetter<T extends VcsShortCommitDetails> implements Disposable, DataGetter<T> {
private static final Logger LOG = Logger.getInstance(AbstractDataGetter.class);
private static final int MAX_LOADING_TASKS = 10;
@NotNull protected final VcsLogStorage myHashMap;
@NotNull private final Map<VirtualFile, VcsLogProvider> myLogProviders;
@NotNull private final VcsCommitCache<Integer, T> myCache;
@NotNull private final SequentialLimitedLifoExecutor<TaskDescriptor> myLoader;
/**
* The sequence number of the current "loading" task.
*/
private long myCurrentTaskIndex = 0;
@NotNull private final Collection<Runnable> myLoadingFinishedListeners = new ArrayList<>();
@NotNull private VcsLogIndex myIndex;
AbstractDataGetter(@NotNull VcsLogStorage hashMap,
@NotNull Map<VirtualFile, VcsLogProvider> logProviders,
@NotNull VcsCommitCache<Integer, T> cache,
@NotNull VcsLogIndex index,
@NotNull Disposable parentDisposable) {
myHashMap = hashMap;
myLogProviders = logProviders;
myCache = cache;
myIndex = index;
Disposer.register(parentDisposable, this);
myLoader = new SequentialLimitedLifoExecutor<>(this, MAX_LOADING_TASKS, task -> {
preLoadCommitData(task.myCommits);
notifyLoaded();
});
}
private void notifyLoaded() {
UIUtil.invokeAndWaitIfNeeded((Runnable)() -> {
for (Runnable loadingFinishedListener : myLoadingFinishedListeners) {
loadingFinishedListener.run();
}
});
}
@Override
public void dispose() {
myLoadingFinishedListeners.clear();
}
@Override
@NotNull
public T getCommitData(@NotNull Integer hash, @NotNull Iterable<Integer> neighbourHashes) {
assert EventQueue.isDispatchThread();
T details = getFromCache(hash);
if (details != null) {
return details;
}
runLoadCommitsData(neighbourHashes);
T result = myCache.get(hash);
assert result != null; // now it is in the cache as "Loading Details" (runLoadCommitsData puts it there)
return result;
}
@Override
public void loadCommitsData(@NotNull List<Integer> hashes, @NotNull Consumer<List<T>> consumer, @Nullable ProgressIndicator indicator) {
assert EventQueue.isDispatchThread();
loadCommitsData(getCommitsMap(hashes), consumer, indicator);
}
private void loadCommitsData(@NotNull final TIntIntHashMap commits, @NotNull final Consumer<List<T>> consumer, @Nullable ProgressIndicator indicator) {
final List<T> result = ContainerUtil.newArrayList();
final TIntHashSet toLoad = new TIntHashSet();
long taskNumber = myCurrentTaskIndex++;
for (int id : commits.keys()) {
T details = getFromCache(id);
if (details == null || details instanceof LoadingDetails) {
toLoad.add(id);
cacheCommit(id, taskNumber);
}
else {
result.add(details);
}
}
if (toLoad.isEmpty()) {
sortCommitsByRow(result, commits);
consumer.consume(result);
}
else {
Task.Backgroundable task = new Task.Backgroundable(null, "Loading Selected Details", true, PerformInBackgroundOption.ALWAYS_BACKGROUND) {
@Override
public void run(@NotNull final ProgressIndicator indicator) {
indicator.checkCanceled();
try {
TIntObjectHashMap<T> map = preLoadCommitData(toLoad);
map.forEachValue(value -> {
result.add(value);
return true;
});
sortCommitsByRow(result, commits);
notifyLoaded();
}
catch (VcsException e) {
LOG.error(e);
}
}
@Override
public void onSuccess() {
consumer.consume(result);
}
};
if (indicator != null) {
ProgressManager.getInstance().runProcessWithProgressAsynchronously(task, indicator);
}
else {
ProgressManager.getInstance().run(task);
}
}
}
private void sortCommitsByRow(@NotNull List<T> result, @NotNull final TIntIntHashMap rowsForCommits) {
ContainerUtil.sort(result, (details1, details2) -> {
int row1 = rowsForCommits.get(myHashMap.getCommitIndex(details1.getId(), details1.getRoot()));
int row2 = rowsForCommits.get(myHashMap.getCommitIndex(details2.getId(), details2.getRoot()));
return Comparing.compare(row1, row2);
});
}
@Override
@Nullable
public T getCommitDataIfAvailable(int hash) {
return getFromCache(hash);
}
@Nullable
private T getFromCache(@NotNull Integer commitId) {
T details = myCache.get(commitId);
if (details != null) {
if (details instanceof LoadingDetails) {
if (((LoadingDetails)details).getLoadingTaskIndex() <= myCurrentTaskIndex - MAX_LOADING_TASKS) {
// don't let old "loading" requests stay in the cache forever
myCache.remove(commitId);
return null;
}
}
return details;
}
return getFromAdditionalCache(commitId);
}
/**
* Lookup somewhere else but the standard cache.
*/
@Nullable
protected abstract T getFromAdditionalCache(int commitId);
private void runLoadCommitsData(@NotNull Iterable<Integer> hashes) {
long taskNumber = myCurrentTaskIndex++;
TIntIntHashMap commits = getCommitsMap(hashes);
TIntHashSet toLoad = new TIntHashSet();
for (int id : commits.keys()) {
cacheCommit(id, taskNumber);
toLoad.add(id);
}
myLoader.queue(new TaskDescriptor(toLoad));
}
private void cacheCommit(final int commitId, long taskNumber) {
// fill the cache with temporary "Loading" values to avoid producing queries for each commit that has not been cached yet,
// even if it will be loaded within a previous query
if (!myCache.isKeyCached(commitId)) {
myCache.put(commitId, (T)new IndexedDetails(myIndex, myHashMap, commitId, taskNumber));
}
}
@NotNull
private static TIntIntHashMap getCommitsMap(@NotNull Iterable<Integer> hashes) {
TIntIntHashMap commits = new TIntIntHashMap();
int row = 0;
for (Integer commitId : hashes) {
commits.put(commitId, row);
row++;
}
return commits;
}
@NotNull
public TIntObjectHashMap<T> preLoadCommitData(@NotNull TIntHashSet commits) throws VcsException {
TIntObjectHashMap<T> result = new TIntObjectHashMap<>();
final MultiMap<VirtualFile, String> rootsAndHashes = MultiMap.create();
commits.forEach(commit -> {
CommitId commitId = myHashMap.getCommitId(commit);
if (commitId != null) {
rootsAndHashes.putValue(commitId.getRoot(), commitId.getHash().asString());
}
return true;
});
for (Map.Entry<VirtualFile, Collection<String>> entry : rootsAndHashes.entrySet()) {
VcsLogProvider logProvider = myLogProviders.get(entry.getKey());
if (logProvider != null) {
List<? extends T> details = readDetails(logProvider, entry.getKey(), ContainerUtil.newArrayList(entry.getValue()));
for (T data : details) {
int index = myHashMap.getCommitIndex(data.getId(), data.getRoot());
result.put(index, data);
}
saveInCache(result);
}
else {
LOG.error("No log provider for root " + entry.getKey().getPath() + ". All known log providers " + myLogProviders);
}
}
return result;
}
public void saveInCache(@NotNull TIntObjectHashMap<T> details) {
UIUtil.invokeAndWaitIfNeeded((Runnable)() -> details.forEachEntry((key, value) -> {
myCache.put(key, value);
return true;
}));
}
@NotNull
protected abstract List<? extends T> readDetails(@NotNull VcsLogProvider logProvider, @NotNull VirtualFile root, @NotNull List<String> hashes)
throws VcsException;
/**
* This listener will be notified when any details loading process finishes.
* The notification will happen in the EDT.
*/
public void addDetailsLoadedListener(@NotNull Runnable runnable) {
myLoadingFinishedListeners.add(runnable);
}
public void removeDetailsLoadedListener(@NotNull Runnable runnable) {
myLoadingFinishedListeners.remove(runnable);
}
private static class TaskDescriptor {
@NotNull private final TIntHashSet myCommits;
private TaskDescriptor(@NotNull TIntHashSet commits) {
myCommits = commits;
}
}
}