Javaのコンソールプログラムの起動をGraalVMのnative-imageで高速にしてみる

# 概要

このエントリでは、[GraalVM](https://www.graalvm.org/)の「[Native Image]」(https://www.graalvm.org/docs/reference-manual/native-image/) の機能を用いて、Javaのコンソールプログラムの起動時間を短くすることを扱います。

## 要約すると

あれこれやったら、特定の計算機(OCIのマイクロインスタンス相当)の性能で、JVMでの起動に1秒程度かかっていたプログラムが、ネイティブバイナリ化することで、起動が100ms程度になりました。

## 想定読者

- Javaについて基本的な知識がある方
- GraalVMのネイティブバイナリ作成について興味がある方

# やりたいこと

- Javaで書いた、コンソールアプリケーションがある。別エントリの「Javaでコンソールでのキー入力を１文字ずつハンドルしたいときにJLineを使う」(https://qiita.com/hrkt/items/885f1c3526af03939c54)で筆者が書いたものです。
    - コンソールのキー入力イベントを１文字ずつ扱いたいので、[JLine3](https://github.com/jline/jline3)を使っています。JLine3がプラットフォーム固有の機能を使用するためにJNIを使うにあたり、JANSI版のターミナルを使用しています（なので、このエントリのような一工夫が必要です）
- Native Imageでどの程度早くなるかを単純に見てみたい

# 用意

## 環境

- Linux(x86_64)もしくはmacOSが使えればよいです。このエントリでは別エントリ「cdr/code-serverとOCIのAlways FreeのMicroインスタンスでVS Codeを動かしてみる」(https://qiita.com/hrkt/items/44c78f71eb72f9d29f38)と同じものを使っています。

## GraalVMとnative-imageの用意

こんな感じで、/opt以下に導入しました。このエントリ執筆時点でのGraalVMは、19.3.1が最新版です。

```console
cd /opt
sudo curl -L -O https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-19.3.1/graalvm-ce-java8-linux-amd64-19.3.1.tar.gz
sudo tar zxvf graalvm-ce-java8-linux-amd64-19.3.1.tar.gz
```

GraalVMのツール「gu」を使い、Native Imageのためのツールを導入します。

```console
sudo  /opt/graalvm-ce-java8-19.3.1/bin//gu install native-image
```

native-imageが動作するには、対象としているプラットフォームで、バイナリをビルドするためのツールキット群が必要です。
このエントリでは、CentOS7を使っているため、下記のようにパッケージ群をインストールします。

```console
sudo yum install -y glibc-devel zlib-devel gcc
```

## 対象のJavaプログラムを用意

このエントリで使うプログラムは、簡単なコンソール電卓です。GitHubのリリースはこちらです。[https://github.com/hrkt/commandline-calculator/releases/tag/0.0.4](https://github.com/hrkt/commandline-calculator/releases/tag/0.0.4)

```
cd master
../gradlew :calculator-cli-without-spring:shadowJar
```

とすると、calculator-cli-without-spring/build/libs/calculator-cli-without-spring-0.0.1-SNAPSHOT-all.jar が出来上がります。このJarファイルは、自作部分と利用来ぶりのJarを一つにまとめてくれる、[「Gradle Shadow」](https://github.com/johnrengelman/shadow)プラグインを用いて、実行に必要なファイルを１つのJARファイルにまとめたものです。出来上がったJARファイルは約760KBでした。


```
$ ls -la calculator-cli-without-spring/build/libs/calculator-cli-without-spring-0.0.1-SNAPSHOT-all.jar
-rw-rw-r--. 1 opc opc 757383 Jan 31 23:13 calculator-cli-without-spring/build/libs/calculator-cli-without-spring-0.0.1-SNAPSHOT-all.jar
```

実行して、「1+1」を実行してみます。

```
[opc@instance-20200117-1627 commandline-calculator]$ java -jar calculator-cli-without-spring/build/libs/calculator-cli-without-spring-0.0.1-SNAPSHOT-all.jar
Calculator is running. Press ctrl-c to exit.(boot in 1272 msec.)
1+1=
1+1
=2
q
[opc@instance-20200117-1627 commandline-calculator]$

```

このとき、プログラムの起動に約1.3秒かかっていることがわかります。時間の計測は、下記のような方法で実施しています。

```Java
            RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
            long uptimeInMillis = runtimeMXBean.getUptime();
            terminal.writer().println(String.format("Calculator is running. Press ctrl-c to exit.(boot in %d msec.)",
                    uptimeInMillis));
```


# Native Imageのビルド

## 準備

上記で作ったJARファイルをいったんほどいて、作業用のスペースに展開します。

```console
cd calculator-cli-without-spring/build/libs
mkdir inside
cd inside
jar xvf ../calculator-cli-without-spring-0.0.1-SNAPSHOT-all.jar
```

このあと、native-imageで利用するためのプロファイル情報を取得するためのディレクトリを作成します。

```console
mkdir -p META-INF/native-image
```

## 実行時のプロファイルを取得

native-image実行前に、普通のJavaプログラムとして実行します。agentとともに動かすことで、動作時にどのようなリフレクションを使っているか、JNIのライブラリを使用しているかなどの、静的な解析では取得できない情報を集めます。

```console
/opt/graalvm-ce-java8-19.3.1/bin/java -agentlib:native-image-agent=config-output-dir=META-INF/native-image -cp . com.hrkt.commandlinecalculator.Main
```

前述したものと同じように「1+1=」を実行して「q」を入力してプログラムを終了した時点で、下記のような情報が集まります。

```console
[opc@instance-20200117-1627 inside]$ ls -la META-INF/native-image/
total 16
drwxrwxr-x. 2 opc opc 109 Jan 31 23:15 .
drwxrwxr-x. 6 opc opc  88 Jan 31 23:15 ..
-rw-rw-r--. 1 opc opc 689 Jan 31 23:15 jni-config.json
-rw-rw-r--. 1 opc opc   4 Jan 31 23:15 proxy-config.json
-rw-rw-r--. 1 opc opc 366 Jan 31 23:15 reflect-config.json
-rw-rw-r--. 1 opc opc 472 Jan 31 23:15 resource-config.json
```

## native-imageでバイナリを作成

ビルドします。
オプションの意味については、[サイトに説明があります。](https://www.graalvm.org/docs/reference-manual/native-image/)

```
 /opt/graalvm-ce-java8-19.3.1/bin/native-image  --no-server --no-fallback  --report-unsupported-elements-at-runtime -cp . com.hrkt.commandlinecalculator.Main
```

これには、分のオーダーで時間がかかります。気長に待ちましょう。このエントリの環境と対象プログラムでは、約5分かかります。

```
[com.hrkt.commandlinecalculator.main:13836]    classlist:  11,879.57 ms
[com.hrkt.commandlinecalculator.main:13836]        (cap):   3,604.46 ms
[com.hrkt.commandlinecalculator.main:13836]        setup:   9,898.60 ms
[com.hrkt.commandlinecalculator.main:13836]   (typeflow):  36,059.72 ms
[com.hrkt.commandlinecalculator.main:13836]    (objects):  23,227.98 ms
[com.hrkt.commandlinecalculator.main:13836]   (features):   3,516.18 ms
[com.hrkt.commandlinecalculator.main:13836]     analysis:  63,682.08 ms
[com.hrkt.commandlinecalculator.main:13836]     (clinit):   1,281.12 ms
[com.hrkt.commandlinecalculator.main:13836]     universe:   3,092.99 ms
[com.hrkt.commandlinecalculator.main:13836]      (parse):  10,531.88 ms
[com.hrkt.commandlinecalculator.main:13836]     (inline):  28,108.04 ms
[com.hrkt.commandlinecalculator.main:13836]    (compile): 176,012.21 ms
[com.hrkt.commandlinecalculator.main:13836]      compile: 227,565.33 ms
[com.hrkt.commandlinecalculator.main:13836]        image:   6,261.29 ms
[com.hrkt.commandlinecalculator.main:13836]        write:   2,044.20 ms
[com.hrkt.commandlinecalculator.main:13836]      [total]: 326,546.79 ms
```

## 実行

出来上がったバイナリを動かしてみます。様子を見るため、数回起動してみます。2桁msecの前半から、遅くても100msec程度までで、起動できていることがわかります。JVMでの起動と比較して、かなり起動が早くなっています。

```
[opc@instance-20200117-1627 inside]$ ./com.hrkt.commandlinecalculator.main
Calculator is running. Press ctrl-c to exit.(boot in 66 msec.)
^C
[opc@instance-20200117-1627 inside]$ ./com.hrkt.commandlinecalculator.main
Calculator is running. Press ctrl-c to exit.(boot in 14 msec.)
^C
[opc@instance-20200117-1627 inside]$ ./com.hrkt.commandlinecalculator.main
Calculator is running. Press ctrl-c to exit.(boot in 16 msec.)
^C
[opc@instance-20200117-1627 inside]$ ./com.hrkt.commandlinecalculator.main
Calculator is running. Press ctrl-c to exit.(boot in 24 msec.)
^C
[opc@instance-20200117-1627 inside]$ ./com.hrkt.commandlinecalculator.main
Calculator is running. Press ctrl-c to exit.(boot in 131 msec.)
^C
[opc@instance-20200117-1627 inside]$ ./com.hrkt.commandlinecalculator.main
Calculator is running. Press ctrl-c to exit.(boot in 27 msec.)
^C

```

## バイナリを確認してみる。

まず、種類を確認してみます。

```
[opc@instance-20200117-1627 inside]$ file com.hrkt.commandlinecalculator.main
com.hrkt.commandlinecalculator.main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=b5ce8429a80c97c4e12c87c516a8b24b59c64086, not stripped
```

ごく普通のELFバイナリですね。どんなライブラリを使っているかというと....


```
[opc@instance-20200117-1627 inside]$ ldd com.hrkt.commandlinecalculator.main
        linux-vdso.so.1 =>  (0x00007fff5abf9000)
        libm.so.6 => /lib64/libm.so.6 (0x00007f74c3bf7000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f74c39db000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007f74c37d7000)
        libz.so.1 => /lib64/libz.so.1 (0x00007f74c35c1000)
        librt.so.1 => /lib64/librt.so.1 (0x00007f74c33b9000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f74c2feb000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f74c3ef9000)
```

Linuxのディストリビューションにおいて、ごく標準的なライブラリを使用しています。で、サイズは....

```
[opc@instance-20200117-1627 inside]$ ls -la com.hrkt.commandlinecalculator.main
-rwxrwxr-x. 1 opc opc 10066064 Feb  1 04:08 com.hrkt.commandlinecalculator.main
```
約10MB。かなり膨らみました。

# まとめ

このエントリでは、Javaのコンソールプログラムの起動をGraalVMのnative-imageで高速にしてみることを扱いました。

「起動」までについては調べましたが、そのあとに継続的に動作しているときの速度について言及しているわけではない点にご注意ください。

# 補足

筆者が書いていたプログラム、JNIを使うために、[別エントリ](https://qiita.com/hrkt/items/885f1c3526af03939c54)で書いているように最初はJNAを使っていたのですが、native-imageがうまく実行できませんでした。GitHubにも、(Using JNA inside a native image does not work #673)[https://github.com/oracle/graal/issues/673]といったやりとりがありました。

このエントリで利用した、JNAを利用している機能に対して、GraalVMのNative Image側ではまだサポートしている状況ではないようです。