Ease creation of GraalVM native images using assisted configuration
This post will show how we can use the new native image trace agent to generate GraalVM native image configuration. The agent can track all usages of dynamic features and thus avoids manual configuration for these aspects.
In a previous article about Fighting cold startup issues for your Kotlin Lambda with GraalVM I have shown how we can speed up cold startup of AWS Lambda functions using GraalVM native images and custom AWS Lambda runtimes.
I played around with this a little bit more to see if the approach is also applicable in larger applications. So I added DynamoDB to the sample and tried to run the result also using a GraalVM native image.
The handler code is still very simple. But adding the AWS SDK for DynamoDB is still making the classpath a lot more complex because it brings in a lot of other dependencies like the Apache HTTP client.
Thus in this scenario the configuration needed to build a GraalVM native images is a lot more complex.
Firstly we need a reflection configuration helping the GraalVM native images tool to add classes it cannot infer through static code analysis because they are used using reflection. In our case this is mostly caused by serialization and deserialization using Jackson. See the reflect.json file in the repo and the documentation on reflection usage and GraalVM native images.
Furthermore some libraries like the Apache HTTP client use Java dynamic proxies. Dynamic proxies are a way to generate dynamic implementations of interfaces at runtime. With this you can route all method invocations through an InvocationHandler. This can be used e.g for AOP-like method interception. SubstrateVM, the VM packaged in a GraalVM machine image, does not provide means for generating and interpreting bytecode at run time. Therefore all dynamic proxy classes need to be generated at native image build time.
In our case this is done via a proxy-conf.json configuration file.
Both files need to be passed to the native image tool as parameters.
But this is still not enough. In one of the dependencies which is brought in with the AWS DynamoDB SDK Apache commons logging is used. To get this to work in a native image we need to replace parts of the implementation. GraalVM offers annotations to tell the native image tool to replace code with different code during the native image build.
And only a few hours of digging through documentation, comments and issue reports later we got the example to work 🤯.
Using the native image trace agent
But there is hope. With release 1.0.0-rc14 GraalVM made all this a lot easier.
We introduced a tracing agent for the Java HotSpot VM that records usages of reflection and JNI that can then be converted to configuration files for the native image generator. This simplifies the process of getting new applications working as native images.
So we can run our code on the HotSpot VM equipped with the mentioned agent. This agents can take the discovered runtime information and can generate configuration files that make all the mentioned steps above obsolete 🎉.
This sounds great — let’s give it a spin.
A straight-forward approach to do this, is to just let this agent run during the JUnit test execution. But this approach was not successful. All the classes that the agent discovers and e.g. adds to the reflection configuration, also need to be present in your real application jar (See https://github.com/oracle/graal/issues/1238). This is very inconvenient. Usually the test classpath is different from the real application classpath. We might have JUnit, Mockito and WireMock on the classpath additionally. So this approach won’t do.
We need to live with the application classpath and go from there. Thus to execute our code while the native image trace agent is running we just go for another main function. We use DynamoDB local running in an external docker container in order to not have it on the classpath. The HTTP calls to the AWS Lambda execution environment that are needed to implement the custom runtime need to be encapsulated in class behind an interface that we can exchange with a mock implementation for the test run (see also example code on the dynamodb-agent branch). When implementing the test code we need to pay close attention that e.g all reflection usages are covered inside test so the agent can discover them.
Now we can run this inside the GraalVM docker image with the trace agent registered. Afterwards we need the native-image-configure tool to translate the resulting trace file into configuration files the native image tool understands. Please see the full sample code in the repo and the GraalVM documenation about assisted configuration). The calls below are needed for GraalVM version 1.0.0-rc15. With rc16 this already become a bit easier. Unfortunately rc16 has a bug that made it unusable in this scenario.
Now the call to the native images tool becomes a bit easier. We can use the ConfigurationFileDirectories parameter to pass in the config directory filled in the previous step.
This builds a native image that can be used to run our handler on AWS Lambda.
Conclusion
The native image trace agent greatly reduces the configuration effort needed to produce a GraalVM native image for more complex application. This feature will ease the adoption of native images.
It would be great if the issue regarding test and application classpath would be solved, so we can just use our normal tests to let the native image trace agent do its work.
The example code can be found in the kotlin-graalvm-custom-aws-lambda-runtime-talk. The version using the native image trace agent resides on the dynamodb-agent branch.