Ballerina Code to GraalVM Executable
This article is written using Ballerina Swan Lake Update 7(2201.7.0). We will explore how to build a GraalVM native executable for a Ballerina application.
Join the DZone community and get the full member experience.
Join For FreeIn the rapidly evolving landscape of software development, creating efficient and high-performing applications is essential. In this article, we will explore the seamless integration of Ballerina and GraalVM, focusing on the process of 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 ideal for microservices architectures.
GraalVM is a cutting-edge, high-performance runtime that supports multiple languages, including Java, JavaScript, Python, and Ruby. Its unique capability lies in offering both Just-In-Time (JIT) compilation and Ahead-of-Time (AOT) compilation, enabling developers to choose the best execution mode for their applications. GraalVM can take Java bytecode and compile it into a standalone native executable, resulting in faster startup times and reduced memory overhead.
When Ballerina meets GraalVM, it becomes the winning formula for cloud-native applications. In this article, we will explore how to build a GraalVM native executable for a real-world Ballerina conference application and how it performs in terms of startup time and memory footprint.
The following shows the outline of the application we are going to build.
GET /conferences
: returns an array of conferences with the name and the idPOST /conferences
: creates a conference resource with a nameGET /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 requesting an external Country Service
endpoint.
Let’s start by setting up Ballerina and GraalVM.
- Download and install Ballerina SwanLake 2201.7.0 or greater. Make sure you download the installer compatible with your local machine’s architecture
- Install GraalVM and configure it appropriately
- Install Visual Studio Code with 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.
# Represents a conference
#
# + id - The id of the conference
# + name - The name of the conference
public type Conference record {|
readonly int id;
string name;
|};
# Represents a conference request
#
# + name - The name of the conference
public type ConferenceRequest record {|
string name;
|};
# Represents a country response
#
# + name - The name of the country
public type Country record {|
string name;
|};
# Represents a extended conference
#
# + name - The name of the conference
# + country - The country of the conference
public type ExtendedConference record {|
string name;
string country;
|};
Create the database client to connect to an in-memory H2 database and the Country Service
.
import ballerinax/java.jdbc;
import ballerina/sql;
import ballerina/http;
import ballerinax/h2.driver as _;
# Represents the configuration of the h2 database
#
# + user - The user of the database
# + password - The password of the database
# + dbName - The file path of the database
public type H2dbConfigs record {|
string user;
string password;
string dbName;
|};
# Represents the conference database client
public isolated client class ConferenceDBClient {
private final jdbc:Client conferenceJDBCClient;
private final http:Client countryClient;
public isolated function init(H2dbConfigs dbConfigs, string countryServiceUrl)
returns error? {
self.conferenceJDBCClient = check new (
"jdbc:h2:mem:" + dbConfigs.dbName,
dbConfigs.user,
dbConfigs.password
);
self.countryClient = check new (countryServiceUrl,
retryConfig = {
count: 3,
interval: 2
}
);
// Reinitialize table
check self.dropTable();
check self.createTable();
}
# Create the conference table in the database
#
# + return - returns an error if the table creation fails
isolated function createTable() returns error? {
_ = check self.conferenceJDBCClient->execute(
`CREATE TABLE conferences (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255))`
);
}
# Drop the conference table in the database
#
# + return - returns an error if the table drop fails
isolated function dropTable() returns error? {
_ = check self.conferenceJDBCClient->execute(
`DROP TABLE IF EXISTS conferences`);
}
# Retrieve all the conferences from the database
#
# + return - retruns an array of conferences or
# an error if the retrieval fails
isolated resource function get conferences()
returns Conference[]|error {
stream<Conference, sql:Error?> conferences = self.conferenceJDBCClient->query(
`SELECT * FROM conferences`);
return from Conference conference in conferences
select conference;
}
# Create a conference in the database
#
# + conference - The conference to be created
# + return - returns an error if the conference creation fails
isolated resource function post conferences(ConferenceRequest conference)
returns error? {
_ = check self.conferenceJDBCClient->execute(
`INSERT INTO conferences (name) VALUES (${conference.name})`);
}
# Retrieve all the conferences with the country from the database
#
# + return - retruns an array of extended conferences
# or an error if the retrieval fails
isolated resource function get conferenceswithcountry()
returns ExtendedConference[]|error {
Conference[] conferences = check self->/conferences;
return from Conference conference in conferences
select {
name: conference.name,
country: check self.getCountry(conference.name)
};
}
# Retrieve the country of a conference by calling the country service
#
# + conference - The conference name
# + return - retruns the country of the conference
# or an error if the retrieval fails
isolated function getCountry(string conference)
returns string|error {
Country country = check self.countryClient->/conferences/[conference]/country;
return country.name;
}
}
Create a custom StatusCodeResponse
to represent the internal errors.
import ballerina/http;
# Represents a Error Detail
#
# + message - The message of the error
# + cause - The cause of the error
public type ErrorDetail record {|
string message;
string cause;
|};
# Represents a Internal Server Error Response
#
# + body - The body of the response
public type ConferenceServerError record {|
*http:InternalServerError;
ErrorDetail body;
|};
Now, we can define the Conference Service
.
import ballerina/http;
import ballerina/mime;
configurable string countryServiceUrl = ?;
configurable H2dbConfigs dbConfigs = ?;
service class ConferenceService {
*http:Service;
final ConferenceDBClient conferenceDBClient;
isolated function init() returns error? {
self.conferenceDBClient = check new (dbConfigs, countryServiceUrl);
}
@http:ResourceConfig {produces: [mime:APPLICATION_JSON]}
isolated resource function get conferences()
returns Conference[]|ConferenceServerError {
do {
return check self.conferenceDBClient->/conferences;
} on fail error err {
return {
body: {
message: "Error occurred while retrieving conferences",
cause: err.message()
}
};
}
}
@http:ResourceConfig {consumes: [mime:APPLICATION_JSON]}
isolated resource function post conferences(ConferenceRequest conference)
returns ConferenceServerError? {
do {
return check self.conferenceDBClient->/conferences.post(conference);
} on fail error err {
return {
body: {
message: "Error occurred while creating conference",
cause: err.message()
}
};
}
}
@http:ResourceConfig {produces: [mime:APPLICATION_JSON]}
isolated resource function get conferenceswithcountry()
returns ExtendedConference[]|ConferenceServerError {
do {
return check self.conferenceDBClient->/conferenceswithcountry;
} on fail error err {
return {
body: {
message: "Error occurred while retrieving conferences with country",
cause: err.message()
}
};
}
}
}
Let’s start the service on a listener dynamically by modifying the main.bal
.
import ballerina/log;
import ballerina/http;
import ballerina/lang.runtime;
configurable int conferenceServicePort = ?;
public function main() returns error? {
http:Listener conferenceListener = check new (conferenceServicePort);
log:printInfo("Starting the listener...");
// Attach the service to the listener.
check conferenceListener.attach(check new ConferenceService());
// Start the listener.
check conferenceListener.'start();
// Register the listener dynamically.
runtime:registerListener(conferenceListener);
log:printInfo("Startup completed. " +
string`Listening on: http://localhost:${conferenceServicePort}`);
}
Before running the application, we need to pass the required configurations via Config.toml
.
conferenceServicePort = 8102
countryServiceUrl = "http://localhost:9000"
# The database related configs
[dbConfigs]
dbName = "conferencedb"
user = "admin"
password = "admin"
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.
$ bal build --graalvm Compiling source tharmigan/conference_service:0.1.0 =========================================================================== GraalVM Native Image: Generating 'conference_service' (executable)... =========================================================================== [1/7] Initializing... (7.8s @ 0.53GB) Version info: 'GraalVM 22.3.1 Java 11 CE' Java version info: '11.0.18+10-jvmci-22.3-b13' C compiler: cc (apple, arm64, 14.0.3) Garbage collector: Serial GC 2 user-specific feature(s) - com.oracle.svm.thirdparty.gson.GsonFeature - io.ballerina.stdlib.crypto.svm.BouncyCastleFeature [2/7] Performing analysis... [***********] (90.6s @ 4.74GB) 28,210 (93.89%) of 30,045 classes reachable 92,277 (81.07%) of 113,830 fields reachable 156,792 (72.71%) of 215,640 methods reachable 1,526 classes, 65 fields, and 3,017 methods registered for reflection 92 classes, 98 fields, and 66 methods registered for JNI access 6 native libraries: -framework CoreServices, ... [3/7] Building universe... (16.6s @ 3.69GB) [4/7] Parsing methods... [****] (15.5s @ 5.11GB) [5/7] Inlining methods... [***] (8.0s @ 3.41GB) [6/7] Compiling methods... [********] (76.3s @ 3.01GB) [7/7] Creating image... (15.8s @ 4.05GB) 100.86MB (58.88%) for code area: 113,643 compilation units 68.52MB (40.00%) for image heap: 529,078 objects and 104 resources 1.92MB ( 1.12%) for other data 171.29MB in total --------------------------------------------------------------------------- Top 10 packages in code area: Top 10 object types in image heap: 15.91MB ballerina.http/2 17.47MB byte[] for code metadata 4.15MB ballerina.http/2.types 15.03MB byte[] for embedded resources 2.83MB ballerina.io/1 7.44MB java.lang.Class 2.30MB ballerina.sql/1 5.63MB byte[] for java.lang.String 1.60MB sun.security.ssl 5.26MB java.lang.String 1.37MB ballerina.file/1 4.05MB byte[] for general heap data 1.21MB ballerina.sql/1.types 2.58MB com.oracle.svm.core.hub 1.20MB com.sun.media.sound 1.38MB byte[] for reflection metadata 1.19MB ballerina.jwt/2 1.08MB java.lang.String[] 1.14MB ballerina.http/2.creators 1.02MB c.o.svm.core.hub.DynamicHub 67.31MB for 1000 more packages 6.44MB for 3894 more object types --------------------------------------------------------------------------- 41.4s (16.9% of total time) in 68 GCs | Peak RSS: 4.15GB | CPU load: 3.88 --------------------------------------------------------------------------- Produced artifacts: /conference-service/target/bin/conference_service(executable) /conference-service/target/bin/conference_service.build_artifacts.txt(txt) =========================================================================== Finished generating 'conference_service' in 4m 3s.
This will build the GraalVM executable and the uber JAR in the target/bin
directory. Now let’s run the Conference Service
native executable.
$ ./target/bin/conference_service time = 2023-07-28T12:36:15.991+05:30 level = INFO module = tharmigan/conference_service/0 message = "Starting the listener..." time = 2023-07-28T12:36:16.003+05:30 level = INFO module = tharmigan/conference_service/0 message = "Startup completed. Listening on: http://localhost:8102"
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.
### Add a new conference
POST http://localhost:8102/conferences
Content-Type: application/json
{
"name": "WSO2Con"
}
### Retrive all conferences
GET http://localhost:8102/conferences
### Add another conference
POST http://localhost:8102/conferences
Content-Type: application/json
{
"name": "KubeCon"
}
### Retrive all conferences with country
GET http://localhost:8102/conferenceswithcountry
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.
To check the startup time, we will be executing the following script, which will log the time of the execution and listener startup.
#!/bin/bash echo "time = $(date +"%Y-%m-%dT%H:%M:%S.%3N%:z") level = INFO module = tharmigan/conference_service message = Executing the Ballerina application" if [ "$1" = "graalvm" ]; then ./target/bin/conference_service else java -jar ./target/bin/conference_service.jar fi
Let’s first run the JAR file.
$ sh run.sh time = 2023-08-01T12:24:43.038+05:30 level = INFO module = tharmigan/conference_service message = Executing the Ballerina application time = 2023-08-01T12:24:44.002+05:30 level = INFO module = tharmigan/conference_service message = "Starting the listener..." time = 2023-08-01T12:24:44.241+05:30 level = INFO module = tharmigan/conference_service message = "Startup completed. Listening on: http://localhost:8102"
The JAR application started in 1203ms. Now, make the requests to the endpoint and execute the following to check the RSS of the process.
$ ps -o pid,rss,command | grep conference_service 21068 183616 java -jar ./target/bin/conference_service.jar 21086 1184 grep conference_service
The RSS is around 180MB. Now, let’s check the native executable.
$ sh run.sh graalvm time = 2023-08-01T12:29:06.470+05:30 level = INFO module = tharmigan/conference_service message = Executing the Ballerina application time = 2023-08-01T12:29:06.512+05:30 level = INFO module = tharmigan/conference_service/0 message = "Starting the listener..." time = 2023-08-01T12:29:06.519+05:30 level = INFO module = tharmigan/conference_service/0 message = "Startup completed. Listening on: http://localhost:8102"
The GraalVM native executable just started in 49ms. The startup time is approximately 25 times less than the JAR execution!
Now, execute the requests and check the RSS of the process.
$ ps -o pid,rss,command | grep conference_service 21154 87808 ./target/bin/conference_service_ballerina 21233 1184 grep conference_service_ballerina
The RSS of the native executable is around 90MB. The memory footprint is approximately half of the JAR execution!
Note: Ballerina supports providing additional native-image build options via --graalvm-build-options=
argument. This can also be configured in the Ballerina.toml
file. For more information, see Configure GraalVM native image build options.
So, the GraalVM native executable can achieve instant startup and a low memory footprint. But there is no such thing as a free lunch. There are trade-offs.
- 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. However, 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 this GitHub repository.
Published at DZone with permission of Tharmigan Krishnananthalingam. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments