smart pointer update: use CPU-cache-friendly sorted arrays instead of a long hash map

This commit is contained in:
peter
2015-10-17 08:34:51 +02:00
parent 9cc39855cb
commit ccf424f4d4
5 changed files with 164 additions and 80 deletions
@@ -38,6 +38,7 @@ import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.PostprocessReformattingAspect;
import com.intellij.psi.impl.source.PsiFileImpl;
import com.intellij.psi.impl.source.tree.FileElement;
import com.intellij.psi.search.GlobalSearchScope;
@@ -46,17 +47,19 @@ import com.intellij.psi.stubs.StubTree;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.PsiUtilBase;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.testFramework.*;
import com.intellij.util.FileContentUtil;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.junit.Assert;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.Collections;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
@PlatformTestCase.WrapInCommand
@SkipSlowTestLocally
@@ -849,6 +852,27 @@ public class SmartPsiElementPointersTest extends CodeInsightTestCase {
assertNotNull(pointer.getElement());
}
public void testManyPsiChangesWithManySmartPointersPerformance() {
String eachTag = "<a>\n" + StringUtil.repeat(" <a> </a>\n", 9) + "</a>\n";
XmlFile file = (XmlFile)configureByText(XmlFileType.INSTANCE, "<root>\n" + StringUtil.repeat(eachTag, 500) + "</root>");
List<XmlTag> tags = ContainerUtil.newArrayList(PsiTreeUtil.findChildrenOfType(file.getDocument(), XmlTag.class));
List<SmartPsiElementPointer> pointers = tags.stream().map(this::createPointer).collect(Collectors.toList());
Random random = new Random();
PlatformTestUtil.startPerformanceTest("smart pointer range update after PSI change", 21000, () -> {
for (int i = 0; i < tags.size(); i++) {
XmlTag tag = tags.get(i);
SmartPsiElementPointer pointer = pointers.get(i);
assertEquals(tag.getName().length(), TextRange.create(pointer.getRange()).getLength());
assertEquals(tag.getName().length(), TextRange.create(pointer.getPsiRange()).getLength());
tag.setName("bar" + random.nextInt(20));
assertEquals(tag.getName().length(), TextRange.create(pointer.getRange()).getLength());
assertEquals(tag.getName().length(), TextRange.create(pointer.getPsiRange()).getLength());
}
PostprocessReformattingAspect.getInstance(myProject).doPostponedFormatting();
}).cpuBound().assertTiming();
}
@NotNull
private <T extends PsiElement> SmartPointerEx<T> createPointer(T element) {
return (SmartPointerEx<T>)getPointerManager().createSmartPsiElementPointer(element);
@@ -45,7 +45,7 @@ class AnchorElementInfo extends SelfElementInfo {
@NotNull PsiFileWithStubSupport containingFile,
int stubId,
@NotNull IStubElementType stubElementType) {
super(containingFile.getProject(), new ProperTextRange(0, 0), anchor.getClass(), containingFile, containingFile.getLanguage(), false);
super(containingFile.getProject(), null, anchor.getClass(), containingFile, containingFile.getLanguage(), false);
myStubElementTypeAndId = pack(stubId, stubElementType);
assert !(anchor instanceof PsiFile) : "FileElementInfo must be used for file: "+anchor;
}
@@ -130,7 +130,7 @@ class AnchorElementInfo extends SelfElementInfo {
if (anchor == null) anchor = element;
myType = anchor.getClass();
setRange(anchor.getTextRange());
myMarkerCache.rangeChanged(markerCacheKey());
myMarkerCache.rangeChanged();
myStubElementTypeAndId = pack(-1, null);
}
}
@@ -21,70 +21,98 @@ import com.intellij.openapi.editor.impl.ManualRangeMarker;
import com.intellij.openapi.editor.impl.event.DocumentEventImpl;
import com.intellij.openapi.editor.impl.event.RetargetRangeMarkers;
import com.intellij.openapi.util.ProperTextRange;
import com.intellij.openapi.util.Trinity;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.NullableFunction;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import gnu.trove.TLongObjectHashMap;
import gnu.trove.TObjectFunction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.ref.Reference;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* @author peter
*/
class MarkerCache {
private static final Comparator<SelfElementInfo> BY_RANGE_KEY = new Comparator<SelfElementInfo>() {
@Override
public int compare(SelfElementInfo info1, SelfElementInfo info2) {
int o1 = info1.getPsiStartOffset();
int o2 = info2.getPsiStartOffset();
if (o1 != o2) return o1 > o2 ? 1 : -1;
o1 = info1.getPsiEndOffset();
o2 = info2.getPsiEndOffset();
if (o1 != o2) return o1 > o2 ? 1 : -1;
return (info1.isForInjected() ? 1 : 0) - (info2.isForInjected() ? 1 : 0);
}
};
private final SmartPointerManagerImpl.FilePointersList myPointers;
private final VirtualFile myVirtualFile;
private volatile Trinity<Integer, TLongObjectHashMap<ManualRangeMarker>, FrozenDocument> myUpdatedRanges;
private volatile UpdatedRanges myUpdatedRanges;
MarkerCache(SmartPointerManagerImpl.FilePointersList pointers, VirtualFile virtualFile) {
myPointers = pointers;
myVirtualFile = virtualFile;
}
private TLongObjectHashMap<ManualRangeMarker> getUpdatedMarkers(@NotNull FrozenDocument frozen, @NotNull List<DocumentEvent> events) {
private UpdatedRanges getUpdatedMarkers(@NotNull FrozenDocument frozen, @NotNull List<DocumentEvent> events) {
int eventCount = events.size();
assert eventCount > 0;
Trinity<Integer, TLongObjectHashMap<ManualRangeMarker>, FrozenDocument> cache = myUpdatedRanges;
if (cache != null && cache.first.intValue() == eventCount) return cache.second;
UpdatedRanges cache = myUpdatedRanges;
if (cache != null && cache.myEventCount == eventCount) return cache;
//noinspection SynchronizeOnThis
synchronized (this) {
cache = myUpdatedRanges;
if (cache != null && cache.first.intValue() == eventCount) return cache.second;
if (cache != null && cache.myEventCount == eventCount) return cache;
TLongObjectHashMap<ManualRangeMarker> answer;
if (cache != null && cache.first < eventCount) {
UpdatedRanges answer;
if (cache != null && cache.myEventCount < eventCount) {
// apply only the new events
answer = cache.second.clone();
frozen = applyEvents(cache.third, events.subList(cache.first, eventCount), answer);
answer = applyEvents(events.subList(cache.myEventCount, eventCount), cache);
}
else {
List<SelfElementInfo> infos = getInfos();
answer = new TLongObjectHashMap<ManualRangeMarker>(infos.size());
for (SelfElementInfo info : infos) {
ProperTextRange range = info.getPsiRange();
long key = info.markerCacheKey();
if (range != null && key != 0) {
boolean forInjected = info.isForInjected();
answer.put(key, new ManualRangeMarker(range, forInjected, forInjected, !forInjected, null));
}
}
frozen = applyEvents(frozen, events, answer);
List<SelfElementInfo> infos = getSortedInfos();
ManualRangeMarker[] markers = createMarkers(infos);
answer = applyEvents(events, new UpdatedRanges(0, frozen, infos, markers));
}
myUpdatedRanges = Trinity.create(eventCount, answer, frozen);
myUpdatedRanges = answer;
return answer;
}
}
private static FrozenDocument applyEvents(@NotNull FrozenDocument frozen,
@NotNull List<DocumentEvent> events,
final TLongObjectHashMap<ManualRangeMarker> map) {
@NotNull
private static ManualRangeMarker[] createMarkers(List<SelfElementInfo> infos) {
ManualRangeMarker[] markers = new ManualRangeMarker[infos.size()];
int i = 0;
while (i < markers.length) {
SelfElementInfo info = infos.get(i);
boolean forInjected = info.isForInjected();
ProperTextRange range = ObjectUtils.assertNotNull(info.getPsiRange());
markers[i] = new ManualRangeMarker(range, forInjected, forInjected, !forInjected, null);
i++;
while (i < markers.length && rangeEquals(infos.get(i), range, forInjected)) {
markers[i] = markers[i - 1];
i++;
}
}
return markers;
}
private static boolean rangeEquals(SelfElementInfo info, ProperTextRange range, boolean injected) {
return range.getStartOffset() == info.getPsiStartOffset() && range.getEndOffset() == info.getPsiEndOffset() && injected == info.isForInjected();
}
private static UpdatedRanges applyEvents(@NotNull List<DocumentEvent> events, final UpdatedRanges struct) {
FrozenDocument frozen = struct.myResultDocument;
ManualRangeMarker[] resultMarkers = struct.myMarkers.clone();
for (DocumentEvent event : events) {
final FrozenDocument before = frozen;
final DocumentEvent corrected;
@@ -99,53 +127,83 @@ class MarkerCache {
((DocumentEventImpl) event).getInitialStartOffset(), ((DocumentEventImpl) event).getInitialOldLength());
}
map.transformValues(new TObjectFunction<ManualRangeMarker, ManualRangeMarker>() {
@Override
public ManualRangeMarker execute(ManualRangeMarker currentRange) {
return currentRange == null ? null : currentRange.getUpdatedRange(corrected, before);
int i = 0;
while (i < resultMarkers.length) {
ManualRangeMarker currentRange = resultMarkers[i];
int sameMarkersEnd = i + 1;
while (sameMarkersEnd < resultMarkers.length && resultMarkers[sameMarkersEnd] == currentRange) {
sameMarkersEnd++;
}
});
ManualRangeMarker updatedRange = currentRange == null ? null : currentRange.getUpdatedRange(corrected, before);
while (i < sameMarkersEnd) {
resultMarkers[i] = updatedRange;
i++;
}
}
}
return frozen;
return new UpdatedRanges(struct.myEventCount + events.size(), frozen, struct.mySortedInfos, resultMarkers);
}
synchronized void updateMarkers(@NotNull FrozenDocument frozen, @NotNull List<DocumentEvent> events) {
TLongObjectHashMap<ManualRangeMarker> updated = getUpdatedMarkers(frozen, events);
UpdatedRanges updated = getUpdatedMarkers(frozen, events);
for (SelfElementInfo info : getInfos()) {
long key = info.markerCacheKey();
if (key != 0) {
ManualRangeMarker newRangeMarker = updated.get(key);
info.setRange(newRangeMarker == null ? null : newRangeMarker.getRange());
}
for (int i = 0; i < updated.myMarkers.length; i++) {
ManualRangeMarker newRangeMarker = updated.myMarkers[i];
updated.mySortedInfos.get(i).setRange(newRangeMarker == null ? null : newRangeMarker.getRange());
}
myUpdatedRanges = null;
}
@NotNull
private List<SelfElementInfo> getInfos() {
return ContainerUtil.findAll(ContainerUtil.map(myPointers.getAlivePointers(), new NullableFunction<SmartPsiElementPointerImpl, SmartPointerElementInfo>() {
@Override
public SmartPointerElementInfo fun(SmartPsiElementPointerImpl pointer) {
return pointer.getElementInfo();
private List<SelfElementInfo> getSortedInfos() {
List<SelfElementInfo> infos = ContainerUtil.newArrayListWithCapacity(myPointers.getSize());
for (Reference<SmartPointerEx> reference : myPointers.getReferences()) {
if (reference != null) {
SmartPointerEx pointer = reference.get();
if (pointer != null) {
SmartPointerElementInfo info = ((SmartPsiElementPointerImpl)pointer).getElementInfo();
if (info instanceof SelfElementInfo && ((SelfElementInfo)info).hasRange()) {
infos.add((SelfElementInfo)info);
}
}
}), SelfElementInfo.class);
}
}
Collections.sort(infos, BY_RANGE_KEY);
return infos;
}
@Nullable
ProperTextRange getUpdatedRange(long rangeKey, @NotNull FrozenDocument frozen, @NotNull List<DocumentEvent> events) {
ManualRangeMarker updated = getUpdatedMarkers(frozen, events).get(rangeKey);
ProperTextRange getUpdatedRange(@NotNull SelfElementInfo info, @NotNull FrozenDocument frozen, @NotNull List<DocumentEvent> events) {
UpdatedRanges struct = getUpdatedMarkers(frozen, events);
int i = Collections.binarySearch(struct.mySortedInfos, info, BY_RANGE_KEY);
ManualRangeMarker updated = i >= 0 ? struct.myMarkers[i] : null;
return updated == null ? null : updated.getRange();
}
synchronized void rangeChanged(long rangeKey) {
if (myUpdatedRanges != null && !myUpdatedRanges.second.contains(rangeKey)) {
myUpdatedRanges = null;
}
void rangeChanged() {
myUpdatedRanges = null;
}
VirtualFile getVirtualFile() {
return myVirtualFile;
}
private static class UpdatedRanges {
private final int myEventCount;
private final FrozenDocument myResultDocument;
private final List<SelfElementInfo> mySortedInfos;
private final ManualRangeMarker[] myMarkers;
public UpdatedRanges(int eventCount,
FrozenDocument resultDocument,
List<SelfElementInfo> sortedInfos, ManualRangeMarker[] markers) {
myEventCount = eventCount;
myResultDocument = resultDocument;
mySortedInfos = sortedInfos;
myMarkers = markers;
}
}
}
@@ -42,12 +42,11 @@ public class SelfElementInfo extends SmartPointerElementInfo {
protected final Language myLanguage;
protected final MarkerCache myMarkerCache;
private final boolean myForInjected;
private boolean myHasRange;
private int myStartOffset;
private int myEndOffset;
SelfElementInfo(@NotNull Project project,
@NotNull ProperTextRange range,
@Nullable ProperTextRange range,
@NotNull Class anchorClass,
@NotNull PsiFile containingFile,
@NotNull Language language,
@@ -60,35 +59,30 @@ public class SelfElementInfo extends SmartPointerElementInfo {
myManager = (SmartPointerManagerImpl)SmartPointerManager.getInstance(project);
myMarkerCache = myManager.getMarkerCache(containingFile.getViewProvider().getVirtualFile());
setRange(range);
myMarkerCache.rangeChanged(markerCacheKey());
myMarkerCache.rangeChanged();
}
void setRange(@Nullable Segment range) {
if (range != null) {
myHasRange = true;
myStartOffset = range.getStartOffset();
myEndOffset = range.getEndOffset();
} else {
myHasRange = false;
myStartOffset = -1;
myEndOffset = -1;
}
}
long markerCacheKey() {
if (!myHasRange) return 0;
long start = myStartOffset;
assert start >= 0;
assert start < Integer.MAX_VALUE;
long packed = (start + 1) | ((long)myEndOffset << 32);
assert packed > 0;
assert packed != Long.MIN_VALUE;
long result = myForInjected ? -packed : packed;
assert result != 0;
return result;
boolean hasRange() {
return myStartOffset >= 0;
}
int getPsiStartOffset() {
return myStartOffset;
}
int getPsiEndOffset() {
return myEndOffset;
}
boolean isForInjected() {
return myForInjected;
@@ -118,7 +112,7 @@ public class SelfElementInfo extends SmartPointerElementInfo {
@Nullable
private ProperTextRange calcPsiRange() {
return myHasRange ? ProperTextRange.create(myStartOffset, myEndOffset) : null;
return hasRange() ? ProperTextRange.create(myStartOffset, myEndOffset) : null;
}
@Override
@@ -179,7 +173,7 @@ public class SelfElementInfo extends SmartPointerElementInfo {
@Override
public void cleanup() {
myHasRange = false;
setRange(null);
}
@Nullable
@@ -270,13 +264,13 @@ public class SelfElementInfo extends SmartPointerElementInfo {
@Override
@Nullable
public Segment getRange() {
if (myHasRange) {
if (hasRange()) {
Document document = getDocumentToSynchronize();
if (document != null) {
PsiDocumentManagerBase documentManager = myManager.getPsiDocumentManager();
List<DocumentEvent> events = documentManager.getEventsSinceCommit(document);
if (!events.isEmpty()) {
return myMarkerCache.getUpdatedRange(markerCacheKey(), (FrozenDocument)documentManager.getLastCommittedDocument(document), events);
return myMarkerCache.getUpdatedRange(this, (FrozenDocument)documentManager.getLastCommittedDocument(document), events);
}
}
}
@@ -344,6 +344,14 @@ public class SmartPointerManagerImpl extends SmartPointerManager {
return size == 0;
}
Reference<SmartPointerEx>[] getReferences() {
return references;
}
int getSize() {
return size;
}
@NotNull
List<SmartPsiElementPointerImpl> getAlivePointers() {
return ContainerUtil.mapNotNull(references, new Function<PointerReference, SmartPsiElementPointerImpl>() {