Red HatのオープンソースソフトウェアエンジニアであるGunnar Morling氏が、JUnitやSpock Frameworkでパフォーマンス低下を検出するために使用可能な、新たなテストユーティリティのJfrUnitを発表した。応答時間などのパフォーマンステスト結果の解析は必ずしも簡単ではない。プロセスやネットワークといった、アプリケーション自体ではないファクタに起因する低下が存在する可能性があるからだ。JfrUnitは、メモリアロケーションやIO、データベースクエリ、その他アプリケーション固有の要素を測定することにより、アプリケーションのパフォーマンステストに使用することができる。
JDK Flight Recorder(JFR)は実行中のアプリケーションから、アプリケーションの診断あるいはプロファイルに使用可能なイベントを収集する。メモリアロケーションからガベージコレクションに至るまで、ほぼ任意のイベントを収集可能である。ツールを直接コマンドラインから実行することもできるが、GUIを持ち、JFRと組み合わせて使用可能なさまざまなプラグインを備えたJDK Mission Controlと併用されることが多い。JfrUnitを使うことで、アプリケーションからのJFRイベントを検証するアサーションの作成が可能になる。
JfrUnitはOpenJDK 16をサポートしており、Maven Centralで公開されている。
<dependency>
<groupId>org.moditect.jfrunit</groupId>
<artifactId>jfrunit</artifactId>
<version>1.0.0.Alpha1</version>
<scope>test</scope>
</dependency>
JUnitテストを実装するには、最初にユニットテストクラスに@JfrEventTest
アノテーションを追加する。ただしテストに@QuarkusTest
アノテーションがマークされていれば、Quarkusテストフレームワークが自動的にJFRレコーディングと通信するので、この必要はない。ガベージコレクションイベントなど特定のイベントを収集するには、@EnableEvent
アノテーションを使用する。プログラムロジックの実行後は、jfrEvents.awaitEvents()
メソッドでJVMあるいはアプリケーションからのJFRイベントを待機した後、発生したイベントに対してアサーションを使用した検証が行われる。
@JfrEventTest
public class GarbageCollectionTest {
public JfrEvents jfrEvents = new JfrEvents();
@Test
@EnableEvent("jdk.GarbageCollection")
public void testGarbageCollectionEvent() throws Exception {
System.gc();
jfrEvents.awaitEvents();
assertThat(jfrEvents).contains(event("jdk.GarbageCollection"));
}
}
Spockフレームワークを使って同じテストを記述することも可能だ。
class GarbageCollectionSpec extends Specification {
JfrEvents jfrEvents = new JfrEvents()
@EnableEvent('jdk.GarbageCollection')
def 'Contains a garbage collection Jfr event'() {
when:
System.gc()
then:
jfrEvents['jdk.GarbageCollection']
}
}
イベント発生の有無の検証以外に、Thread.sleep()
メソッドの実行期間など、イベントの詳細を検証することもできる。
@Test
@EnableEvent("jdk.ThreadSleep")
public void testThreadSleepEvent() throws Exception {
Thread.sleep(42);
jfrEvents.awaitEvents();
assertThat(jfrEvents)
.contains(event("jdk.ThreadSleep")
.with("time", Duration.ofMillis(42)));
}
もっと複雑なシナリオを作ることも可能だ。以下の例では、メモリアロケーションイベントを収集して総和を算出することで、メモリアロケーションが特定の範囲内であることを検証する。
@Test
@EnableEvent("jdk.ObjectAllocationInNewTLAB")
@EnableEvent("jdk.ObjectAllocationOutsideTLAB")
public void testAllocationEvent() throws Exception {
String threadName = Thread.currentThread().getName();
// Application logic which creates objects
jfrEvents.awaitEvents();
long sum = jfrEvents.filter(this::isObjectAllocationEvent)
.filter(event -> event.getThread().getJavaName().equals(threadName))
.mapToLong(this::getAllocationSize)
.sum();
assertThat(sum).isLessThan(43_000_000);
assertThat(sum).isGreaterThan(42_000_000);
}
private boolean isObjectAllocationEvent(RecordedEvent re) {
String name = re.getEventType().getName();
return name.equals("jdk.ObjectAllocationInNewTLAB") ||
name.equals("jdk.ObjectAllocationOutsideTLAB");
}
private long getAllocationSize(RecordedEvent recordedEvent) {
return recordedEvent.getEventType().getName()
.equals("jdk.ObjectAllocationInNewTLAB") ?
recordedEvent.getLong("tlabSize") :
recordedEvent.getLong("allocationSize");
}
ワイルドカード文字"*"を使って複数のイベントを有効化することもできる。例えば@EnableEvent("jdk.ObjectAllocation*")
は、すべてのObjectAllocationイベントのアクティベーションを行う。
jfrEvents.reset()
メソッドを使って収集したイベントをリセットすれば、reset()
メソッド後のイベントが収集されることを保証できる。例として、いくつかのイテレーションを実行して、イテレーション毎の結果を検証するには、
for (int i = 0; i < ITERATIONS; i++) {
// Application logic
jfrEvents.awaitEvents();
// Assertions
jfrEvents.reset();
}
Hibernateなどのフレームワークはそれ自体イベントを発行しないが、そのようなケースではJMCエージェントを使ってイベントを生成することができる。JMCエージェントではSQLクエリイベントが生成されて、データベースに送られるSQLクエリ(の数)を検証することが可能になる。この様子は"Continuous performance regression testing with JfrUnit"セッションで紹介された他、Examples for JfrJnuitにも例が紹介されている。