mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
smart pointer update: use CPU-cache-friendly sorted arrays instead of a long hash map
This commit is contained in:
+26
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -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>() {
|
||||
|
||||
Reference in New Issue
Block a user