Skip to content

Commit 691ca00

Browse files
authored
Detect large strings, and don't grow the buffer when processing them (#397)
Currently, large strings will grow the internal buffer. Instead, have a largeStringMax limit, split strings greater than this limit into segments and write those flushing [but not growing] when needed.
1 parent a0cbc87 commit 691ca00

File tree

2 files changed

+86
-11
lines changed

2 files changed

+86
-11
lines changed

json-core/src/main/java/io/avaje/json/stream/core/JGenerator.java

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class JGenerator implements JsonGenerator {
5656
private static final int OP_END = 4;
5757

5858
private final Grisu3.FastDtoaBuilder doubleBuilder = new Grisu3.FastDtoaBuilder();
59+
private final int largeStringMax;
5960
private byte[] buffer;
6061
private JsonOutput target;
6162
private int lastOp;
@@ -77,6 +78,8 @@ class JGenerator implements JsonGenerator {
7778

7879
JGenerator(final byte[] buffer) {
7980
this.buffer = buffer;
81+
// each char can take up to 6 bytes when Unicode escaped, round down 1/8 number of chars
82+
this.largeStringMax = buffer.length >> 3;
8083
}
8184

8285
@Override
@@ -132,26 +135,50 @@ private void writeByte(final byte value) {
132135

133136
private void writeString(final String value) {
134137
final int len = value.length();
138+
if (len > largeStringMax) {
139+
writeLargeString(value);
140+
return;
141+
}
135142
if (position + (len << 2) + (len << 1) + 2 >= buffer.length) {
136143
enlargeOrFlush(position, (len << 2) + (len << 1) + 2);
137144
}
138-
final byte[] _result = buffer;
139-
_result[position] = QUOTE;
140-
int cur = position + 1;
141-
for (int i = 0; i < len; i++) {
145+
buffer[position++] = QUOTE;
146+
writeStringSegment(value, 0, len);
147+
buffer[position++] = QUOTE;
148+
}
149+
150+
private void writeStringSegment(String value, int i, int end) {
151+
int cur = position;
152+
for (;i < end; i++) {
142153
final char c = value.charAt(i);
143154
if (c > 31 && c != '"' && c != '\\' && c < 126) {
144-
_result[cur++] = (byte) c;
155+
buffer[cur++] = (byte) c;
145156
} else {
146-
writeQuotedString(value, i, cur, len);
157+
writeStringEscape(value, i, cur, end);
147158
return;
148159
}
149160
}
150-
_result[cur] = QUOTE;
151-
position = cur + 1;
161+
position = cur;
162+
}
163+
164+
/** Break a large string into segments and flush when necessary */
165+
private void writeLargeString(String text) {
166+
writeByte(QUOTE);
167+
int left = text.length();
168+
int offset = 0;
169+
while (left > 0) {
170+
final int len = Math.min(largeStringMax, left);
171+
if (position + (len << 2) + (len << 1) + 2 >= buffer.length) {
172+
enlargeOrFlush(position, 0); // just flush
173+
}
174+
writeStringSegment(text, offset, offset + len);
175+
offset += len;
176+
left -= len;
177+
}
178+
writeByte(QUOTE);
152179
}
153180

154-
private void writeQuotedString(final String str, int i, int cur, final int len) {
181+
private void writeStringEscape(final String str, int i, int cur, final int len) {
155182
final byte[] _result = this.buffer;
156183
for (; i < len; i++) {
157184
final char c = str.charAt(i);
@@ -327,8 +354,7 @@ private void writeQuotedString(final String str, int i, int cur, final int len)
327354
}
328355
}
329356
}
330-
_result[cur] = QUOTE;
331-
position = cur + 1;
357+
position = cur;
332358
}
333359

334360
@SuppressWarnings("deprecation")

json-core/src/test/java/io/avaje/json/stream/core/JsonWriterTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,55 @@ void using_names() {
141141

142142
String asJson = os.toString();
143143
assertThat(asJson).isEqualTo("[{\"one\":\"hello\",\"size\":43},{\"one\":\"another\",\"active\":true,\"flags\":[42,43]}]");
144+
}
145+
146+
@Test
147+
void largeString() {
148+
ByteArrayOutputStream os = new ByteArrayOutputStream();
149+
JGenerator dJsonWriter = new JGenerator(200);
150+
dJsonWriter.prepare(JsonOutput.ofStream(os));
151+
152+
JsonWriteAdapter fw = new JsonWriteAdapter(dJsonWriter, HybridBufferRecycler.shared(), true, true);
153+
154+
String largeValue = "_123456789_123456789_123456789_123456789_123456789".repeat(11);
155+
156+
fw.beginObject();
157+
fw.name("key");
158+
fw.value(largeValue);
159+
fw.endObject();
160+
fw.close();
161+
162+
String jsonResult = new String(os.toByteArray(), 0, os.toByteArray().length);
163+
assertThat(jsonResult).isEqualTo("{\"key\":\"" + largeValue + "\"}");
164+
165+
byte[] effectiveBufferSize = dJsonWriter.ensureCapacity(0);
166+
assertThat(effectiveBufferSize.length)
167+
.describedAs("internal buffer should not grow")
168+
.isEqualTo(200);
169+
}
170+
171+
@Test
172+
void largeStringUnicode() {
173+
ByteArrayOutputStream os = new ByteArrayOutputStream();
174+
JGenerator dJsonWriter = new JGenerator(100);
175+
dJsonWriter.prepare(JsonOutput.ofStream(os));
176+
177+
JsonWriteAdapter fw = new JsonWriteAdapter(dJsonWriter, HybridBufferRecycler.shared(), true, true);
178+
179+
String largeValue = "_12£45Ã789ǣ123456789_123456789Ŕ123456789_123456789".repeat(11);
180+
181+
fw.beginObject();
182+
fw.name("key");
183+
fw.value(largeValue);
184+
fw.endObject();
185+
fw.close();
186+
187+
String jsonResult = new String(os.toByteArray(), 0, os.toByteArray().length);
188+
assertThat(jsonResult).isEqualTo("{\"key\":\"" + largeValue + "\"}");
144189

190+
byte[] effectiveBufferSize = dJsonWriter.ensureCapacity(0);
191+
assertThat(effectiveBufferSize.length)
192+
.describedAs("internal buffer should not grow")
193+
.isEqualTo(100);
145194
}
146195
}

0 commit comments

Comments
 (0)