Skip to content

Commit

Permalink
Memoize compiled Jackson (de)serializers.
Browse files Browse the repository at this point in the history
I'm not sure why I need to memoize these myself; I thought Jackson would do it.

This reduces the benchmark score from about 7700us to 170us, a 45x improvement.
  • Loading branch information
prdoyle committed Nov 16, 2024
1 parent dc35e19 commit 2a831c3
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.lang.reflect.Type;
import lombok.RequiredArgsConstructor;
import works.bosk.BoskDriver;
import works.bosk.DriverFactory;
import works.bosk.Identifier;
import works.bosk.Reference;
import works.bosk.StateTreeNode;
Expand All @@ -21,6 +22,10 @@
public class ForwardingDriver implements BoskDriver {
protected final BoskDriver downstream;

public static <RR extends StateTreeNode> DriverFactory<RR> factory() {
return (b,d) -> new ForwardingDriver(d);
}

@Override
public StateTreeNode initialRoot(Type rootType) throws InvalidTypeException, IOException, InterruptedException {
return downstream.initialRoot(rootType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ final class JacksonCompiler {
* @return a newly compiled {@link CompiledSerDes} for values of the given <code>nodeType</code>.
*/
public <T> CompiledSerDes<T> compiled(JavaType nodeType, BoskInfo<?> boskInfo, JacksonPlugin.FieldModerator moderator) {
LOGGER.debug("Compiling SerDes for node type {}", nodeType);
try {
// Record that we're compiling this one to avoid infinite recursion
compilationsInProgress.get().addLast(nodeType);
Expand Down
16 changes: 14 additions & 2 deletions bosk-jackson/src/main/java/works/bosk/jackson/JacksonPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import works.bosk.BoskInfo;
import works.bosk.Catalog;
import works.bosk.Entity;
Expand Down Expand Up @@ -97,14 +98,19 @@ public void setupModule(SetupContext context) {

private final class BoskSerializers extends Serializers.Base {
private final BoskInfo<?> boskInfo;
private final Map<JavaType, JsonSerializer<?>> memo = new ConcurrentHashMap<>();

public BoskSerializers(BoskInfo<?> boskInfo) {
this.boskInfo = boskInfo;
}

@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public JsonSerializer<?> findSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) {
return memo.computeIfAbsent(type, __ -> getJsonSerializer(config, type, beanDesc));
}

@SuppressWarnings({ "unchecked", "rawtypes" })
private JsonSerializer<?> getJsonSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) {
Class theClass = type.getRawClass();
if (theClass.isAnnotationPresent(DerivedRecord.class)) {
return derivedRecordSerializer(config, type, beanDesc);
Expand Down Expand Up @@ -308,14 +314,19 @@ public JsonSerializer<?> findMapSerializer(SerializationConfig config, MapType t

private final class BoskDeserializers extends Deserializers.Base {
private final BoskInfo<?> boskInfo;
private final Map<JavaType, JsonDeserializer<?>> memo = new ConcurrentHashMap<>();

public BoskDeserializers(BoskInfo<?> boskInfo) {
this.boskInfo = boskInfo;
}

@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public JsonDeserializer<?> findBeanDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) {
return memo.computeIfAbsent(type, __ -> getJsonDeserializer(type, config, beanDesc));
}

@SuppressWarnings({ "unchecked", "rawtypes" })
private JsonDeserializer<?> getJsonDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) {
Class theClass = type.getRawClass();
if (theClass.isAnnotationPresent(DerivedRecord.class)) {
return derivedRecordDeserializer(type, config, beanDesc);
Expand Down Expand Up @@ -662,6 +673,7 @@ private static <V> void writeEntryAsField(JsonGenerator gen, Optional<Identifier
* Leaves the parser sitting on the END_ARRAY token. You could call nextToken() to continue with parsing.
*/
private <V> LinkedHashMap<Identifier, V> readMapEntries(JsonParser p, JavaType valueType, DeserializationContext ctxt) throws IOException {
@SuppressWarnings("unchecked")
JsonDeserializer<V> valueDeserializer = (JsonDeserializer<V>) ctxt.findContextualValueDeserializer(valueType, null);
LinkedHashMap<Identifier, V> result = new LinkedHashMap<>();
if (p.currentToken() == START_OBJECT) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package works.bosk.jackson;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.concurrent.atomic.AtomicReference;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.annotations.Warmup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import works.bosk.AbstractBoskTest;
import works.bosk.AbstractRoundTripTest;
import works.bosk.Bosk;
import works.bosk.BoskDriver;
import works.bosk.DriverStack;
import works.bosk.Identifier;
import works.bosk.Path;
import works.bosk.Reference;
import works.bosk.exceptions.InvalidTypeException;

import static java.util.concurrent.TimeUnit.MICROSECONDS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static org.openjdk.jmh.annotations.Mode.AverageTime;
import static works.bosk.jackson.JacksonPluginConfiguration.defaultConfiguration;

@Fork(0)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 10, time = 1)
@OutputTimeUnit(MICROSECONDS)
public class JacksonRoundTripBenchmark extends AbstractRoundTripTest {

@State(Scope.Benchmark)
public static class BenchmarkState {
private Bosk<TestRoot> bosk;
private JacksonPlugin jacksonPlugin;
private ObjectMapper mapper;
private BoskDriver driver;
private BoskDriver downstreamDriver;
private Bosk<TestRoot>.ReadContext context;
private Reference<TestRoot> rootRef;
private Reference<TestEnum> ref5Segments;
private TestRoot root1, root2;
private ThreadLocal<TestRoot> threadLocalRoot;

final Identifier parentID = Identifier.from("parent");
final Identifier child1ID = Identifier.from("child1");

@Setup(Level.Trial)
public void setup() throws InvalidTypeException, JsonProcessingException {
AtomicReference<BoskDriver> downstreamRef = new AtomicReference<>();
this.bosk = setUpBosk(DriverStack.of(
jacksonRoundTripFactory(defaultConfiguration()),
(b,d) -> {
downstreamRef.set(d);
return d;
}
));
this.driver = bosk.driver();
this.downstreamDriver = downstreamRef.get();
this.jacksonPlugin = new JacksonPlugin();
this.mapper = new ObjectMapper().registerModule(jacksonPlugin.moduleFor(bosk));
context = bosk.readContext();
rootRef = bosk.rootReference();
TestRoot localRoot = root1 = rootRef.value();
threadLocalRoot = ThreadLocal.withInitial(() -> localRoot);
ref5Segments = bosk.rootReference().then(TestEnum.class, Path.of(
TestRoot.Fields.entities, "parent",
AbstractBoskTest.TestEntity.Fields.children, "child1",
AbstractBoskTest.TestChild.Fields.testEnum
));

// Make a separate identical state object, cloning via JSON
String json = mapper.writerFor(rootRef.targetClass()).writeValueAsString(root1);
root2 = mapper.readerFor(rootRef.targetClass()).readValue(json);
}

@TearDown(Level.Trial)
public void closeReadContext() {
context.close();
}

}

@Benchmark
@BenchmarkMode(AverageTime)
public void replacementOverhead(BenchmarkState state) {
state.downstreamDriver.submitReplacement(state.rootRef, state.root2);
state.downstreamDriver.submitReplacement(state.rootRef, state.root1);
}

@Benchmark
@BenchmarkMode(AverageTime)
public void replacement(BenchmarkState state) {
state.driver.submitReplacement(state.rootRef, state.root2);
state.driver.submitReplacement(state.rootRef, state.root1);
}

}

0 comments on commit 2a831c3

Please sign in to comment.