Github repo

Goal of this simple benchmark

I watched a nice comparison made by Micronaut’s creator Graeme Rocher:
Micronaut 2.0 vs Quarkus 1.3.1 vs Spring Boot 2.3 Performance on JDK 14

And I read the nice post “A IO thread and a worker thread walk into a bar: a microbenchmark story” by Emmanuel Bernard explaining the benchmark made above.

So the goal of this benchmark is:

  • to include the reactive approach presented in the Quarkus post;
  • also compare the native images produced by by GraalVM for both frameworks;
  • introduce two new tests that have block operations.

Spoiler regarding native image

As the JVM warms up, it has much higher throughput than native images.
But for short lived functions the native image is better, because of the startup time.

  • without -Xmx128m
    • Quarkus JVM - 244,9 MiB (RSS) - Requests per second: 2399.87 -> 28531.32
    • Quarkus Native - 300 MiB (RSS) - Requests per second: 18108.92
  • with -Xmx128m
    • Quarkus JVM - 263,9 MiB (RSS) - Requests per second: 2539.22 -> 28841.80
    • Quarkus Native - 54 MiB (RSS) - Requests per second: 1951.44

JDK and Java versions

$ java --version
openjdk 11.0.8 2020-07-14
OpenJDK Runtime Environment (build 11.0.8+10-post-Ubuntu-0ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.8+10-post-Ubuntu-0ubuntu118.04.1, mixed mode, sharing)
$ javac --version
javac 11.0.8

ps: I was first running with the version below, but then I found that it has some memory issues :/

$ java --version
openjdk 11.0.7 2020-04-14
OpenJDK Runtime Environment GraalVM CE 20.1.0 (build 11.0.7+10-jvmci-20.1-b02)
OpenJDK 64-Bit Server VM GraalVM CE 20.1.0 (build 11.0.7+10-jvmci-20.1-b02, mixed mode, sharing)

Hardware/System Config

  • Ubuntu 18.04.4 LTS
  • 64-bit
  • RAM: 7,7 GB
  • Intel® Core™ i5-7200U CPU @ 2.50GHz × 4
$ uname -a
Linux 4.15.0-112-generic #113-Ubuntu SMP Thu Jul 9 23:41:39 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

Quarkus IO Thread vs Worker Pool

To compare the ‘reactive’ style I copied the code from the quarkus-io-thread folder:
https://github.com/johnaohara/quarkus-iothread-workerpool/tree/1.3.1.Final

For the tests I called:

  • ‘Worker Pool’ -> imperative
  • ‘IO Thread’ -> reactive

Let’s start!

The instructions to create the native images are at the end of this post.

./mvnw clean compile

Micronaut 2.0.0.M2 Quarkus 1.3.1.Final (imperative) Quarkus 1.3.1.Final (reactive)
2.030 s 2.543 s 2.692 s

./mvnw test

Micronaut 2.0.0.M2 Quarkus 1.3.1.Final (imperative) Quarkus 1.3.1.Final (reactive)
4.093 s 7.410 s 7.326 s

./mvnw clean package

Micronaut 2.0.0.M2 Quarkus 1.3.1.Final (imperative) Quarkus 1.3.1.Final (reactive)
6.748 s 12.910 s 13.131 s

Time it takes to start in development mode

  • Micronaut: ./mvnw mn:run
  • Quarkus: ./mvnw quarkus:dev
Micronaut 2.0.0.M2 Quarkus 1.3.1.Final (imperative) Quarkus 1.3.1.Final (reactive)
718 ms 1.420 s 1.252 s

ps: if you disable debug for Quarkus ( -Ddebug ), it will save you around 60 ms … not worth the effort.

It’s nice that both frameworks detect changes:

  • Micronaut
    [INFO] Detected change in .../framework-comparison-2020/micronaut-example-2.0.0.M2/src/main/java/micronaut/example/MessageService.java
    [INFO] Using 'UTF-8' encoding to copy filtered resources.
    [INFO] Copying 2 resources
    [INFO] Changes detected - recompiling the module!
    [INFO] Compiling 4 source files to .../framework-comparison-2020/micronaut-example-2.0.0.M2/target/classes
    
  • Quarkus
    2020-08-07 12:15:47,111 INFO  [io.qua.dev] (vert.x-worker-thread-0) Changed source files detected, recompiling [.../framework-comparison-2020/quarkus-example-reactive-1.3.1.Final/src/main/java/org/acme/MessageService.java]
    2020-08-07 12:15:47,487 INFO  [io.quarkus] (vert.x-worker-thread-0) Quarkus stopped in 0.008s
    __  ____  __  _____   ___  __ ____  ______ 
     --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
     -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
    --\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
    2020-08-07 12:15:47,762 INFO  [io.quarkus] (vert.x-worker-thread-0) code-with-quarkus 1.0.0-SNAPSHOT (powered by Quarkus 1.3.1.Final) started in 0.043s. Listening on: http://0.0.0.0:8080
    2020-08-07 12:15:47,762 INFO  [io.quarkus] (vert.x-worker-thread-0) Profile dev activated. Live Coding activated.
    2020-08-07 12:15:47,762 INFO  [io.quarkus] (vert.x-worker-thread-0) Installed features: [cdi, hibernate-validator, mutiny, vertx, vertx-web]
    2020-08-07 12:15:47,762 INFO  [io.qua.dev] (vert.x-worker-thread-0) Hot replace total time: 0.653s
    

Time it takes to start the jars

java -jar target/micronaut-example-0.1.jar
java -jar target/code-with-quarkus-1.0.0-SNAPSHOT-runner.jar
java -jar target/quarkus-io-thread-runner.jar

Micronaut 2.0.0.M2 Quarkus 1.3.1.Final (imperative) Quarkus 1.3.1.Final (reactive)
827 ms 1.182 s 1.088 s

Time to First Response

It was measured using the time.js script, eg: $ node time.js micronaut-example-0.1.jar

And for the native images: $ node time-native.js micronaut-example

Micronaut 2.0.0.M2 Quarkus 1.3.1.Final (imperative) Quarkus 1.3.1.Final (reactive) Native Quarkus 1.3.1.Final (imperative) Native Quarkus 1.3.1.Final (reactive) Native Micronaut 2.0.0.M2
1428 ms 1499 ms 1301 ms 26 ms 25 ms 36 ms

ab - Requests per Second: /hello/John

java -jar path/to/jar.jar
ab -k -c 20 -n 10000 http://localhost:8080/hello/John

  • -k: keep-alive
  • -c 20: 20 concurrency requests
  • -n 10000: 10.000 requests
Micronaut 2.0.0.M2 Quarkus 1.3.1.Final (imperative) Quarkus 1.3.1.Final (reactive) Native Quarkus 1.3.1.Final (imperative) Native Quarkus 1.3.1.Final (reactive) Native Micronaut 2.0.0.M2
38551.84 27956.78 78252.77 17315.99 39053.04 19053.28

You need to run multiple times to warmp up the JVM!
And it’s incredible how the number grows. For example, for Quarkus (reactive), it starts at 5563.98 and goes to 78252.77

  • ps: I’m not sure this “reactive” style is how most people would code in Quarkus. It’s also a bit lower abstraction. But it’s good to know that we have it at our disposal.

Also one important point about the reactive approach, from Vert.x docs:

The Golden Rule - Don’t Block the Event Loop

We already know that the Vert.x APIs are non blocking and won’t block the event loop, but that’s not much help if you block the event loop yourself in a handler.

If you do that, then that event loop will not be able to do anything else while it’s blocked. If you block all of the event loops in Vertx instance then your application will grind to a complete halt!

So don’t do it! You have been warned.

So let’s do some testings introducing just a little blocking …

ab Test Sleeping 2 milliseconds each 5 requests: /split/John

Micronaut 2.0.0.M2 Quarkus 1.3.1.Final (imperative) Quarkus 1.3.1.Final (reactive) Native Quarkus 1.3.1.Final (imperative) Native Quarkus 1.3.1.Final (reactive) Native Micronaut 2.0.0.M2
14816.86 24395 17365.75 15355.25 14060.33 13223.58

That’s an interesting result! Because this is probably the scenario that you will find in most applications.

It’s nice explained here Quarkus IO Thread Microbencmark:

“When your workload needs to interact with a database or another remote service, it relies on blocking IO. The thread is blocked waiting for the answer. Other requests running on different threads are not slowed down significantly. But this means one thread for every concurrent request, which limits the overall concurrency.”

ab Test Sleeping 2 milliseconds for each request: /sleep

Micronaut 2.0.0.M2 Quarkus 1.3.1.Final (imperative) Quarkus 1.3.1.Final (reactive) Native Quarkus 1.3.1.Final (imperative) Native Quarkus 1.3.1.Final (reactive) Native Micronaut 2.0.0.M2
3137.98 8692.10 3419.26 8457.52 2970.71 2844.78

Memory Comsumption - RSS

!!! Huge difference between JVMs below !!!

  • openjdk 11.0.8 2020-07-14 (1) OpenJDK Runtime Environment (build 11.0.8+10-post-Ubuntu-0ubuntu118.04.1)
  • openjdk 11.0.7 2020-04-14 (2) OpenJDK Runtime Environment GraalVM CE 20.1.0 (build 11.0.7+10-jvmci-20.1-b02)

So I ran with both versions …

Mesuared after running 15x the command below:

ab -k -c 20 -n 10000 http://localhost:8080/hello/John

No -Xmx - openjdk 11.0.8 2020-07-14 (1)

Micronaut 2.0.0.M2 Quarkus 1.3.1.Final (imperative) Quarkus 1.3.1.Final (reactive) Native Quarkus 1.3.1.Final (imperative) Native Quarkus 1.3.1.Final (reactive) Native Micronaut 2.0.0.M2
345,8 MiB 257,5 MiB 200,9 MiB 293,1 MiB 282,6 MiB 427,8 MiB

With -Xmx128m - openjdk 11.0.8 2020-07-14 (1)

Micronaut 2.0.0.M2 Quarkus 1.3.1.Final (imperative) Quarkus 1.3.1.Final (reactive) Native Quarkus 1.3.1.Final (imperative) Native Quarkus 1.3.1.Final (reactive) Native Micronaut 2.0.0.M2
233,6 MiB 270,6 MiB 200,1 MiB 52 MiB 42 MiB 73,5 MiB ¹

¹ You need to run the Micronaut native image like this:

./micronaut-example -Xmx128m -Dio.netty.allocator.maxOrder=8

If you don’t pass the parameter -Dio.netty.allocator.maxOrder=8, you will get the error below. Thank you Adam Kobor for mentioning this.

11:47:11.021 [nioEventLoopGroup-1-9] ERROR i.m.h.s.netty.RoutingInBoundHandler - Micronaut Server Error - No request state present. Cause: Direct buffer memory
java.lang.OutOfMemoryError: Direct buffer memory
	at java.nio.Bits.reserveMemory(Bits.java:175)
	at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:118)
	at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:317)
	at io.netty.buffer.PoolArena$DirectArena.allocateDirect(PoolArena.java:758)
	at io.netty.buffer.PoolArena$DirectArena.newChunk(PoolArena.java:734)
	at io.netty.buffer.PoolArena.allocateNormal(PoolArena.java:245)
	at io.netty.buffer.PoolArena.allocate(PoolArena.java:215)
	at io.netty.buffer.PoolArena.allocate(PoolArena.java:147)
	at io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:356)
	at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187)
	at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:178)
	at io.netty.buffer.AbstractByteBufAllocator.ioBuffer(AbstractByteBufAllocator.java:139)
	at io.netty.channel.DefaultMaxMessagesRecvByteBufAllocator$MaxMessageHandle.allocate(DefaultMaxMessagesRecvByteBufAllocator.java:114)
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:147)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.lang.Thread.run(Thread.java:834)
	at com.oracle.svm.core.thread.JavaThreads.threadStartRoutine(JavaThreads.java:517)
	at com.oracle.svm.core.posix.thread.PosixJavaThreads.pthreadStartRoutine(PosixJavaThreads.java:193)

No -Xmx - GraalVM runtime (2)

Micronaut 2.0.0.M2 Quarkus 1.3.1.Final (imperative) Quarkus 1.3.1.Final (reactive)
608,4 MiB 486,1 MiB 467,6 MiB

With -Xmx128m - GraalVM runtime (2)

Micronaut 2.0.0.M2 Quarkus 1.3.1.Final (imperative) Quarkus 1.3.1.Final (reactive)
498,8 MiB 482,2 MiB 456 MiB

Conclusion

Even what seems a simple benchmark can be hard to do :( …
JDK and JVM versions and their configurations can have great impact on the benchmark.

Both frameworks are fast, use low memory, offers great productivity and have support for native images using GraalVM.

People should also consider other factors when making their choice:

  • Size of community
  • How active is the project
  • How often regressions happen
  • How fast bugs are fixed, and if the fixes are backported¹
  • Quality of documentation
  • Company support

¹ Or should we always migrate to new releases if we want the fixes? I understand it must be a real trouble to have a different release only with the bug fixes. But otherwise users can be stuck in an endless loop: bugs -> upgrade -> bugs

Creating Native Images - GraalVM

I used this version of GraalVM to compile both frameworks:

graalvm-ce-java11-linux-amd64-20.1.0.tar.gz

Instructions for Quarkus

Instructions for Quarkus and GraalVM

$ ./mvnw package -Pnative

The binary was built faster than I thought! 2:17min

[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]    classlist:   5,491.56 ms,  0.96 GB
[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]        (cap):   2,335.22 ms,  0.94 GB
[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]        setup:   4,851.33 ms,  0.94 GB
20:14:27,377 INFO  [org.hib.val.int.uti.Version] HV000001: Hibernate Validator 6.1.2.Final
20:14:29,779 INFO  [org.jbo.threads] JBoss Threads version 3.0.1.Final
[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]     (clinit):     549.80 ms,  2.62 GB
[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]   (typeflow):  25,044.51 ms,  2.62 GB
[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]    (objects):  20,975.97 ms,  2.62 GB
[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]   (features):     470.03 ms,  2.62 GB
[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]     analysis:  48,785.14 ms,  2.62 GB
[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]     universe:   1,254.35 ms,  2.62 GB
[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]      (parse):   5,190.79 ms,  2.62 GB
[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]     (inline):   7,324.82 ms,  2.94 GB
[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]    (compile):  37,087.20 ms,  2.92 GB
[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]      compile:  51,278.90 ms,  2.92 GB
[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]        image:   3,023.71 ms,  2.88 GB
[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]        write:   3,567.54 ms,  2.88 GB
[code-with-quarkus-1.0.0-SNAPSHOT-runner:30080]      [total]: 119,568.72 ms,  2.88 GB
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 125054ms
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  02:17 min
[INFO] Finished at: 2020-08-08T20:16:17-03:00
[INFO] ------------------------------------------------------------------------

$ ./target/code-with-quarkus-1.0.0-SNAPSHOT-runner

__  ____  __  _____   ___  __ ____  ______ 
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ 
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \   
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/   
2020-08-08 12:08:15,077 INFO  [io.quarkus] (main) code-with-quarkus 1.0.0-SNAPSHOT (powered by Quarkus 1.3.1.Final) started in 0.011s. Listening on: http://0.0.0.0:8080
2020-08-08 12:08:15,077 INFO  [io.quarkus] (main) Profile prod activated. 
2020-08-08 12:08:15,077 INFO  [io.quarkus] (main) Installed features: [cdi, hibernate-validator, resteasy]

Instructions for Micronaut

$ native-image –no-server -cp target/micronaut-example-0.1.jar

[micronaut-example:12298]    classlist:   4,894.61 ms,  0.94 GB
[micronaut-example:12298]        (cap):   2,885.53 ms,  0.94 GB
[micronaut-example:12298]        setup:   6,435.89 ms,  0.94 GB
WARNING GR-10238: VarHandle for static field is currently not fully supported. Static field private static volatile java.lang.System$Logger jdk.internal.event.EventHelper.securityLogger is not properly marked for Unsafe access!
Warning: class initialization of class io.micronaut.http.bind.binders.ContinuationArgumentBinder failed with exception java.lang.NoClassDefFoundError: kotlin/TypeCastException. This class will be initialized at run time because option --allow-incomplete-classpath is used for image building. Use the option --initialize-at-run-time=io.micronaut.http.bind.binders.ContinuationArgumentBinder to explicitly request delayed initialization of this class.
Warning: class initialization of class io.micronaut.http.bind.binders.ContinuationArgumentBinder$Companion failed with exception java.lang.NoClassDefFoundError: kotlin/TypeCastException. This class will be initialized at run time because option --allow-incomplete-classpath is used for image building. Use the option --initialize-at-run-time=io.micronaut.http.bind.binders.ContinuationArgumentBinder$Companion to explicitly request delayed initialization of this class.
[micronaut-example:12298]     (clinit):   1,031.40 ms,  2.88 GB
[micronaut-example:12298]   (typeflow):  33,285.50 ms,  2.88 GB
[micronaut-example:12298]    (objects):  31,182.64 ms,  2.88 GB
[micronaut-example:12298]   (features):   3,973.39 ms,  2.88 GB
[micronaut-example:12298]     analysis:  72,308.19 ms,  2.88 GB
[micronaut-example:12298]     universe:   2,084.96 ms,  2.91 GB
[micronaut-example:12298]      (parse):   8,151.94 ms,  2.93 GB
[micronaut-example:12298]     (inline):  11,121.85 ms,  3.27 GB
[micronaut-example:12298]    (compile):  57,107.71 ms,  3.27 GB
[micronaut-example:12298]      compile:  78,775.50 ms,  3.27 GB
[micronaut-example:12298]        image:   4,824.30 ms,  3.26 GB
[micronaut-example:12298]        write:   9,926.53 ms,  3.26 GB
[micronaut-example:12298]      [total]: 180,872.39 ms,  3.26 GB

$ ./micronaut-example

13:57:25.508 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 11ms. Server Running: http://localhost:8080