Гуннар Морлинг, разработчик программного обеспечения с открытым исходным кодом в Red Hat, представил JfrUnit, новую утилиту тестирования, которую можно использовать для обнаружения снижения производительности с JUnit или Spock Framework. Интерпретация результатов тестирования производительности, таких как время отклика, может быть затруднена, поскольку могут быть регрессии, вызванные другими факторами, такими как другие процессы или сеть, а не самим приложением. JfrUnit может использоваться для тестирования производительности приложения путем измерения распределения памяти, операций ввода-вывода, запросов к базе данных или других элементов, зависящих от приложения.

JDK Flight Recorder (JFR) собирает события из запущенного приложения, которые могут использоваться для диагностики или профилирования приложения. Эти события могут быть практически любыми, от выделения памяти до сборки мусора. 

Инструмент можно использовать непосредственно из командной строки, но он часто используется вместе с JDK Mission Control, предоставляющим графический интерфейс и различные плагины, которые можно использовать вместе с JFR. 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 к классу unit теста, если тест не отмечен аннотацией @QuarkusTest, поскольку среда тестирования Quarkus автоматически взаимодействует с записью JFR. Тесты используют аннотацию @EnableEvent для сбора определенных событий, например, событий сборки мусора. После выполнения логики программы метод jfrEvents.awaitEvents() ожидает любых событий JFR от JVM или приложения, прежде чем утверждения будут использоваться для проверки того, что событие произошло:

@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 <span style="margin: 0px; box-sizing: border-box; display: inline; -webkit-box-orient: horizontal; -webkit-box-direction: normal; flex-flow: row wrap; vertical-align: top; color: rgb(166, 127, 89); background: rgba(255, 255, 255, 0.5);" class="token operator">=</span> <span style="margin: 0px; box-sizing: border-box; display: inline; -webkit-box-orient: horizontal; -webkit-box-direction: normal; flex-flow: row wrap; vertical-align: top; color: rgb(0, 119, 170);" class="token keyword">new</span> <span style="margin: 0px; box-sizing: border-box; display: inline; -webkit-box-orient: horizontal; -webkit-box-direction: normal; flex-flow: row wrap; vertical-align: top;" class="token class-name">JfrEvents</span><span style="margin: 0px; box-sizing: border-box; display: inline; -webkit-box-orient: horizontal; -webkit-box-direction: normal; flex-flow: row wrap; vertical-align: top; color: rgb(153, 153, 153);" class="token punctuation">(</span><span style="margin: 0px; box-sizing: border-box; display: inline; -webkit-box-orient: horizontal; -webkit-box-direction: normal; flex-flow: row wrap; vertical-align: top; color: rgb(153, 153, 153);" class="token punctuation">)</span><font style="margin: 0px; box-sizing: border-box;"></font>


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

JfrUnit позволяет создавать и более сложные сценарии. Рассмотрим следующий пример, который собирает события выделения памяти и суммирует их данные, прежде чем утверждать, что выделение памяти находится между определенными значениями:

@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-запросов, поступающих в базу данных. Это продемонстрировано на сессии DevNation Tech Talk Continuous performance regression testing with JfrUnit, а пример доступен на GitHub: Examples for JfrUnit.

Комментарии (0)