Ballerina Meets GraalVM: From Code to Native Executable
- Krishnanathalingam Tharmigan
- Senior Software Engineer - WSO2
This article is based on Ballerina Swan Lake Update 7 (2201.7.0).
In this article, we will explore how to build a GraalVM native executable for a Ballerina application. In the rapidly evolving landscape of software development, creating efficient and high-performing applications is essential. This article explores the seamless integration of Ballerina and GraalVM, with a focus on transforming Ballerina code into a native executable using GraalVM.
Ballerina, is a modern, open source programming language designed explicitly for cloud native development. With its focus on integration and ease of use, Ballerina empowers developers to rapidly build scalable, distributed applications. The language comes equipped with a rich set of connectors for interacting with various protocols, services, and APIs, making it an ideal choice for microservices architectures.
Ballerina, an open source programming language designed for cloud-native development, offers developers a modern and versatile platform. With its emphasis on integration and ease of use, Ballerina enables the rapid development of scalable and distributed applications. The language provides a comprehensive set of connectors for interacting with various protocols, services, and APIs, making it an ideal choice for microservices architectures.
When Ballerina meets GraalVM, it becomes the winning formula for cloud native applications. This article explores how to build a GraalVM native executable for a real-world Ballerina conference application, while also evaluating its performance in terms of startup time and memory footprint.
The image below shows the outline of the application we are going to build.
Image 1: Conference Service Outline
The application has a Conference Service that exposes the following resources:
- GET /conferences: returns an array of conferences with the name and the id
- POST /conferences: creates a conference resource with a name
- GET /conferenceswithcountry: returns an array of conferences with the name and the country
The conference resources are stored and retrieved from an H2 in-memory database. The country of the conference is retrieved by making a request to an external Country Service endpoint.
To get started, let's set up Ballerina and GraalVM:
- Download and install Ballerina Swan Lake 2201.7.0 or a newer version. Make sure you download the installer compatible with your local machine’s architecture.
- Install GraalVM and configure it appropriately.
- Install Visual Studio Code with the Ballerina extension
- Create a ballerina package using the following command:$ bal new conference_service
Create the required records to represent the data involved in this application.
Create the database client to connect to an in-memory H2 database and the Country Service.
Create a custom StatusCodeResponse to represent the internal errors.
Now, we can define the Conference Service.
Let’s start the service on a listener dynamically by modifying the main.bal.
Before running the application, we need to pass the required configurations via Config.toml.
Let’s start the Country Service application using the following docker command:$ docker run -p 9000:9000 ktharmi176/country-service:latest
We are all set to build the Conference Service application now. Run the following command to build the GraalVM native executable for the application.
This will build the GraalVM executable and the uber JAR in the target/bin directory. Now let’s run the Conference Service native executable.
Note: Ballerina language libraries, standard libraries, and Ballerina extended libraries are GraalVM compatible. If we use any GraalVM incompatible libraries, then the compiler will report warnings. For more information, see Evaluate GraalVM compatibility.
You can test the service using the cURL commands or use the REST Client VS code extension. You need to create a .http file to invoke HTTP requests with the REST Client extension. Then you can click on the Send Request action to make a request.
In order to check the startup time, we will be executing the following script, which will log the time of the execution and listener startup.
Let’s first run the JAR file.
The JAR application started in 980ms. Now make the requests to the endpoint and execute the following to check the RSS of the process.
The RSS is around 242MB. Now let’s check the native executable.
The GraalVM native executable just started in 52ms. The startup time is approximately 20 times less than the JAR execution!
Now execute the requests and check the RSS of the process.
The RSS of the native executable is around 93MB. The memory footprint is less than half of the JAR execution!
Image 2: Performance comparison between JAR execution VS GraalVM native executable execution
Note: Ballerina supports providing additional native-image build options via --graalvm-build-options= argument. This can be also configured in the Ballerina.toml file. For more information, see Configure GraalVM native image build options.
GraalVM native executable offers the advantages of instant startup and a low memory footprint. However, there are trade-offs to consider:
- Longer build times due to the image generation process.
- The target platform must be known at build time since the executable is platform-dependent.
- The dynamic features of Java, such as reflection, are not directly supported. This is still possible by adding, but it has to be configured and compiled into the native executable.
- No runtime optimizations. So long-running applications in the traditional JVMs perform better in throughput and latency. But the GraalVM enterprise edition supports Profile-Guided Optimizations(PGO) to achieve better throughput.
In conclusion, GraalVM and Ballerina offer a unique and compelling solution for building cloud-native applications. This solution provides small packaging, instant startup, low memory footprint, and simple and powerful integration capabilities. The Ballerina code can be found in the following GitHub repository.