From ccf424f4d4492e9d997e565b8a722bc482f06a8f Mon Sep 17 00:00:00 2001 From: peter Date: Sat, 17 Oct 2015 08:32:02 +0200 Subject: [PATCH] smart pointer update: use CPU-cache-friendly sorted arrays instead of a long hash map --- .../SmartPsiElementPointersTest.java | 28 ++- .../impl/smartPointers/AnchorElementInfo.java | 4 +- .../psi/impl/smartPointers/MarkerCache.java | 164 ++++++++++++------ .../impl/smartPointers/SelfElementInfo.java | 40 ++--- .../SmartPointerManagerImpl.java | 8 + 5 files changed, 164 insertions(+), 80 deletions(-) diff --git a/java/java-tests/testSrc/com/intellij/psi/impl/smartPointers/SmartPsiElementPointersTest.java b/java/java-tests/testSrc/com/intellij/psi/impl/smartPointers/SmartPsiElementPointersTest.java index 38e03ea9fc5e..1425b9b3ab76 100644 --- a/java/java-tests/testSrc/com/intellij/psi/impl/smartPointers/SmartPsiElementPointersTest.java +++ b/java/java-tests/testSrc/com/intellij/psi/impl/smartPointers/SmartPsiElementPointersTest.java @@ -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 = "\n" + StringUtil.repeat(" \n", 9) + "\n"; + XmlFile file = (XmlFile)configureByText(XmlFileType.INSTANCE, "\n" + StringUtil.repeat(eachTag, 500) + ""); + List tags = ContainerUtil.newArrayList(PsiTreeUtil.findChildrenOfType(file.getDocument(), XmlTag.class)); + List 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 SmartPointerEx createPointer(T element) { return (SmartPointerEx)getPointerManager().createSmartPsiElementPointer(element); diff --git a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/AnchorElementInfo.java b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/AnchorElementInfo.java index c45060bff642..8b3df52d329b 100644 --- a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/AnchorElementInfo.java +++ b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/AnchorElementInfo.java @@ -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); } } diff --git a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/MarkerCache.java b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/MarkerCache.java index 02678e34f0f4..dd2c0028bb71 100644 --- a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/MarkerCache.java +++ b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/MarkerCache.java @@ -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 BY_RANGE_KEY = new Comparator() { + @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, FrozenDocument> myUpdatedRanges; + private volatile UpdatedRanges myUpdatedRanges; MarkerCache(SmartPointerManagerImpl.FilePointersList pointers, VirtualFile virtualFile) { myPointers = pointers; myVirtualFile = virtualFile; } - private TLongObjectHashMap getUpdatedMarkers(@NotNull FrozenDocument frozen, @NotNull List events) { + private UpdatedRanges getUpdatedMarkers(@NotNull FrozenDocument frozen, @NotNull List events) { int eventCount = events.size(); assert eventCount > 0; - Trinity, 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 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 infos = getInfos(); - answer = new TLongObjectHashMap(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 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 events, - final TLongObjectHashMap map) { + @NotNull + private static ManualRangeMarker[] createMarkers(List 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 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() { - @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 events) { - TLongObjectHashMap 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 getInfos() { - return ContainerUtil.findAll(ContainerUtil.map(myPointers.getAlivePointers(), new NullableFunction() { - @Override - public SmartPointerElementInfo fun(SmartPsiElementPointerImpl pointer) { - return pointer.getElementInfo(); + private List getSortedInfos() { + List infos = ContainerUtil.newArrayListWithCapacity(myPointers.getSize()); + for (Reference 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 events) { - ManualRangeMarker updated = getUpdatedMarkers(frozen, events).get(rangeKey); + ProperTextRange getUpdatedRange(@NotNull SelfElementInfo info, @NotNull FrozenDocument frozen, @NotNull List 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 mySortedInfos; + private final ManualRangeMarker[] myMarkers; + + public UpdatedRanges(int eventCount, + FrozenDocument resultDocument, + List sortedInfos, ManualRangeMarker[] markers) { + myEventCount = eventCount; + myResultDocument = resultDocument; + mySortedInfos = sortedInfos; + myMarkers = markers; + } + } } diff --git a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SelfElementInfo.java b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SelfElementInfo.java index 9e1a761383a6..63fb55f566de 100644 --- a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SelfElementInfo.java +++ b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SelfElementInfo.java @@ -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 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); } } } diff --git a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SmartPointerManagerImpl.java b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SmartPointerManagerImpl.java index 178117ecec57..d7ad67908b04 100644 --- a/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SmartPointerManagerImpl.java +++ b/platform/core-impl/src/com/intellij/psi/impl/smartPointers/SmartPointerManagerImpl.java @@ -344,6 +344,14 @@ public class SmartPointerManagerImpl extends SmartPointerManager { return size == 0; } + Reference[] getReferences() { + return references; + } + + int getSize() { + return size; + } + @NotNull List getAlivePointers() { return ContainerUtil.mapNotNull(references, new Function() {