Using the JFR Event Streaming API in Automated Tests - Sip of Java

JDK Flight Recorder (JFR) is a profiling and diagnostic tool that has long been a part of the JDK. For much of that history, JFR could only be used on a deployed application to provide insight into how it is performing. With JDK 14 (JEP 349), the JFR Event Streaming API was added, which provided a way to give a real-time look into what is happening within a Java application. This article will explore how the JFR Event Streaming API could be utilized in automated testing.

Using the Event Streams API in Automated Testing

Below is the complete code example of using a JFR event stream in an automated test. We will step through the key parts of this code example below.

import jdk.jfr.Event;
import jdk.jfr.Name;
import jdk.jfr.consumer.RecordedEvent;
import jdk.jfr.consumer.RecordingStream;

public class TestSampleApplication {

	@Name("TestEvent")
	class TestEvent extends Event {

	}

	@Test
	public void testDoStuff() throws Exception {
		SampleApplication sampleApplication = new SampleApplication();
		List<RecordedEvent> recordedEvents = new ArrayList<>();
		try (RecordingStream rs = new RecordingStream();) {
			rs.enable("jdk.FileRead");
			rs.onEvent("TestEvent", e -> rs.close());
			rs.onEvent("jdk.FileRead", recordedEvents::add);
			rs.startAsync();
			TestEvent testEvent = new TestEvent();
			sampleApplication.doStuff();
			testEvent.commit();
			rs.awaitTermination();
		}
		
		assertFalse(recordedEvents.isEmpty());
		for (RecordedEvent recordedEvent : recordedEvents) {
			if(recordedEvent.getValue("path").equals("/path/to/your/file")) {
				return;
			}
		}
		fail("A readFile event wasn't found!");
	}
}

Initializing the Event Stream

To configure and capture events that will be fired during an automated test, I will use a RecordingStream. RecordingStream implements AutoCloseable, so we can declare it in a try-with-resources block and allow the JVM to ensure the stream is closed and cleaned up when we are done.

try (RecordingStream rs = new RecordingStream();) {
	...
}

Enabling Events

Built-in JFR events, like jdk.FileRead, are not enabled by default, so they will need to be enabled manually using rs.enable(String). Custom events like TestEvent are enabled by default and do not need to be manually enabled.

@Name("TestEvent")
class TestEvent extends Event {

}
...
try (RecordingStream rs = new RecordingStream();) {
	...
	rs.enable("jdk.FileRead");
	...
}

Events can also be configured by passing a settings file to the RecordingStream at initialization, with the appropriate events enabled, like in the example below:

Configuration c = Configuration.create(Path.of("/my/config/file"));
try (RecordingStream rs = new RecordingStream(c);) { 
	...
}

Configuring Event Behavior

What should happen when an event is fired can be configured with rs.onEvent(String, Runnable). In the code below, TestEvent is being used to close the stream, more on that later, and when a jdk.FileRead event occurs, that is being written to a List to be checked later.

rs.onEvent("TestEvent", e -> rs.close());
rs.onEvent("jdk.FileRead", recordedEvents::add);

Starting the Stream, Running the Test, and Closing the Stream

rs.startAsync() is used to start the JFR event stream in a separate thread. The code to be tested, a simple read from a file, is executed with sampleApplication.doStuff();.

The wonkiest part of the code example handles the closing of the stream. This is necessary because events are flushed to the JFR stream about once a second which would often mean some or all events not being written to the stream, leading to inaccurate or inconsistent test results. A brute force way of handling this could be done with Thread.sleep(long), but in large test suites, adding a lot of waits can start to have a significant impact on how long it takes to execute a test suite.

TestEvent with rs.onEvent("TestEvent", e -> rs.close()); and testEvent.commit(); and the rs.awaitTermination() provides a way to close the stream as soon as all relevant events have been flushed to it. The testEvent.commit(); will cause rs.onEvent("TestEvent", e -> rs.close()); to fire after the code under test has finished executing, and rs.awaitTermination() blocks the thread from executing until the thread has been closed. This minimizes any additional wait time in the test. Alternatively, a library like JFRUnit could be used, be sure to check the Additional Reading section for a link.

Asserting Behavior

Naturally, what and how automated tests handle asserting the code being tested will be highly context-specific. This code example demonstrates how to capture events in a List to check later. This is one example, but there are many other valid ways of writing an automated test with JFR.

assertFalse(recordedEvents.isEmpty());
for (RecordedEvent recordedEvent : recordedEvents) {
	if(recordedEvent.getValue("path").equals("/path/to/your/file")) {
		return;
	}
}
fail("A readFile event wasn't found!");

Additional Reading

Monitor Events with Flight Recorder Event Streaming API

JEP 349

JFRUnit

Happy coding!