-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a test to the benchmarks module that runs certain JMH benchmarks with the redhat type pollution agent ( https://github.com/RedHatPerf/type-pollution-agent ) and triggers a test failure if there is excessive type pollution. This works by adding a new test task which includes the agent, and then using JFRUnit to access the JFR events emitted by the agents. If there are too many thrash events for a single concrete type, details are logged and the test fails. Additionally: - Add a JMH benchmark that mimics the TechEmpower benchmark - Fix various type pollution issues that were found using the FullHttpStackBenchmark
- Loading branch information
Showing
11 changed files
with
360 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
benchmarks/src/jmh/java/io/micronaut/http/server/stack/NettyUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package io.micronaut.http.server.stack; | ||
|
||
import io.netty.buffer.ByteBuf; | ||
import io.netty.buffer.CompositeByteBuf; | ||
import io.netty.buffer.PooledByteBufAllocator; | ||
import io.netty.channel.embedded.EmbeddedChannel; | ||
|
||
final class NettyUtil { | ||
static ByteBuf readAllOutboundContiguous(EmbeddedChannel clientChannel) { | ||
ByteBuf requestBytes = PooledByteBufAllocator.DEFAULT.buffer(); | ||
while (true) { | ||
ByteBuf part = clientChannel.readOutbound(); | ||
if (part == null) { | ||
break; | ||
} | ||
requestBytes.writeBytes(part); | ||
} | ||
return requestBytes; | ||
} | ||
|
||
static ByteBuf readAllOutboundComposite(EmbeddedChannel channel) { | ||
CompositeByteBuf response = PooledByteBufAllocator.DEFAULT.compositeBuffer(); | ||
while (true) { | ||
ByteBuf part = channel.readOutbound(); | ||
if (part == null) { | ||
break; | ||
} | ||
response.addComponent(true, part); | ||
} | ||
return response; | ||
} | ||
} |
127 changes: 127 additions & 0 deletions
127
benchmarks/src/jmh/java/io/micronaut/http/server/stack/TfbLikeBenchmark.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
package io.micronaut.http.server.stack; | ||
|
||
import io.micronaut.context.ApplicationContext; | ||
import io.micronaut.context.annotation.Requires; | ||
import io.micronaut.http.MediaType; | ||
import io.micronaut.http.annotation.Controller; | ||
import io.micronaut.http.annotation.Get; | ||
import io.micronaut.http.server.netty.NettyHttpServer; | ||
import io.micronaut.runtime.server.EmbeddedServer; | ||
import io.netty.buffer.ByteBuf; | ||
import io.netty.channel.embedded.EmbeddedChannel; | ||
import io.netty.handler.codec.http.DefaultFullHttpRequest; | ||
import io.netty.handler.codec.http.FullHttpRequest; | ||
import io.netty.handler.codec.http.FullHttpResponse; | ||
import io.netty.handler.codec.http.HttpClientCodec; | ||
import io.netty.handler.codec.http.HttpHeaderNames; | ||
import io.netty.handler.codec.http.HttpMethod; | ||
import io.netty.handler.codec.http.HttpObjectAggregator; | ||
import io.netty.handler.codec.http.HttpResponseStatus; | ||
import io.netty.handler.codec.http.HttpVersion; | ||
import org.junit.jupiter.api.Assertions; | ||
import org.openjdk.jmh.annotations.Benchmark; | ||
import org.openjdk.jmh.annotations.Mode; | ||
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.runner.Runner; | ||
import org.openjdk.jmh.runner.RunnerException; | ||
import org.openjdk.jmh.runner.options.Options; | ||
import org.openjdk.jmh.runner.options.OptionsBuilder; | ||
|
||
import java.nio.charset.StandardCharsets; | ||
import java.util.Map; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
/** | ||
* JMH benchmark that mimics the TechEmpower framework benchmarks. | ||
*/ | ||
public class TfbLikeBenchmark { | ||
public static void main(String[] args) throws RunnerException { | ||
Options opt = new OptionsBuilder() | ||
.include(TfbLikeBenchmark.class.getName() + ".*") | ||
.warmupIterations(20) | ||
.measurementIterations(30) | ||
.mode(Mode.AverageTime) | ||
.timeUnit(TimeUnit.NANOSECONDS) | ||
.forks(1) | ||
.build(); | ||
|
||
new Runner(opt).run(); | ||
} | ||
|
||
@Benchmark | ||
public void test(Holder holder) { | ||
ByteBuf response = holder.exchange(); | ||
if (!holder.responseBytes.equals(response)) { | ||
throw new AssertionError("Response did not match"); | ||
} | ||
response.release(); | ||
} | ||
|
||
@State(Scope.Thread) | ||
public static class Holder { | ||
ApplicationContext ctx; | ||
EmbeddedChannel channel; | ||
ByteBuf requestBytes; | ||
ByteBuf responseBytes; | ||
|
||
@Setup | ||
public void setUp() { | ||
ctx = ApplicationContext.run(Map.of( | ||
"spec.name", "TfbLikeBenchmark", | ||
"micronaut.server.date-header", false // disabling this makes the response identical each time | ||
)); | ||
EmbeddedServer server = ctx.getBean(EmbeddedServer.class); | ||
channel = ((NettyHttpServer) server).buildEmbeddedChannel(false); | ||
|
||
EmbeddedChannel clientChannel = new EmbeddedChannel(); | ||
clientChannel.pipeline().addLast(new HttpClientCodec()); | ||
clientChannel.pipeline().addLast(new HttpObjectAggregator(1000)); | ||
|
||
FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/plaintext"); | ||
request.headers().add(HttpHeaderNames.ACCEPT, "text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7"); | ||
clientChannel.writeOutbound(request); | ||
clientChannel.flushOutbound(); | ||
|
||
requestBytes = NettyUtil.readAllOutboundContiguous(clientChannel); | ||
|
||
// sanity check: run req/resp once and see that the response is correct | ||
responseBytes = exchange(); | ||
clientChannel.writeInbound(responseBytes.retainedDuplicate()); | ||
FullHttpResponse response = clientChannel.readInbound(); | ||
Assertions.assertEquals(HttpResponseStatus.OK, response.status()); | ||
Assertions.assertEquals("text/plain", response.headers().get(HttpHeaderNames.CONTENT_TYPE)); | ||
String expectedResponseBody = "Hello, World!"; | ||
Assertions.assertEquals(expectedResponseBody, response.content().toString(StandardCharsets.UTF_8)); | ||
Assertions.assertEquals(expectedResponseBody.length(), response.headers().getInt(HttpHeaderNames.CONTENT_LENGTH)); | ||
response.release(); | ||
} | ||
|
||
ByteBuf exchange() { | ||
channel.writeInbound(requestBytes.retainedDuplicate()); | ||
channel.runPendingTasks(); | ||
return NettyUtil.readAllOutboundComposite(channel); | ||
} | ||
|
||
@TearDown | ||
public void tearDown() { | ||
ctx.close(); | ||
requestBytes.release(); | ||
responseBytes.release(); | ||
} | ||
} | ||
|
||
@Controller("/plaintext") | ||
@Requires(property = "spec.name", value = "TfbLikeBenchmark") | ||
static class PlainTextController { | ||
|
||
private static final byte[] TEXT = "Hello, World!".getBytes(StandardCharsets.UTF_8); | ||
|
||
@Get(value = "/", produces = MediaType.TEXT_PLAIN) | ||
public byte[] getPlainText() { | ||
return TEXT; | ||
} | ||
} | ||
} |
109 changes: 109 additions & 0 deletions
109
benchmarks/src/typeCheckTest/java/example/TypeThrashingTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package example; | ||
|
||
import io.micronaut.http.server.stack.FullHttpStackBenchmark; | ||
import io.micronaut.http.server.stack.TfbLikeBenchmark; | ||
import io.micronaut.test.typepollution.FocusListener; | ||
import io.micronaut.test.typepollution.ThresholdFocusListener; | ||
import io.micronaut.test.typepollution.TypePollutionTransformer; | ||
import io.netty.handler.codec.http.DefaultFullHttpResponse; | ||
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.Disabled; | ||
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; | ||
|
||
static { | ||
FullHttpStackBenchmark.checkFtlThread = false; | ||
} | ||
|
||
private ThresholdFocusListener focusListener; | ||
|
||
@BeforeAll | ||
static void setupAgent() { | ||
TypePollutionTransformer.install(net.bytebuddy.agent.ByteBuddyAgent.install()); | ||
} | ||
|
||
@BeforeEach | ||
void setUp() { | ||
focusListener = new ThresholdFocusListener(); | ||
FocusListener.setFocusListener((concreteType, interfaceType) -> { | ||
if (concreteType == DefaultFullHttpResponse.class) { | ||
String culprit = StackWalker.getInstance().walk(s -> s.skip(1).dropWhile(f -> f.getClassName().startsWith("io.micronaut.test.")).findFirst().map(StackWalker.StackFrame::getClassName).orElse(null)); | ||
if (culprit != null && (culprit.startsWith("io.netty") || culprit.equals("io.micronaut.http.server.netty.handler.Compressor"))) { | ||
// these DefaultFullHttpResponse flips are false positives, fixed by franz | ||
return; | ||
} | ||
} | ||
|
||
focusListener.onFocus(concreteType, interfaceType); | ||
}); | ||
} | ||
|
||
@AfterEach | ||
void verifyNoTypeThrashing() { | ||
FocusListener.setFocusListener(null); | ||
Assertions.assertTrue(focusListener.checkThresholds(THRESHOLD), "Threshold exceeded, check logs."); | ||
} | ||
|
||
/** | ||
* This is a sample method that demonstrates the thrashing detection. This test should fail | ||
* when enabled. | ||
*/ | ||
@SuppressWarnings("ConstantValue") | ||
@Test | ||
@Disabled | ||
public void sample() { | ||
Object c = new Concrete(); | ||
int j = 0; | ||
for (int i = 0; i < THRESHOLD * 2; i++) { | ||
if (c instanceof A) { | ||
j++; | ||
} | ||
if (c instanceof B) { | ||
j++; | ||
} | ||
} | ||
System.out.println(j); | ||
} | ||
|
||
interface A { | ||
} | ||
|
||
interface B { | ||
} | ||
|
||
static class Concrete implements A, B { | ||
} | ||
|
||
@Test | ||
public void testFromJmh() throws RunnerException { | ||
Options opt = new OptionsBuilder() | ||
.include(Stream.of(FullHttpStackBenchmark.class, TfbLikeBenchmark.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(); | ||
} | ||
} |
Oops, something went wrong.