mirror of
https://github.com/fankes/moshi.git
synced 2025-10-19 16:09:21 +08:00
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:
@@ -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;
|
||||||
|
@@ -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) {
|
||||||
|
@@ -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;
|
||||||
|
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
@@ -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;
|
||||||
|
@@ -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);
|
||||||
|
2
pom.xml
2
pom.xml
@@ -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>
|
||||||
|
Reference in New Issue
Block a user