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

ところでJavaのRaw Sring Literalsはどうなってるの?

Java12に入るとされたRaw String Literalsですが、結局は仕様から落とされました。
理由としては、貴重なクオート文字であるバッククオートをそんな簡単に使っていいの?というのが主題でした。
Raw Stringの議論では、インデントの扱いをどうするかが主で、区切り文字については あまり議論されてなかった気もします。


というところで、新年に入ってBrian Goetz氏が議論を再開しようと呼びかけています。
Raw string literals -- restarting the discussion


James Laskey氏が論点のまとめを出しています。ダイアグラムがわかりやすい。
Enhancing Java String Literals Round 2
http://cr.openjdk.java.net/~jlaskey/Strings/RTL2/index.html

  • 複数行とRawとどっちが大事?→複数行
  • 区切り文字には何を使う?→ダブルクオート
  • クオート文字を使うときは?→エスケープシーケンス


といった感じ。"""になりそうな雰囲気


で、ちょうどSwift5もRaw Stringを導入してるところで、その資料が共有されました。
Fwd: Raw string literals: learning from Swift


Swiftyかどうか、という基準いいですね。
swift-evolution/0200-raw-string-escaping.md at master · apple/swift-evolution


という感じで、調査から入ってる段階なので、まだまだ時間はかかりそう。Java 14ですかね。
実装に入ってしまえば時間はかからない気もするけど。

Truffle言語で関数呼び出しを実装する

Truffleを使って言語を実装してみたのだけど、やはり関数呼び出しもやりたい。
Truffleでの言語実装を最小手数ではじめる - きしだのはてな
簡易Truffle言語に変数を実装する - きしだのはてな

とりあえず関数

今回、関数は埋め込みで呼び出しだけ行ってみます。ということで、とりあえずランダムを返す関数を実装。

public abstract class RandNode extends MathNode {
    static Random rand = new Random();

    @Specialization
    public long rnd() {
        return rand.nextInt(10);
    }
}


ちなみに、native-imageするときstaticイニシャライザはコンパイル時に実行されるので、なにも考えずにネイティブイメージを作ると乱数が固定されて毎回同じ値を返します。
--delay-class-initialization-to-runtime=RandNodeをつける必要があります。
Understanding Class Initialization in GraalVM Native Image Generation

CallTargetを保持する

関数としてはCallTargetが欲しいので、CallTargetを保持するクラスを作ります。

class MathFunction {
    private final RootCallTarget callTarget;

    public MathFunction(RootCallTarget callTarget) {
        this.callTarget = callTarget;
    }    
}


CallTargetを作るときにRootNodeが必要になるのだけど、前回作ったものは変数定義を入れてしまったので、改めてRootNodeを作っておきます。

class FuncRootNode extends RootNode {
    MathNode function;

    public FuncRootNode(TruffleLanguage<?> language, FrameDescriptor frameDescriptor,
            MathNode function) {
        super(language, frameDescriptor);
        this.function = function;
    }

    @Override
    public Object execute(VirtualFrame frame) {
        return function.executeGeneric(frame);
    }
}


そしたら、こんな感じでパースのときにCallTargetを作って保持しておきます。

static MathFunction RAND_FUNC;
MathFunction createBuiltin(FrameDescriptor frame) {
    if (RAND_FUNC == null) {
        RandNode rand = RandNodeGen.create();
        RAND_FUNC = new MathFunction(Truffle.getRuntime().createCallTarget(
                new FunctionNodes.FuncRootNode(this, frame, rand)));
    }
    return RAND_FUNC;
}

実際には関数名->関数オブジェクトのMapに保持することになると思います。

呼び出し

呼び出しは、CallTargetのcallメソッドを呼び出すと行えます。

class MathInvokeNode extends MathNode {
    MathFunction function;

    public MathInvokeNode(MathFunction function) {
        this.function = function;
    }

    @Override
    Object executeGeneric(VirtualFrame frame) {
        return function.callTarget.call();
    }
}

今回はつかってないけど引数があるときはcallの引数で渡します。受け取り側では、VirtualFrameのgetArguments()で。


パースのときに数値以外は変数として扱うようにしたのだけど、randと書くと乱数関数を呼び出すことにします。

    MathNode parseNode(FrameDescriptor frame, String value) {
        try {
            return LongNode.of(value);
        } catch (NumberFormatException ex) {
            if ("rand".equals(value)) {
                return new MathInvokeNode(createBuiltin(frame));


これでこんな感じで動くようになります。

String exp = "12+34+56+aa+rand";
MathCommand.main(new String[]{exp});

呼び出しの最適化

さて、メソッド呼び出しを効率化しましょう。
IndirectCallNode/DirectCallNodeを使います。ここではDirectCallNodeを。

static class InvokeNode extends MathNode {
    DirectCallNode callNode;

    public InvokeNode(MathFunction function) {
        callNode = Truffle.getRuntime()
                .createDirectCallNode(function.callTarget);
    }

    @Override
    Object executeGeneric(VirtualFrame frame) {
        return callNode.call(new Object[0]);
    }
}

これでいいんだけど、実際に言語を作るとき、これだと再帰関数ではこの時点ではcallTargetが準備できてなかったりしてうまういかないことがあります。
なので遅延処理することになります。

static class InvokeNode extends MathNode {
    MathFunction function;
    DirectCallNode callNode;

    public InvokeNode(MathFunction function) {
        this.function = function;
    }

    @Override
    Object executeGeneric(VirtualFrame frame) {
        if (callNode == null) {
            callNode = Truffle.getRuntime().createDirectCallNode(function.callTarget);
        }
        
        return callNode.call(new Object[0]);
    }
}

けど、これ動くには動くけど遅くなるしGraalVMで動かすと文句いわれる。
ということで、@CompilationFinalをつけます。そして値変更時にCompilerDirectives.transferToInterpreterAndInvalidate()でGraalコンパイラに教えてあげる。

static class MathInvokeNode extends MathNode {
    MathFunction function;
    @CompilationFinal
    DirectCallNode callNode;

    public MathInvokeNode(MathFunction function) {
        this.function = function;
    }

    @Override
    Object executeGeneric(VirtualFrame frame) {
        if (callNode == null) {
            CompilerDirectives.transferToInterpreterAndInvalidate();
            callNode = Truffle.getRuntime().createDirectCallNode(function.callTarget);
        }
        
        return callNode.call(new Object[0]);
    }
}


今回のコードだと処理速度に影響はないけど、実際の言語実装したときに10%~30%くらい速くなった。