Introducing Micronaut® AOT: build-time optimizations for your Micronaut applications

Cédric Champeau
graalvm
Published in
5 min readDec 20, 2021

This is a guest blog post by Cédric Champeau.

The Micronaut® Framework is a modern full-stack toolkit backed by the Micronaut Foundation¹. Today the Micronaut Foundation announced a new module in the Micronaut family called Micronaut AOT (for “ahead-of-time”, which in this particular context means build time). Micronaut AOT, developed by Oracle Labs, is an open-source extension to the framework that applies dynamic analysis at build time to optimize Micronaut applications for specific deployment environments.

Micronaut AOT can optimize applications differently according to the target deployment environment and technology stack. For example, it can generate specific optimizations based on whether you will be running the application as a JVM application, or a native executable built with GraalVM Native Image!

As an illustration, let’s look at the startup time of a simple Micronaut application compiled with GraalVM Native Image:

10:56:08.699 [main] INFO io.micronaut.runtime.Micronaut — Startup completed in 133ms. Server Running: http://localhost:8181

You can see that the application started in 133ms, which is already quite fast but not very impressive. We’ve certainly seen native applications starting faster than that. If we try multiple times in a row, we will actually see quite a lot of variance: 122ms, 145ms, etc. On my machine I even saw startup times of 800+ms! To understand this behavior, we need to explain a particular feature which is enabled by default. The framework deduces the deployment environment at runtime and configures the application specifically for, say, Oracle Cloud or AWS when it is deployed. To do this, the framework needs to perform some checks, which involve network requests. This is all done at application startup. Let’s see what the same application startup time is after being optimized by Micronaut AOT:

11:04:05.373 [main] INFO io.micronaut.runtime.Micronaut — Startup completed in 7ms. Server Running: http://localhost:8181

The same application started in just 7ms! And if we retry, this time we get very low variance: between 6 to 8ms startup times. How impressive! This is because Micronaut AOT performed different optimizations, including moving the environment detection from run time to build time. Of course, this means that the application must be built in the same environment it’s going to be deployed to, but that’s the kind of tradeoffs a DevOps team can make to improve startup time and reduce costs.

Optimizations for the JVM too

Not only is Micronaut AOT capable of optimizing native applications, it is also capable of doing the same for applications that run on a “traditional” JVM: it was a design goal to have an optimization framework which can generate different optimizations for native images and for JVM bytecode. To illustrate this, let’s look at the same application startup times in JVM (or JIT, just-in-time compiler) mode. First, without Micronaut AOT optimizations:

11:22:36.852 [main] INFO io.micronaut.runtime.Micronaut — Startup completed in 723ms. Server Running: http://localhost:8181

Unsurprisingly, startup times are slower than in native mode. Now let’s start the same app with the optimizations:

11:23:30.696 [main] INFO io.micronaut.runtime.Micronaut — Startup completed in 419ms. Server Running: http://localhost:8181

We’ve reduced startup time to 58% of the original one, almost twice as fast!

Micronaut AOT startup performance improvements

How does it work?

Micronaut AOT reduces application startup time and deployment size by executing a number of operations during the build. It can precompute bean requirements and perform substitutions at build time, so that only classes that are going to be used in production are included. In the example above, the native executable size was reduced from 55MB to 53MB. You can expect more improvements in memory consumption, startup performance and image size as we ship new Micronaut AOT releases. We designed Micronaut AOT with extension in mind, so other Micronaut libraries can deliver their own optimizations.

In a nutshell, Micronaut AOT is a toolkit which analyzes your Micronaut application and generates sources, resources and metadata which are then exploited by the build tool to generate an optimized application.

Building a native AOT-optimized application (same workflow applies for JVM with different generated sources)

Several optimizations are currently available:

- optimize service loading by pre-scanning the list of available services and implementing a loading strategy that’s fully parallel in the JVM and serial in GraalVM native executables (because classloading is effectively free in native executables)

- convert YAML configuration to Java configuration to make apps startup faster, while reducing the final binary size because YAML parsing is no longer necessary

- cache the environment so that once the application is started, the framework assumes that system properties and environment variables won’t change, saving time when performing lookups

- precompute bean requirements to eliminate beans whose requirements won’t be met at runtime (this can happen if you have transitive dependencies bringing beans that you don’t use, for example)

- deduce the environment at build time, which is the major optimization we used in the example above

- precompute some expensive operations, like converting environment variable names to Micronaut configuration properties

- optimize classloading by avoiding lookup for classes that we know are not on classpath

All those optimizations are opt-in and should be used with an understanding of implications; some of them require you to build on the same environment as deployment, while some others are completely agnostic. Depending on your application and use cases, your mileage may vary.

Trying it out

Micronaut AOT is currently only available if you use at least version 3.2 of the Micronaut framework and the Micronaut Gradle plugin version 3.1.0 or later (Maven support will be added in the future). It is still in experimental stages. Do not hesitate to report bugs or request improvements on our issue tracker. At the moment, AOT can only optimize applications (future work will allow optimization of libraries and cloud functions).

To get started, you will need to apply the io.micronaut.aotplugin which will add an aot configuration block to your build script (the code below works on both the Groovy and Kotlin Gradle DSLs):

That’s all you need. You can run any of those Gradle tasks to try your Micronaut AOT-optimized application:

  • ./gradlew nativeOptimizedCompilewill build an AOT-optimized native-image
  • ./gradlew nativeOptimizedRun will do the same but start the application right after building
  • ./gradlew optimizedRun does the same in “JVM” mode (so it’s similar to run except that it runs the AOT-optimized version)

Micronaut AOT, similarly to what the Micronaut Gradle plugin does by default, supports building AOT-optimized Docker images.

Please refer to the AOT section of the Gradle plugin documentation for more details on available tasks and configuration.

Summary

Micronaut AOT is an extension to the Micronaut Framework which is the foundation to many optimizations that can be implemented at build time but weren’t possible solely with annotation processing. By effectively analyzing the deployment environment, AOT is capable of reducing startup times or distribution size for both native and JVM deliverables.

¹ Micronaut® is a registered trademark of Object Computing, Inc.

--

--

Cédric Champeau
graalvm

Consulting member of technical staff at Oracle Labs, working on the Micronaut Framework. Introvert (http://www.carlkingdom.com/10-myths-about-introverts).