Skip to content

Optimize performance for TokenBuffer as fromValue with ObjectMapper.convertValue #5368

@qsLI

Description

@qsLI

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

No one assigned

    Labels

    3.0Issue planned for initial 3.0 release

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions