Asserting JDK Flight Recorder Events with JfrUnit


Gunnar morling, open source software engineer at Red Hat, introduced JfrUnit, a new testing utility that can be used to detect performance regressions with JUnit or the Spock frame. Interpreting performance test results, such as response times, can be difficult because there may be regressions caused by factors other, such as other processes or the network, than the application itself. same. Jfr unit can be used to test application performance by measuring memory allocation, I / O, database queries, or other application specific items.

JDK Flight Recorder (JFR) collects events from a running application that can be used to diagnose or profile an application. These events can be almost anything from memory allocation to garbage collection. It is possible to use the tool directly from the command line, but it is often used with JDK Mission Control which provides a GUI and various plugins that can be used with JDK Flight Recorder. JfrUnit allows you to create assertions that verify JDK Flight Recorder (JFR) events from within the application.

JfrUnit supports OpenJDK 16 and the dependency is available on Maven Central:


<dependency>
  <groupId>org.moditect.jfrunit</groupId>
  <artifactId>jfrunit</artifactId>
  <version>1.0.0.Alpha1</version>
  <scope>test</scope>
</dependency>

The implementation of a JUnit test begins with the addition of the @JfrEventTest annotation to the unit test class, unless the test is marked with the @QuarkusTest annotation because the Quarkus testing framework automatically interacts with the JFR record. The tests use the @EnableEvent annotation to collect specific events, for example, garbage collection events. After executing the logic of the program, the jfrEvents.awaitEvents() The method waits for all JFR events from the JVM or application, before assertions are used to verify that the event has occurred:


@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"));
    }
}

Alternatively, the Spock framework can be used to write the same test:


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']
    }
}

In addition to checking if an event has occurred, it is also possible to check the details of an event, such as the duration of an event. Thread.sleep() method:


@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)));
}

JfrUnit allows you to create even more complex scenarios. Consider the following example that collects memory allocation events and sums them up before asserting that the memory allocation is between certain values:


@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");
}

Activation of multiple events is also possible using the wildcard “*”, for example, @EnableEvent("jdk.ObjectAllocation*") can be used to activate all ObjectAllocation events.

To reset the collected events, the jfrEvents.reset() can be used to ensure that only events after the reset() method are collected. For example, when performing multiple iterations and asserting the results by iteration:


for (int i = 0; i < ITERATIONS; i++) {
    // Application logic

    jfrEvents.awaitEvents();


    // Assertions

    jfrEvents.reset();
}

Frameworks such as Hibernate do not emit events themselves, but in these cases the JMC Agent can be used to create events. With the JMC agent, SQL query events can be generated, which can then be used to assert the (number of) SQL queries sent to the database. This is demonstrated in the Continuous performance regression testing with JfrUnit session and the example is available on Examples for JfrUnit.





Source link