In this post, I briefly summarize my experience of making cli4clj “GraalVM-ready”.
The part of GraalVM that interested me, right now, was the capability to build native executables for JVM-based software. However, this post is not about introducing GraalVM. For more information about GraalVM, see, e.g., the GraalVM website or Jan’s blog post on Clojure with the GraalVM.
For building the native image, I first created a standalone Jar via “lein uberjar”. Afterwards, I used GraalVM’s native-image to build the native image:
~/r/p/c/cli4clj ((ec7ba86a…)) ~/Downloads/graalvm-ce-1.0.0-rc12/bin/native-image --no-server --report-unsupported-elements-at-runtime -jar target/cli4clj-1.7.2-standalone.jar [cli4clj-1.7.2-standalone:17220] classlist: 5,198.22 ms [cli4clj-1.7.2-standalone:17220] (cap): 1,395.43 ms [cli4clj-1.7.2-standalone:17220] setup: 3,496.17 ms [cli4clj-1.7.2-standalone:17220] (typeflow): 335,004.07 ms [cli4clj-1.7.2-standalone:17220] (objects): 31,205.97 ms [cli4clj-1.7.2-standalone:17220] (features): 746.55 ms [cli4clj-1.7.2-standalone:17220] analysis: 368,574.15 ms [cli4clj-1.7.2-standalone:17220] universe: 7,927.50 ms [cli4clj-1.7.2-standalone:17220] (parse): 9,650.90 ms [cli4clj-1.7.2-standalone:17220] (inline): 5,801.23 ms [cli4clj-1.7.2-standalone:17220] (compile): 50,067.12 ms [cli4clj-1.7.2-standalone:17220] compile: 67,484.15 ms [cli4clj-1.7.2-standalone:17220] image: 4,716.99 ms [cli4clj-1.7.2-standalone:17220] write: 552.31 ms [cli4clj-1.7.2-standalone:17220] [total]: 458,269.09 ms
However, to make this work, I had to do some adjustments to cli4clj to “make GraalVM happy”:
- Add type hints to avoid reflective calls.
- Downgrade Clojure from 1.10 to 1.9 as, currently, Clojure 1.10 does not work with GraalVM.
- Add a customized version of clojure.main/skip-whitespace, which adds type hints and removes the “readLine” case.
Furthermore, there is still a minor lurking issue. The verbose exception printing, which is disabled by default, will not work yet as clojure.stacktrace/print-cause-trace is not “GraalVM friendly” yet. I did not address this aspect yet as it only concerns a minor use case and I wanted to experiment a bit first. In future, if GraalVM support proofs valuable, I will try to fix this.
As a result of the native image generation, I got an ~37 MB executable of the cli4clj example. Note that I used dynamic linking on Linux, which caused the following shared object dependencies:
~/r/p/c/c/dist (master=) ldd cli4clj-1.7.2-standalone linux-vdso.so.1 (0x00007ffdc9fc6000) libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f31d15d2000) libdl.so.2 => /lib64/libdl.so.2 (0x00007f31d15cc000) libz.so.1 => /lib64/libz.so.1 (0x00007f31d15b2000) libcrypt.so.1 => /lib64/libcrypt.so.1 (0x00007f31d1577000) librt.so.1 => /lib64/librt.so.1 (0x00007f31d156d000) libc.so.6 => /lib64/libc.so.6 (0x00007f31d13a7000) /lib64/ld-linux-x86-64.so.2 (0x00007f31d161d000)
So far, I could run the generated executable on Fedora and in Windows with the Windows Subsystem for Linux using a Debian basis.
Below is a simple comparison of running the native executable produced by GraalVM versus the standalone Jar run via Java:
r/p/c/c/dist (master=) time ./cli4clj-1.7.2-standalone cli# q 0.03user 0.06system 0:03.38elapsed 2%CPU (0avgtext+0avgdata 50488maxresident)k 0inputs+8outputs (0major+9253minor)pagefaults 0swaps ~/r/p/c/c/dist (master=) time java -jar cli4clj-1.7.2-standalone.jar cli# q 6.33user 0.20system 0:06.27elapsed 104%CPU (0avgtext+0avgdata 183840maxresident)k 96inputs+8outputs (0major+45981minor)pagefaults 0swaps
Note that the reported system time is rather meaningless as the cli4clj example does not automatically terminate but waits at the command input prompt. So, the reported system time mainly depends on how quickly I pressed “q” for terminating the example application.
The other reported values are not impacted that notably by this peculiarity.
The start-up time of the native version is notably faster, which can also be observed in the reported user time and CPU usage. Furthermore, the memory footprint of the native version is less than a third of the Jar file counterpart.
Furthermore, now (starting with version 1.7.2), you can include cli4clj in your GraalVM-based command line applications.
I hope you find this blog post and cli4clj useful. If you have constructive feedback please let me know.