-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Open
Labels
3.0Issue planned for initial 3.0 releaseIssue planned for initial 3.0 release
Description
Is your feature request related to a problem? Please describe.
ObjectMapper.convertValue will always convert the fromValue to TokenBuffer first, even if the fromValue is already a TokenBuffer. This will suffer a performance penalty.
I wonder if we can reuse the fromValue TokenBuffer if it's safe (I'm not clear about that).
Our scenario is we hold an JsonNode object,every time a request comes in and we use ObjectMapper.convertValue to convert JsonNode to concrete class type for immutability. It's quite cpu consuming.
We should see a great performance gain, If we hold the lower level TokenBuffer and ObjectMapper.convertValue optimize for TokenBuffer.
Some jmh benchmark test result
/**
*# Run complete. Total time: 00:03:26
*
* Benchmark Mode Cnt Score Error Units
* TokenBufferBenchmarkOpt.jsonNodeOpt thrpt 15 20357.253 ± 1965.457 ops/s
* TokenBufferBenchmarkOpt.jsonNodeOpt:·gc.alloc.rate thrpt 15 1770.818 ± 170.991 MB/sec
* TokenBufferBenchmarkOpt.jsonNodeOpt:·gc.alloc.rate.norm thrpt 15 100408.011 ± 0.028 B/op
* TokenBufferBenchmarkOpt.jsonNodeOpt:·gc.churn.G1_Eden_Space thrpt 15 1773.046 ± 171.012 MB/sec
* TokenBufferBenchmarkOpt.jsonNodeOpt:·gc.churn.G1_Eden_Space.norm thrpt 15 100584.583 ± 2933.138 B/op
* TokenBufferBenchmarkOpt.jsonNodeOpt:·gc.churn.G1_Old_Gen thrpt 15 0.003 ± 0.002 MB/sec
* TokenBufferBenchmarkOpt.jsonNodeOpt:·gc.churn.G1_Old_Gen.norm thrpt 15 0.176 ± 0.086 B/op
* TokenBufferBenchmarkOpt.jsonNodeOpt:·gc.count thrpt 15 239.000 counts
* TokenBufferBenchmarkOpt.jsonNodeOpt:·gc.time thrpt 15 167.000 ms
*
* TokenBufferBenchmarkOpt.tokenBufferOpt thrpt 15 31263.631 ± 480.453 ops/s
* TokenBufferBenchmarkOpt.tokenBufferOpt:·gc.alloc.rate thrpt 15 1950.311 ± 29.974 MB/sec
* TokenBufferBenchmarkOpt.tokenBufferOpt:·gc.alloc.rate.norm thrpt 15 72024.007 ± 0.018 B/op
* TokenBufferBenchmarkOpt.tokenBufferOpt:·gc.churn.G1_Eden_Space thrpt 15 1958.613 ± 60.286 MB/sec
* TokenBufferBenchmarkOpt.tokenBufferOpt:·gc.churn.G1_Eden_Space.norm thrpt 15 72327.100 ± 1757.443 B/op
* TokenBufferBenchmarkOpt.tokenBufferOpt:·gc.churn.G1_Old_Gen thrpt 15 0.004 ± 0.002 MB/sec
* TokenBufferBenchmarkOpt.tokenBufferOpt:·gc.churn.G1_Old_Gen.norm thrpt 15 0.164 ± 0.061 B/op
* TokenBufferBenchmarkOpt.tokenBufferOpt:·gc.count thrpt 15 264.000 counts
* TokenBufferBenchmarkOpt.tokenBufferOpt:·gc.time thrpt 15 177.000 ms
*
*/
@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 10, time = 1)
@Measurement(iterations = 15, time = 5)
@Fork(2)
public class TokenBufferBenchmarkOpt {
static ObjectMapper mapper = new MyObjectMapper();
static {
mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES,true);
mapper.configure(JsonParser.Feature.IGNORE_UNDEFINED,true);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
}
private TokenBuffer tokenBuffer;
private JsonNode jsonNode;
@Setup
@SneakyThrows
public final void setup() throws NoSuchAlgorithmException {
List<Person> list = Lists.newArrayList();
for (int i = 0; i < 1000; i++) {
final Person p = new Person();
p.setGender(i % 2== 0 ? Gender.FEMALE : Gender.MALE);
list.add(p);
}
final String s = mapper.writeValueAsString(list);
tokenBuffer = mapper.readValue(s, TokenBuffer.class);
jsonNode = mapper.readValue(s, JsonNode.class);
}
@Benchmark
public List<Person> tokenBufferOpt() {
return mapper.convertValue(tokenBuffer, new TypeReference<List<Person>>() {});
}
@Benchmark
public List<Person> jsonNodeOpt() {
return mapper.convertValue(jsonNode, new TypeReference<List<Person>>() {});
}
public static void main(String[] args) throws RunnerException, ProfilerException {
final Options opt = new OptionsBuilder()
.include(TokenBufferBenchmarkOpt.class.getSimpleName())
.jvmArgs("-Xmx1g", "-Xms1g")
.addProfiler("gc")
.build();
new Runner(opt).run();
}
static class MyObjectMapper extends ObjectMapper {
@Override
protected Object _convert(Object fromValue, JavaType toValueType) throws IllegalArgumentException {
// also, as per [databind#11], consider case for simple cast
/* But with caveats: one is that while everything is Object.class, we don't
* want to "optimize" that out; and the other is that we also do not want
* to lose conversions of generic types.
*/
Class<?> targetType = toValueType.getRawClass();
if (targetType != Object.class
&& !toValueType.hasGenericTypes()
&& targetType.isAssignableFrom(fromValue.getClass())) {
return fromValue;
}
TokenBuffer buf = null;
if (fromValue.getClass() == TokenBuffer.class) {
buf = (TokenBuffer) fromValue;
} else {
// Then use TokenBuffer, which is a JsonGenerator:
buf = new TokenBuffer(this, false);
if (isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)) {
buf = buf.forceUseOfBigDecimal(true);
}
try {
// inlined 'writeValue' with minor changes:
// first: disable wrapping when writing
SerializationConfig config = getSerializationConfig().without(SerializationFeature.WRAP_ROOT_VALUE);
// no need to check for closing of TokenBuffer
_serializerProvider(config).serializeValue(buf, fromValue);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e.getMessage(), e);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
try {
// then matching read, inlined 'readValue' with minor mods:
final JsonParser jp = buf.asParser();
Object result;
// ok to pass in existing feature flags; unwrapping handled by mapper
final DeserializationConfig deserConfig = getDeserializationConfig();
JsonToken t = _initForReading(jp);
if (t == JsonToken.VALUE_NULL) {
DeserializationContext ctxt = createDeserializationContext(jp, deserConfig);
result = _findRootDeserializer(ctxt, toValueType).getNullValue(ctxt);
} else if (t == JsonToken.END_ARRAY || t == JsonToken.END_OBJECT) {
result = null;
} else { // pointing to event other than null
DeserializationContext ctxt = createDeserializationContext(jp, deserConfig);
JsonDeserializer<Object> deser = _findRootDeserializer(ctxt, toValueType);
// note: no handling of unwarpping
result = deser.deserialize(jp, ctxt);
}
jp.close();
return result;
} catch (IOException e) { // should not occur, no real i/o...
throw new IllegalArgumentException(e.getMessage(), e);
}
}
}
}Describe the solution you'd like
static class MyObjectMapper extends ObjectMapper {
@Override
protected Object _convert(Object fromValue, JavaType toValueType) throws IllegalArgumentException {
// also, as per [databind#11], consider case for simple cast
/* But with caveats: one is that while everything is Object.class, we don't
* want to "optimize" that out; and the other is that we also do not want
* to lose conversions of generic types.
*/
Class<?> targetType = toValueType.getRawClass();
if (targetType != Object.class
&& !toValueType.hasGenericTypes()
&& targetType.isAssignableFrom(fromValue.getClass())) {
return fromValue;
}
TokenBuffer buf = null;
if (fromValue.getClass() == TokenBuffer.class) {
buf = (TokenBuffer) fromValue;
} else {
// Then use TokenBuffer, which is a JsonGenerator:
buf = new TokenBuffer(this, false);
if (isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)) {
buf = buf.forceUseOfBigDecimal(true);
}
try {
// inlined 'writeValue' with minor changes:
// first: disable wrapping when writing
SerializationConfig config = getSerializationConfig().without(SerializationFeature.WRAP_ROOT_VALUE);
// no need to check for closing of TokenBuffer
_serializerProvider(config).serializeValue(buf, fromValue);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(e.getMessage(), e);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
// ...
}Usage example
No response
Additional context
No response
Metadata
Metadata
Assignees
Labels
3.0Issue planned for initial 3.0 releaseIssue planned for initial 3.0 release