From b564575d51280452f7ee04394c985ca9058fd022 Mon Sep 17 00:00:00 2001 From: Jonah Graham Date: Fri, 12 May 2017 11:48:41 +0100 Subject: [PATCH] Bug 314428: speedup build console - limit UI thread Re-wrote the BuildConsolePartitioner to do as many operations as possible in the non-UI thread and only to update the UI thread in batches. Key changes: - The UI thread "pulls" changes when it is ready, this means that many lines of output are updated in one go. This compares to the "push" that the non-UI thread used to do. The previous code pushed a change to the UI thread on every line of build output. - The limit on console size is now soft, rather than recalcuating the entire document on each line (i.e. add line at bottom, remove line from top) the document is truncated to the limit size as it reaches 2x the limit size. This calculation is also done more effeciently by tracking the number of lines each partition contains rather than recalculating them. - Folded most of the implementation of BuildConsolePartition into BuildConsolePartitioner. This is because the partioner has a global view and is much more efficient at determining if adjacent partitions can be combined. In addition, rather than having the complexity of splitting partitions that were too long, instead make sure we don't create such partitions in the first place. Change-Id: I47543db3fef754e779684cae44d3316982f1bc0a --- .../eclipse/cdt/core/ErrorParserManager.java | 2 +- .../META-INF/MANIFEST.MF | 4 +- .../BuildConsolePartitionerEditDataTest.java | 136 +++ .../internal/ui/CPluginResources.properties | 3 +- .../ui/buildconsole/BuildConsoleManager.java | 2 +- .../ui/buildconsole/BuildConsolePage.java | 6 +- .../buildconsole/BuildConsolePartition.java | 81 +- .../buildconsole/BuildConsolePartitioner.java | 786 +++++------------- .../BuildConsolePartitionerEditData.java | 327 ++++++++ .../BuildConsoleStreamDecorator.java | 32 +- .../ui/buildconsole/BuildOutputStream.java | 4 +- .../IBuildConsoleStreamDecorator.java | 33 + .../BuildConsolePreferencePage.java | 4 +- 13 files changed, 744 insertions(+), 676 deletions(-) create mode 100644 core/org.eclipse.cdt.ui.tests/ui/org/eclipse/cdt/ui/tests/buildconsole/BuildConsolePartitionerEditDataTest.java create mode 100644 core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePartitionerEditData.java create mode 100644 core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/IBuildConsoleStreamDecorator.java diff --git a/core/org.eclipse.cdt.core/src/org/eclipse/cdt/core/ErrorParserManager.java b/core/org.eclipse.cdt.core/src/org/eclipse/cdt/core/ErrorParserManager.java index 9385c8a2e81..33b9c6468d9 100644 --- a/core/org.eclipse.cdt.core/src/org/eclipse/cdt/core/ErrorParserManager.java +++ b/core/org.eclipse.cdt.core/src/org/eclipse/cdt/core/ErrorParserManager.java @@ -398,7 +398,7 @@ outer: return; } try { - if ( marker != null && outputStream instanceof IErrorMarkeredOutputStream ) { + if ( outputStream instanceof IErrorMarkeredOutputStream ) { IErrorMarkeredOutputStream s = (IErrorMarkeredOutputStream) outputStream; s.write(l, marker); } else { diff --git a/core/org.eclipse.cdt.ui.tests/META-INF/MANIFEST.MF b/core/org.eclipse.cdt.ui.tests/META-INF/MANIFEST.MF index 032741b8d06..623e29aa080 100644 --- a/core/org.eclipse.cdt.ui.tests/META-INF/MANIFEST.MF +++ b/core/org.eclipse.cdt.ui.tests/META-INF/MANIFEST.MF @@ -38,7 +38,9 @@ Require-Bundle: org.eclipse.jface.text, org.eclipse.ltk.core.refactoring;bundle-version="3.4.0", org.eclipse.core.filesystem;bundle-version="1.2.0", org.eclipse.ltk.ui.refactoring, - org.eclipse.osgi + org.eclipse.osgi, + org.mockito, + org.hamcrest Bundle-ActivationPolicy: lazy Bundle-Vendor: Eclipse CDT Bundle-RequiredExecutionEnvironment: JavaSE-1.8 diff --git a/core/org.eclipse.cdt.ui.tests/ui/org/eclipse/cdt/ui/tests/buildconsole/BuildConsolePartitionerEditDataTest.java b/core/org.eclipse.cdt.ui.tests/ui/org/eclipse/cdt/ui/tests/buildconsole/BuildConsolePartitionerEditDataTest.java new file mode 100644 index 00000000000..7b0ce1e13da --- /dev/null +++ b/core/org.eclipse.cdt.ui.tests/ui/org/eclipse/cdt/ui/tests/buildconsole/BuildConsolePartitionerEditDataTest.java @@ -0,0 +1,136 @@ +package org.eclipse.cdt.ui.tests.buildconsole; + +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; + +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; + +import org.eclipse.cdt.core.ProblemMarkerInfo; + +import org.eclipse.cdt.internal.ui.buildconsole.BuildConsolePartition; +import org.eclipse.cdt.internal.ui.buildconsole.BuildConsolePartitionerEditData; +import org.eclipse.cdt.internal.ui.buildconsole.BuildConsolePartitionerEditData.UpdateUIData; +import org.eclipse.cdt.internal.ui.buildconsole.IBuildConsoleStreamDecorator; + +public class BuildConsolePartitionerEditDataTest { + + private static final int DEFAULT_MAX_LINES = 100; + private BuildConsolePartitionerEditData data; + private IBuildConsoleStreamDecorator stream1 = mock(IBuildConsoleStreamDecorator.class); + private IBuildConsoleStreamDecorator stream2 = mock(IBuildConsoleStreamDecorator.class); + private ProblemMarkerInfo marker1 = new ProblemMarkerInfo(null, 0, null, 0, null); + private ProblemMarkerInfo marker2 = new ProblemMarkerInfo(null, 0, null, 0, null); + + @Before + public void before() { + data = new BuildConsolePartitionerEditData(DEFAULT_MAX_LINES); + } + + @Test + public void testBasicOperation() { + data.clear(); + UpdateUIData update0 = data.getUpdate(); + assertThat(update0.getNewContents(), is("")); + assertThat(update0.getStreamsNeedingNotifcation(), is(empty())); + assertThat(update0.needsClearDocumentMarkerManager(), is(true)); + + data.append("Line of text\n", stream1, null); + UpdateUIData update1 = data.getUpdate(); + assertThat(update1.getNewContents(), is("Line of text\n")); + assertThat(update1.getStreamsNeedingNotifcation(), is(Arrays.asList(stream1))); + assertThat(update1.needsClearDocumentMarkerManager(), is(false)); + + data.append("Another line of text\n", stream2, null); + UpdateUIData update2 = data.getUpdate(); + assertThat(update2.getNewContents(), is("Line of text\nAnother line of text\n")); + assertThat(update2.getStreamsNeedingNotifcation(), is(Arrays.asList(stream2))); + assertThat(update2.needsClearDocumentMarkerManager(), is(false)); + } + + @Test + public void testOverflow() { + for (int i = 0; i < DEFAULT_MAX_LINES * 4; i++) { + data.append("Line " + i + "\n", stream1, null); + } + + UpdateUIData update = data.getUpdate(); + assertThat(update.needsClearDocumentMarkerManager(), is(true)); + assertThat(update.getNewPartitions().size(), is(lessThanOrEqualTo(2))); + + String contents = update.getNewContents(); + int newlines = (int) contents.chars().filter(ch -> ch == '\n').count(); + assertThat(newlines, is(lessThan(DEFAULT_MAX_LINES * 2))); + assertThat(newlines, is(greaterThanOrEqualTo(DEFAULT_MAX_LINES))); + + int lastLine = DEFAULT_MAX_LINES * 4 - 1; + assertThat(contents, endsWith("Line " + lastLine + "\n")); + int firstLine = lastLine - newlines + 1; + assertThat(contents, startsWith("Line " + firstLine + "\n")); + } + + @Test + public void testPartitionsCombine() { + data.append("Line\n", stream1, null); + data.append("Line\n", stream1, null); + UpdateUIData update = data.getUpdate(); + + assertThat(update.getNewPartitions(), is(Arrays.asList( + new BuildConsolePartition(stream1, 0, 10, BuildConsolePartition.CONSOLE_PARTITION_TYPE, null, 1)))); + } + + @Test + public void testPartitionsDontCombineOnDifferentStreams() { + data.append("Line\n", stream1, null); + data.append("Line\n", stream2, null); + UpdateUIData update = data.getUpdate(); + + assertThat(update.getNewPartitions(), is(Arrays.asList( + new BuildConsolePartition(stream1, 0, 5, BuildConsolePartition.CONSOLE_PARTITION_TYPE, null, 1), + new BuildConsolePartition(stream2, 5, 5, BuildConsolePartition.CONSOLE_PARTITION_TYPE, null, 1)))); + } + + @Test + public void testPartitionsDontCombineOnDifferentMarkersA() { + data.append("Line\n", stream1, marker1); + data.append("Line\n", stream1, marker2); + UpdateUIData update = data.getUpdate(); + + assertThat(update.getNewPartitions(), is(Arrays.asList( + new BuildConsolePartition(stream1, 0, 5, BuildConsolePartition.INFO_PARTITION_TYPE, marker1, 1), + new BuildConsolePartition(stream1, 5, 5, BuildConsolePartition.INFO_PARTITION_TYPE, marker2, 1)))); + } + + @Test + public void testPartitionsDontCombineOnDifferentMarkersB() { + data.append("Line\n", stream1, null); + data.append("Line\n", stream1, marker2); + UpdateUIData update = data.getUpdate(); + + assertThat(update.getNewPartitions(), is(Arrays.asList( + new BuildConsolePartition(stream1, 0, 5, BuildConsolePartition.CONSOLE_PARTITION_TYPE, null, 1), + new BuildConsolePartition(stream1, 5, 5, BuildConsolePartition.INFO_PARTITION_TYPE, marker2, 1)))); + } + + @Test + public void testPartitionsDontCombineOnDifferentMarkersC() { + data.append("Line\n", stream1, marker1); + data.append("Line\n", stream1, null); + UpdateUIData update = data.getUpdate(); + + assertThat(update.getNewPartitions(), + is(Arrays.asList( + new BuildConsolePartition(stream1, 0, 5, BuildConsolePartition.INFO_PARTITION_TYPE, marker1, 1), + new BuildConsolePartition(stream1, 5, 5, BuildConsolePartition.CONSOLE_PARTITION_TYPE, null, 1)))); + } + +} diff --git a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/CPluginResources.properties b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/CPluginResources.properties index c6ecfc88cd9..8e69e557a00 100644 --- a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/CPluginResources.properties +++ b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/CPluginResources.properties @@ -1,5 +1,5 @@ ############################################################################### -# Copyright (c) 2005, 2013 IBM Corporation and others. +# Copyright (c) 2005, 2017 IBM Corporation and others. # All rights reserved. This program and the accompanying materials # are made available under the terms of the Eclipse Public License v1.0 # which accompanies this distribution, and is available at @@ -22,6 +22,7 @@ ConsolePreferencePage.autoOpenConsole.label=Open console when building ConsolePreferencePage.consoleOnTop.label=Bring console to top when building (if present) ConsolePreferencePage.consoleWrapLines.label=Wrap lines on the console ConsolePreferencePage.consoleLines.label=Limit console output (number of lines): +ConsolePreferencePage.consoleLines.tooltip=This is a fuzzy limit, optimized for best performance. The actual limit will be between this value and 2 times this value. ConsolePreferencePage.consoleLines.errorMessage=Value must be an integer between 10 and 2147483647 ConsolePreferencePage.tabWidth.label=Display tab width: ConsolePreferencePage.tabWidth.errorMessage=Value must be an integer between 1 and 100 diff --git a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsoleManager.java b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsoleManager.java index 0545767cb90..fe99ece5efc 100644 --- a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsoleManager.java +++ b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsoleManager.java @@ -373,7 +373,7 @@ public class BuildConsoleManager implements IBuildConsoleManager, IResourceChang }); } - public BuildConsoleStreamDecorator getStreamDecorator(int type) throws CoreException { + public IBuildConsoleStreamDecorator getStreamDecorator(int type) throws CoreException { switch (type) { case BUILD_STREAM_TYPE_ERROR : return errorStream; diff --git a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePage.java b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePage.java index 936780cf1f4..f31e5422ad8 100644 --- a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePage.java +++ b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePage.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2002, 2015 QNX Software Systems and others. + * Copyright (c) 2002, 2017 QNX Software Systems and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -280,8 +280,8 @@ public class BuildConsolePage extends Page final Object source = event.getSource(); final String property = event.getProperty(); - if (BuildConsole.P_STREAM_COLOR.equals(property) && source instanceof BuildConsoleStreamDecorator) { - BuildConsoleStreamDecorator stream = (BuildConsoleStreamDecorator)source; + if (BuildConsole.P_STREAM_COLOR.equals(property) && source instanceof IBuildConsoleStreamDecorator) { + IBuildConsoleStreamDecorator stream = (IBuildConsoleStreamDecorator)source; if (stream.getConsole().equals(getConsole()) && getControl() != null) { Display display = getControl().getDisplay(); display.asyncExec(new Runnable() { diff --git a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePartition.java b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePartition.java index 399d7fc5b51..f460d167ad3 100644 --- a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePartition.java +++ b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePartition.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2002, 2010 QNX Software Systems and others. + * Copyright (c) 2002, 2017 QNX Software Systems and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -8,6 +8,7 @@ * Contributors: * QNX Software Systems - Initial API and implementation * Dmitry Kozlov (CodeSourcery) - Build error highlighting and navigation + * Jonah Graham (Kichwa Coders) - Significant rewrite, changed model (Bug 314428) *******************************************************************************/ package org.eclipse.cdt.internal.ui.buildconsole; @@ -18,11 +19,14 @@ import org.eclipse.jface.text.TypedRegion; public class BuildConsolePartition extends TypedRegion { /** Associated stream */ - private BuildConsoleStreamDecorator fStream; + private IBuildConsoleStreamDecorator fStream; /** Marker associated with this partition if any */ private ProblemMarkerInfo fMarker; + /** Number of newlines in this region */ + private int fNewlines; + /** Partition type */ public static final String CONSOLE_PARTITION_TYPE = CUIPlugin.getPluginId() + ".CONSOLE_PARTITION_TYPE"; //$NON-NLS-1$ @@ -31,16 +35,12 @@ public class BuildConsolePartition extends TypedRegion { public static final String INFO_PARTITION_TYPE = CUIPlugin.getPluginId() + ".INFO_PARTITION_TYPE"; //$NON-NLS-1$ public static final String WARNING_PARTITION_TYPE = CUIPlugin.getPluginId() + ".WARNING_PARTITION_TYPE"; //$NON-NLS-1$ - public BuildConsolePartition(BuildConsoleStreamDecorator stream, int offset, int length, String type) { - super(offset, length, type); - fStream = stream; - } - - public BuildConsolePartition(BuildConsoleStreamDecorator stream, int offset, int length, String type, - ProblemMarkerInfo marker) { + public BuildConsolePartition(IBuildConsoleStreamDecorator stream, int offset, int length, String type, + ProblemMarkerInfo marker, int newlines) { super(offset, length, type); fStream = stream; fMarker = marker; + fNewlines = newlines; } /** @@ -67,66 +67,23 @@ public class BuildConsolePartition extends TypedRegion { * * @return this partition's stream */ - public BuildConsoleStreamDecorator getStream() { + public IBuildConsoleStreamDecorator getStream() { return fStream; } - /** - * Returns whether this partition is allowed to be combined with the given - * partition. - * - * @param partition - * @return boolean - */ - public boolean canBeCombinedWith(BuildConsolePartition partition) { - // Error partitions never can be combined together - String type = getType(); - if (isProblemPartitionType(type)) { - return false; - } - - int start = getOffset(); - int end = start + getLength(); - int otherStart = partition.getOffset(); - int otherEnd = otherStart + partition.getLength(); - boolean overlap = (otherStart >= start && otherStart <= end) - || (start >= otherStart && start <= otherEnd); - return getStream() != null && overlap && type.equals(partition.getType()) - && getStream().equals(partition.getStream()); - } - - /** - * Returns a new partition representing this and the given parition - * combined. - * - * @param partition - * @return partition - */ - public BuildConsolePartition combineWith(BuildConsolePartition partition) { - int start = getOffset(); - int end = start + getLength(); - int otherStart = partition.getOffset(); - int otherEnd = otherStart + partition.getLength(); - int theStart = Math.min(start, otherStart); - int theEnd = Math.max(end, otherEnd); - return createNewPartition(theStart, theEnd - theStart, CONSOLE_PARTITION_TYPE); - } - - /** - * Creates a new partition of this type with the given offset, and length. - * - * @param offset - * @param length - * @return a new partition with the given range - */ - public BuildConsolePartition createNewPartition(int offset, int length, String type) { - return new BuildConsolePartition(getStream(), offset, length, type, getMarker()); - } - public ProblemMarkerInfo getMarker() { return fMarker; } + /** + * Return number of newlines represented in this partition. + * + * @return number of newlines + */ + public int getNewlines() { + return fNewlines; + } + public static boolean isProblemPartitionType(String type) { return type == BuildConsolePartition.ERROR_PARTITION_TYPE || type == BuildConsolePartition.WARNING_PARTITION_TYPE diff --git a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePartitioner.java b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePartitioner.java index 150052c243b..16aab00f3ad 100644 --- a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePartitioner.java +++ b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePartitioner.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2002, 2016 QNX Software Systems and others. + * Copyright (c) 2002, 2017 QNX Software Systems and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,25 +11,21 @@ * Andrew Gvozdev (Quoin Inc.) - Copy build log (bug 306222) * Alex Collins (Broadcom Corp.) - Global console * Sergey Prigogin (Google) - Performance improvements + * Jonah Graham (Kichwa Coders) - Significant rewrite, changed model (Bug 314428) *******************************************************************************/ package org.eclipse.cdt.internal.ui.buildconsole; import java.io.IOException; import java.io.OutputStream; import java.net.URI; -import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Collection; -import java.util.Deque; -import java.util.Iterator; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.core.filesystem.EFS; import org.eclipse.core.filesystem.IFileStore; -import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; -import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentPartitioner; @@ -48,308 +44,64 @@ import org.eclipse.cdt.core.resources.IConsole; import org.eclipse.cdt.core.resources.ResourcesUtil; import org.eclipse.cdt.ui.CUIPlugin; +import org.eclipse.cdt.internal.ui.buildconsole.BuildConsolePartitionerEditData.UpdateUIData; import org.eclipse.cdt.internal.ui.preferences.BuildConsolePreferencePage; +/* + * XXX the wrap lines is way too slow to be usable on large {@link #fMaxLines} + * (not sure limit, 500 seems ok, 10000 is a problem) Best idea may be to do the + * wrapping "fixed" within the + * {@link #appendToDocument(String, BuildConsoleStreamDecorator, ProblemMarkerInfo)} + */ public class BuildConsolePartitioner implements IDocumentPartitioner, IDocumentPartitionerExtension, IConsole, IPropertyChangeListener { - private static class SynchronizedDeque implements Deque { - private final Deque deque; - - public SynchronizedDeque(Deque deque) { - this.deque = deque; - } - - @Override - public synchronized boolean isEmpty() { - return deque.isEmpty(); - } - - @Override - public synchronized void addFirst(E e) { - deque.addFirst(e); - } - - @Override - public synchronized void addLast(E e) { - deque.addLast(e); - } - - @Override - public synchronized Object[] toArray() { - return deque.toArray(); - } - - @Override - public synchronized T[] toArray(T[] a) { - return deque.toArray(a); - } - - @Override - public synchronized boolean offerFirst(E e) { - return deque.offerFirst(e); - } - - @Override - public synchronized boolean offerLast(E e) { - return deque.offerLast(e); - } - - @Override - public synchronized E removeFirst() { - return deque.removeFirst(); - } - - @Override - public synchronized E removeLast() { - return deque.removeLast(); - } - - @Override - public synchronized E pollFirst() { - return deque.pollFirst(); - } - - @Override - public synchronized E pollLast() { - return deque.pollLast(); - } - - @Override - public synchronized E getFirst() { - return deque.getFirst(); - } - - @Override - public synchronized E getLast() { - return deque.getLast(); - } - - @Override - public synchronized E peekFirst() { - return deque.peekFirst(); - } - - @Override - public synchronized E peekLast() { - return deque.peekLast(); - } - - @Override - public synchronized boolean removeFirstOccurrence(Object o) { - return deque.removeFirstOccurrence(o); - } - - @Override - public synchronized boolean containsAll(Collection c) { - return deque.containsAll(c); - } - - @Override - public synchronized boolean removeLastOccurrence(Object o) { - return deque.removeLastOccurrence(o); - } - - @Override - public synchronized boolean addAll(Collection c) { - return deque.addAll(c); - } - - @Override - public synchronized boolean add(E e) { - return deque.add(e); - } - - @Override - public synchronized boolean removeAll(Collection c) { - return deque.removeAll(c); - } - - @Override - public synchronized boolean offer(E e) { - return deque.offer(e); - } - - @Override - public synchronized boolean retainAll(Collection c) { - return deque.retainAll(c); - } - - @Override - public synchronized E remove() { - return deque.remove(); - } - - @Override - public synchronized E poll() { - return deque.poll(); - } - - @Override - public synchronized E element() { - return deque.element(); - } - - @Override - public synchronized void clear() { - deque.clear(); - } - - @Override - public synchronized boolean equals(Object o) { - return deque.equals(o); - } - - @Override - public synchronized E peek() { - return deque.peek(); - } - - @Override - public synchronized void push(E e) { - deque.push(e); - } - - @Override - public synchronized E pop() { - return deque.pop(); - } - - @Override - public synchronized int hashCode() { - return deque.hashCode(); - } - - @Override - public synchronized boolean remove(Object o) { - return deque.remove(o); - } - - @Override - public synchronized boolean contains(Object o) { - return deque.contains(o); - } - - @Override - public synchronized int size() { - return deque.size(); - } - - @Override - public synchronized Iterator iterator() { - return deque.iterator(); - } - - @Override - public synchronized Iterator descendingIterator() { - return deque.descendingIterator(); - } - } - private IProject fProject; - private int openStreamCount = 0; - /** - * List of partitions + * Active list of partitions, must only be accessed form UI thread which + * provides implicit lock */ - List fPartitions = new ArrayList(5); - - private int fMaxLines; - + List fPartitions = new ArrayList(); /** - * The stream that was last appended to + * Active document, must only be accessed form UI thread which provides + * implicit lock */ - BuildConsoleStreamDecorator fLastStream; - BuildConsoleDocument fDocument; + + /** + * Provides core implementation of partitioner. + */ + BuildConsolePartitionerEditData fEditData; + + /** + * Set to true if there is an asyncExec for the UI update already scheduled. + */ + private AtomicBoolean fEditUiPending = new AtomicBoolean(false); + DocumentMarkerManager fDocumentMarkerManager; - boolean killed; BuildConsoleManager fManager; /** - * A queue of stream entries written to standard out and standard err. - * Entries appended to the end of the queue and removed from the front. + * Encapsulation of variables for log files that must be accessed + * synchronized on fLogFile. The key part we want to synchronize is the + * writes to fLogStream so that different sources (stderr/stdout) don't get + * unnecessarily intermixed. */ - private final Deque fQueue = new SynchronizedDeque( - new ArrayDeque()); - - private URI fLogURI; - private OutputStream fLogStream; - - private class StreamEntry { - public static final int EVENT_APPEND = 0; - public static final int EVENT_OPEN_LOG = 1; - public static final int EVENT_CLOSE_LOG = 2; - public static final int EVENT_OPEN_APPEND_LOG = 3; - - /** Identifier of the stream written to. */ - private BuildConsoleStreamDecorator fStream; - /** The text written */ - private StringBuilder fText = null; - /** Problem marker corresponding to the line of text */ - private ProblemMarkerInfo fMarker; - /** Type of event **/ - private int eventType; - - public StreamEntry(String text, BuildConsoleStreamDecorator stream, ProblemMarkerInfo marker) { - fText = new StringBuilder(text); - fStream = stream; - fMarker = marker; - eventType = EVENT_APPEND; - } - + private static class LogFile { + private OutputStream fLogStream; + private int openStreamCount = 0; /** - * This constructor is used for special events such as clear console or - * close log. - * - * @param event - * - kind of event. + * This value can be obtained independently without a lock. */ - public StreamEntry(int event) { - fText = null; - fStream = null; - fMarker = null; - eventType = event; - } - - /** - * Returns the stream identifier - */ - public BuildConsoleStreamDecorator getStream() { - return fStream; - } - - public void appendText(String text) { - fText.append(text); - } - - public int size() { - return fText.length(); - } - - /** - * Returns the text written - */ - public String getText() { - return fText.toString(); - } - - /** - * Returns error marker - */ - public ProblemMarkerInfo getMarker() { - return fMarker; - } - - /** - * Returns type of event - */ - public int getEventType() { - return eventType; - } + private URI fLogURI; } + /** + * All operations on the log files need to be synchronized on this object + */ + private LogFile fLogFile = new LogFile(); + /** * Construct a partitioner that is not associated with a specific project */ @@ -360,14 +112,11 @@ public class BuildConsolePartitioner public BuildConsolePartitioner(IProject project, BuildConsoleManager manager) { fProject = project; fManager = manager; - fMaxLines = BuildConsolePreferencePage.buildConsoleLines(); + fEditData = new BuildConsolePartitionerEditData(BuildConsolePreferencePage.buildConsoleLines()); fDocument = new BuildConsoleDocument(); fDocument.setDocumentPartitioner(this); fDocumentMarkerManager = new DocumentMarkerManager(fDocument, this); connect(fDocument); - - fLogURI = null; - fLogStream = null; } /** @@ -375,8 +124,10 @@ public class BuildConsolePartitioner * Should be called when opening the output stream. */ public void setStreamOpened() { - fQueue.add(new StreamEntry(StreamEntry.EVENT_OPEN_LOG)); - asyncProcessQueue(); + synchronized (fLogFile) { + fLogFile.openStreamCount++; + logOpen(false); + } } /** @@ -385,8 +136,7 @@ public class BuildConsolePartitioner * has been closed, without emptying the log file. */ public void setStreamAppend() { - fQueue.add(new StreamEntry(StreamEntry.EVENT_OPEN_APPEND_LOG)); - asyncProcessQueue(); + logOpen(true); } /** @@ -396,191 +146,168 @@ public class BuildConsolePartitioner * in the background. */ public void setStreamClosed() { - fQueue.add(new StreamEntry(StreamEntry.EVENT_CLOSE_LOG)); - asyncProcessQueue(); + synchronized (fLogFile) { + fLogFile.openStreamCount--; + if (fLogFile.openStreamCount <= 0) { + fLogFile.openStreamCount = 0; + if (fLogFile.fLogStream != null) { + try { + fLogFile.fLogStream.close(); + } catch (IOException e) { + CUIPlugin.log(e); + } finally { + ResourcesUtil.refreshWorkspaceFiles(fLogFile.fLogURI); + } + fLogFile.fLogStream = null; + } + } + } + } + + /** + * Open the log + * + * @param append + * Set to true if the log should be opened for appending, false + * for overwriting. + */ + private void logOpen(boolean append) { + synchronized (fLogFile) { + fLogFile.fLogURI = fManager.getLogURI(fProject); + if (fLogFile.fLogURI != null) { + try { + IFileStore logStore = EFS.getStore(fLogFile.fLogURI); + // Ensure the directory exists before opening the file + IFileStore dir = logStore.getParent(); + if (dir != null) + dir.mkdir(EFS.NONE, null); + int opts = append ? EFS.APPEND : EFS.NONE; + fLogFile.fLogStream = logStore.openOutputStream(opts, null); + } catch (CoreException e) { + CUIPlugin.log(e); + } finally { + ResourcesUtil.refreshWorkspaceFiles(fLogFile.fLogURI); + } + } + } + } + + private void log(String text) { + synchronized (fLogFile) { + if (fLogFile.fLogStream != null) { + try { + fLogFile.fLogStream.write(text.getBytes()); + fLogFile.fLogStream.flush(); + } catch (IOException e) { + CUIPlugin.log(e); + } + } + } + } + + /** + * Update the UI after a short delay. The reason for a short delay is to try + * and reduce the "frame rate" of the build console updates, this reduces + * the total load on the main thread. User's won't be able to tell that + * there is an extra delay. + * + * A too short time has little effect and a too long time starts to be + * visible to the user. With my experiments to get under 50% CPU utilization + * on the main thread requires at least 35 msec delay between updates. 250 + * msec leads to visible delay to user and ~20% utilization. And finally the + * chosen value, 75 msec leads to ~35% utilization and no user visible + * delay. + */ + private static final int UDPATE_DELAY_MS = 75; + + /** + * @see #UDPATE_DELAY_MS + */ + private void scheduleUpdate() { + Display display = CUIPlugin.getStandardDisplay(); + if (display != null) { + display.timerExec(UDPATE_DELAY_MS, this::updateUI); + } + } + + private void updateUI() { + fEditUiPending.set(false); + UpdateUIData update = fEditData.getUpdate(); + + /* + * We refresh the log file here although not technically a UI operation. + * We used to refresh the file on every log write, but that is very + * expensive and this call has to search the whole workspace to map the + * URI to the corresponding IFile. (At the time everything was done in + * the UI thread, not just the refresh, so this is an improvement.) + * + * XXX: Consider caching the IFile. + * + * XXX: Consider doing the refresh asynchronously in another thread. + * Keep in mind that the log file can easily be written at rate 10x + * faster than Eclipse's refresh mechanism can detect, which is 1ms. + */ + ResourcesUtil.refreshWorkspaceFiles(fLogFile.fLogURI); + + // notify all streams with data we are about to update + update.getStreamsNeedingNotifcation().forEach(this::warnOfContentChange); + + /* + * The order of these statements matters, the setting the contents of + * the document causes listeners to eventually come back and get the + * partitions, so the new partitions have to be in place first + */ + fPartitions = update.getNewPartitions(); + + if (update.needsClearDocumentMarkerManager()) { + fDocumentMarkerManager.clear(); + } + + /* + * This call is slow, it updates the UI as a side effect. + * + * XXX: Doing a set on the whole document means that all the line + * numbers need to be recalculated. This can be optimized further by + * keeping track of what needs to be edited. However, for now this + * optimization has not been done because although this leads to + * increased CPU usage, it does not lead to a delay in total processing + * time, but rather to a decrease in frame rate. Furthermore, if the + * document overflows, the document's line numbers need to be + * recalculated anyway, so little benefit. + */ + fDocument.set(update.getNewContents()); } /** * Adds the new text to the document. * * @param text - * - the text to append. + * the text to append, cannot be null. * @param stream - * - the stream to append to. + * the stream to append to, null means to clear + * everything. + * @param marker + * the marker associated with this line of console output, can be + * null */ - public void appendToDocument(String text, BuildConsoleStreamDecorator stream, ProblemMarkerInfo marker) { - boolean addToQueue = true; - synchronized (fQueue) { - StreamEntry entry = fQueue.peekLast(); - if (entry != null) { - // If last stream is the same and the size of the queued entry - // has not exceeded - // the batch size, avoid creating a new entry and append the new - // text to the last - // entry in the queue. The batch size is adaptive and grows with - // the length of - // the queue. - if (entry.getStream() == stream && entry.getEventType() == StreamEntry.EVENT_APPEND - && entry.getMarker() == marker && entry.size() < 2000 * fQueue.size()) { - entry.appendText(text); - addToQueue = false; - } - } - if (addToQueue) { - fQueue.add(new StreamEntry(text, stream, marker)); - } + public void appendToDocument(String text, IBuildConsoleStreamDecorator stream, ProblemMarkerInfo marker) { + // Log the output to file ASAP, no need to fEditData lock + log(text); + if (stream == null) { + fEditData.clear(); + } else { + fEditData.append(text, stream, marker); } - if (addToQueue) { - asyncProcessQueue(); - } - } - - /** - * Asynchronous processing of stream entries to append to console. Note that - * all these are processed by the same thread - the user-interface thread as - * of {@link Display#asyncExec(Runnable)}. - */ - private void asyncProcessQueue() { - Runnable r = new Runnable() { - @Override - public void run() { - StreamEntry entry; - entry = fQueue.pollFirst(); - if (entry == null) - return; - - switch (entry.getEventType()) { - case StreamEntry.EVENT_OPEN_LOG: - openStreamCount++; - //$FALL-THROUGH$ - case StreamEntry.EVENT_OPEN_APPEND_LOG: - logOpen(entry.getEventType() == StreamEntry.EVENT_OPEN_APPEND_LOG); - break; - case StreamEntry.EVENT_APPEND: - fLastStream = entry.getStream(); - try { - warnOfContentChange(fLastStream); - - if (fLastStream == null) { - // special case to empty document - fPartitions.clear(); - fDocumentMarkerManager.clear(); - fDocument.set(""); //$NON-NLS-1$ - } - String text = entry.getText(); - if (text.length() > 0) { - addStreamEntryToDocument(entry); - log(text); - boolean allowSlack = false; - entry = fQueue.peekFirst(); - if (entry != null && entry.getEventType() == StreamEntry.EVENT_APPEND) { - // Buffer truncation is an expensive operation. - // Allow some slack - // if more data is coming and we will be - // truncating the buffer - // again soon. - allowSlack = true; - } - checkOverflow(allowSlack); - } - } catch (BadLocationException e) { - } - break; - case StreamEntry.EVENT_CLOSE_LOG: - openStreamCount--; - if (openStreamCount <= 0) { - openStreamCount = 0; - logClose(); - } - break; - } - } - - /** - * Open the log - * - * @param append - * Set to true if the log should be opened for appending, - * false for overwriting. - */ - private void logOpen(boolean append) { - fLogURI = fManager.getLogURI(fProject); - if (fLogURI != null) { - try { - IFileStore logStore = EFS.getStore(fLogURI); - // Ensure the directory exists before opening the file - IFileStore dir = logStore.getParent(); - if (dir != null) - dir.mkdir(EFS.NONE, null); - int opts = append ? EFS.APPEND : EFS.NONE; - fLogStream = logStore.openOutputStream(opts, null); - } catch (CoreException e) { - CUIPlugin.log(e); - } finally { - ResourcesUtil.refreshWorkspaceFiles(fLogURI); - } - } - } - - private void log(String text) { - if (fLogStream != null) { - try { - fLogStream.write(text.getBytes()); - if (fQueue.isEmpty()) { - fLogStream.flush(); - } - } catch (IOException e) { - CUIPlugin.log(e); - } finally { - ResourcesUtil.refreshWorkspaceFiles(fLogURI); - } - } - } - - private void logClose() { - if (fLogStream != null) { - try { - fLogStream.close(); - } catch (IOException e) { - CUIPlugin.log(e); - } finally { - ResourcesUtil.refreshWorkspaceFiles(fLogURI); - } - fLogStream = null; - } - } - - }; Display display = CUIPlugin.getStandardDisplay(); if (display != null) { - display.asyncExec(r); - } - } - - private void addStreamEntryToDocument(StreamEntry entry) throws BadLocationException { - ProblemMarkerInfo marker = entry.getMarker(); - if (marker == null) { - // It is plain unmarkered console output - addPartition(new BuildConsolePartition(fLastStream, fDocument.getLength(), - entry.getText().length(), BuildConsolePartition.CONSOLE_PARTITION_TYPE)); - } else { - // this text line in entry is markered with ProblemMarkerInfo, - // create special partition for it. - String errorPartitionType; - if (marker.severity == IMarker.SEVERITY_INFO) { - errorPartitionType = BuildConsolePartition.INFO_PARTITION_TYPE; - } else if (marker.severity == IMarker.SEVERITY_WARNING) { - errorPartitionType = BuildConsolePartition.WARNING_PARTITION_TYPE; - } else { - errorPartitionType = BuildConsolePartition.ERROR_PARTITION_TYPE; + if (!fEditUiPending.getAndSet(true)) { + display.asyncExec(this::scheduleUpdate); } - addPartition(new BuildConsolePartition(fLastStream, fDocument.getLength(), - entry.getText().length(), errorPartitionType, marker)); } - fDocument.replace(fDocument.getLength(), 0, entry.getText()); + } - void warnOfContentChange(BuildConsoleStreamDecorator stream) { + void warnOfContentChange(IBuildConsoleStreamDecorator stream) { if (stream != null) { ConsolePlugin.getDefault().getConsoleManager().warnOfContentChange(stream.getConsole()); } @@ -592,8 +319,7 @@ public class BuildConsolePartitioner } public void setDocumentSize(int nLines) { - fMaxLines = nLines; - checkOverflow(false); + fEditData.setMaxLines(nLines); } @Override @@ -605,7 +331,6 @@ public class BuildConsolePartitioner public void disconnect() { fDocument.setDocumentPartitioner(null); CUIPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(this); - killed = true; } @Override @@ -680,8 +405,9 @@ public class BuildConsolePartitioner public IRegion documentChanged2(DocumentEvent event) { String text = event.getText(); if (getDocument().getLength() == 0) { - // cleared fPartitions.clear(); + fDocumentMarkerManager.clear(); + fEditData.clear(); return new Region(0, 0); } ITypedRegion[] affectedRegions = computePartitioning(event.getOffset(), text.length()); @@ -700,85 +426,6 @@ public class BuildConsolePartitioner return new Region(affectedRegions[0].getOffset(), affectedLength); } - /** - * Checks to see if the console buffer has overflowed, and empties the - * overflow if needed, updating partitions and hyperlink positions. - */ - protected void checkOverflow(boolean allowSlack) { - if (fMaxLines <= 0) - return; - int nLines = fDocument.getNumberOfLines(); - if (nLines <= (allowSlack ? fMaxLines * 2 : fMaxLines) + 1) - return; - - int overflow = 0; - try { - overflow = fDocument.getLineOffset(nLines - fMaxLines); - } catch (BadLocationException e) { - } - // Update partitions - List newParitions = new ArrayList(fPartitions.size()); - Iterator partitions = fPartitions.iterator(); - while (partitions.hasNext()) { - ITypedRegion region = partitions.next(); - if (region instanceof BuildConsolePartition) { - BuildConsolePartition messageConsolePartition = (BuildConsolePartition) region; - - ITypedRegion newPartition = null; - int offset = region.getOffset(); - String type = messageConsolePartition.getType(); - if (offset < overflow) { - int endOffset = offset + region.getLength(); - if (endOffset < overflow || BuildConsolePartition.isProblemPartitionType(type)) { - // Remove partition, - // partitions with problem markers can't be split - - // remove them too. - } else { - // Split partition - int length = endOffset - overflow; - newPartition = messageConsolePartition.createNewPartition(0, length, type); - } - } else { - // Modify partition offset - offset = messageConsolePartition.getOffset() - overflow; - newPartition = messageConsolePartition.createNewPartition(offset, - messageConsolePartition.getLength(), type); - } - if (newPartition != null) { - newParitions.add(newPartition); - } - } - } - fPartitions = newParitions; - fDocumentMarkerManager.moveToFirstError(); - - try { - fDocument.replace(0, overflow, ""); //$NON-NLS-1$ - } catch (BadLocationException e) { - } - } - - /** - * Adds a new partition, combining with the previous partition if possible. - */ - private BuildConsolePartition addPartition(BuildConsolePartition partition) { - if (fPartitions.isEmpty()) { - fPartitions.add(partition); - } else { - int index = fPartitions.size() - 1; - BuildConsolePartition last = (BuildConsolePartition) fPartitions.get(index); - if (last.canBeCombinedWith(partition)) { - // replace with a single partition - partition = last.combineWith(partition); - fPartitions.set(index, partition); - } else { - // different kinds - add a new parition - fPartitions.add(partition); - } - } - return partition; - } - public IConsole getConsole() { return this; } @@ -792,13 +439,16 @@ public class BuildConsolePartitioner @Override public void start(final IProject project) { + synchronized (fLogFile) { + fLogFile.fLogStream = null; + fLogFile.fLogURI = null; + } + Display display = CUIPlugin.getStandardDisplay(); if (display != null) { display.asyncExec(new Runnable() { @Override public void run() { - fLogStream = null; - fLogURI = null; fManager.startConsoleActivity(project); } }); @@ -827,43 +477,11 @@ public class BuildConsolePartitioner fManager.getStreamDecorator(BuildConsoleManager.BUILD_STREAM_TYPE_ERROR)); } - /** This method is useful for future debugging and bug-fixing */ - @SuppressWarnings({ "unused", "nls" }) - private void printDocumentPartitioning() { - System.out.println("Document partitioning: "); - for (ITypedRegion tr : fPartitions) { - BuildConsolePartition p = (BuildConsolePartition) tr; - int start = p.getOffset(); - int end = p.getOffset() + p.getLength(); - String text; - String isError = "U"; - String type = p.getType(); - if (type == BuildConsolePartition.ERROR_PARTITION_TYPE) { - isError = "E"; - } else if (type == BuildConsolePartition.WARNING_PARTITION_TYPE) { - isError = "W"; - } else if (type == BuildConsolePartition.INFO_PARTITION_TYPE) { - isError = "I"; - } else if (type == BuildConsolePartition.CONSOLE_PARTITION_TYPE) { - isError = "C"; - } - try { - text = fDocument.get(p.getOffset(), p.getLength()); - } catch (BadLocationException e) { - text = "N/A"; - } - if (text.endsWith("\n")) { - text = text.substring(0, text.length() - 1); - } - System.out.println(" " + isError + " " + start + "-" + end + ":[" + text + "]"); - } - } - /** * @return {@link URI} location of log file. */ public URI getLogURI() { - return fLogURI; + return fLogFile.fLogURI; } IProject getProject() { diff --git a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePartitionerEditData.java b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePartitionerEditData.java new file mode 100644 index 00000000000..d3af3f96e0b --- /dev/null +++ b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsolePartitionerEditData.java @@ -0,0 +1,327 @@ +/******************************************************************************* + * Copyright (c) 2016, 2017 Kichwa Coders and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Jonah Graham (Kichwa Coders) - Initial API and implementation + *******************************************************************************/ +package org.eclipse.cdt.internal.ui.buildconsole; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.jface.text.ITypedRegion; + +import org.eclipse.cdt.core.ProblemMarkerInfo; + +public class BuildConsolePartitionerEditData { + + /** + * Return data for an update. + */ + public interface UpdateUIData { + /** + * The contents have been changed in such a way that an associated + * DocumentMarkerManager needs to clear its state. This normally happens + * when the document overflows (and older lines are discarded) or when + * document is cleared. + */ + boolean needsClearDocumentMarkerManager(); + + /** + * New contents for the build console. + */ + String getNewContents(); + + /** + * New partitions that match the new contents. + */ + List getNewPartitions(); + + /** + * All the streams that have been written to since the last update. + */ + List getStreamsNeedingNotifcation(); + } + + /** + * The maximum number of lines the document is allowed to have. This is a + * soft limit. 0 or less for unlimited. + */ + private int fMaxLines; + + /** + * Editable partitions, all modifications are made to this copy of the + * partitions, then the UI thread occasionally gets these updates + */ + private List fEditPartitions = new ArrayList(); + + /** + * Set to true when an edit causes the document marker manager to need to be + * cleared out on next UI update. + */ + private boolean fClearDocumentMarkerManager = false; + + /** + * Editable document, all modifications are made to this copy of the + * document, then the UI thread occasionally gets these updates + */ + private StringBuilder fEditStringBuilder = new StringBuilder(); + + /** + * Total number of lines in document + */ + private int fEditLineCount = 0; + + /** + * Set of streams that have been updated since the last UI update. + */ + private Set fEditStreams = new HashSet<>(); + + public BuildConsolePartitionerEditData(int maxLines) { + fMaxLines = maxLines; + } + + public int getMaxLines() { + return fMaxLines; + } + + public void setMaxLines(int fMaxLines) { + this.fMaxLines = fMaxLines; + } + + /** + * Clear the entire document. + */ + public void clear() { + synchronized (this) { + fEditPartitions.clear(); + fClearDocumentMarkerManager = true; + fEditStringBuilder.setLength(0); + fEditLineCount = 0; + } + } + + /** + * + * Adds the new text to the document. + * + * @param text + * the text to append, cannot be null. + * @param stream + * the stream to append to, cannot be null. + * @param marker + * the marker associated with this line of console output, can be + * null + * + */ + public void append(String text, IBuildConsoleStreamDecorator stream, ProblemMarkerInfo marker) { + int newlines = (int) text.chars().filter(ch -> ch == '\n').count(); + synchronized (this) { + fEditStreams.add(stream); + + if (text.length() > 0) { + String partitionType; + if (marker == null) { + partitionType = BuildConsolePartition.CONSOLE_PARTITION_TYPE; + } else if (marker.severity == IMarker.SEVERITY_INFO) { + partitionType = BuildConsolePartition.INFO_PARTITION_TYPE; + } else if (marker.severity == IMarker.SEVERITY_WARNING) { + partitionType = BuildConsolePartition.WARNING_PARTITION_TYPE; + } else { + partitionType = BuildConsolePartition.ERROR_PARTITION_TYPE; + } + if (fEditPartitions.isEmpty()) { + fEditPartitions.add(new BuildConsolePartition(stream, fEditStringBuilder.length(), + text.length(), partitionType, marker, newlines)); + } else { + int index = fEditPartitions.size() - 1; + BuildConsolePartition last = fEditPartitions.get(index); + /* + * Don't permit partitions with markers to be combined + */ + boolean canBeCombined = marker == null && last.getMarker() == null; + /* + * Don't permit a single partition to exceed the maximum + * number of lines of the whole document, this significantly + * simplifies the logic of checkOverflow. + */ + canBeCombined = canBeCombined + && (fMaxLines <= 0 || (last.getNewlines() + newlines < fMaxLines)); + /* + * Don't permit different partition types to be combined + */ + canBeCombined = canBeCombined && Objects.equals(last.getType(), partitionType); + /* + * Don't permit different streams to be combined + */ + canBeCombined = canBeCombined && Objects.equals(last.getStream(), stream); + + if (canBeCombined) { + // replace with a single partition + int combinedOffset = last.getOffset(); + int combinedLength = last.getLength() + text.length(); + int combinedNewlines = last.getNewlines() + newlines; + BuildConsolePartition partition2 = new BuildConsolePartition(last.getStream(), + combinedOffset, combinedLength, BuildConsolePartition.CONSOLE_PARTITION_TYPE, + null, combinedNewlines); + fEditPartitions.set(index, partition2); + } else { + // different kinds - add a new parition + fEditPartitions.add(new BuildConsolePartition(stream, fEditStringBuilder.length(), + text.length(), partitionType, marker, newlines)); + } + } + fEditStringBuilder.append(text); + fEditLineCount += newlines; + + checkOverflow(); + } + } + } + + /** + * Checks to see if the console buffer has overflowed, and empties the + * overflow if needed, updating partitions and hyperlink positions. + */ + public void checkOverflow() { + if (fMaxLines <= 0) { + return; + } + + synchronized (this) { + + /* + * We actually limit the number of lines to 2 x max lines, bringing + * it back to max lines when it overflows. This prevents + * recalculating on every update + */ + if (fEditLineCount <= fMaxLines * 2) + return; + + // Update partitions + + int newHeadIndex = fEditPartitions.size(); + int newNewlineCount = 0; + while (newHeadIndex > 0 && newNewlineCount < fMaxLines) { + newHeadIndex--; + BuildConsolePartition part = fEditPartitions.get(newHeadIndex); + newNewlineCount += part.getNewlines(); + } + + if (newHeadIndex == 0) { + // Nothing to do + return; + } + + int newPartCount = fEditPartitions.size() - newHeadIndex; + int offsetToOffset = fEditPartitions.get(newHeadIndex).getOffset(); + List newParitions = new ArrayList<>(newPartCount); + Iterator partitions = fEditPartitions.listIterator(newHeadIndex); + while (partitions.hasNext()) { + BuildConsolePartition partition = partitions.next(); + + BuildConsolePartition newPartition = new BuildConsolePartition(partition.getStream(), + partition.getOffset() - offsetToOffset, partition.getLength(), partition.getType(), + partition.getMarker(), partition.getNewlines()); + + newParitions.add(newPartition); + } + + fEditPartitions = newParitions; + fClearDocumentMarkerManager = true; + + fEditStringBuilder.delete(0, offsetToOffset); + fEditLineCount = newNewlineCount; + + } + } + + /** + * This method is useful for future debugging and bug-fixing + */ + @SuppressWarnings("nls") + public void printDocumentPartitioning() { + // Non synchronized access, used only for debugging + System.out.println("Document partitioning: "); + for (ITypedRegion tr : fEditPartitions) { + BuildConsolePartition p = (BuildConsolePartition) tr; + int start = p.getOffset(); + int end = p.getOffset() + p.getLength(); + String text; + String isError = "U"; + String type = p.getType(); + if (type == BuildConsolePartition.ERROR_PARTITION_TYPE) { + isError = "E"; + } else if (type == BuildConsolePartition.WARNING_PARTITION_TYPE) { + isError = "W"; + } else if (type == BuildConsolePartition.INFO_PARTITION_TYPE) { + isError = "I"; + } else if (type == BuildConsolePartition.CONSOLE_PARTITION_TYPE) { + isError = "C"; + } + text = fEditStringBuilder.substring(p.getOffset(), p.getLength()); + + if (text.endsWith("\n")) { + text = text.substring(0, text.length() - 1); + } + System.out.println(" " + isError + " " + start + "-" + end + ":[" + text + "]"); + } + } + + /** + * Obtain the next snapshot of data. This update must be processed by the + * UI. i.e. don't call this method unless you are going to handle the update + * now. + * + * @return see {@link UpdateUIData} for details on individual values + * returned. + */ + public UpdateUIData getUpdate() { + boolean clearDocumentMarkerManager; + String newConents; + List newPartitions; + List streamsNeedingNotifcation; + + synchronized (this) { + newConents = fEditStringBuilder.toString(); + newPartitions = new ArrayList<>(fEditPartitions); + clearDocumentMarkerManager = fClearDocumentMarkerManager; + fClearDocumentMarkerManager = false; + streamsNeedingNotifcation = new ArrayList<>(fEditStreams); + fEditStreams.clear(); + } + + return new UpdateUIData() { + + @Override + public boolean needsClearDocumentMarkerManager() { + return clearDocumentMarkerManager; + } + + @Override + public List getStreamsNeedingNotifcation() { + return streamsNeedingNotifcation; + } + + @Override + public List getNewPartitions() { + return newPartitions; + } + + @Override + public String getNewContents() { + return newConents; + } + }; + } + +} \ No newline at end of file diff --git a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsoleStreamDecorator.java b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsoleStreamDecorator.java index 363d1d07360..f0c9a095dee 100644 --- a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsoleStreamDecorator.java +++ b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildConsoleStreamDecorator.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2009, 2010 QNX Software Systems and others. + * Copyright (c) 2009, 2017 QNX Software Systems and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -12,13 +12,13 @@ package org.eclipse.cdt.internal.ui.buildconsole; import org.eclipse.swt.graphics.Color; +import org.eclipse.ui.console.IConsole; - -public class BuildConsoleStreamDecorator { +public class BuildConsoleStreamDecorator implements IBuildConsoleStreamDecorator { private BuildConsole fConsole = null; - + private Color fColor = null; - + /** * Constructs a new stream connected to the given console. * @@ -33,7 +33,8 @@ public class BuildConsoleStreamDecorator { /** * Sets the color of this message stream * - * @param color color of this message stream, possibly null + * @param color + * color of this message stream, possibly null */ public void setColor(Color color) { Color old = fColor; @@ -42,23 +43,14 @@ public class BuildConsoleStreamDecorator { fConsole.firePropertyChange(this, BuildConsole.P_STREAM_COLOR, old, color); } } - - /** - * Returns the color of this message stream, or null - * if default. - * - * @return the color of this message stream, or null - */ + + @Override public Color getColor() { return fColor; } - - /** - * Returns the console this stream is connected to. - * - * @return the console this stream is connected to - */ - public BuildConsole getConsole() { + + @Override + public IConsole getConsole() { return fConsole; } } diff --git a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildOutputStream.java b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildOutputStream.java index 25bd5cbf64a..b415602026f 100644 --- a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildOutputStream.java +++ b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/BuildOutputStream.java @@ -26,11 +26,11 @@ import org.eclipse.cdt.internal.core.IErrorMarkeredOutputStream; */ public class BuildOutputStream extends ConsoleOutputStream implements IErrorMarkeredOutputStream { - final BuildConsoleStreamDecorator fStream; + final IBuildConsoleStreamDecorator fStream; private BuildConsolePartitioner fPartitioner; public BuildOutputStream(BuildConsolePartitioner partitioner, - BuildConsoleStreamDecorator stream) { + IBuildConsoleStreamDecorator stream) { fPartitioner = partitioner; if (fPartitioner.getProject() == null) // Note: The global console log stream should have been diff --git a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/IBuildConsoleStreamDecorator.java b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/IBuildConsoleStreamDecorator.java new file mode 100644 index 00000000000..f4de7f533fc --- /dev/null +++ b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/buildconsole/IBuildConsoleStreamDecorator.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2017 Kichwa Coders and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Jonah Graham (Kichwa Coders) - Initial API and implementation + *******************************************************************************/ +package org.eclipse.cdt.internal.ui.buildconsole; + +import org.eclipse.swt.graphics.Color; +import org.eclipse.ui.console.IConsole; + +public interface IBuildConsoleStreamDecorator { + + /** + * Returns the color of this message stream, or null if + * default. + * + * @return the color of this message stream, or null + */ + Color getColor(); + + /** + * Returns the console this stream is connected to. + * + * @return the console this stream is connected to + */ + IConsole getConsole(); + +} diff --git a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/preferences/BuildConsolePreferencePage.java b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/preferences/BuildConsolePreferencePage.java index a54d8262287..0acc7ec4301 100644 --- a/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/preferences/BuildConsolePreferencePage.java +++ b/core/org.eclipse.cdt.ui/src/org/eclipse/cdt/internal/ui/preferences/BuildConsolePreferencePage.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2013 QNX Software Systems and others. + * Copyright (c) 2000, 2017 QNX Software Systems and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -81,6 +81,8 @@ public class BuildConsolePreferencePage extends FieldEditorPreferencePage implem IntegerFieldEditor buildCount = new IntegerFieldEditor(PREF_BUILDCONSOLE_LINES, CUIPlugin.getResourceString("ConsolePreferencePage.consoleLines.label"), parent); //$NON-NLS-1$ + buildCount.getLabelControl(parent).setToolTipText(CUIPlugin.getResourceString("ConsolePreferencePage.consoleLines.tooltip")); //$NON-NLS-1$ + buildCount.getTextControl(parent).setToolTipText(CUIPlugin.getResourceString("ConsolePreferencePage.consoleLines.tooltip")); //$NON-NLS-1$ buildCount.setErrorMessage(CUIPlugin.getResourceString("ConsolePreferencePage.consoleLines.errorMessage")); //$NON-NLS-1$ buildCount.setValidRange(10, Integer.MAX_VALUE); addField(buildCount);