Making sense of Native Image contents – What code ends up in the executable and who’s to blame?

原文はこちら。
The original article was written by Olya Gupalo (Member of Tech Staff for GraalVM at Oracle) and Ondřej Douda (Java Software Developer at Oracle Labs).
https://medium.com/graalvm/making-sense-of-native-image-contents-741a688dab4d

Javaアプリケーションの実行時のパフォーマンスがクラウドデプロイメントのニーズに合うよう、Native Imageが変換してくれます。生成される実行可能ファイルは素早く始動し、実行時のメモリ使用量も非常に少ないため、リソースの制約があったり高価だったりする場合、そして定期的にサービスをスケールアップ・ダウンするような環境にとって理想的です。

これらのパフォーマンスの利点は、アプリケーションのコードを事前にコンパイルし、アプリケーションのいくつかのクラスを事前に初期化することで得られます。そのため、アプリケーションが起動したときには有用な作業を行う準備ができており、バイトコードの読み込み、解釈、JITコンパイラでのコンパイルなどをするためのインフラストラクチャを必要としません。

しかし、最も重要なことは、Native Imageでビルドされた実行ファイルはスタンドアロンであり実行のためにJVMに依存しないということです。それはガベージコレクタのような必要なランタイムコンポーネントが同じバイナリに組み込まれているためです。また、事前に初期化されたヒープデータとアプリケーション全体のコンパイルされたコードが含まれているため、バイナリは通常、JARファイルよりも大きくなります。

この記事ではGraalVM Dashboardを紹介します。これはWebベースの可視化ツールで、メソッドのコンパイル、到達性、クラスの使い勝手、プロファイリングデータ、事前初期化されたヒープデータ、静的解析結果などの情報を把握するのに役立ちます。言い換えれば、どのクラス、パッケージ、および事前初期化されたオブジェクトが実行ファイルに含まれているのか、特定のパッケージが占有するスペースはどのくらいか、どのクラスのオブジェクトがヒープの大部分を占めているのかを監視できます。これらのデータはすべて、アプリケーションを最適化してバイナリをさらに小さくする方法を理解するのに非常に役立ちます。

GraalVM Dashboard
https://www.graalvm.org/docs/tools/dashboard/?ojr=dashboard

Image for post
GraalVM Dashboard UI

ダッシュボードのインターフェイスは簡単なものです。DashboardタブにはLoad dataボタンがあり、Helpタブをクリックすると左側のメニューが開き、メイン・ウィンドウが表示されます。ダッシュボードには3つの可視化フォーマットがあり、以下で見ていきます。

  • コードサイズの内訳 – プリコンパイルされたパッケージとクラスのサイズを表示します。
  • ヒープサイズの内訳 – 事前初期化されたヒープ上にどのオブジェクトがあるかを表示します。
  • Points-to Exploration – ネイティブイメージに特定のクラスやメソッドが含まれている理由の質問に答えます

GraalVM Dashboardは、ネイティブイメージビルダーによってダンプされたレポートファイルから、イメージの詳細を含むデータを可視化します。
現在のところ、「ネイティブイメージダンプ形式(Native Image Dump Format)」の拡張子(.bgv)のファイルしか受け付けていません。可視化のために取得したいデータタイプに応じて、ネイティブイメージを作成する際に特定のフラグを渡す必要があります。

  • -H:DashboardDump=<path> : ダンプファイルのパスを定義します。
  • -H:+DashboardAll : 利用可能なすべてのデータをダンプします。

GraalVM Dashboard in action

ダッシュボードの適用性を実証するために、同期スレッドと非同期スレッドの実行を行うマルチスレッドのデモアプリケーションをサンプルとして使います。

Java Multithreading Demo For GraalVM Dashboard
https://github.com/graalvm/graalvm-demos/tree/master/multithreading-demo

このサンプル・アプリのビジネス・ロジックは単純で、あまり重要ではありません。それはいくつかのスレッドを起動し、すべてのスレッドがまったく同じ整数の配列をループし、疑似乱数のストリームを生成します。このプログラムは、タスクの同期実行、非同期実行にかかった時間を計算します。

このデモは2個のサブプロジェクトで構成されており、それぞれMacenでビルドされています。まず、Multithread Demo Oversizedに取りかかります。全ての依存関係と共にソースを実行可能JARファイルにパッケージします。

Multithread Demo Oversized
https://github.com/graalvm/graalvm-demos/tree/master/multithreading-demo/multithreading-demo-oversized

筆者たちはNative ImageがインストールされたのGraalVM Enterprise 21.0.0(Java 8 / macOS)ベースでプロジェクトをテストしています。ここに示されているサイズの絶対数は、別のOSやJDK 11ベースのGraalVMの場合大きな違いはありませんが少々異なる可能性があります。とはいえ、この記事の主要ポイントはそういった環境にも当てはまります。

GraalVM 21.0.0
https://www.graalvm.org/downloads/
Native Image ビルダー
https://www.graalvm.org/reference-manual/native-image/#install-native-image

アプリケーションをクローンしてビルドし、実行してください。

$ cd multithreading-demo-oversized/
$ mvn package
$ java -jar target/multithreading-1.0-jar-with-dependencies.jarSynchronous execution for 4 times.
The execution for 4 times takes: 841ms.

Asynchronous threads execution for 4 Threads.
The execution of Thread 1 took: 182ms.
The execution of Thread 2 took: 192ms.
The execution of Thread 3 took: 191ms.
The execution of Thread 4 took: 196ms.
The execution of 4 Threads takes: 280ms.

The build uses the Native Image Maven plugin to build the native binary of the app, and we configured it to produce the diagnostic data with these options:

-H:DashboardDump=dumpfileoversized -H:+DashboardAll

mvn package などを実行するためにこの構成を使うと、ビルドによりdumpfileoversized.bgvが生成されます。このファイルは後でGraalVM Dashboardにアップロードしてプログラムの改善点を探すために使います。

Image for post
Dumping diagnostic data at native image build time

ビルド後、multithreading-image-oversizedイメージを実行し、実行ファイルとJARファイルとのファイルサイズの差を比較できます。

$ ./target/multithreading-image-oversized
Synchronous execution for 4 times.
The execution for 4 times takes: 424ms.

Asynchronous threads execution for 4 Threads.
The execution of Thread 1 took: 229ms.
The execution of Thread 2 took: 202ms.
The execution of Thread 3 took: 225ms.
The execution of Thread 4 took: 211ms.
The execution of 4 Threads takes: 234ms.
Image for post
JAR, native image, and BGV file sizes

ネイティブ実行ファイルにコンパイルすることで、プログラムのサイズが 1,8M から 13M に増加したことがわかります。これは、ネイティブイメージビルダが必要なすべてのランタイムパーツをパッケージ化し、ビルド中にいくつかのデータを事前初期化し、それを実行ファイルに書き出しているからです。この方法では、ウォームアップされる JVM がないので、起動時間はほとんどゼロになります。dumpfileoversized.bgvファイルは、診断データをすべての形式で収集するようにビルドを設定したため、29Mものファイルサイズになっています。Javaアプリケーションによってはダンプファイルのサイズが大きくなる可能性があり、その場合は診断情報を別途書くのは筋にかなっています。

  • -H:+DashboardHeap – イメージヒープの内訳をダンプします。
  • -H:+DashboardCode -メソッドごとのコードサイズの内訳をダンプします。
  • -H:+DashboardPointsTo – ポイント間の分析情報をダンプします。

デフォルトでは、ダンプをBGV形式で生成します。JSONJSONのpretty print形式でもダンプできますが、その場合はコマンドラインで明示的に指定する必要があります。

  • -H:+DashboardJson – ファイルを小さくするために、空白のないJSON形式でダンプします。
  • -H:+DashboardPretty – 人間が読めるJSON形式でダンプします。
  • -H:-DashboardBgv – BGV形式でのダンプを行わず、ダンプを上記の両形式で生成します(コロンの後の”-“に注意してください)。

dumpfileoversized.bgvファイルをGraalVM Dashboardにロードして、ネイティブイメージに何が含まれ、全体的なサイズに何が寄与しているかを確認しましょう。

Image for post
Upload a dump file window

Code Size Breakdown

Code Size Breakdownツールは、プリコンパイルされたコードが結局ネイティブイメージでどうなっているのか、どのJavaパッケージがそのサイズに最も関与しているのかを調べるためのツールです。このツールは、イメージに含まれていたパッケージ、クラス、メソッドの内訳を表示します。パッケージのサイズは、ネイティブイメージのサイズに比例します。その代わりに、ネイティブイメージの内部にどのようなコンテンツがパッケージ化されているかを知るには、-H:+PrintUniverseを実行して、テキスト出力を観察する必要があります。

Image for post
Code Size Breakdown view

ダッシュボードUIでパッケージの四角形をクリックすると、ダッシュボードが選択したパッケージグループに「ズームイン」するので、パッケージのサイズをさらに調べることができます。

上図はmultithreading-image-oversizedのイメージを示しています。一見すると、画像の大部分がcom.fasterxmlというパッケージ(約3Mのサイズ)で構成されていることがわかります。何かを最適化しようとしているのであれば、最大のボトルネックから始めるのが理にかなっており、ダッシュボードの可視化はどこから始めるべきかを特定するのに非常に役立ちます。

Heap Size Breakdown

ネイティブイメージのヒープを占めるオブジェクトやクラスを理解するために、Heap Size Breakdownを使用します。このツールは、ネイティブイメージのヒープに含まれる様々なクラスの事前割り当て済みオブジェクトのサイズを視覚的にまとめたものです。事前割り当て済みオブジェクトは、ネイティブ・イメージのビルド中に事前に割り当てられたオブジェクトで、実行ファイルのデータ・セクションに格納されます。そして、実行時に直接メモリにロードされます。

Image for post
Heap Size Breakdown view

Points-to Explorer and PointTo-SourceLine

Points-to Explorer は、あるメソッドがなぜネイティブイメージに含まれたのか、そのメソッド呼び出しのシーケンス、そしてそのメソッドが将来的に含まれる可能性を避けるためにそれを中断できるかどうかを調べることができます。探索は、entry-pointに到達するまで再帰的にグラフを展開します。Dashboardでは、この可視化のためのエントリーポイント(メソッド)が定義済みでなければならないため、このツールはCode Size Breakdownヒストグラムのリーフタイルからのみアクセス可能です。

Image for post
Points-to Explorer view

GraalVM DashboardはGitHub上のgraalvm.orgのウェブサイトと一緒にホストされており、すべての可視化ロジックはオフラインでクライアントサイドのHTMLページで行われます。ダンプファイルのサイズがブラウザで定義されたメモリ制限を超える場合、クライアントサイドの処理は少し問題になるかもしれません。Points-To分析は、特に大量のデータを生成する可能性があるため、興味のあるポイントがコードサイズの内訳である場合は、完全なデータダンプを読み込むのではなく、個々のコードサイズやヒープデータ診断データを使うとよいでしょう。

IDE としてVS Codeを使用していてPointTo-SourceLine拡張機能がインストールされている場合は、Points-to-Explorerから開いているワークスペースでソース行に素早く移動できます。ソース行情報を持つすべてのノードは、ファイルを開くようにプロンプトが出ます。

GraalVM Dashboard PointTo-SourceLine support for VS Code
https://marketplace.visualstudio.com/items?itemName=oracle-labs-graalvm.dashboard

Using the data

前章で、生成された実行ファイルの中でcom.fasterxmlパッケージが比較的大きなスペースを取ることを指摘しました。コードを微調整することで、イメージサイズを改善しましょう。

サンプルアプリケーションの最初に、Jackson JSON パーサーを使用して、ユーザーの入力値の代わりに使用されるいくつかのデフォルト値の設定ファイルをロードしています。これらの設定値はデモの目的のためのもので、非常にシンプルに作られており、起動時にのみ使用されます。

実行ファイルのサイズを最適化する必要があり、例えば upx などで圧縮することができない場合は、アプリケーションのコードを変更して、より軽量な構造体を使用するか、設定用のプロパティファイルを使用するか、依存関係を削除するか、単純化するのが最善の方法でしょう。

Compressed GraalVM Native Images: the best startup for Java apps comes in tiny packages
https://medium.com/graalvm/compressed-graalvm-native-images-4d233766a214
https://logico-jp.io/2020/12/10/compressed-graalvm-native-images-the-best-startup-for-java-apps-comes-in-tiny-packages/

別のエキスパートレベルの解決策は、設定の初期化をstaticブロックに移してイメージのビルド時にロードするようにし、最終イメージには fasterxml クラスを含めないようにする、というものです。

The other part of our Multithreadingデモのもう一つの部分であるMultithreading Demo Improvedには、変更済みのコードが入っています。ディレクトリを変更し、Mavenでビルドしましょう、数値が大きく変わりました。

Multithreading Demo Improved
https://github.com/graalvm/graalvm-demos/tree/master/multithreading-demo/multithreading-demo-improved

Image for post

ご覧のように、私たちはイメージサイズを 3.7M に縮小できました。また、情報量が少なくなったためにdumpfileimproved.bgv のサイズも小さくなりました。

もう一つ重要なことは、Native Image Mavenプラグインで使用されているビルド引数 --initialize-at-build-time です。この引数により、ネイティブイメージビルダーがイメージビルド時にすべてのクラスを初期化するように指示していたので、上記のパッケージは実行時に使用する必要がありません。これにより、実行ファイルの全体的なサイズが小さくなりました。

最終的にこれらの定数がヒープ空間に直接ロードされると、リソースからconfig.jsonファイルを含める必要がなくなり、イメージサイズがさらに改善されます。

Conclusion

GraalVM Dashboard は、native-imageでビルドされた実行ファイルにどのコードやどのデータがコンパイルされたかという情報を可視化するための興味深い選択肢です。サイズに寄与している最大のコンポーネントを迅速に特定し、最適化プロセスをガイドします。

ダッシュボードは、GraalVM のリリースとは別に、定期的に更新されています。

ダッシュボードで何か問題が発生した場合や、未解決の質問がある場合は、SlackやGitHubで気軽に共有してください。もしくはOndřej DoudaAleksandar ProkopecOlya Gupaloまで直接ご連絡ください。

GraalVM Slack Invitation Page
https://www.graalvm.org/slack-invitation
GitHub
https://github.com/oracle/graal

GraalVM Dashboardは、GraalVMチームが最近内部で積極的に使用しており、その有用性を証明しています。是非お試しいただき、あなたのアプリケーションのためのインサイトを得て洞察を得てください。そして質問やフィードバックをお寄せください。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中