Graalvm is a newcomer in Java ecosystem enabling to compile Java compiled code - bytecode - to native code (C on UNix and C++ on Windows).

The overall target is, indeed, the cloud and more precisely the instances started/stopped very often. In other words, it immediately targets the Function As A Service - FAAS - kind of cloud deployment. The two main known ones are AWS lamda and Azure functions. This does not look that important from a Java developer perspective, but for some kind of people this type of deployment is very nice because it enables the deployment of your business code without caring of all the infrastructure. What's more, it will catch up Java developers as well, so it is important to take some time to understand what we are talking about and how it can impact your development.

What is a function?

To answer the question "what is a function", you can read a lot of definitions but I'd like to start the opposite way and have a look at the available API.

Generally the API are quite flexible to simplify the developer experience, but the most generic one for AWS lamda is the following one:

public class MyLambda {
    public static void onEvent(final InputStream inputStream, // <1>
                               final OutputStream outputStream, // <2>
                               final Context context) { // <3>
        // do something
    }
}

We immediately identify that we have:

  1. Some (optional) incoming data,
  2. Some outcoming data,
  3. Some execution context (can enable to get a logger, the function name, some resource information, etc...)

Doesn't it remind you of something? Here's a sneak peek:

public class MyLambda implements Servlet {
  public void service(final ServletRequest req, final ServletResponse res) {
    // ....
  }
}

Plus, the API is a bit different but we find back the same input, output, and context (in the request in the case of servlet).

In the case of Azure it looks the same:

public class MyFunction {
    @FunctionName("myFunction")
    public HttpResponseMessage<String> hello(
            @HttpTrigger(name = "req", methods = "post", authLevel = AuthorizationLevel.ANONYMOUS)
            final HttpRequestMessage<Optional<Data>> request,
            final ExecutionContext context) {
        // ...
    }
}

Here, the API is a bit more specific and coupled but you find back a context on the execution, a request, and you can create a response - from the request instance.

I didn't mention servlets randomly before. There are abstractions for functions/lambdas using servlet API. So you develop a standard function, and a framework generates under the hood the actual lambda/function and binding - to map the java code to the environment.

However functions are not that closely matching servlet world, even if this can look like from a very high level.

The first difference is that the previous samples were just limited to the HTTP case. Functions can be way more like a scheduled event or an event pushed to a queue. In other words, a function is a callback bound to a particular technology supported by your cloud provider. In any case it is one of the last "current" steps of the serverless computing.

Function and Java

As we saw before, there are generally Java bindings/API/SDK for functions, but there are also ways to bind a native command if you have a custom image in the cloud provider you are using. There is also a function initiative called fn project. This project aims to abstract FAAS - not with servlet, don't worry. This project provides several toolings to developping a function, testing it and most importantly in the context of this post, it relies on docker as the basis of its solution.

At the end, there is some convergence to becoming native (as compiled in C) when doing functions because the current provider integrations tend to execute the function - program - each time an event arrives. In other words, the bootstrap time is very important and it is not acceptable to wait 1 min for your IoC to be ready.

Side note: you may think that you will not need an IoC or some library to write something as simple as a function, but the function is actually a new way to get an entry point - compared to the old style which would be a main(String[]). Thus, behind the entry point you can get complex code, so you want the same comfort and reliability to develop your function.

Quarkus.io

I can't write a blog post about GraalVM without mentionning a newcomer in the server world: Quarkus.io.

This project propulsed by Red Hat integrated most of Red Hat stack with GraalVM to enable compiling a Java JAX-RS service natively. Said like that, it sounds inaccurate to optimize a long running software where you will want to enable caching - the JVM is a good platform for that - and serve a lot of requests. Actually they also took most of the specification - particularly CDI and JPA - and cut most of what can bring some slowness during their bootstrap. At the end you get a complete service starting very fast - which is very consistent with the race to the bootstrap time we have in function world.

GraalVM

GraalVM is a project which will take your main - in bytecode form, inspect all the code path from it, convert the code to native code (.o as in your first C compilation: gcc -o main.o -c main.c) and finally build an executable you can run directly on the platform you ran the command.

There is also an option to statically link your executable so you will not even need any dependency when running your executable, which means you can use docker scratch base image (literally an empty image). This magic takes the form of native-image binary you can find in GraalVM distribution.

GraalVM also has another awesome feature which is to bring some polyglotism to the JVM. It supports Node (javascript), Python, R etc... which is really a game changer for Big Data projects where the polyglotism is just required - you can see how Apache Beam tried to build a GRPC solution to that need. With Graal it is built-in :)

Crest/CLI in that world?

So why my blogpost is talking about a Crest CLI? First of all, if you don't know Crest, it is a Java API to write Command Line Interface (CLI) programs. It is a really nice solution because it stays light, it is command driven - a bit like JAX-RS - and it enables to:

  1. Bind arguments on your commands,
  2. Add interceptors on your commands,
  3. Handle streaming of output data,
  4. Use services (mini-IoC) across commands.

Concretely here is what a service can look like:

public class MyCommands {
    @Command("do-something")
    public void listImages(@Out final PrintStream stdout,
                           @Option("target") final Path target,
                           final MyService service) {
        // ...
    }
}

This command will enable you to execute some logic running the command:

java -jar mycommand-fatjar.jar org.tomitribe.crest.Main \
    do-something --target xxxxxx

 

Do you see now why it can be interesting in the context of functions?

Without explicitly using any proprietary API, you have all you need from a static model to generate a function. The static part is very important because it enables to generate, at build time, the bridge/descriptors, without more knowledge at all! Doing so enables you to benefit from few business advantages:

  • You are no more coupled to any vendor - Crest is open source and has not the disavantage to have a vendor betting a lot of the technology like some other function related abstraction,
  • You can easily test and harness your code - it is a plain function without specific imports,
  • You can change FAAS provider easily depending on their cost, infrastructure change or business objectives - simply change the descriptor generator you use at build time or the bridge you have in place if you use a code bridge,
  • You can reuse the command in other contexts - in some case you expose some tool over HTTP using FAAS but it is something you may need to run on a CI or locally as well. You solve that need by doing a request (thanks curl) on your FAAS but you also pay each time you do ;)
  • Contrat is more explicit, standardized and understandable so it enables better and saner interactions between developers.

So using a CLI API to write function is not that bad idea, but it keeps one drawback of Java platform: it is "slow" to start. Without going deep into details, it will be hard to start in less than 200ms in Java whereas the same software in C will start in 1ms - on the same machine. Here are some figures on my computer:

$ time java -cp ... SomeApp ...

real 0m0,206s
user 0m0,288s
sys 0m0,038s

$ time ./someapp ...

real 0m0,007s
user 0m0,004s
sys 0m0,003s

If your function is rarely used it is not that important, but if your function is often used it will have a real impact on the overall performances of the system, and keep in mind that cloud performances often mean money.

Finally, the side note will be that CLI commands are often suffering from the startup time. So dropping that issue can only be an advantage for such programs. An ecosystem is ready to change now ;)

Crest and GraalVM?

To show you how to use GraalVM to build a native executable from your Crest application, I will take a simple command which will read a docker-compose.yml file and print all the services available. This use case is quite simple but will enable us to start feeling what GraalVM development is.

With crest, it is mainly about creating a class and a service - for the parsing - which will take the path to the yml as an option and print the service to stdout:

public class Docosh {
    @Command("list-images")
    public void listImages(@Out final PrintStream stdout,
                           @Option("docker-compose") final Path compose,
                           final DockerComposeParser parser) {
        parser.extractImages(compose).forEach(stdout::println);
    }
}

This command requires two crest extensions:

  1. Enable Path support - crest supports files out of the box but not yet Path,
  2. Register the parser service.

To enable path support we will register a custom PropertyEditor:

public class PathEditor extends PropertyEditorSupport {

    @Override
    public void setAsText(final String text) throws IllegalArgumentException {
        setValue(Paths.get(text));
    }
}

// registered with
PropertyEditorManager.registerEditor(Path.class, PathEditor.class);

The editor implementation is quite trivial thanks to the Paths#get utility. Then we just have to register our editor using PropertyEditorManager API. This is enough to enable crest to know how to map a Path from a String.

Now we have inject our Path, we need to inject our DockerComposeParser. To do that, the simplest way is to write a custom main:

public static void main(final String... args) throws Exception {
    PropertyEditorManager.registerEditor(Path.class, PathEditor.class);

    final Map<Class<?>, Object> services = new HashMap<>();
    services.put(DockerComposeParser.class, new DockerComposeParser());
    new org.tomitribe.crest.Main().main(new SystemEnvironment(services), args);
}

Nothing crazy here, we fully delegate the main handling to the one of crest but we pass a custom environment initialized with our service mapping.

Note that if you have an IoC you will likely override the findService method of the Environment API to rely on your IoC lookup mecanism.

Just to ensure you follow where we are, here is the dependency we rely on to write this code:

<dependency>
  <groupId>org.tomitribe</groupId>
  <artifactId>tomitribe-crest</artifactId>
  <version>0.10</version>
  <exclusions>
    <exclusion>
      <groupId>org.apache.xbean</groupId>
      <artifactId>xbean-asm5-shaded</artifactId>
    </exclusion>
    <exclusion>
      <groupId>org.apache.xbean</groupId>
      <artifactId>xbean-finder-shaded</artifactId>
    </exclusion>
    <exclusion>
      <groupId>org.apache.bval</groupId>
      <artifactId>bval-jsr</artifactId>
    </exclusion>
    <exclusion>
      <groupId>org.apache.geronimo.specs</groupId>
      <artifactId>geronimo-validation_1.1_spec</artifactId>
    </exclusion>
  </exclusions>
</dependency>

You can notice that we excluded Bean Validation dependencies and XBean (finder to find the commands) because we don't use that feature in our CLI.

So at that point I can build a jar with that code and I'm ready to convert it to a native binary.

To do that we have to download GraalVM on github. Once the VM extracted in GRAALVM_HOME, you wil notice you have a native-image binary in its bin folder. This is what will enable you to build the native executable. If you plan to use it directly - without a build plugin - I recommend you to add it to your PATH.

Note: I have been using the last GraalVM version when writing this application, i.e. 1.0.0-rc14. I encountered few bugs I will workaround in this post but most of them have been fixed for the coming rc15.

The game will be to give to Graal native-image command the classpath of your application and some option to enable its correct compilation.

For our CLI application it will look like:

native-image \
  -cp tomitribe-crest-0.10.jar:tomitribe-crest-api-0.10.jar:tomitribe-util-1.0.0.jar:docosh-1.0-SNAPSHOT.jar \
  --static \
  --verbose \
  -H:Class=com.github.rmannibucau.docker.compose.cli.Launcher \
  -H:Name=docosh-1.0-SNAPSHOT

The main points of that command are:

  1. The classpath to use to build the application, it is typically the application plus some graalvm integration - I will come back to that later,
  2. Static option which enable to statically link the application,
  3. We activate the verbose mode to ensure we know what happens,
  4. The main to use as entry point for the code path analysis (H:Class)
  5. The binary output name (H:Name).

If you run that command you immediately get an error about Bean Validation:

com.oracle.svm.core.util.UserError$UserException: com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved type during parsing: javax.validation.ConstraintViolationException. To diagnose the issue you can use the --allow-incomplete-classpath option. The missing type is then reported at run time when it is accessed the first time.
Detailed message:
Error: com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved type during parsing: javax.validation.ConstraintViolationException. To diagnose the issue you can use the --allow-incomplete-classpath option. The missing type is then reported at run time when it is accessed the first time.
Trace:
	at parsing org.tomitribe.crest.val.BeanValidation.messages(BeanValidation.java:76)
Call path from entry point to org.tomitribe.crest.val.BeanValidation.messages(Exception):
	at org.tomitribe.crest.val.BeanValidation.messages(BeanValidation.java:76)
	at org.tomitribe.crest.cmds.CmdMethod.reportWithHelp(CmdMethod.java:479)
	at org.tomitribe.crest.cmds.CmdMethod.exec(CmdMethod.java:363)
	at org.tomitribe.crest.Main.exec(Main.java:192)
	at org.tomitribe.crest.Main.main(Main.java:144)
	at com.github.rmannibucau.docker.compose.cli.Launcher.main(Launcher.java:41)

 

This is weird because we excluded bean validation dependencies but not the bean validation bridge in crest so it should have worked....except that Crest uses that code to find if it should use bean validation or not to run its command:

static {
    BeanValidationImpl impl = null;
    final ClassLoader loader = BeanValidation.class.getClassLoader();
    try {
        Class.forName("javax.validation.executable.ExecutableValidator", false, loader);
        impl = new BeanValidation11();
    } catch (final ClassNotFoundException e) {
        try {
            Class.forName("org.apache.bval.jsr303.extensions.MethodValidator", false, loader);
            impl = new BVal05();
        } catch (final ClassNotFoundException cnfe) {
            // no-op
        }
    }
    IMPL = impl;
}

This is a common Java pattern which globally means "try if present" but we notice that it uses Class.forName which is handled by GraalVM as a code path which will trigger the code behind that class name. In other words, if we would have used a ServiceLoader or an option instead of "guessing" the implementation we wouldn't have had that issue.

This is exactly where substrate API will help us. Said very shortly, it enables you to monkey patch the stack you are trying to compile enabling you to replace, delete, and modify part of the code. In our case we want to replace the BeanValidation class of Crest by a mock which disables that feature.

First, we will add the substrate dependency to our project:

<dependency>
  <groupId>com.oracle.substratevm</groupId>
  <artifactId>svm</artifactId>
  <version>${graalvm.version}</version>
  <scope>provided</scope>
</dependency>

Once this dependency is present we can write a class respecting the signatures of the BeanValidation one:

public class Substitute_BeanValidation {
    public static boolean isActive() {
        return false;
    }

    public static void validateParameters(
        final Object instance, final Method method, final Object[] parameters) {
        // no-op
    }

    public static void validateParameters(
        final Constructor<?> constructor, final Object[] parameters) {
        // no-op
    }

    public static Iterable<? extends String> messages(final Exception e) {
        return emptyList();
    }
}

To enable this class and make GraalVM know we want to use that in the analyzis instead of Crest code, we will have three things to do:

  1. Mark the class as targetting crest BeanValidation,
  2. Mark all the methods as replacing - subtituting - the original ones,
  3. Make the class final - substrate does not support it otherwise.

Here is what it can look like:

@TargetClass(BeanValidation.class)
public final class Substitute_BeanValidation {
    @Substitute
    public static boolean isActive() {
        return false;
    }

    @Substitute
    public static void validateParameters(
        final Object instance, final Method method, final Object[] parameters) {
        // no-op
    }

    @Substitute
    public static void validateParameters(
        final Constructor<?> constructor, final Object[] parameters) {
        // no-op
    }

    @Substitute
    public static Iterable<? extends String> messages(final Exception e) {
        return emptyList();
    }
}

Now we can recompile our jar and relaunch our native-image command.

Tip: in this post I will put substrate patches in the same jar as the application but this is not required, it is likely better to have a module for that, this way you can keep your java flavor clean and still build a native version of your application.

The output of the build is now clean and should look like:

[docosh-1.0-SNAPSHOT:14067]    classlist:     215.80 ms
[docosh-1.0-SNAPSHOT:14067]        (cap):     729.11 ms
[docosh-1.0-SNAPSHOT:14067]        setup:     950.42 ms
[docosh-1.0-SNAPSHOT:14067]   (typeflow):   3,211.50 ms
[docosh-1.0-SNAPSHOT:14067]    (objects):   3,524.30 ms
[docosh-1.0-SNAPSHOT:14067]   (features):     111.67 ms
[docosh-1.0-SNAPSHOT:14067]     analysis:   6,950.70 ms
[docosh-1.0-SNAPSHOT:14067]     universe:     166.90 ms
[docosh-1.0-SNAPSHOT:14067]      (parse):     410.62 ms
[docosh-1.0-SNAPSHOT:14067]     (inline):     655.26 ms
[docosh-1.0-SNAPSHOT:14067]    (compile):   2,550.93 ms
[docosh-1.0-SNAPSHOT:14067]      compile:   3,891.88 ms
[docosh-1.0-SNAPSHOT:14067]        image:     514.70 ms
[docosh-1.0-SNAPSHOT:14067]        write:      71.64 ms
[docosh-1.0-SNAPSHOT:14067]      [total]:  12,810.02 ms

Don't hesistate to have a look at the details to the output, it shows all the phases GraalVM goes through to go from your bytecode to a native version of it. It also shows the time of each phase. If the analyzis is too long for the application you are building it can mean you want to drop - @Delete -  some part of the application or substitute it to get a boost.

Now we should have a docosh-1.0-SNAPSHOT binary we can run:

$ ./docosh-1.0-SNAPSHOT
Unknown command: help

Exception in thread "main" java.lang.NullPointerException
	at org.tomitribe.crest.Main.exec(Main.java:192)
	at org.tomitribe.crest.Main.main(Main.java:144)
	at com.github.rmannibucau.docker.compose.cli.Launcher.main(Launcher.java:41)

Outch, this is not really the expected output! The positive thing is that it runs our code and we see it tried to default to help command because we didn't select on the command line any particular command.

The NullPointerException we see here, means that the help command itself was not found by crest in its command registry - crest help command is registered as any other command internally.

How can it happen?

Help is a normal command except it is instantiated internally by the framework. The direct implication is that help is identified by reflection so we have to enable help to be registered.

To do that we have to register the help methods reflection metadata to be kept during GraalVM preparation - before it compiles to native code. The way to do it is to create a GraalVM feature, register it through @AutomaticFeature and register help class methods for reflection:

@AutomaticFeature
public class Feature_RegisterReflections implements Feature {
    @Override
    public void beforeAnalysis(final BeforeAnalysisAccess access) {
        RuntimeReflection.register(Help.class.getMethods());
    }
}

Now that it is ready we can run again our program and we now get:

$ ./docosh-1.0-SNAPSHOT
Commands:

   help

The good point is that we don't have any error. The bad point is that we still didn't register our command. There are few options to do that but I will take the Service Provider Interface option. it consists in implementing a Commands.Loader which will return the commands:

// don't forget
// the META-INF/services/org.tomitribe.crest.cmds.processors.Commands$Loader
public class DocoshLoader implements Commands.Loader {
    @Override
    public Iterator<Class<?>> iterator() {
        return Stream.<Class<?>>of(Docosh.class).iterator();
    }
}

We relaunch our native-image command and our native program and this time we get... the same! This is because our command relies on reflection as well so we must register it as the Help command. Let's add it in our feature and retry. This time here is what we get:

$ ./docosh-1.0-SNAPSHOT
Commands:

   help
   list-images 

So now our command is registered, let's call it:

$ ./docosh-1.0-SNAPSHOT list-images --docker-compose=docker-compose.yml
Cannot convert to 'java.nio.file.Path' for 'docker-compose'. No PropertyEditor

Usage: list-images [options]

Options:
  --docker-compose=<Path>
Exception in thread "main" java.lang.IllegalArgumentException: Cannot convert to 'java.nio.file.Path' for 'docker-compose'. No PropertyEditor
	at org.tomitribe.util.editor.Converter.convert(Converter.java:101)
	at org.tomitribe.crest.cmds.CmdMethod.fillOptionParameter(CmdMethod.java:603)
	at org.tomitribe.crest.cmds.CmdMethod.convert(CmdMethod.java:589)
	at org.tomitribe.crest.cmds.CmdMethod.convert(CmdMethod.java:525)
	at org.tomitribe.crest.cmds.CmdMethod.parse(CmdMethod.java:510)
	at org.tomitribe.crest.cmds.CmdMethod.exec(CmdMethod.java:361)
	at org.tomitribe.crest.Main.exec(Main.java:196)
	at org.tomitribe.crest.Main.main(Main.java:144)
	at com.github.rmannibucau.docker.compose.cli.Launcher.main(Launcher.java:41)

This is because there is no property editor to create a Path instance from a String....but wait, we registered it in our main right? Sure! But we didn't register it to be instantiable in GraalVM.

So let's add it in out feature:

RuntimeReflection.register(PathEditor.class);
RuntimeReflection.registerForReflectiveInstantiation(PathEditor.class);

As you can notice the registration is different here, we do not register methods but the class itself and its no-arg constructor to enable the PropertyEditorManager to instantiate it. After another round trip of compilation-run here is what you will get now:

$ ./docosh-1.0-SNAPSHOT list-images --docker-compose=docker-compose.yml
Exception in thread "main" java.lang.NullPointerException
	at com.oracle.svm.reflect.helpers.ExceptionHelpers.createFailedCast(ExceptionHelpers.java:30)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.tomitribe.crest.cmds.targets.SimpleBean.invoke(SimpleBean.java:34)

Still does not look good though. Here, the command was found but the command method was not static. So crest tried to instantiate the command class to get an instance to execute the command and failed because we didn't register it as instantiable. Let's add it and try again adding the following line in our feature:

RuntimeReflection.registerForReflectiveInstantiation(Docosh.class);

After this line added, the output of our command is:

$ ./docosh-1.0-SNAPSHOT list-images --docker-compose=docker-compose.yml
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.tomitribe.crest.cmds.targets.SimpleBean.invoke(SimpleBean.java:34)
	at org.tomitribe.crest.cmds.CmdMethod.doInvoke(CmdMethod.java:463)
	at org.tomitribe.crest.cmds.CmdMethod.exec(CmdMethod.java:371)
	at org.tomitribe.crest.cmds.CmdMethod.exec(CmdMethod.java:367)
	at org.tomitribe.crest.Main.exec(Main.java:196)
	at org.tomitribe.crest.Main.main(Main.java:144)
	at com.github.rmannibucau.docker.compose.cli.Launcher.main(Launcher.java:46)

This one is not that trivial. Long story short, when invoking the command method, it has missed a parameter. After a quick check out to the signature, it is probably the last one, the service - some debugging will confirm that. It is one of the bugs the rc14 of GraalVM has and which is fixed on master - if you want to recompile substrate from sources. GraalVM does not manage to follow the code of our environment usage - which is done through a threadlocal and the service parameter lookup.

Are we done yet? Not really, you can generally help GraalVM yb expliciting some code path either in your main or in some substitution. Here we will just modify our main to explicitly bind our environment in the ThreadLocal which is enough to let GraalVM get back on track:

public static void main(final String... args) throws Exception {
    PropertyEditorManager.registerEditor(Path.class, PathEditor.class);

    final Map<Class<?>, Object> services = new HashMap<>();
    services.put(DockerComposeParser.class, new DockerComposeParser());

    //
    // this is the develish hack
    //
    Environment.ENVIRONMENT_THREAD_LOCAL.set(environment);

    new org.tomitribe.crest.Main().main(new SystemEnvironment(services), args);
}

Let's recompile/run our program - I took one of the first google responses for docker-compose.yml:

$ ./docosh-1.0-SNAPSHOT list-images --docker-compose=docker-compose.yml
redis (sameersbn/redis:4.0.9-1)
postgresql (sameersbn/postgresql:10)
gitlab (sameersbn/gitlab:11.8.3)

It worked :) Are we gone now? Not yet, we still have two things to do:

  1. Make that part of our build,
  2. Compare it to the program in plain Java.

To integrate that command in our build we can of course use a bash script but GraamVM has actually a nice maven plugin so let's use it:

<plugin>
  <groupId>com.oracle.substratevm</groupId>
  <artifactId>native-image-maven-plugin</artifactId>
  <version>${graalvm.version}</version>
  <executions>
    <execution>
      <id>native</id>
      <phase>package</phase>
      <goals>
        <goal>native-image</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <mainClass>com.github.rmannibucau.docker.compose.cli.Launcher</mainClass>
    <imageName>${project.artifactId}-${project.version}</imageName>
    <buildArgs>--static --verbose</buildArgs>
  </configuration>
</plugin>

Using current JAVA_HOME distribution as a GraalVM distribution, this will simply execute the same command we used to build a native binary in our target/ folder. However, make sure to run the maven command with GraalVM as JAVA_HOME otherwise it will fail.

Here is an example if you don't want to modify your environment globally:

JAVA_HOME="$GRAALVM_HOME" mvn clean install

Note that by default the binary is not attached to the project so it is not deployed. If you want to do it you will need to use build helper maven plugin but take care as it is not portable. So make sure to put a classifier that is related to your build architecture.

Now let's compare the two executions. First the native execution:

$ time ./target/docosh-1.0-SNAPSHOT list-images --docker-compose=/tmp/docker-compose.yml
redis (sameersbn/redis:4.0.9-1)
postgresql (sameersbn/postgresql:10)
gitlab (sameersbn/gitlab:11.8.3)

real	0m0,004s
user	0m0,001s
sys	0m0,003s

Now the same in plain java (8):

$ time java -cp target/docosh-1.0-SNAPSHOT.jar:target/dependency/tomitribe-crest-0.10.jar:target/dependency/tomitribe-crest-api-0.10.jar:target/dependency/tomitribe-util-1.0.0.jar com.github.rmannibucau.docker.compose.cli.Launcher list-images --docker-compose=/tmp/docker-compose.yml
redis (sameersbn/redis:4.0.9-1)
postgresql (sameersbn/postgresql:10)
gitlab (sameersbn/gitlab:11.8.3)

real	0m0,176s
user	0m0,239s
sys	0m0,045s

We immediately see the difference and if you repeat the executions multiple times it stays around 200ms of difference. For such an utility it is a lot and anything closer to native tools is better.

In terms of memory usage it is impressive as well. Java results are:

Maximum resident set size (kbytes): 39096
Average resident set size (kbytes): 0

And native version results are:

Maximum resident set size (kbytes): 6684
Average resident set size (kbytes): 0

In terms of memory state at the end of the program it is even more surprising. Java will exit with this state:

==21630== HEAP SUMMARY:
==21630==     in use at exit: 33,574,459 bytes in 4,494 blocks
==21630==   total heap usage: 11,622 allocs, 7,128 frees, 47,160,079 bytes allocated
==21630==
==21630== LEAK SUMMARY:
==21630==    definitely lost: 4,489 bytes in 26 blocks
==21630==    indirectly lost: 4,392 bytes in 15 blocks
==21630==      possibly lost: 234,218 bytes in 251 blocks
==21630==    still reachable: 33,331,360 bytes in 4,202 blocks
==21630==                       of which reachable via heuristic:
==21630==                         newarray           : 22,536 bytes in 1 blocks
==21630==         suppressed: 0 bytes in 0 blocks
==21630== Rerun with --leak-check=full to see details of leaked memory

Whereas the native execution exists with:

==21827==
==21827== HEAP SUMMARY:
==21827==     in use at exit: 0 bytes in 0 blocks
==21827==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==21827==
==21827== All heap blocks were freed -- no leaks are possible

However don't think it is because it allocates/deallocates each time an object it is no more needed. There is a GC in graal as in the JVM, even if it tends to behave a bit slower than native JVM one in "normal" concurrent applications....for now.

In terms of CPU the same kind of difference can be seen:

$ /usr/bin/time -v ./target/docosh-1.0-SNAPSHOT ...
	Percent of CPU this job got: 100%
$ /usr/bin/time -v java -cp ...
	Percent of CPU this job got: 154%

Finally, in terms of disk space GraalVM binary is fat compared to our small module but don't forget you need a JVM to run the jar flavor:

-rwxrwxr-x 1 rmannibucau rmannibucau  11M mars  24 17:45 docosh-1.0-SNAPSHOT
-rw-rw-r-- 1 rmannibucau rmannibucau 106K mars  24 17:47 tomitribe-crest-0.10.jar
-rw-rw-r-- 1 rmannibucau rmannibucau  12K mars  24 17:47 tomitribe-crest-api-0.10.jar
-rw-rw-r-- 1 rmannibucau rmannibucau  89K mars  24 17:47 tomitribe-util-1.0.0.jar
-rw-rw-r-- 1 rmannibucau rmannibucau  21K mars  24 17:45 docosh-1.0-SNAPSHOT.jar

So at the end we can compare the native flavor of 11M to the JVM one of (106+12+89+21)K+229M~=230M!

Is GraalVM really the Java Graal?

It looks so, however keep in mind some few points before you go run to start converting your applications:

  • Compilation times are not that fast for real applications and it can really slow down your productivity if you don't rely on Java in development,
  • Testing is crucial, as seen in this post, you can compile but not run correctly, so ensure to test your binary before deploying it. This is true for all the code considered by GraalVM so this means you must have 100% coverage for your code and the libraries you use - at least the parts you use,
  • Tooling is not yet perfect. I shared some code to enable reflection but some automotion can be done, typically a generic feature can be written and each library should integrate with GraalVM to make the starting cost very low (typically any mapper, IoC can find most classes it will see and therefore force their reflection metadata to be generated),
  • Docker scratch mode is not always possible, typically if you put the binary we built - even with static option, you will get this error:
docker: Error response from daemon: OCI runtime create failed: container_linux.go:348: starting container process caused "exec: \"/bin/sh\": stat /bin/sh: no such file or directory": unknown.
ERRO[0000] error waiting for container: context canceled

This is because glic is not statically linked, however busybox docker image is very light and provides it so we just have to use it in our FROM clause - or in your Jib configuration if you followed one of my previous blogposts (dockerize-tomee-embedded or put-your-jlink-application-in-docker):

FROM busybox
COPY ./docosh-1.0-SNAPSHOT /opt/docosh/bin/docosh
ENTRYPOINT /opt/docosh/bin/docosh

Then you can run your binary and in our case it takes only few more megs:

REPOSITORY TAG    IMAGE ID     CREATED       SIZE
docosh     latest e3bcccf0ab8c 6 seconds ago 12.4MB

So yes GraalVM can change the way we do Java, it is still in RC and doing this program I encountered even more bugs - like the -ljvm flag was wrongly passed to the linker - but it still is an active project that must be followed along. It will also be interesting to see how it will compete with jlink (see blogposts part 1, part 2 and part 3 about it) since both are owned by Oracle and GraalVM clearly makes jlink outdated... and therefore the not yet adopted Java Platform Module System!

From the same author:

In the same category: