Introducing Microsoft GCToolkit

原文はこちら。
The original article was written by Kirk Pepperdine (Principal Engineer at Microsoft).
https://devblogs.microsoft.com/java/introducing-microsoft-gctoolkit/

MicrosoftのJava Engineering GroupはオープンソースのMicrosot GCToolkitを発表、GitHubに公開しました。

Microsoft GCToolKit
https://github.com/microsoft/gctoolkit

GCToolkitはJavaのGCログアフィルを分析するためのライブラリ群です。このツールキットは、GCログファイルを個別のイベントに解析し、それらのイベントからデータを集約するためのAPIを提供します。これにより、GCログが示すJava仮想マシン (JVM) の管理されたメモリの状態に対し、あらゆる複雑な解析を作成できます。この記事では、このプロジェクトを最大限に活用するための主要な機能を紹介します。

Java仮想マシン(JVM)の管理対象メモリは、Javaヒープと呼ばれるメモリバッファ、Javaヒープにデータを取り込む作業を行うアロケータ、そしてガベージコレクション(GC)の3つで構成されています。GCは、Javaヒープ内の使用されなくなったメモリを回収する役割を担っていますが、この用語はメモリ管理の婉曲表現として使われることが多く、GCのチューニングやコレクターのチューニングは、JVMのメモリ管理サブシステムのチューニングを意味するものと理解して使われることが多いようです。

さらに重要なことは、コレクターの設定が最適でない場合、アプリケーションがより多くのCPUとメモリを必要とし、同時にエンドユーザーのエクスペリエンスを低下させるということが昔から知られていることです。言い換えれば、チューニングが不十分だと、ランタイムが高価になり、ユーザーが不幸になるということです。最適なGCのチューニングのためには、いくつかの懸念事項の間で微妙なバランスをとる必要があり、しかもこの懸念事項はすべてツールの支援がないと簡単に把握できない、という課題がありますが、GCToolKitはこの課題を解決してくれます。ではGCToolkitについてご紹介していきます。

GCToolkit Modules 

GCToolkitは、API、GCログファイルパーサー、Vert.xベースのメッセージングバックプレーンをカバーする3つのJavaモジュールで構成されています。 APIモジュールはGCToolkitのエントリーポイントで、パーサーとVert.xを使用してGCログファイルを解析し、いくつかのメソッドコールに分解するという詳細を隠します。パーサーモジュールは、正規表現と最も堅牢なGCログパーサーとして長年にわたって開発されてきたコードの集合体です。Vert.xベースのメッセージングバックプレーンは、2つのメッセージバスを使用します。一つ目のメッセージバスは、DataSourceからのストリームです。現在の実装は、GCログファイルからログの行をストリームすることです。このメッセージバスのリスナーは、データソースからのデータを、GCサイクルまたはセーフポイントを表すイベントに変換するパーサーです。これらのイベントは、イベントバスで公開されます。イベントバス上のリスナーは、興味のあるイベントを受信して処理できます。

Aggregators and Aggregations 

パーサーは、個別のJVMイベント(GCサイクルイベントやセーフポイントイベント)を発行します。これにより、これらのイベントからデータを取得して分析するコードを書くことが可能になります。どのようなデータを分析したいか、どのような分析を行いたいかは、あなた次第です。GCToolkit は、GC ログファイルのデータをキャプチャして分析するためのシンプルな Aggregator/Aggregationフレームワークを提供します。

イベントを捕捉するコードのことをAggregator、データを分析するコードのことをAggregationと呼びます。Aggregatorは、分析に使用するために複数の異なるイベントを捕捉できます。例えば、ヒープの占有率を分析するために、ポーズイベントを捕捉できます。Aggregatorは、イベントを捕捉し、関連するデータを抽出して、データをAggregationに渡します。Aggregationは、データを意味のある分析結果(GC後のヒープ占有率の合計など)にまとめます。

Example

GCサイクルが完了した後のトータルヒープ占有率をレポートする例を見て、実際にやってみましょう。以下のコードは、APIの主要な要素を利用した最小限の実装です。

public class Main { 
    public static void main(String[] args) throws Exception { 
        var path = Path.of(args[0]); 
        var logFile = new SingleGCLogFile(path); 
        var gcToolKit = new GCToolKit(); 
        var jvm = gcToolKit.analyze(logFile); 
        var results = jvm.getAggregation(HeapOccupancyAfterCollectionSummary.class); 
        System.out.println(results.toString()); 
    } 
}

流れは以下の通りです。まずDataSourceを作成します。今回の場合、DataSourceはGCLogFIleであり、具体的には全てのデータが1つのファイルに含まれています。つづいてGCToolkitのインスタンスを作成します。これにより、DataSourceの処理をサポートするために必要なすべてのトラス(訳注:橋などで使われる構造部材)を構築するプロセスが始まります。GCToolkitのインスタンスができたら、DataSourceをパラメータにしてanalyzeメソッドを呼び出して、GCToolkitを使用できます。戻り値はJavaVirtualMachineです。これは、JVMの状態と構成を問い合わせ可能なAPIです。ここでは、HeapOccupancyAfterCollectionSummary Aggregatorに関連するAggregatorを求めています。最後に、結果を処理します。この単純な例では、結果をターミナルに出力しますが、データはグラフや表など、より人間にわかりやすい形式で表示することもできます。

ここでは、AggregatorもHeapOccupancyAfterCollectionSummary Aggregationもサンプルに登場しないという、ちょっとしたマジックがあります。これらのクラスは、Javaのモジュール・システム・ディスカバリー・サービスを使ってAPIに提供されます。GCToolkit がこれらのクラスをどのように発見し、利用するかを理解する前に、まずその実装を見てみましょう。

@Aggregates({EventSource.G1GC,EventSource.GENERATIONAL,EventSource.ZGC}) 
public class HeapOccupancyAfterCollection extends Aggregator<HeapOccupancyAfterCollectionAggregation> { 

    public HeapOccupancyAfterCollection(HeapOccupancyAfterCollectionAggregation aggregation) { 
        super(aggregation); 
        register(GenerationalGCPauseEvent.class, this::extractHeapOccupancy); 
        register(G1GCPauseEvent.class, this::extractHeapOccupancy); 
        register(ZGCCycle.class,this::extractHeapOccupancy); 
    } 

    private void extractHeapOccupancy(GenerationalGCPauseEvent event) { 
        aggregation()
                .addDataPoint(event.getGarbageCollectionType(), 
                              event.getDateTimeStamp(), 
                              event.getHeap().getOccupancyAfterCollection()); 
    } 

    private void extractHeapOccupancy(G1GCPauseEvent event) { 
        aggregation()
                .addDataPoint(event.getGarbageCollectionType(), 
                              event.getDateTimeStamp(), 
                              event.getHeap().getOccupancyAfterCollection()); 
    } 

    private void extractHeapOccupancy(ZGCCycle event) { 
        aggregation()
                .addDataPoint(event.getGarbageCollectionType(), 
                              event.getDateTimeStamp(), 
                              event.getLive().getReclaimEnd()); 
    } 
}

上記のコードは、このAggregatorが取り扱い予定のイベントソースを示す@Aggregatesアノテーションで始まります。ここからわかるように、このAggregationは、G1GC、旧世代のGC、およびZGCと連動するように設計されています。

コンストラクタでは、このAggregatorが動作する特定のイベントを、対応するコンシューマ・メソッドに登録します。すべてのGCポーズイベントは、コレクションフェーズ前後のヒープ占有率を報告します。これにより、個々のイベントではなく、スーパークラスを登録することができます.最後に、個々のメソッドは関心のあるデータを収集し、それをAggregationに渡します。ここでAggregationは、入ってくるイベントのビューとして機能します。この場合、AggregationはHeapOccupancyAfterCollectionAggregationであり、addDataPointという1つのメソッドを定義するインターフェースです。HeapOccupancyAfterCollectionというAggregatorのコンストラクタのパラメータがインターフェイスであることに注目してください。これにより、Aggregatorは特定のユースケースに固有のAggregationを生成することができます。

以下は、HeapOccupancyAfterCollectionSummaryの実装です。

@Collates(HeapOccupancyAfterCollection.class) 
public class HeapOccupancyAfterCollectionSummary implements HeapOccupancyAfterCollectionAggregation { 

    private HashMap<GarbageCollectionTypes, XYDataSet> aggregations = new HashMap<>(); 

    public void addDataPoint(GarbageCollectionTypes gcType, DateTimeStamp timeStamp, long heapOccupancy) { 
        var dataSet = aggregations.computeIfAbsent(gcType, k -> new XYDataSet()); 
        dataSet.add(timeStamp.getTimeStamp(),heapOccupancy); 
    } 

    public HashMap<GarbageCollectionTypes, XYDataSet> get() { 
        return aggregations; 
    } 
}

この実装は、@Collatesアノテーションで始まります。これは、この実装がHeapOccupancyAfterCollectionと連携することを意図していることをAPIに伝えるものです。実装の残りの部分は、使用目的に適した形でデータを収集します。例えば、XYDataSetは、X-Y散布図の描画をサポートすることを意図しています。

最後のマジックは、サンプルのmodule-infoにある

provides Aggregation with HeapOccupancyAfterCollectionSummary;

です。これにより、サンプルはサービスプロバイダになります。GCToolkitがインスタンス化されると、Aggregationサービスを提供するモジュールを探します。従って、GCToolkitのanalyzeメソッドが要求すると、Aggregator/Aggregationが自動的にロードされ、使用されます。GCToolkitは、サービスプロバイダーのパラダイムを使用しない場合、プログラムでAggregationクラスを登録するためのAPIも提供しています。

Making it a Module 

最後に、module-info.javaでAggregationのためにHeapOccupancyAfterCollectionSummaryの実装を提供するよう構成します。

module gctoolkit.sample { 
    requires gctoolkit.api; 
    requires gctoolkit.vertx; 
    requires java.logging;

    exports com.microsoft.gctoolkit.sample.aggregation to gctoolkit.vertx;

    provides Aggregation with HeapOccupancyAfterCollectionSummary; 
} 

ここからわかるように、サンプルモジュールは3つのGCToolkitモジュールをそれぞれ必要とします。このモジュールは、gctoolkit.vertxモジュールにアグリゲーションパッケージをexportしています。この依存関係は、報告されている既知のバグの回避策であり、修正される予定です。

最後に、このアプリを実行してみましょう。このプロジェクトには、コマンドラインからアプリを実行する方法を示す、サンプルのシェルスクリプトが含まれています。コマンドラインでは、-pパラメータでモジュールのパスを設定し、-mパラメータでmainクラスを指定します。この GC ログの場合、出力は次のようになります。

$ ./sample.sh 
Collected 3 different collection types. 

Contribute!

コントリビューションしたいとか、ただフォローしたいという方は、是非以下のURLからどうぞ。

Discussions
https://github.com/microsoft/gctoolkit/discussions

コメントを残す

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

WordPress.com ロゴ

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

Twitter 画像

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

Facebook の写真

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

%s と連携中