From 07916c1af9769ded99ce6770db1348db4e062921 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 23 Oct 2020 00:25:15 +0200 Subject: [PATCH] Optimize system print streams when processing ANSI sequences --- .../org/fusesource/jansi/AnsiConsole.java | 64 ++++- .../jansi/AnsiNoSyncOutputStream.java | 228 ++++++++++++++++++ .../jansi/BufferedNoSyncOutputStream.java | 69 ++++++ 3 files changed, 357 insertions(+), 4 deletions(-) create mode 100644 jansi/src/main/java/org/fusesource/jansi/AnsiNoSyncOutputStream.java create mode 100644 jansi/src/main/java/org/fusesource/jansi/BufferedNoSyncOutputStream.java diff --git a/jansi/src/main/java/org/fusesource/jansi/AnsiConsole.java b/jansi/src/main/java/org/fusesource/jansi/AnsiConsole.java index 628cad3a..0721ddff 100644 --- a/jansi/src/main/java/org/fusesource/jansi/AnsiConsole.java +++ b/jansi/src/main/java/org/fusesource/jansi/AnsiConsole.java @@ -15,16 +15,20 @@ */ package org.fusesource.jansi; -import static org.fusesource.jansi.internal.CLibrary.STDERR_FILENO; -import static org.fusesource.jansi.internal.CLibrary.STDOUT_FILENO; -import static org.fusesource.jansi.internal.CLibrary.isatty; - +import java.io.BufferedOutputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.io.PrintStream; +import java.lang.reflect.Field; +import java.nio.charset.Charset; import java.util.Locale; +import static org.fusesource.jansi.internal.CLibrary.STDERR_FILENO; +import static org.fusesource.jansi.internal.CLibrary.STDOUT_FILENO; +import static org.fusesource.jansi.internal.CLibrary.isatty; + /** * Provides consistent access to an ANSI aware console PrintStream or an ANSI codes stripping PrintStream * if not on a terminal (see @@ -194,7 +198,16 @@ public void close() throws IOException { * @since 1.17 */ public static PrintStream wrapPrintStream(final PrintStream ps, int fileno) { + PrintStream result = doWrapPrintStream(ps, fileno); + if (result != ps) { + if (!Boolean.getBoolean("jansi.no-optimize")) { + result = optimize(ps, result); + } + } + return result; + } + private static PrintStream doWrapPrintStream(final PrintStream ps, int fileno) { // If the jansi.passthrough property is set, then don't interpret // any of the ansi sequences. if (Boolean.getBoolean("jansi.passthrough")) { @@ -256,6 +269,49 @@ public void close() { }; } + /** + * Optimize the wrapped print stream for improved performances. + * + * Instead of trying to filter on the PrintStream level, which is slow + * because of the need for synchronization, we extract the underlying + * OutputStream, wrap it for ansi sequences processing, and recreate + * a buffering PrintStream above. The benefit is that the ansi sequences + * will be processed in batches without having to deal with encoding issues. + */ + private static PrintStream optimize(PrintStream original, PrintStream wrapped) { + try { + OutputStream out = original; + while (out instanceof FilterOutputStream) { + out = field(FilterOutputStream.class, out, "out"); + } + if (wrapped instanceof AnsiPrintStream) { + AnsiProcessor ap = field(AnsiPrintStream.class, wrapped, "ap"); + out = new AnsiNoSyncOutputStream(new BufferedNoSyncOutputStream(out), ap); + } + // grab charset + OutputStreamWriter charOut = field(PrintStream.class, original, "charOut"); + Object se = field(OutputStreamWriter.class, charOut, "se"); + Charset cs = field(se.getClass(), se, "cs"); + // create print stream + wrapped = new PrintStream(new BufferedOutputStream(out), true, cs.name()) { + @Override + public void close() { + write(AnsiNoSyncOutputStream.RESET_CODE, 0, AnsiNoSyncOutputStream.RESET_CODE.length); + super.close(); + } + }; + } catch (Exception e) { + // ignore + } + return wrapped; + } + + private static T field(Class oClass, Object obj, String name) throws Exception { + Field f = oClass.getDeclaredField(name); + f.setAccessible(true); + return (T) f.get(obj); + } + /** * If the standard out natively supports ANSI escape codes, then this just * returns System.out, otherwise it will provide an ANSI aware PrintStream diff --git a/jansi/src/main/java/org/fusesource/jansi/AnsiNoSyncOutputStream.java b/jansi/src/main/java/org/fusesource/jansi/AnsiNoSyncOutputStream.java new file mode 100644 index 00000000..de80ae2e --- /dev/null +++ b/jansi/src/main/java/org/fusesource/jansi/AnsiNoSyncOutputStream.java @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2009-2020 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; + +/** + * A ANSI print stream extracts ANSI escape codes written to + * an output stream and calls corresponding AnsiProcessor.process* methods. + * This particular class is not synchronized for improved performances. + * + *

For more information about ANSI escape codes, see + * Wikipedia article + * + * @author Guillaume Nodet + * @since 1.0 + * @see AnsiProcessor + */ +public class AnsiNoSyncOutputStream extends FilterOutputStream { + + public static final byte[] RESET_CODE = "\033[0m".getBytes(); + + private static final int LOOKING_FOR_FIRST_ESC_CHAR = 0; + private static final int LOOKING_FOR_SECOND_ESC_CHAR = 1; + private static final int LOOKING_FOR_NEXT_ARG = 2; + private static final int LOOKING_FOR_STR_ARG_END = 3; + private static final int LOOKING_FOR_INT_ARG_END = 4; + private static final int LOOKING_FOR_OSC_COMMAND = 5; + private static final int LOOKING_FOR_OSC_COMMAND_END = 6; + private static final int LOOKING_FOR_OSC_PARAM = 7; + private static final int LOOKING_FOR_ST = 8; + private static final int LOOKING_FOR_CHARSET = 9; + + private static final int FIRST_ESC_CHAR = 27; + private static final int SECOND_ESC_CHAR = '['; + private static final int SECOND_OSC_CHAR = ']'; + private static final int BEL = 7; + private static final int SECOND_ST_CHAR = '\\'; + private static final int SECOND_CHARSET0_CHAR = '('; + private static final int SECOND_CHARSET1_CHAR = ')'; + + private final AnsiProcessor ap; + private final static int MAX_ESCAPE_SEQUENCE_LENGTH = 100; + private final byte[] buffer = new byte[MAX_ESCAPE_SEQUENCE_LENGTH]; + private int pos = 0; + private int startOfValue; + private final ArrayList options = new ArrayList(); + private int state = LOOKING_FOR_FIRST_ESC_CHAR; + + public AnsiNoSyncOutputStream(OutputStream os, AnsiProcessor ap) { + super(os); + this.ap = ap; + } + + /** + * {@inheritDoc} + */ + @Override + public void write(int data) throws IOException { + switch (state) { + case LOOKING_FOR_FIRST_ESC_CHAR: + if (data == FIRST_ESC_CHAR) { + buffer[pos++] = (byte) data; + state = LOOKING_FOR_SECOND_ESC_CHAR; + } else { + out.write(data); + } + break; + + case LOOKING_FOR_SECOND_ESC_CHAR: + buffer[pos++] = (byte) data; + if (data == SECOND_ESC_CHAR) { + state = LOOKING_FOR_NEXT_ARG; + } else if (data == SECOND_OSC_CHAR) { + state = LOOKING_FOR_OSC_COMMAND; + } else if (data == SECOND_CHARSET0_CHAR) { + options.add(0); + state = LOOKING_FOR_CHARSET; + } else if (data == SECOND_CHARSET1_CHAR) { + options.add(1); + state = LOOKING_FOR_CHARSET; + } else { + reset(false); + } + break; + + case LOOKING_FOR_NEXT_ARG: + buffer[pos++] = (byte) data; + if ('"' == data) { + startOfValue = pos - 1; + state = LOOKING_FOR_STR_ARG_END; + } else if ('0' <= data && data <= '9') { + startOfValue = pos - 1; + state = LOOKING_FOR_INT_ARG_END; + } else if (';' == data) { + options.add(null); + } else if ('?' == data) { + options.add('?'); + } else if ('=' == data) { + options.add('='); + } else { + reset(ap.processEscapeCommand(options, data)); + } + break; + default: + break; + + case LOOKING_FOR_INT_ARG_END: + buffer[pos++] = (byte) data; + if (!('0' <= data && data <= '9')) { + String strValue = new String(buffer, startOfValue, (pos - 1) - startOfValue); + Integer value = Integer.valueOf(strValue); + options.add(value); + if (data == ';') { + state = LOOKING_FOR_NEXT_ARG; + } else { + reset(ap.processEscapeCommand(options, data)); + } + } + break; + + case LOOKING_FOR_STR_ARG_END: + buffer[pos++] = (byte) data; + if ('"' != data) { + String value = new String(buffer, startOfValue, (pos - 1) - startOfValue); + options.add(value); + if (data == ';') { + state = LOOKING_FOR_NEXT_ARG; + } else { + reset(ap.processEscapeCommand(options, data)); + } + } + break; + + case LOOKING_FOR_OSC_COMMAND: + buffer[pos++] = (byte) data; + if ('0' <= data && data <= '9') { + startOfValue = pos - 1; + state = LOOKING_FOR_OSC_COMMAND_END; + } else { + reset(false); + } + break; + + case LOOKING_FOR_OSC_COMMAND_END: + buffer[pos++] = (byte) data; + if (';' == data) { + String strValue = new String(buffer, startOfValue, (pos - 1) - startOfValue); + Integer value = Integer.valueOf(strValue); + options.add(value); + startOfValue = pos; + state = LOOKING_FOR_OSC_PARAM; + } else if ('0' <= data && data <= '9') { + // already pushed digit to buffer, just keep looking + } else { + // oops, did not expect this + reset(false); + } + break; + + case LOOKING_FOR_OSC_PARAM: + buffer[pos++] = (byte) data; + if (BEL == data) { + String value = new String(buffer, startOfValue, (pos - 1) - startOfValue); + options.add(value); + reset(ap.processOperatingSystemCommand(options)); + } else if (FIRST_ESC_CHAR == data) { + state = LOOKING_FOR_ST; + } else { + // just keep looking while adding text + } + break; + + case LOOKING_FOR_ST: + buffer[pos++] = (byte) data; + if (SECOND_ST_CHAR == data) { + String value = new String(buffer, startOfValue, (pos - 2) - startOfValue); + options.add(value); + reset(ap.processOperatingSystemCommand(options)); + } else { + state = LOOKING_FOR_OSC_PARAM; + } + break; + + case LOOKING_FOR_CHARSET: + options.add((char) data); + reset(ap.processCharsetSelect(options)); + break; + } + + // Is it just too long? + if (pos >= buffer.length) { + reset(false); + } + } + + /** + * Resets all state to continue with regular parsing + * @param skipBuffer if current buffer should be skipped or written to out + * @throws IOException + */ + private void reset(boolean skipBuffer) throws IOException { + if (!skipBuffer) { + out.write(buffer, 0, pos); + } + pos = 0; + startOfValue = 0; + options.clear(); + state = LOOKING_FOR_FIRST_ESC_CHAR; + } + +} diff --git a/jansi/src/main/java/org/fusesource/jansi/BufferedNoSyncOutputStream.java b/jansi/src/main/java/org/fusesource/jansi/BufferedNoSyncOutputStream.java new file mode 100644 index 00000000..90999535 --- /dev/null +++ b/jansi/src/main/java/org/fusesource/jansi/BufferedNoSyncOutputStream.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2009-2020 the original author(s). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.fusesource.jansi; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * A simple buffering output stream with no synchronization. + */ +public class BufferedNoSyncOutputStream extends FilterOutputStream { + + protected final byte buf[] = new byte[8192]; + protected int count; + + public BufferedNoSyncOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int b) throws IOException { + if (count >= buf.length) { + flushBuffer(); + } + buf[count++] = (byte) b; + } + + @Override + public void write(byte b[], int off, int len) throws IOException { + if (len >= buf.length) { + flushBuffer(); + out.write(b, off, len); + return; + } + if (len > buf.length - count) { + flushBuffer(); + } + System.arraycopy(b, off, buf, count, len); + count += len; + } + + private void flushBuffer() throws IOException { + if (count > 0) { + out.write(buf, 0, count); + count = 0; + } + } + + @Override + public void flush() throws IOException { + flushBuffer(); + out.flush(); + } + +}