Bug 575275 - faster console

* better mutlithreading,
* bigger buffer,
* faster single byte charset decoding,
* trim console before drawing (faster and looks nicer)
* removed unused Class and members

=> debugged application runs faster if it outputs a lot

Change-Id: Id3fbcbdd2c261271efa90dd81a1dc1b333459dcb
Signed-off-by: Joerg Kubitz <jkubitz-eclipse@gmx.de>
Reviewed-on: https://git.eclipse.org/r/c/platform/eclipse.platform.debug/+/190929
Tested-by: Platform Bot <platform-bot@eclipse.org>
diff --git a/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/OutputStreamMonitor.java b/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/OutputStreamMonitor.java
index fb09bc6..218d870 100644
--- a/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/OutputStreamMonitor.java
+++ b/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/OutputStreamMonitor.java
@@ -98,7 +98,8 @@
 	 *            unused if only the binary interface is used
 	 */
 	public OutputStreamMonitor(InputStream stream, Charset charset) {
-		fStream = new BufferedInputStream(stream, 8192);
+		// java.lang.ProcessImpl returns a buffered stream anyway
+		fStream = stream instanceof BufferedInputStream ? stream : new BufferedInputStream(stream);
 		fCharset = charset;
 		fDecoder = new StreamDecoder(charset == null ? Charset.defaultCharset() : charset);
 		fDone = new AtomicBoolean(false);
@@ -162,9 +163,7 @@
 	 */
 	private void fireStreamAppended(final byte[] data, int offset, int length) {
 		if (!fListeners.isEmpty()) {
-			StringBuilder sb = new StringBuilder();
-			fDecoder.decode(sb, data, offset, length);
-			final String text = sb.toString();
+			String text = fDecoder.decode(data, offset, length);
 			for (final IStreamListener listener : fListeners) {
 				SafeRunner.run(new ISafeRunnable() {
 					@Override
@@ -211,10 +210,8 @@
 		if (fCachedDecodedContents != null) {
 			return fCachedDecodedContents;
 		}
-		StringBuilder sb = new StringBuilder();
 		byte[] data = getData();
-		fBufferedDecoder.decode(sb, data, 0, data.length);
-		fCachedDecodedContents = sb.toString();
+		fCachedDecodedContents = fBufferedDecoder.decode(data, 0, data.length);
 		return fCachedDecodedContents;
 	}
 
@@ -276,11 +273,8 @@
 				currentTime = System.currentTimeMillis();
 				if (currentTime - lastSleep > 1000) {
 					lastSleep = currentTime;
-					try {
-						// just give up CPU to maintain UI responsiveness.
-						Thread.sleep(1);
-					} catch (InterruptedException e) {
-					}
+					Thread.yield();// just give up CPU to maintain UI
+									// responsiveness.
 				}
 			}
 		} finally {
diff --git a/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/StreamDecoder.java b/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/StreamDecoder.java
index 7cb15a7..f64d3d4 100644
--- a/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/StreamDecoder.java
+++ b/org.eclipse.debug.core/core/org/eclipse/debug/internal/core/StreamDecoder.java
@@ -20,6 +20,7 @@
 import java.nio.charset.CharsetDecoder;
 import java.nio.charset.CoderResult;
 import java.nio.charset.CodingErrorAction;
+import java.util.Set;
 
 /**
  * Wraps CharsetDecoder to decode a byte stream statefully to characters.
@@ -29,20 +30,30 @@
 public class StreamDecoder {
 	// For more context see https://bugs.eclipse.org/bugs/show_bug.cgi?id=507664
 
-	static private final int BUFFER_SIZE = 4096;
+	/** size of java.io.BufferedInputStream.DEFAULT_BUFFER_SIZE **/
+	private static final int INPUT_BUFFER_SIZE = 8192;
 
+	private final Charset charset;
 	private final CharsetDecoder decoder;
 	private final ByteBuffer inputBuffer;
 	private final CharBuffer outputBuffer;
-	private boolean finished;
+	private volatile boolean finished;
+
+	/**
+	 * Incomplete list of known Single Byte Character Sets (see
+	 * sun.nio.cs.SingleByte) which do not need a buffer
+	 **/
+	Set<String> singlebyteCharsetNames = Set.of("ISO_8859_1", "US_ASCII", "windows-1250", "windows-1251", "windows-1252", "windows-1253", "windows-1254", "windows-1255", "windows-1256", "windows-1257", "windows-1258"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$ //$NON-NLS-9$ //$NON-NLS-10$ //$NON-NLS-11$
 
 	public StreamDecoder(Charset charset) {
-		this.decoder = charset.newDecoder();
-		this.decoder.onMalformedInput(CodingErrorAction.REPLACE);
-		this.decoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
-		this.inputBuffer = ByteBuffer.allocate(StreamDecoder.BUFFER_SIZE);
-		this.inputBuffer.flip();
-		this.outputBuffer = CharBuffer.allocate(StreamDecoder.BUFFER_SIZE);
+		this.charset = charset;
+		CharsetDecoder d = charset.newDecoder();
+		d.onMalformedInput(CodingErrorAction.REPLACE);
+		d.onUnmappableCharacter(CodingErrorAction.REPLACE);
+		boolean unbuffered = singlebyteCharsetNames.contains(charset.name());
+		this.decoder = unbuffered ? null : d;
+		this.inputBuffer = unbuffered ? null : ByteBuffer.allocate(StreamDecoder.INPUT_BUFFER_SIZE).flip();
+		this.outputBuffer = unbuffered ? null : CharBuffer.allocate((int) (StreamDecoder.INPUT_BUFFER_SIZE * d.maxCharsPerByte()));
 		this.finished = false;
 	}
 
@@ -81,12 +92,31 @@
 		} while (!finishedReading);
 	}
 
-	public void decode(StringBuilder consumer, byte[] buffer, int offset, int length) {
+	public String decode(byte[] buffer, int offset, int length) {
+		if (this.decoder == null) {
+			// fast path for single byte encodings
+			return new String(buffer, offset, length, charset);
+		}
+		StringBuilder builder = new StringBuilder();
+		decode(builder, buffer, offset, length);
+		return builder.toString();
+	}
+
+	private void decode(StringBuilder consumer, byte[] buffer, int offset, int length) {
 		this.internalDecode(consumer, buffer, offset, length);
 		this.consume(consumer);
 	}
 
-	public void finish(StringBuilder consumer) {
+	public String finish() {
+		if (this.decoder == null) {
+			return ""; //$NON-NLS-1$
+		}
+		StringBuilder builder = new StringBuilder();
+		finish(builder);
+		return builder.toString();
+	}
+
+	private void finish(StringBuilder consumer) {
 		if (this.finished) {
 			return;
 		}
diff --git a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java
index ae51f9f..bc1157c 100644
--- a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java
+++ b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java
@@ -28,10 +28,10 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.Queue;
 import java.util.Random;
+import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
 import org.eclipse.core.runtime.ILogListener;
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.IStatus;
@@ -78,12 +78,12 @@
 	 * Number of received log messages with severity error while running a
 	 * single test method.
 	 */
-	private final AtomicInteger loggedErrors = new AtomicInteger();
+	private final Queue<IStatus> loggedErrors = new ConcurrentLinkedQueue<>();
 
 	/** Listener to count error messages while testing. */
 	private final ILogListener errorLogListener = (IStatus status, String plugin) -> {
 		if (status.matches(IStatus.ERROR)) {
-			loggedErrors.incrementAndGet();
+			loggedErrors.add(status);
 		}
 	};
 
@@ -107,7 +107,7 @@
 		activePage.activate(consoleView);
 
 		// add error listener
-		loggedErrors.set(0);
+		loggedErrors.clear();
 		Platform.addLogListener(errorLogListener);
 	}
 
@@ -117,7 +117,16 @@
 		Platform.removeLogListener(errorLogListener);
 		super.tearDown();
 
-		assertEquals("Test triggered errors in IOConsole.", 0, loggedErrors.get());
+		assertNoError();
+	}
+
+	private void assertNoError() {
+		loggedErrors.forEach(status -> {
+			if (status.getException() != null) {
+				throw new AssertionError("Test triggered errors in IOConsole", status.getException());
+			}
+		});
+		assertTrue("Test triggered errors in IOConsole: " + loggedErrors.stream().toString(), loggedErrors.isEmpty());
 	}
 
 	/**
@@ -309,7 +318,7 @@
 			}
 			closeConsole(c);
 		}
-		assertEquals("Test triggered errors in IOConsole.", 0, loggedErrors.get());
+		assertNoError();
 	}
 
 	/**
@@ -436,7 +445,7 @@
 		assertEquals("Wrong number of lines.", 4, c.getDocument().getNumberOfLines());
 
 		closeConsole(c);
-		assertEquals("Test triggered errors in IOConsole.", 0, loggedErrors.get());
+		assertNoError();
 	}
 
 	/**
@@ -518,7 +527,7 @@
 			c.verifyContentByLine("1.", 0).verifyContentByLine("2.", 1).verifyPartitions();
 		}
 		closeConsole(c);
-		assertEquals("Test triggered errors in IOConsole.", 0, loggedErrors.get());
+		assertNoError();
 	}
 
 	/**
@@ -564,7 +573,7 @@
 			assertTrue("Line breaks did not overwrite text.", !c.getDocument().get().contains("err"));
 		}
 		closeConsole(c);
-		assertEquals("Test triggered errors in IOConsole.", 0, loggedErrors.get());
+		assertNoError();
 	}
 
 	/**
@@ -722,7 +731,7 @@
 		c.verifyContent("()").verifyPartitions();
 
 		closeConsole(c);
-		assertEquals("Test triggered errors in IOConsole.", 0, loggedErrors.get());
+		assertNoError();
 	}
 
 	/**
@@ -844,7 +853,7 @@
 		}
 		c.verifyPartitions();
 		closeConsole(c, "#");
-		assertEquals("Test triggered errors in IOConsole.", 0, loggedErrors.get());
+		assertNoError();
 	}
 
 	/**
diff --git a/org.eclipse.ui.console/META-INF/MANIFEST.MF b/org.eclipse.ui.console/META-INF/MANIFEST.MF
index a278788..d3fbb40 100644
--- a/org.eclipse.ui.console/META-INF/MANIFEST.MF
+++ b/org.eclipse.ui.console/META-INF/MANIFEST.MF
@@ -2,7 +2,7 @@
 Bundle-ManifestVersion: 2
 Bundle-Name: %pluginName
 Bundle-SymbolicName: org.eclipse.ui.console; singleton:=true
-Bundle-Version: 3.11.100.qualifier
+Bundle-Version: 3.11.200.qualifier
 Bundle-Activator: org.eclipse.ui.console.ConsolePlugin
 Bundle-Vendor: %providerName
 Bundle-Localization: plugin
diff --git a/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsoleOutputStream.java b/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsoleOutputStream.java
index bbe638b..38cd40b 100644
--- a/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsoleOutputStream.java
+++ b/org.eclipse.ui.console/src/org/eclipse/ui/console/IOConsoleOutputStream.java
@@ -43,7 +43,7 @@
 	/**
 	 * Flag indicating whether this stream has been closed.
 	 */
-	private boolean closed = false;
+	private volatile boolean closed;
 
 	/**
 	 * The console's document partitioner.
@@ -53,26 +53,28 @@
 	/**
 	 * The console this stream is attached to.
 	 */
-	private IOConsole console;
+	private final IOConsole console;
 
 	/**
 	 * Flag indicating that the console should be activated when data
 	 * is written to this stream.
 	 */
-	private boolean activateOnWrite = false;
+	private volatile boolean activateOnWrite;
 
 	/**
 	 * The color used to decorate data written to this stream.
 	 */
-	private Color color;
+	private volatile Color color;
 
 	/**
 	 * The font style used to decorate data written to this stream.
 	 */
-	private int fontStyle;
+	private volatile int fontStyle;
 
+	/** synchronized access for atomic update **/
 	private StreamDecoder decoder;
 
+	/** synchronized access for atomic update **/
 	private boolean prependCR;
 
 	/**
@@ -159,7 +161,7 @@
 	 * Returns true if the stream has been closed
 	 * @return true is the stream has been closed, false otherwise.
 	 */
-	public synchronized boolean isClosed() {
+	public boolean isClosed() {
 		return closed;
 	}
 
@@ -169,14 +171,13 @@
 			// Closeable#close() has no effect if already closed
 			return;
 		}
-		StringBuilder builder = new StringBuilder();
 		if (prependCR) { // force writing of last /r
 			prependCR = false;
-			builder.append('\r');
+			notifyParitioner("\r"); //$NON-NLS-1$
 		}
-		this.decoder.finish(builder);
-		if (builder.length() > 0) {
-			notifyParitioner(builder.toString());
+		String s = this.decoder.finish();
+		if (s.length() > 0) {
+			notifyParitioner(s);
 		}
 		console.streamClosed(this);
 		closed = true;
@@ -196,9 +197,8 @@
 		if (closed) {
 			throw new IOException("Output Stream is closed"); //$NON-NLS-1$
 		}
-		StringBuilder builder = new StringBuilder();
-		this.decoder.decode(builder, b, off, len);
-		encodedWrite(builder.toString());
+		String s = this.decoder.decode(b, off, len);
+		encodedWrite(s);
 	}
 
 	@Override
@@ -271,7 +271,7 @@
 		}
 		if (newencoding.endsWith("\r")) { //$NON-NLS-1$
 			prependCR = true;
-			newencoding = new String(newencoding.substring(0, newencoding.length() - 1));
+			newencoding = newencoding.substring(0, newencoding.length() - 1);
 		}
 		notifyParitioner(newencoding);
 	}
@@ -323,10 +323,9 @@
 		if (closed) {
 			throw new IOException("Output Stream is closed"); //$NON-NLS-1$
 		}
-		StringBuilder builder = new StringBuilder();
-		this.decoder.finish(builder);
-		if (builder.length() > 0) {
-			this.encodedWrite(builder.toString());
+		String s = this.decoder.finish();
+		if (s.length() > 0) {
+			this.encodedWrite(s);
 		}
 		this.decoder = new StreamDecoder(charset);
 	}
diff --git a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleDocument.java b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleDocument.java
index 2d6aef4..f5c4427 100644
--- a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleDocument.java
+++ b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/ConsoleDocument.java
@@ -99,4 +99,10 @@
 	public synchronized Position[] getPositions(String category) throws BadPositionCategoryException {
 		return super.getPositions(category);
 	}
+
+	/** for debug only **/
+	@Override
+	public synchronized String toString() {
+		return get();
+	}
 }
diff --git a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java
index 0e448a6..3158162 100644
--- a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java
+++ b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/IOConsolePartitioner.java
@@ -28,6 +28,9 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -113,10 +116,19 @@
 	 * elements. (see also {@link #checkPartitions()})
 	 */
 	private final ArrayList<IOConsolePartition> partitions = new ArrayList<>();
-	/** Blocks of data that have not yet been appended to the document. */
-	private final ArrayList<PendingPartition> pendingPartitions = new ArrayList<>();
-	/** Total length of pending partitions content. */
-	private int pendingSize;
+	/**
+	 * max ~ 16MB when debugged application outputs faster then we can handle. Seems
+	 * to be a could compromise between memory and speed we can handle. Eclipse IDE
+	 * may use that memory size ~3 times during processing (buffer+document+parser)
+	 */
+	private static final int MAX_BUFFER_BYTES = 16_000_000;
+	/**
+	 * Queue of buffers that have not yet been appended to the document. Every
+	 * buffer will hold up to 8192 byte - but typically a single line output if
+	 * handled fast. The debugged application will block if the limit is exceeded.
+	 */
+	private final BlockingQueue<PendingPartition> pendingPartitions = new LinkedBlockingQueue<>(
+			MAX_BUFFER_BYTES / 8192);
 	/** Job that appends pending partitions to the document. */
 	private final QueueProcessingJob queueJob = new QueueProcessingJob();
 	/** Job that trims console content if it exceeds {@link #highWaterMark}. */
@@ -130,7 +142,6 @@
 	 * </p>
 	 */
 	private DocUpdateType updateType = DocUpdateType.INPUT;
-	private IRegion changedRegion;
 	/**
 	 * A list of partitions containing input from the console, that have not been
 	 * appended to the input stream yet. No guarantees on element order.
@@ -214,11 +225,7 @@
 
 	@Override
 	public void disconnect() {
-		synchronized (pendingPartitions) {
-			pendingPartitions.clear();
-			pendingSize = 0;
-			pendingPartitions.notifyAll();
-		}
+		pendingPartitions.clear();
 		synchronized (partitions) {
 			trimJob.cancel();
 			queueJob.cancel();
@@ -283,10 +290,7 @@
 		if (streamsClosed) {
 			// do not expect new data since all streams are closed
 			// check if pending data is queued
-			final boolean morePending;
-			synchronized (pendingPartitions) {
-				morePending = !pendingPartitions.isEmpty();
-			}
+			final boolean morePending = !pendingPartitions.isEmpty();
 			if (morePending) {
 				queueJob.schedule();
 			} else {
@@ -430,10 +434,8 @@
 		if (document != null && highWaterMark > 0) {
 			int length = document.getLength();
 			if (length > highWaterMark) {
-				if (trimJob.getState() == Job.NONE) { // if the job isn't already running
-					trimJob.setTrimLineOffset(length - lowWaterMark);
-					trimJob.schedule();
-				}
+				// do trim synchronous to prevent drawing trimmed text - we are already in UI and have the lock
+				trim(length - lowWaterMark, true);
 			}
 		}
 	}
@@ -442,10 +444,7 @@
 	 * Clears the console content.
 	 */
 	public void clearBuffer() {
-		synchronized (pendingPartitions) {
-			pendingPartitions.clear();
-			pendingSize = 0;
-		}
+		pendingPartitions.clear();
 		synchronized (partitions) {
 			if (document != null) {
 				trimJob.setTrimOffset(document.getLength());
@@ -484,7 +483,7 @@
 				// update and trim jobs are triggered by this partitioner and all partitioning
 				// changes are applied separately
 				case OUTPUT:
-					return changedRegion;
+					return null; // changedRegion was never assigned
 				case TRIM:
 					return null; // trim does not change partition types
 
@@ -692,37 +691,27 @@
 		if (s == null) {
 			return;
 		}
-		synchronized (pendingPartitions) {
-			final PendingPartition lastPending = pendingPartitions.size() > 0
-					? pendingPartitions.get(pendingPartitions.size() - 1)
-					: null;
-			if (lastPending != null && lastPending.stream == stream) {
-				lastPending.append(s);
-			} else {
-				pendingPartitions.add(new PendingPartition(stream, s));
-			}
+		PendingPartition partition = new PendingPartition(stream, s);
+		while (!offer(partition)) {
+			helpProgress();
+		}
+		queueJob.schedule();
+	}
 
-			if (pendingSize > 1000) {
-				queueJob.schedule();
-			} else {
-				queueJob.schedule(50);
-			}
+	private void helpProgress() {
+		if (Display.getCurrent() != null) {
+			// make sure pendingPartitions can take
+			queueJob.processPendingPartitions();
+		} else {
+			Thread.yield(); // give UI thread chance to proceed
+		}
+	}
 
-			if (pendingSize > 160000) {
-				if (Display.getCurrent() == null) {
-					try {
-						// Block thread to give UI time to process pending output.
-						// Do not wait forever. Current thread and UI thread might share locks. An
-						// example is bug 421303 where current thread and UI thread both write to
-						// console and therefore both need the write lock for IOConsoleOutputStream.
-						pendingPartitions.wait(1000);
-					} catch (InterruptedException e) {
-					}
-				} else {
-					// If we are in UI thread we cannot lock it, so process queued output.
-					queueJob.processPendingPartitions();
-				}
-			}
+	private boolean offer(PendingPartition p) {
+		try {
+			return pendingPartitions.offer(p, 10, TimeUnit.MILLISECONDS);
+		} catch (InterruptedException e) {
+			return false;
 		}
 	}
 
@@ -730,17 +719,17 @@
 	 * Holds data until updateJob can be run and the document can be updated.
 	 */
 	private class PendingPartition {
-		StringBuilder text = new StringBuilder(8192);
-		IOConsoleOutputStream stream;
+		private final CharSequence text;
+		private final IOConsoleOutputStream stream;
 
-		PendingPartition(IOConsoleOutputStream stream, String text) {
+		PendingPartition(IOConsoleOutputStream stream, CharSequence text) {
 			this.stream = stream;
-			append(text);
+			this.text = text;
 		}
 
-		void append(String moreText) {
-			text.append(moreText);
-			pendingSize += moreText.length();
+		@Override
+		public String toString() {
+			return text.toString();
 		}
 	}
 
@@ -755,8 +744,6 @@
 		private int atOutputPartitionIndex = -1;
 		/** The pending number of characters to replace in document. */
 		private int replaceLength;
-		/** The pending content to be inserted in document. */
-		private StringBuilder content;
 		/** The offset in document where to apply the next replace. */
 		private int nextWriteOffset;
 
@@ -784,10 +771,7 @@
 		 */
 		@Override
 		public boolean shouldRun() {
-			synchronized (pendingPartitions) {
-				final boolean shouldRun = pendingPartitions.size() > 0;
-				return shouldRun;
-			}
+			return !pendingPartitions.isEmpty();
 		}
 
 		/**
@@ -795,22 +779,29 @@
 		 * update partitioning.
 		 */
 		private void processPendingPartitions() {
-			final List<PendingPartition> pendingCopy;
-			final int size;
-			synchronized (pendingPartitions) {
-				pendingCopy = new ArrayList<>(pendingPartitions);
-				size = pendingSize;
-				pendingPartitions.clear();
-				pendingSize = 0;
-				pendingPartitions.notifyAll();
+			final List<PendingPartition> pendingCopy = new ArrayList<>();
+			// draining the whole buffer here is important - this way we get as much data as
+			// available and may skip to draw text that exceeds the Console buffer size
+			// anyway (see checkBufferSize()).
+			pendingPartitions.drainTo(pendingCopy);
+			int sizeHint = 0;
+			if (pendingCopy.isEmpty()) {
+				return;
+			}
+			IOConsoleOutputStream stream = pendingCopy.get(0).stream;
+			for (PendingPartition p : pendingCopy) {
+				if (p.stream != stream) {
+					break;
+				}
+				sizeHint += p.text.length();
 			}
 			synchronized (partitions) {
 				if (document != null) {
-					applyStreamOutput(pendingCopy, size);
+					applyStreamOutput(pendingCopy, sizeHint);
 				}
+				checkFinished();
+				checkBufferSize(); // needs partitions synchronized
 			}
-			checkFinished();
-			checkBufferSize();
 		}
 
 		/**
@@ -835,17 +826,19 @@
 			// resulting in multiple partitions but if all the content is appended to the
 			// document there is only one update required to add the actual content.
 			nextWriteOffset = outputOffset;
-			content = new StringBuilder(sizeHint);
+			StringBuilder content = new StringBuilder(sizeHint);
 			replaceLength = 0;
 			atOutputPartition = null;
 			atOutputPartitionIndex = -1;
 
 			for (PendingPartition pending : pendingCopy) {
 				// create matcher to find control characters in pending content (if enabled)
-				final Matcher controlCharacterMatcher = controlPattern != null ? controlPattern.matcher(pending.text)
+				CharSequence text = pending.text;
+				IOConsoleOutputStream stream = pending.stream;
+				final Matcher controlCharacterMatcher = controlPattern != null ? controlPattern.matcher(text)
 						: null;
 
-				for (int textOffset = 0; textOffset < pending.text.length();) {
+				for (int textOffset = 0; textOffset < text.length();) {
 					// Process pending content in chunks.
 					// Processing is primary split on control characters since there interpretation
 					// is easier if all content changes before are already applied.
@@ -866,11 +859,11 @@
 						partEnd = controlCharacterMatcher.start();
 						foundControlCharacter = true;
 					} else {
-						partEnd = pending.text.length();
+						partEnd = text.length();
 						foundControlCharacter = false;
 					}
 
-					partititonContent(pending.stream, pending.text, textOffset, partEnd);
+					partititonContent(stream, text, textOffset, partEnd, content);
 					textOffset = partEnd;
 
 					// finished processing of regular content before control characters
@@ -891,7 +884,7 @@
 							// move virtual output cursor one step back for each \b
 							// but stop at current line start and skip any input partitions
 							int backStepCount = controlCharacterMatch.length();
-							if (partitions.size() == 0) {
+							if (partitions.isEmpty()) {
 								outputOffset = 0;
 								break;
 							}
@@ -947,7 +940,7 @@
 							}
 							outputOffset = document.getLength();
 							nextWriteOffset = outputOffset;
-							partititonContent(pending.stream, vtab, 0, vtab.length());
+							partititonContent(stream, vtab, 0, vtab.length(), content);
 							break;
 
 						case 0:
@@ -968,7 +961,6 @@
 				}
 			}
 			applyOutputToDocument(content.toString(), nextWriteOffset, replaceLength);
-			content = null;
 		}
 
 		/**
@@ -988,7 +980,8 @@
 		 * @param offset    the start offset (inclusive) within text to partition
 		 * @param endOffset the end offset (exclusive) within text to partition
 		 */
-		private void partititonContent(IOConsoleOutputStream stream, CharSequence text, int offset, int endOffset) {
+		private void partititonContent(IOConsoleOutputStream stream, CharSequence text, int offset, int endOffset,
+				StringBuilder content) {
 			int textOffset = offset;
 			while (textOffset < endOffset) {
 				// Process content part. This part never contains control characters.
@@ -1180,27 +1173,25 @@
 		 * @param offset trims console content up to this offset
 		 */
 		public void setTrimOffset(int offset) {
+			// XXX an arbitrary offset would cause follow up errors if we trim a delim
+			// exactly between \r and \n
 			truncateOffset = offset;
 			truncateToOffsetLineStart = false;
 		}
 
-		/**
-		 * Sets the trim offset.
-		 *
-		 * @param offset trims output up to the line containing this offset
-		 */
-		public void setTrimLineOffset(int offset) {
-			truncateOffset = offset;
-			truncateToOffsetLineStart = true;
-		}
-
 		@Override
 		public IStatus runInUIThread(IProgressMonitor monitor) {
 			synchronized (partitions) {
-				if (document == null) {
-					return Status.OK_STATUS;
-				}
+				trim(truncateOffset, truncateToOffsetLineStart);
+			}
+			return Status.OK_STATUS;
+		}
 
+	}
+
+	private void trim(int truncateOffset, boolean truncateToOffsetLineStart) {
+		if (document != null) {
+			{
 				try {
 					int length = document.getLength();
 					int cutOffset = truncateOffset;
@@ -1242,7 +1233,6 @@
 					log(e);
 				}
 			}
-			return Status.OK_STATUS;
 		}
 	}
 
diff --git a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/StreamDecoder.java b/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/StreamDecoder.java
deleted file mode 100644
index e334aa2..0000000
--- a/org.eclipse.ui.console/src/org/eclipse/ui/internal/console/StreamDecoder.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Andreas Loth and others.
- *
- * This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License 2.0
- * which accompanies this distribution, and is available at
- * https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- *
- * Contributors:
- *     Andreas Loth - initial API and implementation
- *******************************************************************************/
-
-package org.eclipse.ui.internal.console;
-
-import java.nio.charset.Charset;
-
-
-/**
- * @deprecated class was moved to
- *             {@link org.eclipse.debug.internal.core.StreamDecoder}
- */
-@Deprecated
-public class StreamDecoder extends org.eclipse.debug.internal.core.StreamDecoder {
-
-	public StreamDecoder(Charset charset) {
-		super(charset);
-	}
-}