less allocations on async console highlighting (IDEA-174645)

instead of several objects per line, allocate one object
for the whole added fragment

but still process each line separately to have the same cancelability

the whole job can also be aborted immediately if it's outdated
This commit is contained in:
peter
2017-06-20 14:43:08 +02:00
parent 3d8962a96d
commit 6fbb53c80e
2 changed files with 82 additions and 45 deletions
@@ -22,6 +22,7 @@ import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.editor.impl.DocumentImpl;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.util.ProgressIndicatorUtils;
import com.intellij.util.TimeoutUtil;
@@ -33,6 +34,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author peter
@@ -42,7 +44,7 @@ class AsyncFilterRunner {
private static final ExecutorService ourExecutor = AppExecutorUtil.createBoundedApplicationPoolExecutor("console filters", 1);
private final EditorHyperlinkSupport myHyperlinks;
private final Editor myEditor;
private final Queue<LineHighlighter> myQueue = new ConcurrentLinkedQueue<>();
private final Queue<HighlighterJob> myQueue = new ConcurrentLinkedQueue<>();
@NotNull private List<FilterResult> myResults = new ArrayList<>();
AsyncFilterRunner(EditorHyperlinkSupport hyperlinks, Editor editor) {
@@ -53,7 +55,7 @@ class AsyncFilterRunner {
void highlightHyperlinks(final Filter customFilter, final int startLine, final int endLine) {
if (endLine < 0) return;
queueTasks(customFilter, startLine, endLine);
myQueue.offer(new HighlighterJob(customFilter, startLine, endLine, myEditor.getDocument()));
if (ApplicationManager.getApplication().isWriteAccessAllowed()) {
runTasks();
highlightAvailableResults();
@@ -139,42 +141,15 @@ class AsyncFilterRunner {
}
}
private void queueTasks(Filter filter, int startLine, int endLine) {
Document document = myEditor.getDocument();
int markerOffset = document.getLineEndOffset(endLine);
RangeMarker marker = document.createRangeMarker(markerOffset, markerOffset);
for (int line = startLine; line <= endLine; line++) {
myQueue.offer(processLine(document, filter, line, markerOffset, marker));
}
}
@NotNull
private LineHighlighter processLine(Document document, Filter filter, int line, int initialMarkerOffset, RangeMarker marker) {
int lineEnd = document.getLineEndOffset(line);
int endOffset = lineEnd + (lineEnd < document.getTextLength() ? 1 /* for \n */ : 0);
CharSequence text = EditorHyperlinkSupport.getLineSequence(document, line, true);
return () -> runFilterForLine(initialMarkerOffset, marker, filter, endOffset, text);
}
@Nullable
private FilterResult runFilterForLine(int initialMarkerOffset, RangeMarker marker, Filter filter, int endOffset, CharSequence lineText) {
if (!marker.isValid() || marker.getEndOffset() == 0) return null;
Filter.Result result = checkRange(filter, endOffset, filter.applyFilter(lineText.toString(), endOffset));
return result == null ? null : () -> {
if (marker.isValid()) {
myHyperlinks.highlightHyperlinks(result, marker.getStartOffset() - initialMarkerOffset);
}
};
}
private void runTasks() {
if (myEditor.isDisposed()) return;
while (!myQueue.isEmpty()) {
ProgressManager.checkCanceled();
LineHighlighter highlighter = myQueue.peek();
addLineResult(highlighter.runFilterForLine());
HighlighterJob highlighter = myQueue.peek();
while (highlighter.hasUnprocessedLines()) {
ProgressManager.checkCanceled();
addLineResult(highlighter.analyzeNextLine());
}
LOG.assertTrue(highlighter == myQueue.remove());
}
}
@@ -192,12 +167,61 @@ class AsyncFilterRunner {
return result;
}
private interface LineHighlighter {
@Nullable FilterResult runFilterForLine();
}
private interface FilterResult {
void applyHighlights();
}
private class HighlighterJob {
private AtomicInteger startLine;
private final int endLine;
private final int initialMarkerOffset;
private final RangeMarker endMarker;
private final Filter filter;
private final Document snapshot;
HighlighterJob(Filter filter, int startLine, int endLine, Document document) {
this.startLine = new AtomicInteger(startLine);
this.endLine = endLine;
this.filter = filter;
initialMarkerOffset = document.getLineEndOffset(endLine);
endMarker = document.createRangeMarker(initialMarkerOffset, initialMarkerOffset);
snapshot = ((DocumentImpl)document).freeze();
}
boolean hasUnprocessedLines() {
return !isOutdated() && startLine.get() <= endLine;
}
@Nullable
AsyncFilterRunner.FilterResult analyzeNextLine() {
int line = startLine.get();
Filter.Result result = analyzeLine(line);
LOG.assertTrue(line == startLine.getAndIncrement());
return result == null ? null : () -> {
if (!isOutdated()) {
myHyperlinks.highlightHyperlinks(result, getOffsetDelta());
}
};
}
Filter.Result analyzeLine(int line) {
int lineStart = snapshot.getLineStartOffset(line);
if (lineStart + getOffsetDelta() < 0) return null;
String lineText = EditorHyperlinkSupport.getLineText(snapshot, line, true);
int endOffset = lineStart + lineText.length();
return checkRange(filter, endOffset, filter.applyFilter(lineText, endOffset));
}
boolean isOutdated() {
return !endMarker.isValid() || endMarker.getEndOffset() == 0;
}
int getOffsetDelta() {
return endMarker.getStartOffset() - initialMarkerOffset;
}
}
}
@@ -209,11 +209,16 @@ public class ConsoleViewImplTest extends LightPlatformTestCase {
@NotNull
static ConsoleViewImpl createConsole() {
return createConsole(false);
}
@NotNull
private static ConsoleViewImpl createConsole(boolean usePredefinedMessageFilter) {
Project project = getProject();
ConsoleViewImpl console = new ConsoleViewImpl(project,
GlobalSearchScope.allScope(project),
false,
false);
usePredefinedMessageFilter);
console.getComponent(); // initConsoleEditor()
ProcessHandler processHandler = new NopProcessHandler();
processHandler.startNotify();
@@ -235,9 +240,21 @@ public class ConsoleViewImplTest extends LightPlatformTestCase {
}).assertTiming());
}
public void testLargeConsolePerformance() throws Exception {
withCycleConsole(UISettings.getInstance().getConsoleCycleBufferSizeKb(), console ->
PlatformTestUtil.startPerformanceTest("console print", 9000, () -> {
console.clear();
for (int i=0; i<10_000_000; i++) {
console.print("hello\n", ConsoleViewContentType.NORMAL_OUTPUT);
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue();
}
console.waitAllRequests();
}).assertTiming());
}
public void testPerformanceOfMergeableTokens() throws Exception {
withCycleConsole(1000, console ->
PlatformTestUtil.startPerformanceTest("console print", 5500, () -> {
PlatformTestUtil.startPerformanceTest("console print", 3500, () -> {
console.clear();
for (int i=0; i<10_000_000; i++) {
console.print("xxx\n", ConsoleViewContentType.NORMAL_OUTPUT);
@@ -257,12 +274,8 @@ public class ConsoleViewImplTest extends LightPlatformTestCase {
UISettings.getInstance().setOverrideConsoleCycleBufferSize(true);
UISettings.getInstance().setConsoleCycleBufferSizeKb(capacityKB);
// create new to reflect changed buffer size
ConsoleViewImpl console = createConsole();
ConsoleViewImpl console = createConsole(true);
try {
ConsoleBuffer.useCycleBuffer();
ConsoleBuffer.getCycleBufferSize();
UISettings.getInstance();// instantiate early
runnable.consume(console);
}
finally {