Skip to content

Commit

Permalink
major optimization to slow path in json parse
Browse files Browse the repository at this point in the history
  • Loading branch information
esaulpaugh committed Feb 5, 2025
1 parent 53aadb4 commit e115cfb
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 111 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ Also includes optimized implementations of:

headlong depends on gson v2.1 or greater at runtime and v2.11.0 or greater at compile time. Test suite should take less than one minute to run. Test packages require junit. Jar size is ~128 KiB. Java 8+.

For best JSON parsing performance, make sure objects are compact and that the "type" field is positioned first. See ABIJSON.tryParseStreaming. This can be done via the method `Function#toJson(boolean)` while giving `false` as the argument (no pretty print).
For better JSON array parsing performance, use ABIJSON methods which accept a `Set<TypeEnum>` by which to filter objects by type. For best performance, json should be compact and "type" should be the first key in functions, events, and errors. This can be done via the method `ABIObject#toJson(boolean)` while supplying `false` as the argument (no pretty print).

See the wiki for more, such as packed encoding (and decoding) and RLP Object Notation: https://github.com/esaulpaugh/headlong/wiki

Expand Down
149 changes: 39 additions & 110 deletions src/main/java/com/esaulpaugh/headlong/abi/ABIJSON.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,12 @@
*/
package com.esaulpaugh.headlong.abi;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.Strictness;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.CharArrayReader;
import java.io.CharArrayWriter;
import java.io.Closeable;
import java.io.IOException;
Expand Down Expand Up @@ -76,10 +72,6 @@ private ABIJSON() {}
static final String PAYABLE = "payable"; // to mark as nonpayable, do not specify any stateMutability
private static final String INTERNAL_TYPE = "internalType";

private static final TypeAdapter<JsonElement> JSON_ELEMENT_ADAPTER = new Gson().getAdapter(JsonElement.class);
private static final int INITIAL_SIZE_PRETTY = 512;
private static final int INITIAL_SIZE_COMPACT = 256;

/**
* Selects all objects with type {@link TypeEnum#FUNCTION}.
*
Expand Down Expand Up @@ -163,7 +155,7 @@ public static <T extends ABIObject> List<T> parseABIField(int flags, String obje
//----------------------------------------------------------------------------------------------------------------------
static String toJson(ABIObject o, boolean pretty) {
try {
final Writer stringOut = new NonSyncWriter(pretty ? INITIAL_SIZE_PRETTY : INITIAL_SIZE_COMPACT); // can also use StringWriter or CharArrayWriter, but this is faster
final Writer stringOut = new NonSyncWriter(pretty ? 512 : 256); // can also use StringWriter or CharArrayWriter, but this is faster
final JsonWriter out = new JsonWriter(stringOut);
if (pretty) {
out.setIndent(" ");
Expand Down Expand Up @@ -272,10 +264,6 @@ public void write(final String str, int off, final int len) {
this.count = newCount;
}

char[] getBuf() {
return buf;
}

@Override
public String toString() {
return new String(buf, 0, count);
Expand Down Expand Up @@ -371,106 +359,53 @@ private static <T extends ABIObject> T parseABIObject(JsonReader reader, Set<Typ

static <T extends ABIObject> T tryParseStreaming(JsonReader reader, Set<TypeEnum> types, MessageDigest digest, int flags) throws IOException {
reader.beginObject();
NonSyncWriter charsWriter = null;
JsonWriter jsonWriter = null;
TypeEnum t = TypeEnum.FUNCTION;
do {
String name = reader.nextName();
if (TYPE.equals(name)) {
t = TypeEnum.parse(reader.nextString());
if (!types.contains(t)) {
while (reader.peek() != JsonToken.END_OBJECT) {
reader.skipValue();
String name = null;
TupleType<?> inputs = TupleType.EMPTY;
TupleType<?> outputs = TupleType.EMPTY;
String stateMutability = null;
boolean anonymous = false;
try {
do {
final String key = reader.nextName();
switch (key) {
case TYPE:
t = TypeEnum.parse(reader.nextString());
if (!types.contains(t)) {
while (reader.peek() != JsonToken.END_OBJECT) {
reader.skipValue();
}
return null;
}
reader.endObject();
return null;
}
if (jsonWriter == null) {
return finishParse(t, reader, digest, flags);
}
} else {
if (jsonWriter == null) {
jsonWriter = new JsonWriter((charsWriter = new NonSyncWriter(INITIAL_SIZE_PRETTY))).beginObject();
continue;
case NAME: name = reader.nextString(); continue;
case INPUTS: inputs = parseTupleType(reader, flags); continue;
case OUTPUTS: outputs = parseTupleType(reader, flags); continue;
case STATE_MUTABILITY: stateMutability = reader.nextString(); continue;
case ANONYMOUS: anonymous = reader.nextBoolean(); continue;
default: reader.skipValue();
}
jsonWriter.name(name);
// read is equivalent to JsonParser.parseReader, Streams.parse, and TypeAdapters.JSON_ELEMENT.read
JSON_ELEMENT_ADAPTER.write(jsonWriter, JSON_ELEMENT_ADAPTER.read(reader));
}
} while (reader.peek() != JsonToken.END_OBJECT);
reader.endObject();
jsonWriter.endObject();
JsonReader r = reader(charsWriter.getBuf());
r.beginObject();
return finishParse(t, r, digest, flags);
} while (reader.peek() != JsonToken.END_OBJECT);
return finishParse(t, name, inputs, outputs, stateMutability, anonymous, digest);
} finally {
reader.endObject();
}
}

@SuppressWarnings("unchecked")
private static <T extends ABIObject> T finishParse(TypeEnum t, JsonReader reader, MessageDigest digest, int flags) throws IOException {
private static <T extends ABIObject> T finishParse(TypeEnum t, String name, TupleType<?> inputs, TupleType<?> outputs, String stateMutability, boolean anonymous, MessageDigest digest) {
switch (t.ordinal()) {
case TypeEnum.ORDINAL_FUNCTION:
case TypeEnum.ORDINAL_RECEIVE:
case TypeEnum.ORDINAL_FALLBACK:
case TypeEnum.ORDINAL_CONSTRUCTOR: return (T) parseFunction(t, reader, digest, flags);
case TypeEnum.ORDINAL_EVENT: return (T) parseEvent(reader, flags);
case TypeEnum.ORDINAL_ERROR: return (T) parseError(reader, flags);
case TypeEnum.ORDINAL_CONSTRUCTOR: return (T) new Function(t, name, inputs, outputs, stateMutability, digest);
case TypeEnum.ORDINAL_EVENT: return (T) new Event<>(name, anonymous, inputs, inputs.indexed);
case TypeEnum.ORDINAL_ERROR: return (T) new ContractError<>(name, inputs);
default: throw new AssertionError();
}
}

private static Function parseFunction(TypeEnum t, JsonReader reader, MessageDigest digest, int flags) throws IOException {
String name = null;
TupleType<?> inputs = TupleType.EMPTY;
TupleType<?> outputs = TupleType.EMPTY;
String stateMutability = null;
while (reader.peek() != JsonToken.END_OBJECT) {
switch (reader.nextName()) {
case NAME: name = reader.nextString(); continue;
case INPUTS: inputs = parseTupleType(reader, false, flags); continue;
case OUTPUTS: outputs = parseTupleType(reader, false, flags); continue;
case STATE_MUTABILITY: stateMutability = reader.nextString(); continue;
default: reader.skipValue();
}
}
reader.endObject();
return new Function(t, name, inputs, outputs, stateMutability, digest);
}

private static Event<?> parseEvent(JsonReader reader, int flags) throws IOException {
String name = null;
boolean anonymous = false;
TupleType<?> tt = null;
while (reader.peek() != JsonToken.END_OBJECT) {
switch (reader.nextName()) {
case NAME: name = reader.nextString(); continue;
case ANONYMOUS: anonymous = reader.nextBoolean(); continue;
case INPUTS: tt = parseTupleType(reader, true, flags); continue;
default: reader.skipValue();
}
}
reader.endObject();
return new Event<>(
name,
anonymous,
tt,
tt.indexed
);
}

private static ContractError<?> parseError(JsonReader reader, int flags) throws IOException {
String name = null;
TupleType<?> tt = null;
while (reader.peek() != JsonToken.END_OBJECT) {
switch (reader.nextName()) {
case NAME: name = reader.nextString(); continue;
case INPUTS: tt = parseTupleType(reader, false, flags); continue;
default: reader.skipValue();
}
}
reader.endObject();
return new ContractError<>(name, tt);
}

private static TupleType<?> parseTupleType(final JsonReader reader, final boolean eventParams, final int flags) throws IOException {
private static TupleType<?> parseTupleType(final JsonReader reader, final int flags) throws IOException {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return TupleType.empty(flags);
Expand All @@ -485,7 +420,7 @@ private static TupleType<?> parseTupleType(final JsonReader reader, final boolea
ABIType<?>[] elements = new ABIType<?>[8];
String[] names = new String[8];
String[] internalTypes = new String[8];
boolean[] indexed = eventParams ? new boolean[8] : null;
boolean[] indexed = new boolean[8];
StringBuilder canonicalType = TupleType.newTypeBuilder();
boolean dynamic = false;

Expand All @@ -500,7 +435,7 @@ private static TupleType<?> parseTupleType(final JsonReader reader, final boolea
while (reader.peek() != JsonToken.END_OBJECT) {
switch (reader.nextName()) {
case TYPE: type = reader.nextString(); continue;
case COMPONENTS: e = parseTupleType(reader, false, flags); continue;
case COMPONENTS: e = parseTupleType(reader, flags); continue;
case NAME: name = reader.nextString(); continue;
case INTERNAL_TYPE: internalType = reader.nextString(); continue;
case INDEXED: isIndexed = reader.nextBoolean(); continue;
Expand All @@ -525,9 +460,7 @@ private static TupleType<?> parseTupleType(final JsonReader reader, final boolea
if (internalType != null) {
internalTypes[i] = internalType.equals(e.canonicalType) ? e.canonicalType : internalType;
}
if (eventParams) {
indexed[i] = isIndexed;
}
indexed[i] = isIndexed;

i++;
if (reader.peek() == JsonToken.END_ARRAY) {
Expand All @@ -538,7 +471,7 @@ private static TupleType<?> parseTupleType(final JsonReader reader, final boolea
Arrays.copyOf(elements, i),
Arrays.copyOf(names, i),
Arrays.copyOf(internalTypes, i),
eventParams ? Arrays.copyOf(indexed, i) : null,
Arrays.copyOf(indexed, i),
flags
);
}
Expand All @@ -548,15 +481,11 @@ private static TupleType<?> parseTupleType(final JsonReader reader, final boolea
elements = Arrays.copyOf(elements, newLen);
names = Arrays.copyOf(names, newLen);
internalTypes = Arrays.copyOf(internalTypes, newLen);
indexed = eventParams ? Arrays.copyOf(indexed, newLen) : null;
indexed = Arrays.copyOf(indexed, newLen);
}
}
}

private static JsonReader reader(char[] chars) {
return strict(new CharArrayReader(chars));
}

private static JsonReader reader(String json) {
return strict(new StringReader(json));
}
Expand Down

0 comments on commit e115cfb

Please sign in to comment.