Optimize reading one of several expected values with Selection

This isn't yet public API.

This relies on an unreleased Okio API.

This has a significant impact on performance. I measured parsing performance
improve from 89k ops/sec to 140k ops/sec on one benchmark.
This commit is contained in:
jwilson
2016-04-17 20:15:07 -10:00
parent 40e323c805
commit 4925755ffa
7 changed files with 228 additions and 10 deletions

View File

@@ -218,7 +218,7 @@ final class BufferedSinkJsonWriter extends JsonWriter {
private void writeDeferredName() throws IOException { private void writeDeferredName() throws IOException {
if (deferredName != null) { if (deferredName != null) {
beforeName(); beforeName();
string(deferredName); string(sink, deferredName);
deferredName = null; deferredName = null;
} }
} }
@@ -232,7 +232,7 @@ final class BufferedSinkJsonWriter extends JsonWriter {
} }
writeDeferredName(); writeDeferredName();
beforeValue(); beforeValue();
string(value); string(sink, value);
pathIndices[stackSize - 1]++; pathIndices[stackSize - 1]++;
return this; return this;
} }
@@ -331,7 +331,11 @@ final class BufferedSinkJsonWriter extends JsonWriter {
stackSize = 0; stackSize = 0;
} }
private void string(String value) throws IOException { /**
* Writes {@code value} as a string literal to {@code sink}. This wraps the value in double quotes
* and escapes those characters that require it.
*/
static void string(BufferedSink sink, String value) throws IOException {
String[] replacements = REPLACEMENT_CHARS; String[] replacements = REPLACEMENT_CHARS;
sink.writeByte('"'); sink.writeByte('"');
int last = 0; int last = 0;

View File

@@ -552,6 +552,23 @@ final class BufferedSourceJsonReader extends JsonReader {
return result; return result;
} }
@Override int selectName(Selection selection) throws IOException {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
if (p != PEEKED_DOUBLE_QUOTED_NAME) {
return -1;
}
int result = source.select(selection.doubleQuoteSuffix);
if (result != -1) {
peeked = PEEKED_NONE;
pathNames[stackSize - 1] = selection.strings[result];
}
return result;
}
@Override public String nextString() throws IOException { @Override public String nextString() throws IOException {
int p = peeked; int p = peeked;
if (p == PEEKED_NONE) { if (p == PEEKED_NONE) {
@@ -579,6 +596,23 @@ final class BufferedSourceJsonReader extends JsonReader {
return result; return result;
} }
@Override int selectString(Selection selection) throws IOException {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
if (p != PEEKED_DOUBLE_QUOTED) {
return -1;
}
int result = source.select(selection.doubleQuoteSuffix);
if (result != -1) {
peeked = PEEKED_NONE;
pathIndices[stackSize - 1]++;
}
return result;
}
@Override public boolean nextBoolean() throws IOException { @Override public boolean nextBoolean() throws IOException {
int p = peeked; int p = peeked;
if (p == PEEKED_NONE) { if (p == PEEKED_NONE) {

View File

@@ -116,11 +116,14 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
private final ClassFactory<T> classFactory; private final ClassFactory<T> classFactory;
private final Map<String, FieldBinding<?>> fieldsMap; private final Map<String, FieldBinding<?>> fieldsMap;
private final FieldBinding<?>[] fieldsArray; private final FieldBinding<?>[] fieldsArray;
private final JsonReader.Selection selection;
ClassJsonAdapter(ClassFactory<T> classFactory, Map<String, FieldBinding<?>> fieldsMap) { ClassJsonAdapter(ClassFactory<T> classFactory, Map<String, FieldBinding<?>> fieldsMap) {
this.classFactory = classFactory; this.classFactory = classFactory;
this.fieldsMap = new LinkedHashMap<>(fieldsMap); this.fieldsMap = new LinkedHashMap<>(fieldsMap);
this.fieldsArray = fieldsMap.values().toArray(new FieldBinding[fieldsMap.size()]); this.fieldsArray = fieldsMap.values().toArray(new FieldBinding[fieldsMap.size()]);
this.selection = JsonReader.Selection.of(
fieldsMap.keySet().toArray(new String[fieldsMap.size()]));
} }
@Override public T fromJson(JsonReader reader) throws IOException { @Override public T fromJson(JsonReader reader) throws IOException {
@@ -141,13 +144,19 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
try { try {
reader.beginObject(); reader.beginObject();
while (reader.hasNext()) { while (reader.hasNext()) {
String name = reader.nextName(); int index = reader.selectName(selection);
FieldBinding<?> fieldBinding = fieldsMap.get(name); FieldBinding<?> fieldBinding;
if (fieldBinding != null) { if (index != -1) {
fieldBinding.read(reader, result); fieldBinding = fieldsArray[index];
} else { } else {
reader.skipValue(); String name = reader.nextName();
fieldBinding = fieldsMap.get(name);
if (fieldBinding == null) {
reader.skipValue();
continue;
}
} }
fieldBinding.read(reader, result);
} }
reader.endObject(); reader.endObject();
return result; return result;

View File

@@ -17,7 +17,11 @@ package com.squareup.moshi;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import okio.Buffer;
import okio.BufferedSource; import okio.BufferedSource;
import okio.ByteString;
/** /**
* Reads a JSON (<a href="http://www.ietf.org/rfc/rfc7159.txt">RFC 7159</a>) * Reads a JSON (<a href="http://www.ietf.org/rfc/rfc7159.txt">RFC 7159</a>)
@@ -272,6 +276,12 @@ public abstract class JsonReader implements Closeable {
*/ */
public abstract String nextName() throws IOException; public abstract String nextName() throws IOException;
/**
* If the next token is a {@linkplain Token#NAME property name} that's in {@code selection}, this
* consumes it and returns its index. Otherwise this returns -1 and no name is consumed.
*/
abstract int selectName(Selection selection) throws IOException;
/** /**
* Returns the {@linkplain Token#STRING string} value of the next token, consuming it. If the next * Returns the {@linkplain Token#STRING string} value of the next token, consuming it. If the next
* token is a number, this method will return its string form. * token is a number, this method will return its string form.
@@ -280,6 +290,12 @@ public abstract class JsonReader implements Closeable {
*/ */
public abstract String nextString() throws IOException; public abstract String nextString() throws IOException;
/**
* If the next token is a {@linkplain Token#STRING string} that's in {@code selection}, this
* consumes it and returns its index. Otherwise this returns -1 and no string is consumed.
*/
abstract int selectString(Selection selection) throws IOException;
/** /**
* Returns the {@linkplain Token#BOOLEAN boolean} value of the next token, consuming it. * Returns the {@linkplain Token#BOOLEAN boolean} value of the next token, consuming it.
* *
@@ -347,6 +363,39 @@ public abstract class JsonReader implements Closeable {
*/ */
abstract void promoteNameToValue() throws IOException; abstract void promoteNameToValue() throws IOException;
/**
* A set of strings to be chosen with {@link #selectName} or {@link #selectString}. This prepares
* the encoded values of the strings so they can be read directly from the input source. It cannot
* read arbitrary encodings of the strings: if any of a string's characters are unnecessarily
* escaped in the source JSON, that string will not be selected. Similarly, if the string is
* unquoted or uses single quotes in the source JSON, it will not be selected. Client code that
* uses this class should fall back to another mechanism to accommodate this possibility.
*/
static final class Selection {
final String[] strings;
final List<ByteString> doubleQuoteSuffix;
public Selection(String[] strings, List<ByteString> doubleQuoteSuffix) {
this.strings = strings;
this.doubleQuoteSuffix = doubleQuoteSuffix;
}
public static Selection of(String... strings) {
try {
ByteString[] result = new ByteString[strings.length];
Buffer buffer = new Buffer();
for (int i = 0; i < strings.length; i++) {
BufferedSinkJsonWriter.string(buffer, strings[i]);
buffer.readByte(); // Skip the leading double quote (but leave the trailing one).
result[i] = buffer.readByteString();
}
return new Selection(strings.clone(), Arrays.asList(result));
} catch (IOException e) {
throw new AssertionError(e);
}
}
}
/** /**
* A structure, name, or value type in a JSON-encoded string. * A structure, name, or value type in a JSON-encoded string.
*/ */

View File

@@ -216,11 +216,13 @@ final class StandardJsonAdapters {
private final Class<T> enumType; private final Class<T> enumType;
private final Map<String, T> nameConstantMap; private final Map<String, T> nameConstantMap;
private final String[] nameStrings; private final String[] nameStrings;
private final T[] constants;
private final JsonReader.Selection selection;
public EnumJsonAdapter(Class<T> enumType) { public EnumJsonAdapter(Class<T> enumType) {
this.enumType = enumType; this.enumType = enumType;
try { try {
T[] constants = enumType.getEnumConstants(); constants = enumType.getEnumConstants();
nameConstantMap = new LinkedHashMap<>(); nameConstantMap = new LinkedHashMap<>();
nameStrings = new String[constants.length]; nameStrings = new String[constants.length];
for (int i = 0; i < constants.length; i++) { for (int i = 0; i < constants.length; i++) {
@@ -230,12 +232,16 @@ final class StandardJsonAdapters {
nameConstantMap.put(name, constant); nameConstantMap.put(name, constant);
nameStrings[i] = name; nameStrings[i] = name;
} }
selection = JsonReader.Selection.of(nameStrings);
} catch (NoSuchFieldException e) { } catch (NoSuchFieldException e) {
throw new AssertionError("Missing field in " + enumType.getName(), e); throw new AssertionError("Missing field in " + enumType.getName(), e);
} }
} }
@Override public T fromJson(JsonReader reader) throws IOException { @Override public T fromJson(JsonReader reader) throws IOException {
int index = reader.selectString(selection);
if (index != -1) return constants[index];
String name = reader.nextString(); String name = reader.nextString();
T constant = nameConstantMap.get(name); T constant = nameConstantMap.get(name);
if (constant != null) return constant; if (constant != null) return constant;

View File

@@ -33,6 +33,7 @@ import static com.squareup.moshi.JsonReader.Token.NUMBER;
import static com.squareup.moshi.JsonReader.Token.STRING; import static com.squareup.moshi.JsonReader.Token.STRING;
import static com.squareup.moshi.TestUtil.newReader; import static com.squareup.moshi.TestUtil.newReader;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
public final class BufferedSourceJsonReaderTest { public final class BufferedSourceJsonReaderTest {
@@ -1770,6 +1771,121 @@ public final class BufferedSourceJsonReaderTest {
} }
} }
@Test public void selectName() throws IOException {
JsonReader.Selection abc = JsonReader.Selection.of("a", "b", "c");
JsonReader reader = newReader("{\"a\": 5, \"b\": 5, \"c\": 5, \"d\": 5}");
reader.beginObject();
assertEquals("$.", reader.getPath());
assertEquals(0, reader.selectName(abc));
assertEquals("$.a", reader.getPath());
assertEquals(5, reader.nextInt());
assertEquals("$.a", reader.getPath());
assertEquals(1, reader.selectName(abc));
assertEquals("$.b", reader.getPath());
assertEquals(5, reader.nextInt());
assertEquals("$.b", reader.getPath());
assertEquals(2, reader.selectName(abc));
assertEquals("$.c", reader.getPath());
assertEquals(5, reader.nextInt());
assertEquals("$.c", reader.getPath());
// A missed selectName() doesn't advance anything, not even the path.
assertEquals(-1, reader.selectName(abc));
assertEquals("$.c", reader.getPath());
assertEquals(JsonReader.Token.NAME, reader.peek());
assertEquals("d", reader.nextName());
assertEquals("$.d", reader.getPath());
assertEquals(5, reader.nextInt());
assertEquals("$.d", reader.getPath());
reader.endObject();
}
@Test public void selectString() throws IOException {
JsonReader.Selection abc = JsonReader.Selection.of("a", "b", "c");
JsonReader reader = newReader("[\"a\", \"b\", \"c\", \"d\"]");
reader.beginArray();
assertEquals("$[0]", reader.getPath());
assertEquals(0, reader.selectString(abc));
assertEquals("$[1]", reader.getPath());
assertEquals(1, reader.selectString(abc));
assertEquals("$[2]", reader.getPath());
assertEquals(2, reader.selectString(abc));
assertEquals("$[3]", reader.getPath());
// A missed selectName() doesn't advance anything, not even the path.
assertEquals(-1, reader.selectString(abc));
assertEquals("$[3]", reader.getPath());
assertEquals(JsonReader.Token.STRING, reader.peek());
assertEquals("d", reader.nextString());
assertEquals("$[4]", reader.getPath());
reader.endArray();
}
/** Select doesn't match unquoted strings. */
@Test public void selectStringUnquoted() throws IOException {
JsonReader.Selection abc = JsonReader.Selection.of("a", "b", "c");
JsonReader reader = newReader("[a]");
reader.setLenient(true);
reader.beginArray();
assertEquals(-1, reader.selectString(abc));
assertEquals("a", reader.nextString());
reader.endArray();
}
/** Select doesn't match single quoted strings. */
@Test public void selectStringSingleQuoted() throws IOException {
JsonReader.Selection abc = JsonReader.Selection.of("a", "b", "c");
JsonReader reader = newReader("['a']");
reader.setLenient(true);
reader.beginArray();
assertEquals(-1, reader.selectString(abc));
assertEquals("a", reader.nextString());
reader.endArray();
}
/** Select doesn't match unnecessarily-escaped strings. */
@Test public void selectUnnecessaryEscaping() throws IOException {
JsonReader.Selection abc = JsonReader.Selection.of("a", "b", "c");
JsonReader reader = newReader("[\"\\u0061\"]");
reader.beginArray();
assertEquals(-1, reader.selectString(abc));
assertEquals("a", reader.nextString());
reader.endArray();
}
/** Select does match necessarily escaping. The decoded value is used in the path. */
@Test public void selectNecessaryEscaping() throws IOException {
JsonReader.Selection selection = JsonReader.Selection.of("\n", "\u0000", "\"");
JsonReader reader = newReader("{\"\\n\": 5,\"\\u0000\": 5, \"\\\"\": 5}");
reader.beginObject();
assertEquals(0, reader.selectName(selection));
assertEquals(5, reader.nextInt());
assertEquals("$.\n", reader.getPath());
assertEquals(1, reader.selectName(selection));
assertEquals(5, reader.nextInt());
assertEquals("$.\u0000", reader.getPath());
assertEquals(2, reader.selectName(selection));
assertEquals(5, reader.nextInt());
assertEquals("$.\"", reader.getPath());
reader.endObject();
}
private void assertDocument(String document, Object... expectations) throws IOException { private void assertDocument(String document, Object... expectations) throws IOException {
JsonReader reader = newReader(document); JsonReader reader = newReader(document);
reader.setLenient(true); reader.setLenient(true);

View File

@@ -28,7 +28,7 @@
<java.version>1.7</java.version> <java.version>1.7</java.version>
<!-- Dependencies --> <!-- Dependencies -->
<okio.version>1.6.0</okio.version> <okio.version>1.8.0-SNAPSHOT</okio.version>
<!-- Test Dependencies --> <!-- Test Dependencies -->
<junit.version>4.12</junit.version> <junit.version>4.12</junit.version>