VCS: line status tracker IDEA-62358

- use QueueProcessor with ability to cancel tasks (QueueProcessorRemovePartner)
- reuse loaded base revision if version didn't changed
This commit is contained in:
irengrig
2011-02-28 18:25:31 +03:00
parent d5d5d614b9
commit 6ec244ef8e
6 changed files with 256 additions and 52 deletions
@@ -0,0 +1,83 @@
/*
* Copyright 2000-2011 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.util.concurrency;
import com.intellij.openapi.project.Project;
import com.intellij.util.Consumer;
import java.util.HashMap;
import java.util.Map;
/**
* @author irengrig
* Date: 2/28/11
* Time: 3:41 PM
*/
public class QueueProcessorRemovePartner<Key, Task> {
private final Map<Key, Task> myMap;
private final QueueProcessor<Key> myProcessor;
private final Consumer<Task> myConsumer;
private final Object myLock;
public QueueProcessorRemovePartner(final Project project, Consumer<Task> consumer) {
myConsumer = consumer;
myMap = new HashMap<Key, Task>();
myLock = new Object();
myProcessor = new QueueProcessor<Key>(new Consumer<Key>() {
@Override
public void consume(Key key) {
final Task task;
synchronized (myLock) {
task = myMap.remove(key);
}
if (task != null) {
myConsumer.consume(task);
}
}
}, project.getDisposed(), true);
}
public void add(final Key key, final Task task) {
synchronized (myLock) {
myMap.put(key, task);
}
myProcessor.add(key);
}
public void remove(final Key key) {
synchronized (myLock) {
myMap.remove(key);
}
}
public boolean isEmpty() {
synchronized (myLock) {
return myMap.isEmpty();
}
}
public void clear() {
synchronized (myLock) {
myMap.clear();
}
}
public boolean containsKey(final Key key) {
synchronized (myLock) {
return myMap.containsKey(key);
}
}
}
@@ -101,5 +101,8 @@
<implementation-class>com.intellij.openapi.vcs.impl.VcsFileStatusProvider</implementation-class>
<skipForDefaultProject/>
</component>
<component><interface-class>com.intellij.openapi.vcs.impl.VcsBaseContentProvider</interface-class>
<implementation-class>com.intellij.openapi.vcs.impl.VcsFileStatusProvider</implementation-class>
</component>
</project-components>
</components>
@@ -30,6 +30,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsBundle;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -60,6 +61,9 @@ public class LineStatusTracker {
private boolean myBulkUpdate;
private final Application myApplication;
@Nullable
private RevisionPack myBaseRevisionNumber;
private String myPreviousBaseRevision;
private LineStatusTracker(final Document document, final Document upToDateDocument, final Project project) {
myApplication = ApplicationManager.getApplication();
@@ -71,12 +75,16 @@ public class LineStatusTracker {
myRanges = new ArrayList<Range>();
}
public void initialize(@NotNull final String upToDateContent) {
public void initialize(@NotNull final String upToDateContent, @NotNull RevisionPack baseRevisionNumber) {
ApplicationManager.getApplication().assertIsDispatchThread();
synchronized (myLock) {
LOG.assertTrue(BaseLoadState.LOADING == myBaseLoaded);
try {
if (myBaseRevisionNumber != null && myBaseRevisionNumber.after(baseRevisionNumber)) return;
myBaseRevisionNumber = baseRevisionNumber;
myPreviousBaseRevision = null;
myUpToDateDocument.setReadOnly(false);
myUpToDateDocument.replaceString(0, myUpToDateDocument.getTextLength(), upToDateContent);
myUpToDateDocument.setReadOnly(true);
@@ -93,6 +101,20 @@ public class LineStatusTracker {
}
}
public void useCachedBaseRevision(final RevisionPack number) {
synchronized (myLock) {
assert myBaseRevisionNumber != null;
if (myPreviousBaseRevision == null || myBaseRevisionNumber.after(number)) return;
initialize(myPreviousBaseRevision, number);
}
}
public boolean canUseBaseRevision(final RevisionPack number) {
synchronized (myLock) {
return myBaseRevisionNumber != null && myBaseRevisionNumber.equals(number) && myPreviousBaseRevision != null;
}
}
private void reinstallRanges() {
myApplication.assertReadAccessAllowed();
@@ -194,18 +216,22 @@ public class LineStatusTracker {
* @return true if was cleared and base revision contents load should be started
* false -> load was already started; after contents is loaded,
*/
public boolean resetForBaseRevisionLoad() {
public void resetForBaseRevisionLoad() {
myApplication.assertReadAccessAllowed();
synchronized (myLock) {
if (BaseLoadState.LOADING == myBaseLoaded) return false;
// there can be multiple resets before init -> take from document only firts time -> when right after install(),
// where myPreviousBaseRevision become null
if (BaseLoadState.LOADED.equals(myBaseLoaded) && myPreviousBaseRevision == null) {
myPreviousBaseRevision = myUpToDateDocument.getText();
}
myUpToDateDocument.setReadOnly(false);
myUpToDateDocument.setText("");
myUpToDateDocument.setReadOnly(true);
removeHighlightersFromMarkupModel();
myRanges.clear();
myBaseLoaded = BaseLoadState.LOADING;
return true;
return;
}
}
@@ -546,10 +572,6 @@ public class LineStatusTracker {
return new LineStatusTracker(doc, document, project);
}
public BaseLoadState getBaseLoaded() {
return myBaseLoaded;
}
public void baseRevisionLoadFailed() {
synchronized (myLock) {
myBaseLoaded = BaseLoadState.FAILED;
@@ -561,4 +583,42 @@ public class LineStatusTracker {
FAILED,
LOADED
}
public static class RevisionPack {
private final long myNumber;
private final VcsRevisionNumber myRevision;
public RevisionPack(long number, VcsRevisionNumber revision) {
myNumber = number;
myRevision = revision;
}
public long getNumber() {
return myNumber;
}
public VcsRevisionNumber getRevision() {
return myRevision;
}
public boolean after(final RevisionPack previous) {
if (myRevision.equals(previous.getRevision())) return false;
return myNumber > previous.getNumber();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
RevisionPack that = (RevisionPack)o;
return myRevision.equals(that.getRevision());
}
@Override
public int hashCode() {
return myRevision.hashCode();
}
}
}
@@ -45,24 +45,21 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.AbstractVcs;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.openapi.vcs.FileStatusListener;
import com.intellij.openapi.vcs.FileStatusManager;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.changes.committed.AbstractCalledLater;
import com.intellij.openapi.vcs.ex.LineStatusTracker;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileAdapter;
import com.intellij.openapi.vfs.VirtualFileEvent;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.util.Alarm;
import com.intellij.util.Consumer;
import com.intellij.util.concurrency.QueueProcessorRemovePartner;
import com.intellij.util.containers.HashMap;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
public class LineStatusTrackerManager implements ProjectComponent, LineStatusTrackerManagerI {
@@ -81,25 +78,32 @@ public class LineStatusTrackerManager implements ProjectComponent, LineStatusTra
private final Map<Document, LineStatusTracker> myLineStatusTrackers;
// !!! no state queries and self lock for add/remove
// removal from here - not under write action
private final Map<Document, Alarm> myLineStatusUpdateAlarms;
private final QueueProcessorRemovePartner<Document, BaseRevisionLoader> myPartner;
@NonNls protected static final String IGNORE_CHANGEMARKERS_KEY = "idea.ignore.changemarkers";
private final ProjectLevelVcsManagerImpl myVcsManager;
private final VcsFileStatusProvider myStatusProvider;
private final ProjectLevelVcsManager myVcsManager;
private final VcsBaseContentProvider myStatusProvider;
private final Application myApplication;
private final FileEditorManager myFileEditorManager;
private final Disposable myDisposable;
private long myLoadCounter;
public LineStatusTrackerManager(final Project project, final ProjectLevelVcsManagerImpl vcsManager, final VcsFileStatusProvider statusProvider,
public LineStatusTrackerManager(final Project project, final ProjectLevelVcsManager vcsManager, final VcsBaseContentProvider statusProvider,
final Application application, final FileEditorManager fileEditorManager) {
myLoadCounter = 0;
myProject = project;
myVcsManager = vcsManager;
myStatusProvider = statusProvider;
myApplication = application;
myFileEditorManager = fileEditorManager;
myLineStatusTrackers = new HashMap<Document, LineStatusTracker>();
myLineStatusUpdateAlarms = Collections.synchronizedMap(new HashMap<Document, Alarm>());
myPartner = new QueueProcessorRemovePartner<Document,BaseRevisionLoader>(myProject, new Consumer<BaseRevisionLoader>() {
@Override
public void consume(BaseRevisionLoader baseRevisionLoader) {
baseRevisionLoader.run();
}
});
project.getMessageBus().connect().subscribe(DocumentBulkUpdateListener.TOPIC, new DocumentBulkUpdateListener.Adapter() {
public void updateStarted(final Document doc) {
@@ -119,16 +123,13 @@ public class LineStatusTrackerManager implements ProjectComponent, LineStatusTra
synchronized (myLock) {
for (final LineStatusTracker tracker : myLineStatusTrackers.values()) {
final Document document = tracker.getDocument();
final Alarm alarm = myLineStatusUpdateAlarms.remove(document);
if (alarm != null) {
alarm.cancelAllRequests();
}
myPartner.remove(document);
tracker.release();
}
myLineStatusTrackers.clear();
assert myLineStatusUpdateAlarms.isEmpty();
myLineStatusUpdateAlarms.clear();
assert myPartner.isEmpty();
myPartner.clear();
}
}
};
@@ -214,8 +215,6 @@ public class LineStatusTrackerManager implements ProjectComponent, LineStatusTra
if (! shouldBeInstalled) {
releaseTracker(document);
return;
} else if ((LineStatusTracker.BaseLoadState.LOADING == tracker.getBaseLoaded())) {
return; // will be recalculated
} else {
tracker.resetForBaseRevisionLoad();
startAlarm(document, virtualFile);
@@ -230,10 +229,7 @@ public class LineStatusTrackerManager implements ProjectComponent, LineStatusTra
if ((! myProject.isOpen()) || myProject.isDisposed()) return;
synchronized (myLock) {
final Alarm alarm = myLineStatusUpdateAlarms.remove(document);
if (alarm != null) {
alarm.cancelAllRequests();
}
myPartner.remove(document);
final LineStatusTracker tracker = myLineStatusTrackers.remove(document);
if (tracker != null) {
tracker.release();
@@ -265,7 +261,7 @@ public class LineStatusTrackerManager implements ProjectComponent, LineStatusTra
private void installTracker(final VirtualFile virtualFile, final Document document) {
synchronized (myLock) {
if (myLineStatusTrackers.containsKey(document)) return;
assert !myLineStatusUpdateAlarms.containsKey(document);
assert ! myPartner.containsKey(document);
final LineStatusTracker tracker = LineStatusTracker.createOn(document, myProject);
myLineStatusTrackers.put(document, tracker);
@@ -277,33 +273,22 @@ public class LineStatusTrackerManager implements ProjectComponent, LineStatusTra
private void startAlarm(final Document document, final VirtualFile virtualFile) {
myApplication.assertReadAccessAllowed();
final Alarm alarm = new Alarm(Alarm.ThreadToUse.SHARED_THREAD);
synchronized (myLock) {
myLineStatusUpdateAlarms.put(document, alarm);
myPartner.add(document, new BaseRevisionLoader(document, virtualFile));
}
alarm.addRequest(new BaseRevisionLoader(alarm, document, virtualFile), 10);
}
private class BaseRevisionLoader implements Runnable {
private final Alarm myAlarm;
private final VirtualFile myVirtualFile;
private final Document myDocument;
private BaseRevisionLoader(final Alarm alarm, final Document document, final VirtualFile virtualFile) {
myAlarm = alarm;
private BaseRevisionLoader(final Document document, final VirtualFile virtualFile) {
myDocument = document;
myVirtualFile = virtualFile;
}
@Override
public void run() {
myAlarm.cancelAllRequests();
synchronized (myLock) {
final Alarm removed = myLineStatusUpdateAlarms.remove(myDocument);
if (removed == null) {
return;
}
}
if ((! myProject.isOpen()) || myProject.isDisposed()) return;
if (! myVirtualFile.isValid()) {
@@ -312,6 +297,31 @@ public class LineStatusTrackerManager implements ProjectComponent, LineStatusTra
return;
}
final VcsRevisionNumber baseRevision = myStatusProvider.getBaseRevision(myVirtualFile);
if (baseRevision == null) {
log("installTracker() for file " + myVirtualFile.getPath() + " failed: null returned for base revision number");
reportTrackerBaseLoadFailed();
return;
}
// loads are sequential (in single threaded QueueProcessor);
// so myLoadCounter can't take less value for greater base revision -> the only thing we want from it
final LineStatusTracker.RevisionPack revisionPack = new LineStatusTracker.RevisionPack(myLoadCounter, baseRevision);
++ myLoadCounter;
synchronized (myLock) {
final LineStatusTracker tracker = myLineStatusTrackers.get(myDocument);
if (tracker != null && tracker.canUseBaseRevision(revisionPack)) {
nonModalAliveInvokeLater(new Runnable() {
@Override
public void run() {
log("installTracker() for file " + myVirtualFile.getPath() + " base revision number already in tracker");
tracker.useCachedBaseRevision(revisionPack);
}
});
// already ok revision
return;
}
}
final String lastUpToDateContent = myStatusProvider.getBaseVersionContent(myVirtualFile);
if (lastUpToDateContent == null) {
log("installTracker() for file " + myVirtualFile.getPath() + " failed: no up to date content");
@@ -320,17 +330,22 @@ public class LineStatusTrackerManager implements ProjectComponent, LineStatusTra
}
final String converted = StringUtil.convertLineSeparators(lastUpToDateContent);
myApplication.invokeLater(new Runnable() {
final Runnable runnable = new Runnable() {
public void run() {
synchronized (myLock) {
log("initializing tracker for file " + myVirtualFile.getPath());
final LineStatusTracker tracker = myLineStatusTrackers.get(myDocument);
if (tracker != null) {
tracker.initialize(converted);
tracker.initialize(converted, revisionPack);
}
}
}
}, new Condition() {
};
nonModalAliveInvokeLater(runnable);
}
private void nonModalAliveInvokeLater(Runnable runnable) {
myApplication.invokeLater(runnable, ModalityState.NON_MODAL, new Condition() {
@Override
public boolean value(final Object ignore) {
return (! myProject.isOpen()) || myProject.isDisposed();
@@ -0,0 +1,32 @@
/*
* Copyright 2000-2011 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.openapi.vcs.impl;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.Nullable;
/**
* @author irengrig
* Date: 2/28/11
* Time: 1:12 PM
*/
public interface VcsBaseContentProvider {
@Nullable
String getBaseVersionContent(VirtualFile file);
@Nullable
VcsRevisionNumber getBaseRevision(VirtualFile file);
}
@@ -27,6 +27,7 @@ import com.intellij.openapi.vcs.FileStatus;
import com.intellij.openapi.vcs.ProjectLevelVcsManager;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vcs.readOnlyHandler.ReadonlyStatusHandlerImpl;
import com.intellij.openapi.vcs.rollback.RollbackEnvironment;
import com.intellij.openapi.vfs.VirtualFile;
@@ -35,7 +36,7 @@ import org.jetbrains.annotations.Nullable;
/**
* @author yole
*/
public class VcsFileStatusProvider implements FileStatusProvider {
public class VcsFileStatusProvider implements FileStatusProvider, VcsBaseContentProvider {
private final Project myProject;
private final FileStatusManagerImpl myFileStatusManager;
private final ProjectLevelVcsManager myVcsManager;
@@ -133,8 +134,9 @@ public class VcsFileStatusProvider implements FileStatusProvider {
}
}
@Override
@Nullable
String getBaseVersionContent(final VirtualFile file) {
public String getBaseVersionContent(final VirtualFile file) {
final Change change = ChangeListManager.getInstance(myProject).getChange(file);
if (change != null) {
final ContentRevision beforeRevision = change.getBeforeRevision();
@@ -166,4 +168,13 @@ public class VcsFileStatusProvider implements FileStatusProvider {
return null;
}
@Override
public VcsRevisionNumber getBaseRevision(VirtualFile file) {
final Change change = ChangeListManager.getInstance(myProject).getChange(file);
if (change != null && change.getBeforeRevision() != null && (! FileStatus.ADDED.equals(change.getFileStatus()))) {
return change.getBeforeRevision().getRevisionNumber();
}
return null;
}
}