Skip to content

Commit 2c45398

Browse files
committed
Add benchmarks for JSON deserialization including randomized map keys
Most benchmarks use the same set of input data for each iteration, which is heavily biased toward caches. This benchmark is meant to stress canonicalzation cache misses in a way that we observe in some production systems. My implementation isn't ideal because I'm doing a fair bit of work to generate randomized data within the measured component of the benchmark, however I've profiled the results and most time is not spent within the setup portion, and the benchmark setup is comparable between configurations such that the results should be comparable between flags. Initially I began this investigation based on the InternCache, which I expected to be the primary bottle-neck for reasons listed here: https://shipilev.net/jvm/anatomy-quarks/10-string-intern/ There is a measurable impact disabling interning, particularly with smaller heap sizes, but there's a much larger improvement when we opt out of canonicalization entirely, especially in paths which rely on the ByteQuadsCanonicalizer rather than CharsToNameCanonicalizer.
1 parent 199aaae commit 2c45398

File tree

1 file changed

+151
-0
lines changed

1 file changed

+151
-0
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package com.fasterxml.jackson.perf.json;
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import com.fasterxml.jackson.core.JsonFactory;
6+
import com.fasterxml.jackson.core.JsonParser;
7+
import com.fasterxml.jackson.core.type.TypeReference;
8+
import com.fasterxml.jackson.databind.DeserializationFeature;
9+
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import com.fasterxml.jackson.databind.ObjectReader;
11+
import org.openjdk.jmh.annotations.Benchmark;
12+
import org.openjdk.jmh.annotations.BenchmarkMode;
13+
import org.openjdk.jmh.annotations.Mode;
14+
import org.openjdk.jmh.annotations.OutputTimeUnit;
15+
import org.openjdk.jmh.annotations.Param;
16+
import org.openjdk.jmh.annotations.Scope;
17+
import org.openjdk.jmh.annotations.Setup;
18+
import org.openjdk.jmh.annotations.State;
19+
20+
import java.io.ByteArrayInputStream;
21+
import java.io.IOException;
22+
import java.io.InputStreamReader;
23+
import java.nio.charset.StandardCharsets;
24+
import java.util.Map;
25+
import java.util.concurrent.ThreadLocalRandom;
26+
import java.util.concurrent.TimeUnit;
27+
import java.util.function.Supplier;
28+
29+
@State(Scope.Benchmark)
30+
@BenchmarkMode(Mode.AverageTime)
31+
@OutputTimeUnit(TimeUnit.MICROSECONDS)
32+
public class JsonArbitraryFieldNameBenchmark {
33+
34+
public enum FactoryMode {
35+
DEFAULT() {
36+
@Override
37+
JsonFactory apply(JsonFactory factory) {
38+
return factory;
39+
}
40+
},
41+
NO_INTERN() {
42+
@Override
43+
JsonFactory apply(JsonFactory factory) {
44+
return factory.disable(JsonFactory.Feature.INTERN_FIELD_NAMES);
45+
}
46+
},
47+
NO_CANONICALIZE() {
48+
@Override
49+
JsonFactory apply(JsonFactory factory) {
50+
return factory.disable(JsonFactory.Feature.CANONICALIZE_FIELD_NAMES);
51+
}
52+
};
53+
54+
abstract JsonFactory apply(JsonFactory factory);
55+
}
56+
57+
/**
58+
* Ideally we would not generate inputs within the measured component of the benchmark.
59+
*/
60+
public enum InputType {
61+
INPUT_STREAM() {
62+
@Override
63+
JsonParser create(JsonFactory factory, Supplier<String> jsonSupplier) throws IOException {
64+
return factory.createParser(new ByteArrayInputStream(jsonSupplier.get().getBytes(StandardCharsets.UTF_8)));
65+
}
66+
},
67+
READER() {
68+
@Override
69+
JsonParser create(JsonFactory factory, Supplier<String> jsonSupplier) throws IOException {
70+
// Instead of using 'new StringReader(jsonSupplier.get())', we construct an InputStreamReader
71+
// to more closely match overhead of INPUT_STREAM for comparison.
72+
return factory.createParser(new InputStreamReader(
73+
new ByteArrayInputStream(jsonSupplier.get().getBytes(StandardCharsets.UTF_8)),
74+
StandardCharsets.UTF_8));
75+
}
76+
};
77+
78+
abstract JsonParser create(JsonFactory factory, Supplier<String> jsonSupplier) throws IOException;
79+
}
80+
81+
public enum InputShape {
82+
RANDOM_KEY_MAP(
83+
new TypeReference<Map<String, Boolean>>() {},
84+
() -> "{\"" + ThreadLocalRandom.current().nextInt() + "\":true}"),
85+
BEAN_WITH_RANDOM_KEY_MAP(
86+
new TypeReference<SimpleClass>() {},
87+
() -> "{\"fieldWithMap\":{\"" + ThreadLocalRandom.current().nextInt()
88+
+ "\":true},\"stringOne\":\"a\",\"stringTwo\":\"a\",\"stringThree\":\"a\"}");
89+
90+
private final TypeReference<?> typereference;
91+
private final Supplier<String> jsonSupplier;
92+
InputShape(TypeReference<?> typereference, Supplier<String> jsonSupplier) {
93+
this.typereference = typereference;
94+
this.jsonSupplier = jsonSupplier;
95+
}
96+
}
97+
98+
@Param
99+
public InputShape shape;
100+
101+
@Param
102+
public InputType type;
103+
104+
@Param
105+
public FactoryMode mode;
106+
107+
private JsonFactory factory;
108+
private ObjectReader reader;
109+
110+
@Setup
111+
public void setup() {
112+
factory = mode.apply(new JsonFactory());
113+
ObjectMapper mapper = new ObjectMapper(factory)
114+
// Use FAIL_ON_UNKNOWN_PROPERTIES to ensure the benchmark inputs are valid
115+
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
116+
reader = mapper.readerFor(shape.typereference);
117+
}
118+
119+
@Benchmark
120+
public Object parse() throws IOException {
121+
try (JsonParser parser = type.create(factory, shape.jsonSupplier)) {
122+
return reader.readValue(parser);
123+
}
124+
}
125+
126+
/**
127+
* This type primarily exists to wrap a map, but has additional
128+
* fields to cover a mix of reused and arbitrary json keys.
129+
*/
130+
public static final class SimpleClass {
131+
public Map<String, Boolean> fieldWithMap;
132+
133+
public String stringOne;
134+
135+
public String stringTwo;
136+
137+
public String stringThree;
138+
139+
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
140+
SimpleClass(
141+
@JsonProperty("fieldWithMap") Map<String, Boolean> fieldWithMap,
142+
@JsonProperty("stringOne") String stringOne,
143+
@JsonProperty("stringTwo") String stringTwo,
144+
@JsonProperty("stringThree") String stringThree) {
145+
this.fieldWithMap = fieldWithMap;
146+
this.stringOne = stringOne;
147+
this.stringTwo = stringTwo;
148+
this.stringThree = stringThree;
149+
}
150+
}
151+
}

0 commit comments

Comments
 (0)