Skip to content

Commit

Permalink
Merge pull request #897 from micronaut-projects/thrash-test
Browse files Browse the repository at this point in the history
Add type thrashing test
  • Loading branch information
graemerocher authored Jul 23, 2024
2 parents 680d114 + 615ce93 commit c14dbfd
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 8 deletions.
29 changes: 29 additions & 0 deletions benchmarks/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ plugins {
id("me.champeau.jmh") version "0.7.2"
}

sourceSets {
create("typeCheckTest") {
compileClasspath += sourceSets.jmh.output
runtimeClasspath += sourceSets.jmh.output
}
}

dependencies {
annotationProcessor(projects.micronautSerdeProcessor)
annotationProcessor(mn.micronaut.inject.java)
Expand All @@ -19,6 +26,16 @@ dependencies {

jmh(libs.jmh.core)
runtimeOnly(mnLogging.logback.classic)

typeCheckTestImplementation(mnTest.junit.jupiter.engine)
typeCheckTestImplementation(mnTest.micronaut.test.type.pollution)
typeCheckTestImplementation(mnTest.bytebuddy)
typeCheckTestImplementation(mnTest.bytebuddy.agent)
}

configurations {
typeCheckTestImplementation.extendsFrom(jmhImplementation, implementation)
typeCheckTestRuntimeOnly.extendsFrom(jmhRuntimeOnly, runtimeOnly)
}

jmh {
Expand All @@ -29,6 +46,18 @@ jmh {
duplicateClassesStrategy = DuplicatesStrategy.EXCLUDE
}

tasks.register('typeCheckTest', Test) {
description = "Runs type check tests."
group = "verification"

testClassesDirs = sourceSets.typeCheckTest.output.classesDirs
classpath = sourceSets.typeCheckTest.runtimeClasspath

useJUnitPlatform()
}

check.dependsOn typeCheckTest

shadowJar {
mergeServiceFiles()
}
Expand Down
60 changes: 60 additions & 0 deletions benchmarks/src/jmh/java/io/micronaut/serde/ComboBenchmark.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.micronaut.serde;

import io.micronaut.context.ApplicationContext;
import io.micronaut.core.type.Argument;
import io.micronaut.json.JsonMapper;
import io.micronaut.serde.data.StringArrayField;
import io.micronaut.serde.jackson.JacksonJsonMapper;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Param;
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.infra.Blackhole;

import java.io.IOException;

public class ComboBenchmark {

@State(Scope.Thread)
public static class Holder {
@Param
TestCase testCase;

JsonMapper jsonMapper;
ApplicationContext ctx;

@Setup
public void setUp() {
ctx = ApplicationContext.run();
jsonMapper = ctx.getBean(JacksonJsonMapper.class).createSpecific(testCase.type);
}

@TearDown
public void tearDown() {
ctx.close();
}
}

public enum TestCase {
STRING_ARRAY_FIELD(Argument.of(StringArrayField.class), new StringArrayField() {{ strs = new String[] { "foo", "bar" }; }}),
STRING(Argument.of(String.class), "foo");

final Argument<?> type;
final Object input;

TestCase(Argument<?> type, Object input) {
this.type = type;
this.input = input;
}
}

@SuppressWarnings({"rawtypes", "unchecked"})
@Benchmark
public Object serde(Holder holder, Blackhole bh) throws IOException {
byte[] bytes = holder.jsonMapper.writeValueAsBytes((Argument) holder.testCase.type, holder.testCase.input);
bh.consume(bytes);
return holder.jsonMapper.readValue(bytes, holder.testCase.type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.micronaut.serde;

import io.micronaut.test.typepollution.FocusListener;
import io.micronaut.test.typepollution.ThresholdFocusListener;
import io.micronaut.test.typepollution.TypePollutionTransformer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class TypeThrashingTest {
static final int THRESHOLD = 10_000;

private ThresholdFocusListener focusListener;

@BeforeAll
static void setupAgent() {
TypePollutionTransformer.install(net.bytebuddy.agent.ByteBuddyAgent.install());
}

@BeforeEach
void setUp() {
focusListener = new ThresholdFocusListener();
FocusListener.setFocusListener(focusListener);
}

@AfterEach
void verifyNoTypeThrashing() {
FocusListener.setFocusListener(null);
Assertions.assertTrue(focusListener.checkThresholds(THRESHOLD), "Threshold exceeded, check logs.");
}

@Test
public void testFromJmh() throws RunnerException {
Options opt = new OptionsBuilder()
.include(Stream.of(ComboBenchmark.class)
.map(Class::getName)
.collect(Collectors.joining("|", "(", ")"))
+ ".*")
.warmupIterations(0)
.measurementIterations(1)
.mode(Mode.SingleShotTime)
.timeUnit(TimeUnit.NANOSECONDS)
.forks(0)
.measurementBatchSize(THRESHOLD * 2)
.shouldFailOnError(true)
.build();

new Runner(opt).run();
}
}
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ groovy = "4.0.18"
micronaut = "4.5.4"
micronaut-platform = "4.5.0"
micronaut-docs = "2.0.0"
micronaut-test = "4.3.0"
micronaut-test = "4.4.0"
micronaut-discovery = "4.3.0"
micronaut-logging = "1.2.3"
micronaut-oracle-cloud = "4.1.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,10 @@ public class DefaultSerdeRegistry implements SerdeRegistry {
private final List<BeanDefinition<Deserializer>> deserializers = new ArrayList<>(100);
private final List<BeanDefinition<Serde>> internalSerdes = new ArrayList<>(100);

private final Map<TypeKey, Serializer<?>> serializerMap = new ConcurrentHashMap<>(50);
// if there is a single Serde that is part of the serializerMap *and* deserializerMap, this can
// lead to interface type check thrashing. For that reason, we wrap the serializer side with
// a wrapper object.
private final Map<TypeKey, SerializerWrapper> serializerMap = new ConcurrentHashMap<>(50);
private final Map<TypeKey, Deserializer<?>> deserializerMap = new ConcurrentHashMap<>(50);

private final BeanContext beanContext;
Expand Down Expand Up @@ -501,9 +504,9 @@ public <T> Collection<BeanIntrospection<? extends T>> getDeserializableSubtypes(
public <T> Serializer<? super T> findSerializer(Argument<? extends T> type) throws SerdeException {
Objects.requireNonNull(type, "Type cannot be null");
final TypeKey key = new TypeKey(type);
final Serializer<?> serializer = serializerMap.get(key);
if (serializer != null) {
return (Serializer<? super T>) serializer;
SerializerWrapper wrapper = serializerMap.get(key);
if (wrapper != null) {
return (Serializer<? super T>) wrapper.serializer;
}
if (type.getType().equals(Object.class)) {
return objectSerializer;
Expand All @@ -522,14 +525,14 @@ public <T> Serializer<? super T> findSerializer(Argument<? extends T> type) thro
ser = getBean(definition);
}
if (ser != null) {
serializerMap.put(key, ser);
serializerMap.put(key, new SerializerWrapper(ser));
return (Serializer<? super T>) ser;
}
if (key.getType().isArray()) {
serializerMap.put(key, objectArraySerde);
serializerMap.put(key, new SerializerWrapper(objectArraySerde));
return (Serializer<? super T>) objectArraySerde;
}
serializerMap.put(key, objectSerializer);
serializerMap.put(key, new SerializerWrapper(objectSerializer));
return objectSerializer;
}

Expand Down Expand Up @@ -714,4 +717,6 @@ public String toString() {

}

private record SerializerWrapper(Serializer<?> serializer) {
}
}

0 comments on commit c14dbfd

Please sign in to comment.