IDEA-124155 Performance problem on live search in long lines

This commit is contained in:
Alexey Kudravtsev
2014-04-28 13:58:56 +04:00
parent 6ce34839db
commit ba23594a68
9 changed files with 197 additions and 72 deletions
@@ -203,12 +203,12 @@ public class UpdateHighlightersUtil {
final boolean[] changed = {false};
RangeMarkerTree.sweep(new RangeMarkerTree.Generator<HighlightInfo>(){
@Override
public boolean generate(Processor<HighlightInfo> processor) {
public boolean generateInStartOffsetOrder(@NotNull Processor<HighlightInfo> processor) {
return ContainerUtil.process(infos, processor);
}
}, new SweepProcessor<HighlightInfo>() {
@Override
public boolean process(int offset, HighlightInfo info, boolean atStart, Collection<HighlightInfo> overlappingIntervals) {
public boolean process(int offset, HighlightInfo info, boolean atStart, @NotNull Collection<HighlightInfo> overlappingIntervals) {
if (!atStart) return true;
if (!info.isFromInjection() && info.getEndOffset() < document.getTextLength() && (info.getEndOffset() <= startOffset || info.getStartOffset()>=endOffset)) return true; // injections are oblivious to restricting range
@@ -276,12 +276,12 @@ public class UpdateHighlightersUtil {
final boolean[] changed = {false};
RangeMarkerTree.sweep(new RangeMarkerTree.Generator<HighlightInfo>(){
@Override
public boolean generate(final Processor<HighlightInfo> processor) {
public boolean generateInStartOffsetOrder(@NotNull final Processor<HighlightInfo> processor) {
return ContainerUtil.process(infos, processor);
}
}, new SweepProcessor<HighlightInfo>() {
@Override
public boolean process(int offset, HighlightInfo info, boolean atStart, Collection<HighlightInfo> overlappingIntervals) {
public boolean process(int offset, HighlightInfo info, boolean atStart, @NotNull Collection<HighlightInfo> overlappingIntervals) {
if (!atStart) {
return true;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2011 JetBrains s.r.o.
* 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.
@@ -15,8 +15,10 @@
*/
package com.intellij.openapi.editor.ex;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
public interface SweepProcessor<T> {
boolean process(int offset, T interval, boolean atStart, Collection<T> overlappingIntervals);
boolean process(int offset, T interval, boolean atStart, @NotNull Collection<T> overlappingIntervals);
}
@@ -558,8 +558,38 @@ public abstract class IntervalTreeImpl<T extends MutableInterval> extends RedBla
return processContaining(root.getRight(), offset, processor, modCountBefore, delta);
}
public interface PeekableIterator<T> extends DisposableIterator<T> {
T peek();
PeekableIterator EMPTY = new PeekableIterator() {
@Override
public Object peek() {
return null;
}
@Override
public void dispose() {
}
@Override
public boolean hasNext() {
return false;
}
@Override
public Object next() {
return null;
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove");
}
};
}
@NotNull
DisposableIterator<T> overlappingIterator(@NotNull final TextRangeInterval rangeInterval) {
PeekableIterator<T> overlappingIterator(@NotNull final TextRangeInterval rangeInterval) {
TextRange.assertProperRange(rangeInterval);
l.readLock().lock();
@@ -570,13 +600,13 @@ public abstract class IntervalTreeImpl<T extends MutableInterval> extends RedBla
final IntervalNode<T> firstOverlap = findMinOverlappingWith(getRoot(), rangeInterval, modCount, 0);
if (firstOverlap == null) {
l.readLock().unlock();
return DisposableIterator.EMPTY;
return PeekableIterator.EMPTY;
}
final int firstOverlapDelta = firstOverlap.computeDeltaUpToRoot();
final int firstOverlapStart = firstOverlap.intervalStart() + firstOverlapDelta;
final int modCountBefore = modCount;
return new DisposableIterator<T>() {
return new PeekableIterator<T>() {
private IntervalNode<T> currentNode = firstOverlap;
private int deltaUpToRootExclusive = firstOverlapDelta-firstOverlap.delta;
private int indexInCurrentList = 0;
@@ -624,6 +654,12 @@ public abstract class IntervalTreeImpl<T extends MutableInterval> extends RedBla
return t;
}
@Override
public T peek() {
if (!hasNext()) throw new NoSuchElementException();
return current;
}
@Override
public void remove() {
throw new IncorrectOperationException();
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2010 JetBrains s.r.o.
* 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.
@@ -305,14 +305,14 @@ public class RangeMarkerTree<T extends RangeMarkerEx> extends IntervalTreeImpl<T
public boolean sweep(final int start, final int end, @NotNull final SweepProcessor<T> sweepProcessor) {
return sweep(new Generator<T>() {
@Override
public boolean generate(Processor<T> processor) {
public boolean generateInStartOffsetOrder(@NotNull Processor<T> processor) {
return processOverlappingWith(start, end, processor);
}
}, sweepProcessor);
}
public interface Generator<T> {
boolean generate(Processor<T> processor);
boolean generateInStartOffsetOrder(@NotNull Processor<T> processor);
}
public static <T extends Segment> boolean sweep(@NotNull Generator<T> generator, @NotNull final SweepProcessor<T> sweepProcessor) {
@@ -323,7 +323,7 @@ public class RangeMarkerTree<T extends RangeMarkerEx> extends IntervalTreeImpl<T
}
});
final List<T> starts = new ArrayList<T>();
if (!generator.generate(new Processor<T>() {
if (!generator.generateInStartOffsetOrder(new Processor<T>() {
@Override
public boolean process(T marker) {
// decide whether previous marker ends here or new marker begins
@@ -28,6 +28,8 @@ import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.editor.markup.TextAttributes;
import org.jetbrains.annotations.NotNull;
import java.util.Comparator;
public interface RangeHighlighterEx extends RangeHighlighter, RangeMarkerEx {
RangeHighlighterEx[] EMPTY_ARRAY = new RangeHighlighterEx[0];
boolean isAfterEndOfLine();
@@ -36,8 +38,13 @@ public interface RangeHighlighterEx extends RangeHighlighter, RangeMarkerEx {
int getAffectedAreaStartOffset();
int getAffectedAreaEndOffset();
@Override
long getId();
void setTextAttributes(@NotNull TextAttributes textAttributes);
Comparator<RangeHighlighterEx> BY_AFFECTED_START_OFFSET = new Comparator<RangeHighlighterEx>() {
@Override
public int compare(RangeHighlighterEx r1, RangeHighlighterEx r2) {
return r1.getAffectedAreaStartOffset() - r2.getAffectedAreaStartOffset();
}
};
}
@@ -44,7 +44,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
public class MarkupModelImpl extends UserDataHolderBase implements MarkupModelEx {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.MarkupModelImpl");
@@ -52,16 +54,19 @@ public class MarkupModelImpl extends UserDataHolderBase implements MarkupModelEx
private RangeHighlighter[] myCachedHighlighters;
private final List<MarkupModelListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private final RangeHighlighterTree myHighlighterTree;
private final RangeHighlighterTree myHighlighterTree; // this tree holds regular highlighters with target = HighlighterTargetArea.EXACT_RANGE
private final RangeHighlighterTree myHighlighterTreeForLines; // this tree holds line range highlighters with target = HighlighterTargetArea.LINES_IN_RANGE
MarkupModelImpl(@NotNull DocumentEx document) {
myDocument = document;
myHighlighterTree = new RangeHighlighterTree(myDocument, this);
myHighlighterTree = new RangeHighlighterTree(document, this);
myHighlighterTreeForLines = new RangeHighlighterTree(document, this);
}
@Override
public void dispose() {
myHighlighterTree.dispose();
myHighlighterTreeForLines.dispose();
}
@Override
@@ -96,10 +101,12 @@ public class MarkupModelImpl extends UserDataHolderBase implements MarkupModelEx
public RangeHighlighter[] getAllHighlighters() {
ApplicationManager.getApplication().assertIsDispatchThread();
if (myCachedHighlighters == null) {
int size = myHighlighterTree.size();
int size = myHighlighterTree.size() + myHighlighterTreeForLines.size();
if (size == 0) return RangeHighlighter.EMPTY_ARRAY;
List<RangeHighlighterEx> list = new ArrayList<RangeHighlighterEx>(size);
myHighlighterTree.process(new CommonProcessors.CollectProcessor<RangeHighlighterEx>(list));
CommonProcessors.CollectProcessor<RangeHighlighterEx> collectProcessor = new CommonProcessors.CollectProcessor<RangeHighlighterEx>(list);
myHighlighterTree.process(collectProcessor);
myHighlighterTreeForLines.process(collectProcessor);
myCachedHighlighters = list.toArray(new RangeHighlighter[list.size()]);
}
return myCachedHighlighters;
@@ -150,7 +157,11 @@ public class MarkupModelImpl extends UserDataHolderBase implements MarkupModelEx
boolean greedyToRight,
int layer) {
ApplicationManager.getApplication().assertIsDispatchThread();
myHighlighterTree.addInterval(marker, start, end, greedyToLeft, greedyToRight, layer);
treeFor(marker).addInterval(marker, start, end, greedyToLeft, greedyToRight, layer);
}
private RangeHighlighterTree treeFor(RangeHighlighter marker) {
return marker.getTargetArea() == HighlighterTargetArea.EXACT_RANGE ? myHighlighterTree : myHighlighterTreeForLines;
}
@Override
@@ -169,7 +180,7 @@ public class MarkupModelImpl extends UserDataHolderBase implements MarkupModelEx
myCachedHighlighters = null;
if (!segmentHighlighter.isValid()) return;
boolean removed = myHighlighterTree.removeInterval((RangeHighlighterEx)segmentHighlighter);
boolean removed = treeFor(segmentHighlighter).removeInterval((RangeHighlighterEx)segmentHighlighter);
LOG.assertTrue(removed);
}
@@ -181,6 +192,7 @@ public class MarkupModelImpl extends UserDataHolderBase implements MarkupModelEx
}
myCachedHighlighters = null;
myHighlighterTree.clear();
myHighlighterTreeForLines.clear();
}
@Override
@@ -235,30 +247,69 @@ public class MarkupModelImpl extends UserDataHolderBase implements MarkupModelEx
@Override
public boolean containsHighlighter(@NotNull final RangeHighlighter highlighter) {
ApplicationManager.getApplication().assertIsDispatchThread();
return !myHighlighterTree
.processOverlappingWith(highlighter.getStartOffset(), highlighter.getEndOffset(), new Processor<RangeHighlighterEx>() {
@Override
public boolean process(RangeHighlighterEx h) {
return h.getId() != ((RangeHighlighterEx)highlighter).getId();
}
});
Processor<RangeHighlighterEx> equalId = new Processor<RangeHighlighterEx>() {
@Override
public boolean process(RangeHighlighterEx h) {
return h.getId() != ((RangeHighlighterEx)highlighter).getId();
}
};
return !treeFor(highlighter).processOverlappingWith(highlighter.getStartOffset(), highlighter.getEndOffset(), equalId);
}
@Override
public boolean processRangeHighlightersOverlappingWith(int start, int end, @NotNull Processor<? super RangeHighlighterEx> processor) {
TextRangeInterval rangeInterval = roundToLineBoundaries(start, end);
return myHighlighterTree.processOverlappingWith(rangeInterval.getStartOffset(), rangeInterval.getEndOffset(), processor);
if (!myHighlighterTree.processOverlappingWith(start, end, processor)) return false;
TextRangeInterval lines = roundToLineBoundaries(start, end);
return myHighlighterTreeForLines.processOverlappingWith(lines.getStartOffset(), lines.getEndOffset(), processor);
}
@Override
public boolean processRangeHighlightersOutside(int start, int end, @NotNull Processor<? super RangeHighlighterEx> processor) {
return myHighlighterTree.processOverlappingWithOutside(start, end, processor);
return myHighlighterTree.processOverlappingWithOutside(start, end, processor)
&& myHighlighterTreeForLines.processOverlappingWithOutside(start, end, processor);
}
@Override
@NotNull
public DisposableIterator<RangeHighlighterEx> overlappingIterator(int startOffset, int endOffset) {
return myHighlighterTree.overlappingIterator(roundToLineBoundaries(startOffset, endOffset));
IntervalTreeImpl.PeekableIterator<RangeHighlighterEx> exact = myHighlighterTree.overlappingIterator(new TextRangeInterval(startOffset, endOffset));
IntervalTreeImpl.PeekableIterator<RangeHighlighterEx> lines = myHighlighterTreeForLines.overlappingIterator(roundToLineBoundaries(startOffset, endOffset));
return merge(exact, lines);
}
@NotNull
private static <T extends RangeHighlighterEx> DisposableIterator<T> merge(@NotNull final IntervalTreeImpl.PeekableIterator<T> iterator1, @NotNull final IntervalTreeImpl.PeekableIterator<T> iterator2) {
return new DisposableIterator<T>() {
@Override
public void dispose() {
iterator1.dispose();
iterator2.dispose();
}
@Override
public boolean hasNext() {
return iterator1.hasNext() || iterator2.hasNext();
}
@Override
public T next() {
T t1 = iterator1.hasNext() ? iterator1.peek() : null;
T t2 = iterator2.hasNext() ? iterator2.peek() : null;
if (t1 == null) {
return iterator2.next();
}
if (t2 == null) {
return iterator1.next();
}
int compare = RangeHighlighterEx.BY_AFFECTED_START_OFFSET.compare(t1, t2);
return (compare < 0 ? iterator1 : iterator2).next();
}
@Override
public void remove() {
throw new NoSuchElementException();
}
};
}
@NotNull
@@ -271,7 +322,22 @@ public class MarkupModelImpl extends UserDataHolderBase implements MarkupModelEx
@Override
public boolean sweep(int start, int end, @NotNull SweepProcessor<RangeHighlighterEx> sweepProcessor) {
TextRangeInterval rangeInterval = roundToLineBoundaries(start, end);
return myHighlighterTree.sweep(rangeInterval.getStartOffset(), rangeInterval.getEndOffset(), sweepProcessor);
TextRangeInterval lines = roundToLineBoundaries(start, end);
List<RangeHighlighterEx> linesInRange = new ArrayList<RangeHighlighterEx>();
myHighlighterTreeForLines.processOverlappingWith(lines.getStartOffset(), lines.getEndOffset(), new CommonProcessors.CollectProcessor<RangeHighlighterEx>(linesInRange));
if (linesInRange.isEmpty()) {
return myHighlighterTree.sweep(start, end, sweepProcessor);
}
final List<RangeHighlighterEx> highlighters = new ArrayList<RangeHighlighterEx>();
myHighlighterTree.processOverlappingWith(start, end, new CommonProcessors.CollectProcessor<RangeHighlighterEx>(highlighters));
highlighters.addAll(linesInRange);
Collections.sort(highlighters, RangeHighlighterEx.BY_AFFECTED_START_OFFSET);
return RangeMarkerTree.sweep(new RangeMarkerTree.Generator<RangeHighlighterEx>() {
@Override
public boolean generateInStartOffsetOrder(@NotNull Processor<RangeHighlighterEx> processor) {
return ContainerUtil.process(highlighters, processor);
}
}, sweepProcessor);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2013 JetBrains s.r.o.
* 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.
@@ -38,7 +38,9 @@ class RangeHighlighterImpl extends RangeMarkerImpl implements RangeHighlighterEx
int end,
int layer,
@NotNull HighlighterTargetArea target,
TextAttributes textAttributes, boolean greedyToLeft, boolean greedyToRight) {
TextAttributes textAttributes,
boolean greedyToLeft,
boolean greedyToRight) {
super((DocumentEx)model.getDocument(), start, end,false);
data = new RangeHighlighterData(model, target, textAttributes) {
@@ -24,16 +24,21 @@ import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.ex.*;
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
import com.intellij.openapi.editor.markup.*;
import com.intellij.openapi.editor.markup.EffectType;
import com.intellij.openapi.editor.markup.HighlighterLayer;
import com.intellij.openapi.editor.markup.HighlighterTargetArea;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.util.Comparing;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.Processor;
import com.intellij.util.CommonProcessors;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public final class IterationState {
@@ -75,12 +80,6 @@ public final class IterationState {
return result;
}
};
private static final Comparator<RangeHighlighterEx> BY_AFFECTED_START_OFFSET = new Comparator<RangeHighlighterEx>() {
@Override
public int compare(RangeHighlighterEx r1, RangeHighlighterEx r2) {
return r1.getAffectedAreaStartOffset() - r2.getAffectedAreaStartOffset();
}
};
private final TextAttributes myMergedAttributes = new TextAttributes();
@@ -193,15 +192,9 @@ public final class IterationState {
// we have to get all highlighters in advance and sort them by affected offsets
// since these can be different from the real offsets the highlighters are sorted by in the tree. (See LINES_IN_RANGE perverts)
final List<RangeHighlighterEx> list = new ArrayList<RangeHighlighterEx>();
markupModel.processRangeHighlightersOverlappingWith(start, end, new Processor<RangeHighlighterEx>() {
@Override
public boolean process(RangeHighlighterEx highlighter) {
list.add(highlighter);
return true;
}
});
markupModel.processRangeHighlightersOverlappingWith(start, end, new CommonProcessors.CollectProcessor<RangeHighlighterEx>(list));
highlighters = list.isEmpty() ? RangeHighlighterEx.EMPTY_ARRAY : list.toArray(new RangeHighlighterEx[list.size()]);
Arrays.sort(highlighters, BY_AFFECTED_START_OFFSET);
Arrays.sort(highlighters, RangeHighlighterEx.BY_AFFECTED_START_OFFSET);
int skipped = 0;
while (i < highlighters.length) {
@@ -15,9 +15,7 @@
*/
package com.intellij.openapi.editor.impl;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.command.impl.UndoManagerImpl;
import com.intellij.openapi.command.undo.UndoManager;
@@ -25,11 +23,14 @@ import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.ex.MarkupModelEx;
import com.intellij.openapi.editor.ex.RangeHighlighterEx;
import com.intellij.openapi.editor.ex.RangeMarkerEx;
import com.intellij.openapi.editor.markup.HighlighterTargetArea;
import com.intellij.openapi.editor.markup.MarkupModel;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.ThrowableComputable;
import com.intellij.openapi.util.Trinity;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDocumentManager;
@@ -37,7 +38,10 @@ import com.intellij.psi.impl.PsiDocumentManagerImpl;
import com.intellij.psi.impl.PsiToDocumentSynchronizer;
import com.intellij.testFramework.LeakHunter;
import com.intellij.testFramework.LightPlatformTestCase;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.testFramework.Timings;
import com.intellij.util.CommonProcessors;
import com.intellij.util.ThrowableRunnable;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
@@ -57,29 +61,20 @@ public class RangeMarkerTest extends LightPlatformTestCase {
return;
}
boolean oldVerify = RedBlackTree.VERIFY;
RedBlackTree.VERIFY = true;
RedBlackTree.VERIFY = !isPerformanceTest();
final Throwable[] ex = {null};
try {
if (getTestName(false).contains("NoCommand")) {
super.runTest();
return;
}
CommandProcessor.getInstance().executeCommand(getProject(), new Runnable() {
@Override
public void run() {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
try {
RangeMarkerTest.super.runTest();
}
catch (Throwable throwable) {
ex[0] = throwable;
}
}
});
}
}, "", null);
WriteCommandAction.runWriteCommandAction(getProject(), new ThrowableComputable<Void, Throwable>() {
@Override
public Void compute() throws Throwable {
RangeMarkerTest.super.runTest();
return null;
}
});
}
finally {
RedBlackTree.VERIFY = oldVerify;
@@ -1071,4 +1066,28 @@ public class RangeMarkerTest extends LightPlatformTestCase {
m.dispose();
assertFalse(m.isValid());
}
public void testRangeHighlighterLinesInRangeForLongLinePerformance() throws Exception {
final int N = 50000;
Document document = EditorFactory.getInstance().createDocument(StringUtil.repeatSymbol('x', 2*N));
final MarkupModelEx markupModel = (MarkupModelEx)DocumentMarkupModel.forDocument(document, ourProject, true);
for (int i=0; i<N-1;i++) {
markupModel.addRangeHighlighter(2*i, 2*i+1, 0, null, HighlighterTargetArea.EXACT_RANGE);
}
markupModel.addRangeHighlighter(N/2, N/2+1, 0, null, HighlighterTargetArea.LINES_IN_RANGE);
PlatformTestUtil.startPerformanceTest("slow highlighters lookup", (int)(N*Math.log(N)/1000), new ThrowableRunnable() {
@Override
public void run() {
List<RangeHighlighterEx> list = new ArrayList<RangeHighlighterEx>();
CommonProcessors.CollectProcessor<RangeHighlighterEx> coll = new CommonProcessors.CollectProcessor<RangeHighlighterEx>(list);
for (int i=0; i<N-1;i++) {
list.clear();
markupModel.processRangeHighlightersOverlappingWith(2*i, 2*i+1, coll);
assertEquals(2, list.size()); // 1 line plus one exact range marker
}
}
}).assertTiming();
}
}