#chiroito ’s blog

Java を中心とした趣味の技術について

JFR Under the hood : ローカルで動くJFR Event Streamingのリアルタイム性

結論

ローカルに作られるリポジトリからイベントを読み込みます。イベントは非同期でこのファイルに書込まれます。そのため、本当の意味でのリアルタイムにはイベントは処理されないため気を付けましょう。

JFR Event Streamingとは

Java 14 で追加された JFR Event Streaming、以下の様に書くと JVM の内部のイベントとかをストリーミングで処理してくれる素晴らしい機能です。

import jdk.jfr.Configuration;
import jdk.jfr.consumer.EventStream;
import jdk.jfr.consumer.RecordingStream;
import java.io.IOException;
import java.text.ParseException;
import java.time.Duration;
import java.util.concurrent.TimeUnit;

public class JfrEventStreamingLocalAsync {
    public static void main(String... args) {
        try (EventStream es = new RecordingStream(Configuration.getConfiguration("default"))) {
            es.onEvent("jdk.JVMInformation", System.out::println);
            es.startAsync();
            creatingEventsProcess();
            es.awaitTermination(Duration.ofSeconds(2));
        } catch (ParseException | IOException | InterruptedException e) {
            System.err.println("Couldn't start JFR Event Streaming");
        }
    }
}

この機能、ローカル実行とリモート実行ができ、それぞれでEventStreamインスタンスを作る方法が異なります。

リモート実行はEventStream.openRepository(Path)メソッドを使用してEventStreamインスタンスを作るので「あ~JFRのリポジトリから取るんだな~。ですよね~」と思ってました。

また、ローカル実行の場合はEventStreamクラスのサブクラスであるRecordingStreamコンストラクタを使用します。このコンストラクタにどの設定でJFRを開始するかを指定してインスタンスを作ります。ローカル実行の場合はstartする時に新たに JFR の記録を開始します。そのため、JFRが内部で持つバッファを見ていてイベントが追加された瞬間にリアルタイムにイベントが処理されるのだろうなぁと思っていました。

読む場所はどこか

「Javaからバッファを見るAPIが追加されてるだろうから見てみるか」と思ってソースを見たところ、EventDirectoryStreamとなっていました。これはリポジトリからイベントを読み込むクラスです。このクラスのコンストラクタは第2引数にリポジトリのパスを指定します。しかし、この引数はnullが渡されていました。

    public RecordingStream() {
        Utils.checkAccessFlightRecorder();
        AccessControlContext acc = AccessController.getContext();
        this.recording = new Recording();
        try {
            PlatformRecording pr = PrivateAccess.getInstance().getPlatformRecording(recording);
            this.directoryStream = new EventDirectoryStream(acc, null, SecuritySupport.PRIVILEGED, pr);
        } catch (IOException ioe) {
            this.recording.close();
            throw new IllegalStateException(ioe.getMessage());
        }
    }

https://github.com/openjdk/jdk/blob/6d137a36169956171bfd0afef91e8ce29b568e33/src/jdk.jfr/share/classes/jdk/jfr/consumer/RecordingStream.java#L88

「バッファから読む場合は null なのかな?」と思って読み進めてみると、パスを決めるために実行されるRepositoryFilesクラスのprivate boolean updatePaths() throws IOExceptionメソッドに以下の様なコメントが記載されていました。

// Always get the latest repository if 'jcmd JFR.configure
// repositorypath=...' has been executed
SafePath sf = Repository.getRepository().getRepositoryPath();

https://github.com/openjdk/jdk/blob/6d137a36169956171bfd0afef91e8ce29b568e33/src/jdk.jfr/share/classes/jdk/jfr/internal/consumer/RepositoryFiles.java#L166

どうやら JFR の内部 API である Repositoryクラスを使用して最新のリポジトリを取得しているようです。このクラスはシングルトンで、名前の通り JFR のリポジトリを管理しています。

まとめ

これまでのソースコードで、JFR Event Streaming をローカル実行すると、その JVM にある最新のリポジトリからイベントを読み込んでいるのが分かります。JVM によって記録されるイベントは非同期でこのファイルへ書込み、JFR Event Streaming は書込まれたイベントを都度読み込んで処理されます。そのため、イベントの処理はリアルタイムではなく少し遅れて処理されます。