JOHNZON-367 Optimize buffering when new Buffered interface is supported
diff --git a/johnzon-core/src/main/java/org/apache/johnzon/core/Buffered.java b/johnzon-core/src/main/java/org/apache/johnzon/core/Buffered.java
new file mode 100644
index 0000000..83f2897
--- /dev/null
+++ b/johnzon-core/src/main/java/org/apache/johnzon/core/Buffered.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.apache.johnzon.core;
+
+/**
+ * A <tt>Buffered</tt> is a source or destination of data that is buffered
+ * before writing or reading. The bufferSize method allows all participants
+ * in the underlying stream to align on this buffer size for optimization.
+ *
+ * This interface is designed in the spirit of {@code java.io.Flushable} and
+ * {@code java.io.Closeable}
+ *
+ * @since 1.2.17
+ */
+public interface Buffered {
+
+ /**
+ * The buffer size used by this stream while reading input or before writing
+ * output to the underlying stream.
+ */
+ int bufferSize();
+}
diff --git a/johnzon-core/src/main/java/org/apache/johnzon/core/JsonGeneratorFactoryImpl.java b/johnzon-core/src/main/java/org/apache/johnzon/core/JsonGeneratorFactoryImpl.java
index 8d1f6ec..87e23a7 100644
--- a/johnzon-core/src/main/java/org/apache/johnzon/core/JsonGeneratorFactoryImpl.java
+++ b/johnzon-core/src/main/java/org/apache/johnzon/core/JsonGeneratorFactoryImpl.java
@@ -20,6 +20,7 @@
import static java.util.Arrays.asList;
+import java.io.Flushable;
import java.io.OutputStream;
import java.io.Writer;
import java.nio.charset.Charset;
@@ -39,7 +40,8 @@
);
//key caching currently disabled
private final boolean pretty;
- private final BufferStrategy.BufferProvider<char[]> bufferProvider;
+ private final Buffer buffer;
+ private volatile Buffer customBuffer;
public JsonGeneratorFactoryImpl(final Map<String, ?> config) {
@@ -52,26 +54,54 @@
throw new IllegalArgumentException("buffer length must be greater than zero");
}
- this.bufferProvider = getBufferProvider().newCharProvider(bufferSize);
+ this.buffer = new Buffer(getBufferProvider().newCharProvider(bufferSize), bufferSize);
}
@Override
public JsonGenerator createGenerator(final Writer writer) {
- return new JsonGeneratorImpl(writer, bufferProvider, pretty);
+ return new JsonGeneratorImpl(writer, getBufferProvider(writer), pretty);
}
@Override
public JsonGenerator createGenerator(final OutputStream out) {
- return new JsonGeneratorImpl(out, bufferProvider, pretty);
+ return new JsonGeneratorImpl(out, getBufferProvider(out), pretty);
}
@Override
public JsonGenerator createGenerator(final OutputStream out, final Charset charset) {
- return new JsonGeneratorImpl(out,charset, bufferProvider, pretty);
+ return new JsonGeneratorImpl(out,charset, getBufferProvider(out), pretty);
+ }
+
+ private BufferStrategy.BufferProvider<char[]> getBufferProvider(final Flushable flushable) {
+ if (!(flushable instanceof Buffered)) {
+ return buffer.provider;
+ }
+
+ final int bufferSize = Buffered.class.cast(flushable).bufferSize();
+
+ if (customBuffer != null && customBuffer.size == bufferSize) {
+ return customBuffer.provider;
+ }
+
+ synchronized (this) {
+ customBuffer = new Buffer(getBufferProvider().newCharProvider(bufferSize), bufferSize);
+ return customBuffer.provider;
+ }
}
@Override
public Map<String, ?> getConfigInUse() {
return Collections.unmodifiableMap(internalConfig);
}
+
+ private static final class Buffer {
+ private final BufferStrategy.BufferProvider<char[]> provider;
+ private final int size;
+
+ private Buffer(final BufferStrategy.BufferProvider<char[]>
+ bufferProvider, final int bufferSize) {
+ this.provider = bufferProvider;
+ this.size = bufferSize;
+ }
+ }
}
diff --git a/johnzon-core/src/main/java/org/apache/johnzon/core/Snippet.java b/johnzon-core/src/main/java/org/apache/johnzon/core/Snippet.java
index 9903331..ba16ed9 100644
--- a/johnzon-core/src/main/java/org/apache/johnzon/core/Snippet.java
+++ b/johnzon-core/src/main/java/org/apache/johnzon/core/Snippet.java
@@ -24,9 +24,11 @@
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
-import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
+import java.util.function.Supplier;
import static org.apache.johnzon.core.JsonGeneratorFactoryImpl.GENERATOR_BUFFER_LENGTH;
@@ -123,11 +125,13 @@
*/
class Buffer implements Closeable {
private final JsonGenerator generator;
- private final SnippetOutputStream snippet;
+ private final SnippetWriter snippet;
+ private Runnable flush;
private Buffer() {
- this.snippet = new SnippetOutputStream(max);
+ this.snippet = new SnippetWriter(max);
this.generator = generatorFactory.createGenerator(snippet);
+ this.flush = generator::flush;
}
private void write(final JsonValue value) {
@@ -215,7 +219,7 @@
}
private boolean terminate() {
- generator.flush();
+ flush.run();
return snippet.terminate();
}
@@ -228,172 +232,181 @@
public void close() {
generator.close();
}
- }
-
- /**
- * Specialized OutputStream with three internal states:
- * Writing, Completed, Truncated.
- *
- * When there is still space left for more json, the
- * state will be Writing
- *
- * If the last write brought is exactly to the end of
- * the max length, the state will be Completed.
- *
- * If the last write brought us over the max length, the
- * state will be Truncated.
- */
- static class SnippetOutputStream extends OutputStream {
-
- private final ByteArrayOutputStream buffer;
- private OutputStream mode;
-
- public SnippetOutputStream(final int max) {
- this.buffer = new ByteArrayOutputStream(Math.min(max, 8192));
- this.mode = new Writing(max, buffer);
- }
-
- public String get() {
- return buffer.toString();
- }
/**
- * Calling this method implies the need to continue
- * writing and a question on if that is ok.
+ * Specialized Writer with three internal states:
+ * Writing, Completed, Truncated.
*
- * It impacts internal state in the same way as
- * calling a write method.
+ * When there is still space left for more json, the
+ * state will be Writing
*
- * @return true if no more writes are possible
+ * If the last write brought is exactly to the end of
+ * the max length, the state will be Completed.
+ *
+ * If the last write brought us over the max length, the
+ * state will be Truncated.
*/
- public boolean terminate() {
- if (mode instanceof Truncated) {
- return true;
+ class SnippetWriter extends Writer implements Buffered {
+
+ private final ByteArrayOutputStream buffer;
+ private Mode mode;
+ private Supplier<Integer> bufferSize;
+
+ public SnippetWriter(final int max) {
+ final int size = Math.min(max, 8192);
+
+ this.buffer = new ByteArrayOutputStream(size);
+ this.mode = new Writing(max, new OutputStreamWriter(buffer));
+
+ /*
+ * The first time the buffer size is requested, disable flushing
+ * as we know our requested buffer size will be respected
+ */
+ this.bufferSize = () -> {
+ // disable flushing
+ flush = () -> {
+ };
+ // future calls can just return the size
+ bufferSize = () -> size;
+ return size;
+ };
}
- if (mode instanceof Completed) {
- mode = new Truncated();
- return true;
- }
-
- return false;
- }
-
- public boolean isTruncated() {
- return mode instanceof Truncated;
- }
-
- @Override
- public void write(final int b) throws IOException {
- mode.write(b);
- }
-
- @Override
- public void write(final byte[] b) throws IOException {
- mode.write(b);
- }
-
- @Override
- public void write(final byte[] b, final int off, final int len) throws IOException {
- mode.write(b, off, len);
- }
-
- @Override
- public void flush() throws IOException {
- mode.flush();
- }
-
- @Override
- public void close() throws IOException {
- mode.close();
- }
-
- class Writing extends OutputStream {
- private final int max;
- private int count;
- private final OutputStream out;
-
- public Writing(final int max, final OutputStream out) {
- this.max = max;
- this.out = out;
+ public String get() {
+ return buffer.toString();
}
@Override
- public void write(final int b) throws IOException {
- if (++count < max) {
- out.write(b);
- } else {
- maxReached(new Truncated());
+ public int bufferSize() {
+ return bufferSize.get();
+ }
+
+ /**
+ * Calling this method implies the need to continue
+ * writing and a question on if that is ok.
+ *
+ * It impacts internal state in the same way as
+ * calling a write method.
+ *
+ * @return true if no more writes are possible
+ */
+ public boolean terminate() {
+ if (mode instanceof Truncated) {
+ return true;
+ }
+
+ if (mode instanceof Completed) {
+ mode = new Truncated();
+ return true;
+ }
+
+ return false;
+ }
+
+ public boolean isTruncated() {
+ return mode instanceof Truncated;
+ }
+
+ @Override
+ public void write(final char[] cbuf, final int off, final int len) throws IOException {
+ mode.write(cbuf, off, len);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ mode.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ mode.close();
+ }
+
+ abstract class Mode extends Writer {
+ @Override
+ public void flush() throws IOException {
+ }
+
+ @Override
+ public void close() throws IOException {
}
}
- @Override
- public void write(final byte[] b) throws IOException {
- write(b, 0, b.length);
- }
+ class Writing extends Mode {
+ private final int max;
+ private int count;
+ private final Writer writer;
- @Override
- public void write(final byte[] b, final int off, final int len) throws IOException {
- final int remaining = max - count;
+ public Writing(final int max, final Writer writer) {
+ this.max = max;
+ this.writer = writer;
+ }
- if (remaining <= 0) {
+ @Override
+ public void write(final char[] cbuf, final int off, final int len) throws IOException {
+ final int remaining = max - count;
- maxReached(new Truncated());
+ if (remaining <= 0) {
- } else if (len == remaining) {
+ maxReached(new Truncated());
- count += len;
- out.write(b, off, remaining);
- maxReached(new Completed());
+ } else if (len == remaining) {
- } else if (len > remaining) {
+ count += len;
+ writer.write(cbuf, off, remaining);
+ maxReached(new Completed());
- count += len;
- out.write(b, off, remaining);
- maxReached(new Truncated());
+ } else if (len > remaining) {
- } else {
- count += len;
- out.write(b, off, len);
+ count += len;
+ writer.write(cbuf, off, remaining);
+ maxReached(new Truncated());
+
+ } else {
+ count += len;
+ writer.write(cbuf, off, len);
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ writer.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ writer.close();
+ }
+
+ private void maxReached(final Mode mode) throws IOException {
+ SnippetWriter.this.mode = mode;
+ writer.flush();
+ writer.close();
}
}
- private void maxReached(final OutputStream mode) throws IOException {
- SnippetOutputStream.this.mode = mode;
- out.flush();
- out.close();
- }
- }
+ /**
+ * Signifies the last write was fully written, but there is
+ * no more space for future writes.
+ */
+ class Completed extends Mode {
- /**
- * Signifies the last write was fully written, but there is
- * no more space for future writes.
- */
- class Completed extends OutputStream {
- @Override
- public void write(final int b) throws IOException {
- SnippetOutputStream.this.mode = new Truncated();
- }
-
- @Override
- public void write(final byte[] b, final int off, final int len) throws IOException {
- if (len > 0) {
- SnippetOutputStream.this.mode = new Truncated();
+ @Override
+ public void write(final char[] cbuf, final int off, final int len) throws IOException {
+ if (len > 0) {
+ SnippetWriter.this.mode = new Truncated();
+ }
}
}
- }
- /**
- * Signifies the last write was not completely written and there was
- * no more space for this or future writes.
- */
- static class Truncated extends OutputStream {
- @Override
- public void write(final int b) throws IOException {
- }
-
- @Override
- public void write(final byte[] b, final int off, final int len) throws IOException {
+ /**
+ * Signifies the last write was not completely written and there was
+ * no more space for this or future writes.
+ */
+ class Truncated extends Mode {
+ @Override
+ public void write(final char[] cbuf, final int off, final int len) throws IOException {
+ }
}
}
}