I am a fond Clojure user, and love to use it for both server-side projects and small command-line tools. I love its expressivity and concision, and I like the fact that I can have a small “script” and hack on it. The problem I have is that while this is fine for interactive use (I don’t mind waiting a second to see results) or for server side projects (where initialization happens just once), it is not a sweet spot for a command-line tool that I want to deploy to automate something.

In this case, the cost of firing up the JVM, Clojure and the script itself, vastly outnumbers the actual processing happening, that is maybe scanning some files; so I see CPU spikes when my cron jobs run those tasks. Nothing bad, but it’s quite useless to load a complex environment to process a 100k text file and then throw everything away.

Enter GraalVM

GraalVM is an extension of the Java virtual machine to support more languages and execution modes. The Graal project includes a new high performance Java compiler which can be used in a just-in-time configuration on the HotSpot VM, or in an ahead-of-time configuration on the Substrate VM.

Substrate VM is a framework that allows ahead-of-time compilation of Java applications under closed-world assumption into executable images or shared objects.

This means that we can create a Clojure command-line app, compile it as :aot :all into a set of Java class files, and then compile those into a single, standalone binary file.

As all initialization is run before compiling into a binary file, it is already present in the generated image and therefore does not require running again! for Clojure, with its complex initialization phase, this is a major win.

Limits of GraalVM / Substrate VM

Substrate VM does not support all features of Java to keep the implementation small and concise, and also to allow aggressive ahead-of-time optimizations (though it does some pretty impresive things, e.g. most of Java reflection).

The biggest issue that we will face is that dynamic class loading is not allowed. We may not care much about that if we have a closed project where all classes are defined at compile time, but as Clojure itself uses it in clojure.lang.RT.resourceAsStream, at the moment we have to tell Graal not to abort at compile time but to defer such exceptions to runtime. Not very elegant, but it works.

This also means that not all valid Java libraries will work out-of-the box in GraalVM - see e.g. Akka and Graal’s native image tool.

The second limit of GraalVM is that in order to compile an even moderately simple executable, it will require a lot of RAM - 6G to 8G not being uncommon from my own experiments.

Our first GraalVM Clojure application

To get started, we want to implement the simple “toycalc” app that ships with CLI-matic - a handy library that lets you define declaratively the CLI parameters your application accepts, and takes care of generating online help, validating paramaters, casting values and all of those nitty-gritty tasks you really do not want to handle when writing a command-line app.

The Clojure side

First, we create a new Leiningen project:

lein new testgraal

Its main class will be testgraal.core.

So we edit its project.clj to include our dependencies and set everything up for AOT compilation:

(defproject testgraal "0.1"
  :description "My first test with GraalVM"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.9.0"]
                 [cli-matic "0.1.14"]]
  :main testgraal.core
  :aot  :all)

We then edit src/testgraal/core.clj; copy and paste the source from https://github.com/l3nz/cli-matic/blob/master/examples/toycalc.clj and modify the namespace declaration so that this namespace is compiled AOT.

(ns testgraal.core
  (:require [cli-matic.core :refer [run-cmd]])
  (:gen-class))

(defn add_numbers
  "Sums A and B together, and prints it in base `base`"
  [{:keys [a1 a2 base]}]
  (println
   (Integer/toString (+ a1 a2) base)))

(defn subtract_numbers
  "Subtracts B from A, and prints it in base `base` "
  [{:keys [pa pb base]}]
  (println
   (Integer/toString (- pa pb) base)))

(def CONFIGURATION
  {:app         {:command     "toycalc"
                 :description "A command-line toy calculator"
                 :version     "0.1"}
   :global-opts [{:option  "base"
                  :as      "The number base for output"
                  :type    :int
                  :default 10}]
   :commands    [{:command     "add" :short "a"
                  :description ["Adds two numbers together"
                                ""
                                "Looks great, doesn't it?"]
    :opts        [{:option "a1" :short "a" :env "AA" :as "First addendum" :type :int :default 0}
                  {:option "a2" :short "b" :as "Second addendum" :type :int :default 0}]
                  :runs        add_numbers}
                 {:command     "sub"  :short "s"
                  :description "Subtracts parameter B from A"
                  :opts        [{:option "pa" :short "a" :as "Parameter A" :type :int :default 0}
                                {:option "pb" :short "b" :as "Parameter B" :type :int :default 0}]
                  :runs        subtract_numbers}]})

(defn -main
  "This is our entry point.
  Just pass parameters and configuration.
  Commands (functions) will be invoked as appropriate."
  [& args]
  (run-cmd args CONFIGURATION))                                

At this point we can have Leiningen generate everything we need:

lein compile && lein uberjar

And we get a single, standalone JAR file under target:

$ ls -l target/
total 5480
drwxr-xr-x 7 ll staff     224 Jul 19 21:55 classes
drwxr-xr-x 3 ll staff      96 Jul 19 21:55 stale
-rw-r--r-- 1 ll staff 4698514 Jul 19 21:55 testgraal-0.1-standalone.jar
-rw-r--r-- 1 ll staff  298095 Jul 19 21:55 testgraal-0.1.jar

And we can test it:

$ java -jar target/testgraal-0.1-standalone.jar --help
NAME:
 toycalc - A command-line toy calculator

Creating a standalone executable on Linux

Now for the fun part: create a CentOS 7 Linux VM with at least 4-6G of RAM; log in and download the latest version of GraalVM Community Edition from GitHub:

wget https://github.com/oracle/graal/releases/download/vm-1.0.0-rc4/graalvm-ce-1.0.0-rc4-linux-amd64.tar.gz
tar zxvf graalvm-ce-1.0.0-rc4-linux-amd64.tar.gz

There is no actual installation to do - for the moment we will just run files within the uncompresed archive.

We will also need zlib for compiling:

yum install zlib-devel

Believe it or not, that’s all we have to do.

Now we copy our testgraal-0.1-standalone.jar uberjar to our work directory, and run the native-image executable of GraalVM:

# ./graalvm-ce-1.0.0-rc4/bin/native-image \
                             -H:+ReportUnsupportedElementsAtRuntime \
                             -J-Xmx3G -J-Xms3G --no-server \
                             -jar testgraal-0.1-standalone.jar
   classlist:   1,397.10 ms
       (cap):     673.76 ms
       setup:   1,087.60 ms
   (objects):  14,990.15 ms
  (features):   4,894.45 ms
    analysis:  78,338.83 ms
    universe:   7,191.33 ms
     (parse):   5,807.91 ms
    (inline):   4,237.25 ms
   (compile):  25,749.35 ms
     compile:  37,432.01 ms
       image:   3,398.08 ms
       write:     662.61 ms
     [total]: 129,597.44 ms

Please note that I specify:

  • -H:+ReportUnsupportedElementsAtRuntime so Graal won’t abort for dynamic class loading;
  • -J-Xmx3G -J-Xms3G so we can specify the Java heap (not really needed here, but you may need this option to compile larger projects);
  • --no-server so it runs as one single process without spawning a compiler daemon.

And after a couple of minutes and a lot of CPU under the bridge….

# ls -l
total 265392
drwxr-xr-x  8 root root      4096 Jul 19 21:38 graalvm-ce-1.0.0-rc4
-rw-r--r--  1 root root 236474318 Jul 17 18:02 graalvm-ce-1.0.0-rc4-linux-amd64.tar.gz
-rwxr-xr-x  1 root root  25873378 Jul 19 22:12 testgraal-0.1-standalone
-rw-r--r--  1 root root   4698514 Jul 19 21:55 testgraal-0.1-standalone.jar

There it is! the testgraal-0.1-standalone executable, ~25M that we can compress to ~7M with gzip.

Results

If a normal execution of out testgraal takes a couple of seconds on our VM, using Graal’s own Java VM…..

# time ./graalvm-ce-1.0.0-rc4/bin/java -jar testgraal-0.1-standalone.jar -?
NAME:
 toycalc - A command-line toy calculator

USAGE:
 toycalc [global-options] command [command options] [arguments...]

VERSION:
 0.0.1

COMMANDS:
   add, a               Adds two numbers together
   sub, s               Subtracts parameter B from A

GLOBAL OPTIONS:
       --base N  10  The number base for output
   -?, --help


real    0m1.563s
user    0m2.755s
sys     0m0.156s

If we run the compiled image we get the very same result….

# time ./testgraal-0.1-standalone -?
NAME:
 toycalc - A command-line toy calculator

USAGE:
 toycalc [global-options] command [command options] [arguments...]

VERSION:
 0.0.1

COMMANDS:
   add, a               Adds two numbers together
   sub, s               Subtracts parameter B from A

GLOBAL OPTIONS:
       --base N  10  The number base for output
   -?, --help


real    0m0.005s
user    0m0.003s
sys     0m0.002s

Same result, but 300x faster in real time, and 900x faster in term of user time. Not half bad, I’d say!

So you have no excuses not to build your CLI tools in Clojure now.

Happy hacking!

See also