QuarkusのHibernate ORM with Panacheでid不要ならPanacheEntityBaseを使う

Quarkusを試していて、Hibernate ORM with Panacheを使ってみると、ちょっとハマった。

こんな感じのEntityクラスを作った。

@Entity
@Table(name = "users")
@Data
public class User extends PanacheEntity {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long userId;

    @Column(name = "user_name")
    private String userName;

   ...

そうするとUser.listAll()を呼び出したときにこんなエラー。idフィールドがないと言ってる。

Caused by: org.postgresql.util.PSQLException: ERROR: column user0_.id does not exist

いろいろ試したのだけど、なんのことはない、PanacheEntityidが定義されているので、代わりにPanacheEntityBaseを使えばよかった。

@Entity
@Table(name = "users")
@Data
public class User extends PanacheEntityBase {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long userId;

    @Column(name = "user_name")
    private String userName;

   ...

jpackageでJavaアプリのインストーラーを作る

Javaアプリケーションのインストールパッケージを作れるjpackageが、どうもJDK13に入りそうな勢いでEarly Accessが出てたので試してみます。
jpackageは、JavaFXにあったjpackagerをベースにしたパッケージングツールです。
JEP 343: Packaging Tool

EAはこちらからダウンロードできます。開発中のJDK13をベースにしてると書いてありますね。
jpackage

Windowsで必要なもの

Windowsインストーラーを作るには、iscc.exeが必要で、これはInno Setupに入ってます。
jrsoftware.org // Jordan Russell's Software

あと、MSI形式のインストーラーを作るにはlight.exeとcandle.exeが必要で、これはWix toolsに入ってます。
WiX Toolset
zipでwix311-binaries.zipをダウンロード・解凍してパスを通すのがいいと思います。

インストーラを作る

ではインストーラを作ってみます。
とりあえずウィンドウを作るアプリを作ってみます。

import javax.swing.*;

public class MyFrame {
    public static void main(String[] args) {
        var f = new JFrame("My Package");
        var t = new JTextArea();
        f.add(t);
        var b = new JButton("Hello");
        f.add("North", b);
        b.addActionListener(al -> t.append("Hello\n"));
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setSize(500, 400);
        f.setVisible(true);
    }
}

ボタンを押すとHelloと表示されるだけのアプリ
実行画面

jpackageではclassファイル単体ではパッケージを作れないようなので、一度jarを作ります。

> javac MyFrame.java

> mkdir target

> jar -cf target/mypack.jar MyFrame.class

そしたら、jpackageでインストーラを作成

>jpackage create-installer -ooutput pack -input target --name MyApp --main-class MyFrame --main-jar mypack.jar
Warning: Windows Defender may prevent jpackage from functioning. If there is an issue, it can be addressed
 by either disabling realtime monitoring, or adding an exclusion for the directory "C:\Users\naoki\AppData\Local\Temp\".
Warning: Windows Defender may prevent jpackage from functioning. If there is an issue, it can be addressed
 by either disabling realtime monitoring, or adding an exclusion for the directory "C:\Users\naoki\AppData\Local\Temp\".
Warning: Windows Defender may prevent jpackage from functioning. If there is an issue, it can be addressed
 by either disabling realtime monitoring, or adding an exclusion for the directory "C:\Users\naoki\AppData\Local\Temp\".

こんな感じでexeとmsiインストーラができています。--typeで形式を指定することもできます。

>dir pack
 C:\Users\naoki\Documents\mypack\pack のディレクトリ

2019/03/06  05:39    <DIR>          .
2019/03/06  05:39    <DIR>          ..
2019/03/06  05:38        33,266,979 MyApp-1.0.exe
2019/03/06  05:39        53,588,671 MyApp-1.0.msi

exeが33MBでmsiが53MBですね。
exeを実行すると、インストーラ画面がでます。
インストーラ画面

インストール実行すると、あのインストール画面が。
インストール実行

スタートメニューにも追加されています。 スタートメニュー

もちろん、このアイコンをクリックすれば起動します。
実行画面

ところで、msiをダブルクリックするといきなりインストール始まるので注意です。

ファイルはWindowsの場合C:\Program Filesにインストールされます。
jshell.exeなど、JDKのファイルも含まれてることがわかります。 JDKもインストールされる

JDKまるごと含まれてると考えたら、50MBでインストーラは妥当な感じですね。

「アプリと機能」からアンインストールもできます。
アプリと機能

JDKだけのインストーラも作れるみたい。なかなか便利ですね。

MicronautのメトリクスとPrometheus

MicronautでPrometheusでメトリクス取りたい話

Micronautはmicrometer経由でprometheusに対応してるので、create-appするとき--featuresにmicrometer-prometheusつけるとそれっぽいdependencyや設定を生成してくれます。

$ mn create-app foo --features micrometer-prometheus

Gradleはこんな感じ。

    compile "io.micronaut.configuration:micronaut-micrometer-core"
    compile "io.micronaut.configuration:micronaut-micrometer-registry-prometheus"

設定はこんな感じ。

micronaut:
    metrics:
        enabled: true
        export:
            prometheus:
                enabled: true
                step: PT1M
                descriptions: true

それで http://localhost:8080/metrics/ にアクセスするとメトリクス名の一覧がとれます。

{
  "names": [
    "executor",
    "executor.active",
    "executor.completed",
    "executor.pool.size",
    "executor.queued",
    "hikaricp.connections",
...
    "process.uptime",
    "system.cpu.count",
    "system.cpu.usage"
  ]
}

さらにその名前を指定すると具体的な値がとれる感じ
http://localhost:8080/metrics/hikaricp.connections

{
  "name": "hikaricp.connections",
  "measurements": [
    {
      "statistic": "VALUE",
      "value": 0
    }
  ],
  "availableTags": [
    {
      "tag": "pool",
      "values": [
        "HikariPool-1"
      ]
    }
  ]
}

ということでDockerでPrometheusをたてます。
こんな感じの設定ファイルを適当にローカルに置いておきます。targetsの位置はホストのIP。

global:
    scrape_interval: 5s
    
scrape_configs:
    - job_name: 'mystat'
      metrics_path: '/prometheus'
      static_configs:
          - targets:
              - '172.17.0.1:8080'

あとはDockerにその位置をおしえてあげればOK

$ docker run -d --name mystat-prom -p 9090:9090 \
   -v /c/Users/naoki/NetBeansProjects/my-stat-mn:/prom-data \
   prom/prometheus --config.file=/prom-data/prometheus.yml

とするとPrometheusは起動するんですが、MicronautからPrometheus用の情報がでてきません。

なんかこんな感じでEndpointを作ってあげる必要があるっぽい。

import io.micrometer.prometheus.PrometheusMeterRegistry;
import io.micronaut.management.endpoint.annotation.Endpoint;
import io.micronaut.management.endpoint.annotation.Read;
import javax.inject.Inject;

@Endpoint(id = "prometheus", value="/prometheus",
   defaultEnabled=true, defaultSensitive=false)
public class PrometheusEndPoint {
    @Inject
    private PrometheusMeterRegistry prometheus;

    @Read
    public String scrape() {
        return prometheus.scrape();
    }
}

Micronaut: How to get metrics in the Prometheus format? - Stack Overflow

そしたらPrometheusでデータがとれるようになりました。

f:id:nowokay:20190217213656p:plain
Prometheus

PostgreSQLのリアクティブアクセスをネイティブ化できなかった

とりあえずメモ的に。
R2DBCとmicronaut+reactive-pg-clientの2通りでnative-imageを試したけど、nettyまわりでだめだったという話

R2DBC

リアクティブデータアクセスとしては本命?
R2DBC
R2DBCはセントラルリポジトリにないので、springのリポジトリを使います。

    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>    
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>        
    </repositories>

R2DBCのライブラリはこれ。

        <dependency>
            <groupId>io.r2dbc</groupId>
            <artifactId>r2dbc-postgresql</artifactId>
            <version>1.0.0.M6</version>
        </dependency>

コードはこんな感じで。

import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;
import io.r2dbc.postgresql.PostgresqlConnectionFactory;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        PostgresqlConnectionConfiguration conf = 
                PostgresqlConnectionConfiguration.builder()
                                                 .host("localhost")
                                                 .port(5432)
                                                 .database("mystat")
                                                 .username("mystat")
                                                 .password("pass").build();
        PostgresqlConnectionFactory connFact = 
                new PostgresqlConnectionFactory(conf);
        ExecutorService es = Executors.newSingleThreadExecutor();
        Scheduler sc = Schedulers.fromExecutorService(es);

        connFact.create()
                .map(conn -> conn.createStatement("select * from USERS"))
                .flatMapMany(stmt -> stmt.execute())
                .flatMap(result -> result.map((row, meta) -> 
                        String.format("handle:%s name:%s",
                    row.get("user_handle"), row.get("user_name"))))
                .subscribeOn(sc)
                .doOnTerminate(() -> es.shutdown())
                .subscribe(System.out::println);
    }
}

Schedulerにはelastic()が楽なんだろうけど、コマンドラインだと処理が終わる前にコマンドが終了して何も出力されないので、ExecutorServiceを用意してfromExecutorService(es)を使います。で、doOnTerminateshutdown()
もっといい方法があったら教えてください。
native-imageするのでJava8文法しか使えないからvarはナシ。newとかbuilderとかはvarで受けたいけど。

そしてnetty関係でダメでした。

R2DBC-client

R2DBCはちょっとラップして便利にしてくれるclientがあるので使ってみる。

        <dependency>
            <groupId>io.r2dbc</groupId>
            <artifactId>r2dbc-client</artifactId>
            <version>1.0.0.M6</version>
        </dependency>

こんな感じ。ちょっと短くなった。

import io.r2dbc.client.R2dbc;
import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;
import io.r2dbc.postgresql.PostgresqlConnectionFactory;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        PostgresqlConnectionConfiguration conf = 
                PostgresqlConnectionConfiguration.builder()
                                                 .host("localhost")
                                                 .port(5432)
                                                 .database("mystat")
                                                 .username("mystat")
                                                 .password("pass").build();
        PostgresqlConnectionFactory connFact = 
                new PostgresqlConnectionFactory(conf);
        ExecutorService es = Executors.newSingleThreadExecutor();
        Scheduler sc = Schedulers.fromExecutorService(es);
        
        R2dbc r2dbc = new R2dbc(connFact);
        r2dbc.inTransaction(h -> 
                h.select("select * from USERS")
                        .mapRow(row -> row.get("user_name")))
                .subscribeOn(sc)
                .doOnTerminate(() -> es.shutdown())
                .subscribe(o -> System.out.println(o));
    }
}

まあ、もとのR2DBCでこけてるので、もちろんnative-imageはnettyまわりでこける。

Reactive Postgres Client

もうひとつ、PostgreSQL用にはReactive Postgres Clientというのがある。
Reactive Postgres Client | reactive-pg-client

Micronaut経由で使ってみたらnative-image対応してくれてるんでは、と試してみる。
とはいえ、Webでやるのは面倒なので、CLIとして使ってみます。
Micronaut: 11. Standalone Command Line Applications

Micoronautのプロジェクト作成コマンドmnで大枠を作ります。

$ mn create-cli-app my-cli --features postgres-reactive

build.gradleにソースレベルなどの設定を追加

sourceCompatibility="1.8"
targetCompatibility="1.8"

application.ymlにPostgreSQL関係の設定がすでに書いてあるので、適切に変更。ここで、clientの下にあるべきものたちのインデントがちゃんとしてない気がするので修正してます。

postgres:
    reactive:
        client:
            port: 5432
            host: localhost
            database: mystat
            user: mystat
            password: pass
            # maxSize: 5

修正のPRでも送るか~と思ったけど、ぺろぺろっとできる修正じゃなさそうなので保留
あとは、MyCliCommand.javaができてるはずなので、処理を書きます。

public void run() {
    // business logic here
    if (verbose) {
        System.out.println("Hi!");
    }
    data();
}

@Inject
PgPool client;

public void data() {
    client.rxQuery("select * from users")
            .map(rowSet -> {
                List<String> result = new ArrayList<>();
                PgIterator ite = rowSet.iterator();
                while(ite.hasNext()) {
                    Row row = ite.next();
                    result.add(row.getString("user_name"));
                }
                return result;
            })
            .blockingGet()
            .stream()
            .forEach(System.out::println);
}

println("Hi!")は元からある処理。
PgPoolをInjectするだけで使えます。io.reactiverse.pgclient.PgPoolではなくio.reactiverse.reactivex.pgclient.PgPoolなので注意。
しかし、R2DBCはReactorだけどReactive-pg-clientはRxJavaなので戸惑う。

そしてnative-imageしてみるけど、やはりnetty関係でだめだった。

そのうちリベンジする。

PostgreSQLへのJDBCアクセスをネイティブ化する

PostgreSQLへのJDBCアクセスがあるコードをGraalVMでネイティブイメージ化するとき、org.postgresql.core.v3.ConnectionFactoryImplの対応が必要だったのでメモ

たとえばこんな感じでPostgreSQLにアクセスします。

public class Main {
    public static void main(String[] args) throws SQLException {
        try (Connection conn = DriverManager.getConnection(
                "jdbc:postgresql://localhost:5432/mystat", "mystat", "pass");
             Statement stmt = conn.createStatement();
             ResultSet result = stmt.executeQuery("select * from users")) {
            while (result.next()) {
                System.out.printf("name:%s handle:%s %n",
                        result.getString("user_name"),
                        result.getString("user_handle"));
            }
        }
    }
}

JDBCドライバは42.2.5を使ってます。

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.2.5</version>
</dependency>

これをネイティブコンパイルすると、なんかエラーが出ました。

$ native-image -jar AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies.jar
Build on Server(pid: 153, port: 56330)*
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:153]    classlist:   2,296.43 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:153]        (cap):   3,746.53 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:153]        setup:   5,098.37 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:153]     analysis:   5,581.32 ms
error: unsupported features in 4 methods
Detailed message:
Error: com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved type during parsing: com.sun.jna.Platform. 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.postgresql.sspi.SSPIClient.isSSPISupported(SSPIClient.java:90)
Call path from entry point to org.postgresql.sspi.SSPIClient.isSSPISupported():
        at org.postgresql.sspi.SSPIClient.isSSPISupported(SSPIClient.java:90)

ぐぐってみると、なんかスタブが必要そう。

native-image fails to compile PostgreSQL JDBC client. · Issue #727 · oracle/graal

このようなクラスで対応できるみたいです。

https://github.com/katox/graal-pg-client/blob/master/src/main/java/stub/InternalConnectionFactoryImpl.java

コンパイルするにはSubstrateVMのライブラリが必要です。

<dependency>
    <groupId>com.oracle.substratevm</groupId>
    <artifactId>svm</artifactId>
    <version>1.0.0-rc11</version>
    <scope>provided</scope>
</dependency>

いけました!

$ native-image -jar AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies.jar
Build on Server(pid: 939, port: 58056)*
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]    classlist:   2,031.76 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]        (cap):   2,046.18 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]        setup:   3,208.73 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]   (typeflow):   5,918.46 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]    (objects):   4,188.45 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]   (features):     367.38 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]     analysis:  10,700.59 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]     universe:     441.69 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]      (parse):     690.96 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]     (inline):   1,360.90 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]    (compile):   5,880.47 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]      compile:   8,622.91 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]        image:     932.34 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]        write:     365.44 ms
[AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies:939]      [total]:  26,385.43 ms

ただ、なんかもう一度native-imageするとエラーがでます。サーバーを使わないようにしたほうがよさそう。

ネイティブコンパイルすると、やたら速いのですごく違和感がありますね。

$ time java -jar AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies.jar
name:John Smith handle:john

real    0m0.699s
user    0m0.609s
sys     0m0.500s
$ time ./AccessPostgres-1.0-SNAPSHOT-jar-with-dependencies
name:John Smith handle:john

real    0m0.033s
user    0m0.000s
sys     0m0.016s

Windows UpdateでSteinberg系がダメになった

表題のとおりなんだけど、またWindows UpdateVST関係がダメになった。 今回は次の2点

  • Cubaseのライセンスが壊れた
  • Cantabileが起動できなくなった(起動中になにかインストールを始めて進まなかった)

Cubaseのライセンスについては、eLicenserの新しいのをインストールしてライセンスの補修をやったら2度目でライセンスが修復された。
ダウンロード | Steinberg

Cantabileが起動できなくなった件は、Cubaseのライセンスが復活したあとでCubaseを起動するとき「VSTオーディオシステムの準備中」と出たのでUR22系かなと思ってそのメッセージで検索したら次のサイトが出た。
【Steinberg共通】ソフトウェアの起動時、「インストールの準備中」と表示されて起動できない場合、どうすればいいですか?(Windows 環境のみ) - ヤマハ

まさにこれだったので、ドライバーの新しいのを入れたら復旧した。
ダウンロード | Steinberg

あと、地味にこれがHatena Blog初投稿になった

Scalaをネイティブコンパイルする

以前、Kotlinをネイティブコンパイルするという話を書いたので、今回はScalaをネイティブコンパイルしてみます。
Kotlinをネイティブコンパイルする - きしだのはてな


今回はGraalVMのnative-imageでのネイティブ化とScala Nativeでのネイティブ化を比較してみます。
1/19のScala福岡2019でのLTを整理しなおしたものでもあります。

インストール

sbtをインストールしておけば、あとは全部sbtがやってくれます。
Kotlinの場合と同様、WSLで試すのでsdkmanを使います。

$ sdk update
$ sdk install sbt


Macの場合はbrew

$ brew update
$ brew install sbt

ふつうにHello World

まずはHello Worldしてみます。sbt new scala/hello-world.g8とするとHello worldプロジェクトが作れます。プロジェクト名を聞かれるので、hello-scalaとしておきます。

scala$ sbt new scala/hello-world.g8
[info] Set current project to scala (in build file:/home/naoki/scala/)
[info] Set current project to scala (in build file:/home/naoki/scala/)

A template to demonstrate a minimal Scala application

name [Hello World template]: hello-scala

Template applied in /home/naoki/scala/./hello-scala


hello-scalaフォルダができているので、そこに移動します。

scala$ cd hello-scala


こんな感じのファイルができています。

hello-scala$ find . -type f
./build.sbt
./project/build.properties
./src/main/scala/Main.scala


Main.scalaはこんな感じ

object Main extends App {
  println("Hello, World!")
}


fat jarが作りたいのでassemblyプラグインを使う設定を書きます。

hello-scala$ vi project/assembly.sbt


assembly.sbtにはこんな記述

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.9")


ではsbt assemblyでコンパイル

hello-scala$ sbt assembly
[info] Loading settings for project hello-scala-build from assembly.sbt ...
[info] Loading project definition from /home/naoki/scala/hello-scala/project
...
[info] Packaging /home/naoki/scala/hello-scala/target/scala-2.12/hello-world-assembly-1.0.jar ...
[info] Done packaging.
[success] Total time: 53 s, completed Jan 20, 2019 4:18:02 PM


target/scala-2.12にコンパイル結果ができています。

hello-scala$ ls target/scala-2.12/
classes  hello-world-assembly-1.0.jar  resolution-cache


では実行。

hello-scala$ java -jar target/scala-2.12/hello-world-assembly-1.0.jar
Hello, World!


Scala完全に理解した。

GraalVMでネイティブ化

ではGraalVMのnative-imageを使ってネイティブ化してみます。

hello-scala$ native-image -jar target/scala-2.12/hello-world-assembly-1.0.jar -H:Name=hello
Build on Server(pid: 6548, port: 50317)
[hello:6548]    classlist:  20,464.64 ms
[hello:6548]        (cap):   1,883.26 ms
[hello:6548]        setup:   2,212.70 ms
[hello:6548]   (typeflow):   1,512.00 ms
[hello:6548]    (objects):     653.95 ms
[hello:6548]   (features):      58.45 ms
[hello:6548]     analysis:   2,292.44 ms
[hello:6548]     universe:     115.83 ms
[hello:6548]      (parse):     109.53 ms
[hello:6548]     (inline):     419.27 ms
[hello:6548]    (compile):   1,165.83 ms
[hello:6548]      compile:   1,821.09 ms
[hello:6548]        image:     127.89 ms
[hello:6548]        write:     142.38 ms
[hello:6548]      [total]:  27,227.41 ms


そして実行

hello-scala$ ./hello
Hello, World!


GraalVM完全に理解してた。


あと、たまにこんなエラーが出るけど、もう一回実行すればOKのはず

hello-scala $ native-image -jar target/scala-2.12/hello-world-assembly-1.0.jar -H:Name=fib
Build on Server(pid: 6540, port: 54548)
Could not connect to image build server running on port 54548
Underlying exception: java.net.ConnectException: Connection refused
Error: Processing image build request failed

Scala Nativeでネイティブ化

Scalaには、Scala Nativeというネイティブコンパイルプロジェクトがあるので、これを使ってみます。
sbt new scala-native/scala-native.g8でScala Native用のプロジェクトが作れます。

scala$ sbt new scala-native/scala-native.g8
[info] Set current project to scala (in build file:/home/naoki/scala/)
[info] Set current project to scala (in build file:/home/naoki/scala/)

A minimal project that uses Scala Native.

name [Scala Native Seed Project]: native-hello

Template applied in /home/naoki/scala/./native-hello


こんな感じのファイルができています。

scala$ cd native-hello
native-hello$ find . -type f
./build.sbt
./project/build.properties
./project/plugins.sbt
./src/main/scala/Main.scala


Main.scalaはこんな感じ

object Main {
  def main(args: Array[String]): Unit =
    println("Hello, world!")
}


sbt nativeLinkでネイティブコンパイルします。

native-hello$ sbt nativeLink
[info] Loading settings for project native-hello-build from plugins.sbt ...
[info] Loading project definition from /home/naoki/scala/native-hello/project
...
[error] In file included from /home/naoki/scala/native-hello/target/scala-2.11/native/lib/gc/immix/Heap.c:11:
[error] /home/naoki/scala/native-hello/target/scala-2.11/native/lib/gc/immix/StackTrace.h:4:10: fatal error: 'libunwind.h' file not found
[error] #include <libunwind.h>
[error]          ^
[error] 1 error generated.


あれ・・・。WSLではネイティブコンパイルできない模様。Windowsにsbtをインストールすれば問題なくできるのだけど、GraalVMはWindowsに対応してないのでnative-imageとの比較ができず・・・


ということでMacでやってみました。

native-hello$ sbt nativeLink
[info] Loading settings for project native-hello-build from plugins.sbt ...
...
[info] Compiling to native code (601 ms)
[info] Linking native code (immix gc) (113 ms)
[success] Total time: 9 s, completed 2019/01/21 2:57:46


実行

native-hello$ ./target/scala-2.11/native-hello-out 
Hello, world!


ということで、Scala Native完全に理解した

比較してみる

では、サイズと起動を含めた実行時間を比較してみます。
まずサイズ。

scala $ du -h hello-scala/target/scala-2.12/hello-world-assembly-1.0.jar 
 16M	hello-scala/target/scala-2.12/hello-world-assembly-1.0.jar
scala $ du -h hello-scala/hello
2.3M	hello-scala/hello
scala $ du -h native-hello/target/scala-2.11/native-hello-out 
2.0M	native-hello/target/scala-2.11/native-hello-out

GraalVMのnative-imageで2.3M、Scala Nativeは2Mと、あまり変わらない感じですね。
あと、fat-jarが大きい。


起動も含めた実行時間。

scala $ time java -jar hello-scala/target/scala-2.12/hello-world-assembly-1.0.jar 
Hello, World!

real	0m0.443s
user	0m0.499s
sys	0m0.045s

scala $ time ./hello-scala/hello
Hello, World!

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

scala $ time ./native-hello/target/scala-2.11/native-hello-out 
Hello, world!

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

やはりJVMでの起動は遅くて、GraalVMのnative-imageやScala Nativeは速いです。Scala Nativeのほうがちょっと速い。この傾向はKotlin Nativeも同じですね。

処理時間の比較

では、処理時間を計測してみましょう。再帰フィボナッチを書いてみます。
こんな感じで5回ほど実行したあとの時間を計測してみます。

import java.lang.System

object Main extends App {
  def fib(i: Int): Int = {
    if (i < 2)
      i
    else
      fib(i - 2) + fib(i - 1)
  }

  (0 to 5) foreach (_ => fib(31))
  val start = System.currentTimeMillis()
  val f = fib(46)
  val time = System.currentTimeMillis() - start
  println("Hello, World!"+f)
  println(time)
}


ではsbt assemblyしてnative-imageします。

hello-scala $ sbt assembly
[info] Loading settings for project hello-scala-build from assembly.sbt ...
...
[info] Done packaging.
[success] Total time: 5 s, completed 2019/01/21 3:28:56
hello-scala $ native-image -jar target/scala-2.12/hello-world-assembly-1.0.jar -H:Name=fib
Build on Server(pid: 6540, port: 54548)
[fib:6540]    classlist:   6,444.40 ms
...
[fib:6540]      [total]:  16,221.11 ms


そして実行

hello-scala $ ./fib
Bus error: 10

あれー、なんかエラー。


WSLだと詳細なエラーが出てくれたので、これを参考に。

fib$ ./fib
...
Full Stacktrace:


  RSP 00007fffdbac4b70 RIP 0000000000402471  [image code] Main$delayedInit$body.apply(Main.scala:3)
  RSP 00007fffdbac4b90 RIP 00000000004da5d1  [image code] scala.Function0.apply$mcV$sp(Function0.scala:34)
  RSP 00007fffdbac4b90 RIP 00000000004da5d1  [image code] scala.Function0.apply$mcV$sp$(Function0.scala:34)
  RSP 00007fffdbac4b90 RIP 00000000004da5d1  [image code] scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
  RSP 00007fffdbac4b90 RIP 00000000004da5d1  [image code] scala.App.$anonfun$main$1$adapted(App.scala:76)
  RSP 00007fffdbac4b90 RIP 00000000004da5d1  [image code] scala.App$$Lambda$70d7d477fd024eedba5ecc3ce869ad73954cc12b.apply(Unknown Source)
  RSP 00007fffdbac4bc0 RIP 00000000004db450  [image code] scala.collection.immutable.List.foreach(List.scala:388)
  RSP 00007fffdbac4c00 RIP 00000000004fa035  [image code] scala.App.main(App.scala:76)
  RSP 00007fffdbac4c40 RIP 000000000040269e  [image code] scala.App.main$(App.scala:74)
  RSP 00007fffdbac4c40 RIP 000000000040269e  [image code] Main$.main(Main.scala:3)
  RSP 00007fffdbac4c40 RIP 000000000040269e  [image code] Main.main(Main.scala)
  RSP 00007fffdbac4c40 RIP 000000000040269e  [image code] com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:164)
  RSP 00007fffdbac4c90 RIP 00000000004117c9  [image code] com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)
...
Use runtime option -R:-InstallSegfaultHandler if you don't want to use SubstrateSegfaultHandler.

Bye bye ...


delayedInitでエラーが出てます。どうも、startやfなどがstaticフィールドになっていて、ここの初期化で問題が発生してそう。
ということで、mainメソッドを定義する書き方に変えてみます。

import java.lang.System

object Main {
  def fib(i: Int): Int = {
    if (i < 2)
      i
    else
      fib(i - 2) + fib(i - 1)
  }

  def main(args: Array[String]) {
    (0 to 5) foreach (_ => fib(31))
    val start = System.currentTimeMillis()
    val f = fib(46)
    val time = System.currentTimeMillis() - start
    println("Hello, World!"+f)
    println(time)
  }
}


動きまんた!

hello-scala $ ./fib
Hello, World!1836311903
10565


JVMで実行するとこんな感じ。

hello-scala $ java -jar target/scala-2.12/hello-world-assembly-1.0.jar 
Hello, World!1836311903
6588


Scala Nativeのほうも同様に書き換えて実行してみます。

native-hello $ vi src/main/scala/Main.scala 
native-hello $ sbt nativeLink
[info] Loading settings for project native-hello-build from plugins.sbt ...
...
[info] Compiling to native code (952 ms)
[info] Linking native code (immix gc) (114 ms)
[success] Total time: 9 s, completed 2019/01/21 3:39:05
native-hello $ ./target/scala-2.11/native-hello-out 
Hello, World!1836311903
11730


やはりJVMで実行するほうが実行時のプロファイルを使った最適化ができる分、実行が速いですね。そして、Scala NativeよりもGraalVMのnative-imageのほうが少し速そう。


というとこで、LLVMなんだからそこまで遅くないんではというkmizuさんからの指摘があり、xuwei_kさんから最適化オプションつけたら速くなるんではというアドバイスもあったので試してみました。
build.sbtにnativeCompileOptions:=Seq("-O3")を追加してみます。

scalaVersion := "2.11.12"

// Set to false or remove if you want to show stubs as linking errors
nativeLinkStubs := true
nativeCompileOptions := Seq("-O3")
enablePlugins(ScalaNativePlugin)
native-hello $ sbt nativeLink
[info] Loading settings for project native-hello-build from plugins.sbt ...
...
[info] Linking native code (immix gc) (104 ms)
[success] Total time: 7 s, completed 2019/01/23 0:40:53
native-hello $ ./target/scala-2.11/native-hello-out 
Hello, World!1836311903
8583


だいぶ速くなりました!

Javaライブラリを使う

試しに、Date Time APIを使ってみます。

println(java.time.LocalDateTime.now())


まずはGraalVMのnative-image

hello-scala $ vi src/main/scala/Main.scala 
hello-scala $ sbt assembly
[info] Loading settings for project hello-scala-build from assembly.sbt ...
...
[info] Done packaging.
[success] Total time: 5 s, completed 2019/01/21 3:54:12
hello-scala $ java -jar target/scala-2.12/hello-world-assembly-1.0.jar 
Hello, World!1836311903
7108
2019-01-21T03:54:23.324248
hello-scala $ native-image -jar target/scala-2.12/hello-world-assembly-1.0.jar -H:Name=fib
Build on Server(pid: 6540, port: 54548)
[fib:6540]    classlist:   6,195.24 ms
...
[fib:6540]      [total]:  14,723.41 ms
hello-scala $ ./fib 
Hello, World!1836311903
10687
2019-01-21T03:55:22.131

JVM版とは秒以下の表示でちょっと挙動が違いますが、ちゃんと動いています。


Scala Nativeではjava.time.LocalDateTimeがリンクできないというエラーになりました。

native-hello $ vi src/main/scala/Main.scala 
native-hello $ sbt nativeLink
[info] Loading settings for project native-hello-build from plugins.sbt ...
...
[info] Linking (928 ms)
[error] cannot link: @java.time.LocalDateTime
[error] cannot link: @java.time.LocalDateTime$
[error] cannot link: @java.time.LocalDateTime$::now_java.time.LocalDateTime
[error] unable to link
[error] (Compile / nativeLink) unable to link
[error] Total time: 5 s, completed 2019/01/21 3:46:10


GraalVMのnative-imageは入力がjarなので、動的クラスロードなどの制約は強いですが結構自由にJavaのライブラリが使えます。一方でScala Nativeでは入力がScalaソースなので、Scalaソースを用意するかSystem.currentTimeMillisのように専用に用意されたライブラリが必要です。
Scalaで書いたサーバーをネイティブ化する、という用途にはGraalVMのnative-imageのほうが向いています。まだ実用には厳しいですが、成熟すれば期待ができます。Scala Nativeのほうは、そういう方向性ではなさそう。

まとめ

こんな感じですかね。サーバーコードをネイティブ化という場合には、GraalVMのnative-imageにがんばってもらう感じ。

プロダクト 入力 HelloWorldサイズ Scala言語対応 Javaライブラリ
Scala JVM Scalaソース 16MB 完全 ほぼ完全に使える
Scala Native Scalaソース 2.0MB 使えないものあるかも ほぼ使えない
GraalVM native-image バイトコード 2.3MB 使えないものあり 制約はあるが使える

起動時間は
JVM >>(越えられない壁)>> GraalVM > Scala Native
実行時間は最適化しない状態では
Scala Native > GraalVM >> JVM
という感じになりました。
Scala Nativeに-O3オプションをつけると
GraalVM >> Scala Native > JVM
という感じに。Scala Native使うときは、最適化をちゃんと指定したほうがよさげ。
(右のほうが速い)


AMDプロセッサだとまた違う傾向になるかも?
https://twitter.com/kmizu/status/1087236983322173440