/* * Copyright 2000-2014 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.vcs.log.data; import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.Task; import com.intellij.openapi.progress.impl.ProgressManagerImpl; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.Consumer; import com.intellij.util.Function; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.UIUtil; import com.intellij.vcs.log.*; import com.intellij.vcs.log.graph.GraphCommit; import com.intellij.vcs.log.graph.GraphCommitImpl; import com.intellij.vcs.log.graph.PermanentGraph; import com.intellij.vcs.log.impl.RequirementsImpl; import com.intellij.vcs.log.util.StopWatch; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; public class VcsLogRefresherImpl implements VcsLogRefresher { private static final Logger LOG = Logger.getInstance(VcsLogRefresherImpl.class); @NotNull private final Project myProject; @NotNull private final VcsLogHashMap myHashMap; @NotNull private final Map myProviders; @NotNull private final VcsUserRegistryImpl myUserRegistry; @NotNull private final Map myTopCommitsDetailsCache; @NotNull private final Consumer myExceptionHandler; private final int myRecentCommitCount; @NotNull private final SingleTaskController mySingleTaskController; @NotNull private DataPack myDataPack = EmptyDataPack.getInstance(); public VcsLogRefresherImpl(@NotNull final Project project, @NotNull VcsLogHashMap hashMap, @NotNull Map providers, @NotNull final VcsUserRegistryImpl userRegistry, @NotNull Map topCommitsDetailsCache, @NotNull final Consumer dataPackUpdateHandler, @NotNull Consumer exceptionHandler, int recentCommitsCount) { myProject = project; myHashMap = hashMap; myProviders = providers; myUserRegistry = userRegistry; myTopCommitsDetailsCache = topCommitsDetailsCache; myExceptionHandler = exceptionHandler; myRecentCommitCount = recentCommitsCount; Consumer dataPackUpdater = new Consumer() { @Override public void consume(@NotNull DataPack dataPack) { myDataPack = dataPack; dataPackUpdateHandler.consume(dataPack); } }; mySingleTaskController = new SingleTaskController(dataPackUpdater) { @Override protected void startNewBackgroundTask() { UIUtil.invokeLaterIfNeeded(new Runnable() { @Override public void run() { ProgressManagerImpl.runProcessWithProgressAsynchronously(new MyRefreshTask(myDataPack)); } }); } }; } @NotNull @Override public DataPack readFirstBlock() { try { Map> refs = loadRefsFromVcs(myProviders); Set roots = myProviders.keySet(); Map requirements = prepareSimpleRequirements(roots, myRecentCommitCount); Map>> commits = loadRecentCommitsFromVcs(myProviders, requirements, myUserRegistry, myTopCommitsDetailsCache, myHashMap); List> compoundLog = compound(commits.values()); DataPack dataPack = DataPack.build(compoundLog, new RefsModel(refs, myHashMap.asIndexGetter()), myHashMap.asIndexGetter(), myHashMap.asHashGetter(), myProviders, false); mySingleTaskController.request(RefreshRequest.RELOAD_ALL); // build/rebuild the full log in bg return dataPack; } catch (VcsException e) { myExceptionHandler.consume(e); return EmptyDataPack.getInstance(); } } @Override public void refresh(@NotNull Collection rootsToRefresh) { if (!rootsToRefresh.isEmpty()) { mySingleTaskController.request(new RefreshRequest(rootsToRefresh)); } } @NotNull private static Map prepareSimpleRequirements(@NotNull Collection roots, final int commitCount) { final VcsLogProvider.Requirements requirements = new VcsLogProvider.Requirements() { @Override public int getCommitCount() { return commitCount; } }; return ContainerUtil.map2Map(roots, new Function>() { @Override public Pair fun(VirtualFile file) { return Pair.create(file, requirements); } }); } @NotNull private static Map> loadRefsFromVcs(@NotNull Map providers) throws VcsException { final StopWatch sw = StopWatch.start("loading refs"); final Map> refs = ContainerUtil.newHashMap(); new ProviderIterator() { @Override void each(@NotNull VirtualFile root, @NotNull VcsLogProvider provider) throws VcsException { refs.put(root, provider.readAllRefs(root)); sw.rootCompleted(root); } }.iterate(providers); sw.report(); return refs; } @NotNull private static Map>> loadRecentCommitsFromVcs( @NotNull Map providers, @NotNull final Map requirements, @NotNull final VcsUserRegistryImpl userRegistry, @NotNull final Map topCommitsDetailsCache, @NotNull final VcsLogHashMap hashMap) throws VcsException { final StopWatch sw = StopWatch.start("loading commits"); final Map>> commits = ContainerUtil.newHashMap(); new ProviderIterator() { @Override public void each(@NotNull VirtualFile root, @NotNull VcsLogProvider provider) throws VcsException { List metadatas = provider.readFirstBlock(root, requirements.get(root)); storeUsersAndDetails(metadatas, userRegistry, topCommitsDetailsCache); commits.put(root, compactCommits(metadatas, hashMap)); sw.rootCompleted(root); } }.iterate(providers); userRegistry.flush(); sw.report(); return commits; } /** * Compounds logs from different repositories into a single multi-repository log. */ @NotNull private static List> compound(@NotNull Collection>> commits) { StopWatch sw = StopWatch.start("multi-repo join"); List> joined = new VcsLogMultiRepoJoiner().join(commits); sw.report(); return joined; } @NotNull private static List> compactCommits(@NotNull List commits, @NotNull final VcsLogHashMap hashMap) { StopWatch sw = StopWatch.start("compacting commits"); List> map = ContainerUtil.map(commits, new Function>() { @NotNull @Override public GraphCommit fun(@NotNull TimedVcsCommit commit) { return compactCommit(commit, hashMap); } }); hashMap.flush(); sw.report(); return map; } @NotNull private static GraphCommitImpl compactCommit(@NotNull TimedVcsCommit commit, @NotNull VcsLogHashMap hashMap) { return new GraphCommitImpl(hashMap.getCommitIndex(commit.getId()), ContainerUtil.map(commit.getParents(), hashMap.asIndexGetter()), commit.getTimestamp()); } private static void storeUsersAndDetails(@NotNull List metadatas, @NotNull VcsUserRegistryImpl userRegistry, @NotNull Map topCommitsDetailsCache) { for (VcsCommitMetadata detail : metadatas) { userRegistry.addUser(detail.getAuthor()); userRegistry.addUser(detail.getCommitter()); topCommitsDetailsCache.put(detail.getId(), detail); } } private class MyRefreshTask extends Task.Backgroundable { @NotNull private DataPack myCurrentDataPack; // collects loaded info from different roots, which refresh was requested consecutively within a single task private final Map myLoadedInfos = ContainerUtil.newHashMap(); private class LogAndRefs { List> log; Collection refs; LogAndRefs(Collection refs, List> commits) { this.refs = refs; this.log = commits; } } MyRefreshTask(@NotNull DataPack currentDataPack) { super(VcsLogRefresherImpl.this.myProject, "Refreshing history...", false); myCurrentDataPack = currentDataPack; } @Override public void run(@NotNull ProgressIndicator indicator) { DataPack dataPack = myCurrentDataPack; while (true) { List requests = mySingleTaskController.popRequests(); Collection rootsToRefresh = getRootsToRefresh(requests); if (rootsToRefresh.isEmpty()) { mySingleTaskController.taskCompleted(dataPack); break; } dataPack = doRefresh(rootsToRefresh); } } @NotNull private Collection getRootsToRefresh(@NotNull List requests) { Collection rootsToRefresh = ContainerUtil.newArrayList(); for (RefreshRequest request : requests) { if (request == RefreshRequest.RELOAD_ALL) { myCurrentDataPack = EmptyDataPack.getInstance(); return myProviders.keySet(); } rootsToRefresh.addAll(request.rootsToRefresh); } return rootsToRefresh; } @NotNull private DataPack doRefresh(@NotNull Collection roots) { StopWatch sw = StopWatch.start("refresh"); PermanentGraph permanentGraph = myCurrentDataPack.isFull() ? myCurrentDataPack.getPermanentGraph() : null; Map> currentRefs = myCurrentDataPack.getRefsModel().getAllRefsByRoot(); try { if (permanentGraph != null) { int commitCount = myRecentCommitCount; for (int attempt = 0; attempt <= 1; attempt++) { loadLogAndRefs(roots, currentRefs, commitCount); List> compoundLog = compoundLoadedLogs(myLoadedInfos.values()); Map> allNewRefs = getAllNewRefs(myLoadedInfos, currentRefs); List> joinedFullLog = join(compoundLog, permanentGraph.getAllCommits(), currentRefs, allNewRefs); if (joinedFullLog == null) { commitCount *= 5; } else { return DataPack.build(joinedFullLog, new RefsModel(allNewRefs, myHashMap.asIndexGetter()), myHashMap.asIndexGetter(), myHashMap.asHashGetter(), myProviders, true); } } // couldn't join => need to reload everything; if 5000 commits is still not enough, it's worth reporting: LOG.error("Couldn't join " + commitCount + " recent commits to the log (" + permanentGraph.getAllCommits().size() + " commits)", new Attachment("recent_commits", toLogString(myLoadedInfos))); } Pair, Map>> fullLogAndRefs = loadFullLog(); return DataPack.build(fullLogAndRefs.first, myProviders, new RefsModel(fullLogAndRefs.second, myHashMap.asIndexGetter()), true); } catch (Exception e) { myExceptionHandler.consume(e); return EmptyDataPack.getInstance(); } finally { sw.report(); } } private String toLogString(Map infos) { StringBuilder sb = new StringBuilder(); for (Map.Entry entry : infos.entrySet()) { sb.append(entry.getKey().getName()); sb.append(" LOG:\n"); sb.append(StringUtil.join(entry.getValue().log, new Function, String>() { @Override public String fun(GraphCommit commit) { return commit.getId() + "<-" + StringUtil.join(commit.getParents(), ","); } }, "\n")); sb.append("\nREFS:\n"); sb.append(StringUtil.join(entry.getValue().refs, new Function() { @Override public String fun(VcsRef ref) { return ref.getName() + "(" + myHashMap.getCommitIndex(ref.getCommitHash()) + ")"; } }, ",")); } return sb.toString(); } @NotNull private List> compoundLoadedLogs(@NotNull Collection logsAndRefs) { return compound(ContainerUtil.map(logsAndRefs, new Function>>() { @Override public List> fun(LogAndRefs refs) { return refs.log; } })); } @NotNull private Map> getAllNewRefs(@NotNull Map newInfo, @NotNull Map> previousRefs) { Map> result = ContainerUtil.newHashMap(); for (VirtualFile root : previousRefs.keySet()) { result.put(root, newInfo.containsKey(root) ? newInfo.get(root).refs : previousRefs.get(root)); } return result; } private void loadLogAndRefs(@NotNull Collection roots, @NotNull Map> prevRefs, int commitCount) throws VcsException { Map providers = getProviders(roots); Map> refs = loadRefsFromVcs(providers); Map requirements = prepareRequirements(roots, commitCount, prevRefs, refs); Map>> commits = loadRecentCommitsFromVcs(providers, requirements, myUserRegistry, myTopCommitsDetailsCache, myHashMap); for (VirtualFile root : roots) { myLoadedInfos.put(root, new LogAndRefs(refs.get(root), commits.get(root))); } } @NotNull private Map prepareRequirements(@NotNull Collection roots, int commitCount, @NotNull Map> prevRefs, @NotNull Map> newRefs) { Map requirements = ContainerUtil.newHashMap(); for (VirtualFile root : roots) { requirements.put(root, new RequirementsImpl(commitCount, true, getRefsForRoot(prevRefs, root), getRefsForRoot(newRefs, root))); } return requirements; } @NotNull private Set getRefsForRoot(@NotNull Map> map, @NotNull VirtualFile root) { Collection refs = map.get(root); return refs == null ? Collections.emptySet() : new HashSet(refs); } @NotNull private Map getProviders(@NotNull Collection roots) { Map providers = ContainerUtil.newHashMap(); for (VirtualFile root : roots) { providers.put(root, myProviders.get(root)); } return providers; } @Nullable private List> join(@NotNull List> recentCommits, @NotNull List> fullLog, @NotNull Map> previousRefs, @NotNull Map> newRefs) { StopWatch sw = StopWatch.start("joining new commits"); Function ref2Int = new Function() { @NotNull @Override public Integer fun(@NotNull VcsRef ref) { return myHashMap.getCommitIndex(ref.getCommitHash()); } }; Collection prevRefIndices = ContainerUtil.map(ContainerUtil.concat(previousRefs.values()), ref2Int); Collection newRefIndices = ContainerUtil.map(ContainerUtil.concat(newRefs.values()), ref2Int); try { List> commits = new VcsLogJoiner>().addCommits(fullLog, prevRefIndices, recentCommits, newRefIndices).first; sw.report(); return commits; } catch (VcsLogRefreshNotEnoughDataException e) { LOG.info(e); } catch (IllegalStateException e) { LOG.error(e); } return null; } @NotNull private Pair, Map>> loadFullLog() throws VcsException { StopWatch sw = StopWatch.start("full log reload"); Collection>> commits = readFullLogFromVcs(); List> graphCommits = compound(commits); Map> refMap = loadRefsFromVcs(myProviders); PermanentGraph permanentGraph = DataPack.buildPermanentGraph(graphCommits, new RefsModel(refMap, myHashMap.asIndexGetter()), myHashMap.asIndexGetter(), myHashMap.asHashGetter(), myProviders); sw.report(); return Pair.create(permanentGraph, refMap); } @NotNull private Collection>> readFullLogFromVcs() throws VcsException { final StopWatch sw = StopWatch.start("read full log from VCS"); final Collection>> logs = ContainerUtil.newArrayList(); new ProviderIterator() { @Override void each(@NotNull VirtualFile root, @NotNull VcsLogProvider provider) throws VcsException { final List> graphCommits = ContainerUtil.newArrayList(); provider.readAllHashes(root, new Consumer() { @Override public void consume(@NotNull VcsUser user) { myUserRegistry.addUser(user); } }, new Consumer() { @Override public void consume(TimedVcsCommit commit) { graphCommits.add(compactCommit(commit, myHashMap)); } }); logs.add(graphCommits); sw.rootCompleted(root); } }.iterate(myProviders); myUserRegistry.flush(); sw.report(); return logs; } } private static class RefreshRequest { private static RefreshRequest RELOAD_ALL = new RefreshRequest(Collections.emptyList()); @NotNull private final Collection rootsToRefresh; RefreshRequest(@NotNull Collection rootsToRefresh) { this.rootsToRefresh = rootsToRefresh; } } private static abstract class ProviderIterator { abstract void each(@NotNull VirtualFile root, @NotNull VcsLogProvider provider) throws VcsException; final void iterate(@NotNull Map providers) throws VcsException { for (Map.Entry entry : providers.entrySet()) { each(entry.getKey(), entry.getValue()); } } } }