Skip to content

Commit 07916c1

Browse files
committed
Optimize system print streams when processing ANSI sequences
1 parent f6e8a9a commit 07916c1

File tree

3 files changed

+357
-4
lines changed

3 files changed

+357
-4
lines changed

jansi/src/main/java/org/fusesource/jansi/AnsiConsole.java

+60-4
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,20 @@
1515
*/
1616
package org.fusesource.jansi;
1717

18-
import static org.fusesource.jansi.internal.CLibrary.STDERR_FILENO;
19-
import static org.fusesource.jansi.internal.CLibrary.STDOUT_FILENO;
20-
import static org.fusesource.jansi.internal.CLibrary.isatty;
21-
18+
import java.io.BufferedOutputStream;
2219
import java.io.FilterOutputStream;
2320
import java.io.IOException;
2421
import java.io.OutputStream;
22+
import java.io.OutputStreamWriter;
2523
import java.io.PrintStream;
24+
import java.lang.reflect.Field;
25+
import java.nio.charset.Charset;
2626
import java.util.Locale;
2727

28+
import static org.fusesource.jansi.internal.CLibrary.STDERR_FILENO;
29+
import static org.fusesource.jansi.internal.CLibrary.STDOUT_FILENO;
30+
import static org.fusesource.jansi.internal.CLibrary.isatty;
31+
2832
/**
2933
* Provides consistent access to an ANSI aware console PrintStream or an ANSI codes stripping PrintStream
3034
* if not on a terminal (see
@@ -194,7 +198,16 @@ public void close() throws IOException {
194198
* @since 1.17
195199
*/
196200
public static PrintStream wrapPrintStream(final PrintStream ps, int fileno) {
201+
PrintStream result = doWrapPrintStream(ps, fileno);
202+
if (result != ps) {
203+
if (!Boolean.getBoolean("jansi.no-optimize")) {
204+
result = optimize(ps, result);
205+
}
206+
}
207+
return result;
208+
}
197209

210+
private static PrintStream doWrapPrintStream(final PrintStream ps, int fileno) {
198211
// If the jansi.passthrough property is set, then don't interpret
199212
// any of the ansi sequences.
200213
if (Boolean.getBoolean("jansi.passthrough")) {
@@ -256,6 +269,49 @@ public void close() {
256269
};
257270
}
258271

272+
/**
273+
* Optimize the wrapped print stream for improved performances.
274+
*
275+
* Instead of trying to filter on the PrintStream level, which is slow
276+
* because of the need for synchronization, we extract the underlying
277+
* OutputStream, wrap it for ansi sequences processing, and recreate
278+
* a buffering PrintStream above. The benefit is that the ansi sequences
279+
* will be processed in batches without having to deal with encoding issues.
280+
*/
281+
private static PrintStream optimize(PrintStream original, PrintStream wrapped) {
282+
try {
283+
OutputStream out = original;
284+
while (out instanceof FilterOutputStream) {
285+
out = field(FilterOutputStream.class, out, "out");
286+
}
287+
if (wrapped instanceof AnsiPrintStream) {
288+
AnsiProcessor ap = field(AnsiPrintStream.class, wrapped, "ap");
289+
out = new AnsiNoSyncOutputStream(new BufferedNoSyncOutputStream(out), ap);
290+
}
291+
// grab charset
292+
OutputStreamWriter charOut = field(PrintStream.class, original, "charOut");
293+
Object se = field(OutputStreamWriter.class, charOut, "se");
294+
Charset cs = field(se.getClass(), se, "cs");
295+
// create print stream
296+
wrapped = new PrintStream(new BufferedOutputStream(out), true, cs.name()) {
297+
@Override
298+
public void close() {
299+
write(AnsiNoSyncOutputStream.RESET_CODE, 0, AnsiNoSyncOutputStream.RESET_CODE.length);
300+
super.close();
301+
}
302+
};
303+
} catch (Exception e) {
304+
// ignore
305+
}
306+
return wrapped;
307+
}
308+
309+
private static <T, O> T field(Class<O> oClass, Object obj, String name) throws Exception {
310+
Field f = oClass.getDeclaredField(name);
311+
f.setAccessible(true);
312+
return (T) f.get(obj);
313+
}
314+
259315
/**
260316
* If the standard out natively supports ANSI escape codes, then this just
261317
* returns System.out, otherwise it will provide an ANSI aware PrintStream
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
/*
2+
* Copyright (C) 2009-2020 the original author(s).
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.fusesource.jansi;
17+
18+
import java.io.FilterOutputStream;
19+
import java.io.IOException;
20+
import java.io.OutputStream;
21+
import java.util.ArrayList;
22+
23+
/**
24+
* A ANSI print stream extracts ANSI escape codes written to
25+
* an output stream and calls corresponding <code>AnsiProcessor.process*</code> methods.
26+
* This particular class is not synchronized for improved performances.
27+
*
28+
* <p>For more information about ANSI escape codes, see
29+
* <a href="http://en.wikipedia.org/wiki/ANSI_escape_code">Wikipedia article</a>
30+
*
31+
* @author Guillaume Nodet
32+
* @since 1.0
33+
* @see AnsiProcessor
34+
*/
35+
public class AnsiNoSyncOutputStream extends FilterOutputStream {
36+
37+
public static final byte[] RESET_CODE = "\033[0m".getBytes();
38+
39+
private static final int LOOKING_FOR_FIRST_ESC_CHAR = 0;
40+
private static final int LOOKING_FOR_SECOND_ESC_CHAR = 1;
41+
private static final int LOOKING_FOR_NEXT_ARG = 2;
42+
private static final int LOOKING_FOR_STR_ARG_END = 3;
43+
private static final int LOOKING_FOR_INT_ARG_END = 4;
44+
private static final int LOOKING_FOR_OSC_COMMAND = 5;
45+
private static final int LOOKING_FOR_OSC_COMMAND_END = 6;
46+
private static final int LOOKING_FOR_OSC_PARAM = 7;
47+
private static final int LOOKING_FOR_ST = 8;
48+
private static final int LOOKING_FOR_CHARSET = 9;
49+
50+
private static final int FIRST_ESC_CHAR = 27;
51+
private static final int SECOND_ESC_CHAR = '[';
52+
private static final int SECOND_OSC_CHAR = ']';
53+
private static final int BEL = 7;
54+
private static final int SECOND_ST_CHAR = '\\';
55+
private static final int SECOND_CHARSET0_CHAR = '(';
56+
private static final int SECOND_CHARSET1_CHAR = ')';
57+
58+
private final AnsiProcessor ap;
59+
private final static int MAX_ESCAPE_SEQUENCE_LENGTH = 100;
60+
private final byte[] buffer = new byte[MAX_ESCAPE_SEQUENCE_LENGTH];
61+
private int pos = 0;
62+
private int startOfValue;
63+
private final ArrayList<Object> options = new ArrayList<Object>();
64+
private int state = LOOKING_FOR_FIRST_ESC_CHAR;
65+
66+
public AnsiNoSyncOutputStream(OutputStream os, AnsiProcessor ap) {
67+
super(os);
68+
this.ap = ap;
69+
}
70+
71+
/**
72+
* {@inheritDoc}
73+
*/
74+
@Override
75+
public void write(int data) throws IOException {
76+
switch (state) {
77+
case LOOKING_FOR_FIRST_ESC_CHAR:
78+
if (data == FIRST_ESC_CHAR) {
79+
buffer[pos++] = (byte) data;
80+
state = LOOKING_FOR_SECOND_ESC_CHAR;
81+
} else {
82+
out.write(data);
83+
}
84+
break;
85+
86+
case LOOKING_FOR_SECOND_ESC_CHAR:
87+
buffer[pos++] = (byte) data;
88+
if (data == SECOND_ESC_CHAR) {
89+
state = LOOKING_FOR_NEXT_ARG;
90+
} else if (data == SECOND_OSC_CHAR) {
91+
state = LOOKING_FOR_OSC_COMMAND;
92+
} else if (data == SECOND_CHARSET0_CHAR) {
93+
options.add(0);
94+
state = LOOKING_FOR_CHARSET;
95+
} else if (data == SECOND_CHARSET1_CHAR) {
96+
options.add(1);
97+
state = LOOKING_FOR_CHARSET;
98+
} else {
99+
reset(false);
100+
}
101+
break;
102+
103+
case LOOKING_FOR_NEXT_ARG:
104+
buffer[pos++] = (byte) data;
105+
if ('"' == data) {
106+
startOfValue = pos - 1;
107+
state = LOOKING_FOR_STR_ARG_END;
108+
} else if ('0' <= data && data <= '9') {
109+
startOfValue = pos - 1;
110+
state = LOOKING_FOR_INT_ARG_END;
111+
} else if (';' == data) {
112+
options.add(null);
113+
} else if ('?' == data) {
114+
options.add('?');
115+
} else if ('=' == data) {
116+
options.add('=');
117+
} else {
118+
reset(ap.processEscapeCommand(options, data));
119+
}
120+
break;
121+
default:
122+
break;
123+
124+
case LOOKING_FOR_INT_ARG_END:
125+
buffer[pos++] = (byte) data;
126+
if (!('0' <= data && data <= '9')) {
127+
String strValue = new String(buffer, startOfValue, (pos - 1) - startOfValue);
128+
Integer value = Integer.valueOf(strValue);
129+
options.add(value);
130+
if (data == ';') {
131+
state = LOOKING_FOR_NEXT_ARG;
132+
} else {
133+
reset(ap.processEscapeCommand(options, data));
134+
}
135+
}
136+
break;
137+
138+
case LOOKING_FOR_STR_ARG_END:
139+
buffer[pos++] = (byte) data;
140+
if ('"' != data) {
141+
String value = new String(buffer, startOfValue, (pos - 1) - startOfValue);
142+
options.add(value);
143+
if (data == ';') {
144+
state = LOOKING_FOR_NEXT_ARG;
145+
} else {
146+
reset(ap.processEscapeCommand(options, data));
147+
}
148+
}
149+
break;
150+
151+
case LOOKING_FOR_OSC_COMMAND:
152+
buffer[pos++] = (byte) data;
153+
if ('0' <= data && data <= '9') {
154+
startOfValue = pos - 1;
155+
state = LOOKING_FOR_OSC_COMMAND_END;
156+
} else {
157+
reset(false);
158+
}
159+
break;
160+
161+
case LOOKING_FOR_OSC_COMMAND_END:
162+
buffer[pos++] = (byte) data;
163+
if (';' == data) {
164+
String strValue = new String(buffer, startOfValue, (pos - 1) - startOfValue);
165+
Integer value = Integer.valueOf(strValue);
166+
options.add(value);
167+
startOfValue = pos;
168+
state = LOOKING_FOR_OSC_PARAM;
169+
} else if ('0' <= data && data <= '9') {
170+
// already pushed digit to buffer, just keep looking
171+
} else {
172+
// oops, did not expect this
173+
reset(false);
174+
}
175+
break;
176+
177+
case LOOKING_FOR_OSC_PARAM:
178+
buffer[pos++] = (byte) data;
179+
if (BEL == data) {
180+
String value = new String(buffer, startOfValue, (pos - 1) - startOfValue);
181+
options.add(value);
182+
reset(ap.processOperatingSystemCommand(options));
183+
} else if (FIRST_ESC_CHAR == data) {
184+
state = LOOKING_FOR_ST;
185+
} else {
186+
// just keep looking while adding text
187+
}
188+
break;
189+
190+
case LOOKING_FOR_ST:
191+
buffer[pos++] = (byte) data;
192+
if (SECOND_ST_CHAR == data) {
193+
String value = new String(buffer, startOfValue, (pos - 2) - startOfValue);
194+
options.add(value);
195+
reset(ap.processOperatingSystemCommand(options));
196+
} else {
197+
state = LOOKING_FOR_OSC_PARAM;
198+
}
199+
break;
200+
201+
case LOOKING_FOR_CHARSET:
202+
options.add((char) data);
203+
reset(ap.processCharsetSelect(options));
204+
break;
205+
}
206+
207+
// Is it just too long?
208+
if (pos >= buffer.length) {
209+
reset(false);
210+
}
211+
}
212+
213+
/**
214+
* Resets all state to continue with regular parsing
215+
* @param skipBuffer if current buffer should be skipped or written to out
216+
* @throws IOException
217+
*/
218+
private void reset(boolean skipBuffer) throws IOException {
219+
if (!skipBuffer) {
220+
out.write(buffer, 0, pos);
221+
}
222+
pos = 0;
223+
startOfValue = 0;
224+
options.clear();
225+
state = LOOKING_FOR_FIRST_ESC_CHAR;
226+
}
227+
228+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright (C) 2009-2020 the original author(s).
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.fusesource.jansi;
17+
18+
import java.io.FilterOutputStream;
19+
import java.io.IOException;
20+
import java.io.OutputStream;
21+
22+
/**
23+
* A simple buffering output stream with no synchronization.
24+
*/
25+
public class BufferedNoSyncOutputStream extends FilterOutputStream {
26+
27+
protected final byte buf[] = new byte[8192];
28+
protected int count;
29+
30+
public BufferedNoSyncOutputStream(OutputStream out) {
31+
super(out);
32+
}
33+
34+
@Override
35+
public void write(int b) throws IOException {
36+
if (count >= buf.length) {
37+
flushBuffer();
38+
}
39+
buf[count++] = (byte) b;
40+
}
41+
42+
@Override
43+
public void write(byte b[], int off, int len) throws IOException {
44+
if (len >= buf.length) {
45+
flushBuffer();
46+
out.write(b, off, len);
47+
return;
48+
}
49+
if (len > buf.length - count) {
50+
flushBuffer();
51+
}
52+
System.arraycopy(b, off, buf, count, len);
53+
count += len;
54+
}
55+
56+
private void flushBuffer() throws IOException {
57+
if (count > 0) {
58+
out.write(buf, 0, count);
59+
count = 0;
60+
}
61+
}
62+
63+
@Override
64+
public void flush() throws IOException {
65+
flushBuffer();
66+
out.flush();
67+
}
68+
69+
}

0 commit comments

Comments
 (0)