Fly with Java Recorder

Jan @Novoj Novotný

Go through presentation with me
https://bit.ly/flyjr

Java Flight Recorder

What is it?

  1. low-overhead tool integrated deeply in JVM
  2. collects runtime statistics in form of events
  3. originated in (BEA) JRockit JVM
  4. made freely available in JDK 11 (current version is 23)
  5. actively developed and extended
  6. tied to JVM, so can be used from Kotlin, Scala, Groovy (you name it)

What it could be used for?

  • Metrics collection (instead of JMX Beans)
  • Performance analysis
  • Memory leaks detection (with a little bit of luck)
  • Poor man tracking user interactions (what features are used and how often)
  • Post-mortem analysis
  • Gathering resources for analysis from your clients
  • Regression testing (JFR Unit)

Java Mission Control

Command line access

					
# Start & finish with JVM
java -jar my-application.jar \
     -XX:StartFlightRecording=name=my-application_recording,dumponexit=true,filename=myrecording.jfr
					
				

or

					
# retrieve Java PID
$ jps
# use jcmd to start recording for particular PID
$ jcmd 1 JFR.start duration=10s filename=recording.jfr
# use jcmd to dump current results of recording
$ jcmd 1 JFR.dump filename=recording.jfr
# use jcmd to stop recording
$ jcmd 1 JFR.stop
					
				
Support for multiple recordings at a time!

Reading results

					
# prints summary of recording (how many events, duration, allocation sizes, etc.)
$ jfr summary recording.jfr
# prints all events in recording
$ jfr print recording.jfr
# prints filtered events in recording
$ jfr print recording.jfr --event jdk.GarbageCollection
# exports events to CSV
$ jfr print recording.jfr --event jdk.GarbageCollection --format csv
# concatenates multiple recordings into single file
$ jfr assemble /path concatenated-file.jfr
# divide large recording into smaller chunks
$ jfr disassemble --max-chunks 5 /path/to/recording.jfr
					
				

But you should probably want to use Java Mission Control or such tool.

Remote usage

Programmatic usage

					
public class JfrTest {

	public static void main(String[] args) throws InterruptedException {
		// Create a new recording
		try (Recording recording = new Recording()) {

			// Limit memory allocations: only capture allocations larger than 1MB (1048576 bytes)
			recording.setSettings(Map.of(
				"jdk.ObjectAllocationInNewTLAB#threshold", "1 ms", // Only record events if they exceed 1ms
				"jdk.ObjectAllocationOutsideTLAB#threshold", "1 ms",  // Threshold for non-TLAB allocations
				"jdk.GarbageCollection#threshold", "10 ms",           // Record GC events if GC pause exceeds 10ms
				"jdk.CPULoad#period", "500 ms",                       // Set sampling period to 500ms for CPU load
				"jdk.ThreadSleep#threshold", "1 ms"                       // Record if thread sleep exceeds 1ms
			));

			// Start the recording
			recording.start();

			// Keep the recording running for some time, then stop it
			Thread.sleep(10000); // Simulate workload for 10 seconds
			recording.stop();
		}
	}

}
					
				

Programmatic usage - streaming

					
public class JfrTest {

	public static void main(String[] args) {
		try (RecordingStream stream = new RecordingStream()) {
			// Set a sampling interval for CPU load events
			stream.enable("jdk.CPULoad").withPeriod(Duration.of(500, ChronoUnit.MILLIS));

			// Set a threshold for memory allocations (only large allocations)
			stream.enable("jdk.ObjectAllocationInNewTLAB").withThreshold(Duration.of(1, ChronoUnit.MILLIS));
			stream.enable("jdk.ObjectAllocationOutsideTLAB").withThreshold(Duration.of(1, ChronoUnit.MILLIS));

			// Handle the event in real-time
			stream.onEvent(event -> {
				System.out.println(event.getEventType().getName() + ": " + event);
			});

			// Start the stream
			stream.start();
		}
	}

}
					
				

Your own events

					
@Name("io.evitadb.system.BackgroundTaskTimedOut")
@EventGroup(
	value = "io.evitadb.system",
	name = "evitaDB - System",
	description = "evitaDB events related to system-wide operations such as tasks, threads, etc."
)
@Category({"evitaDB", "System"})
@Description("Event that is raised when a background task has timed out and has been canceled.")
@Label("Background task timed out")
public class BackgroundTaskTimedOutEvent extends AbstractSystemEvent {

	/**
	 * Number of timed out tasks.
	 */
	@Label("Timed out tasks")
	@Description("Number of timed out and canceled tasks.")
	@ExportMetric(metricType = MetricType.COUNTER)
	private final int timedOutTasks;

	/**
	 * The name of the background task.
	 */
	@Label("Task name")
	@Description("Name of the background task.")
	@ExportMetricLabel
	final String taskName;

	public BackgroundTaskTimedOutEvent(@Nonnull String taskName, int timedOutTasks) {
		this.taskName = taskName;
		this.timedOutTasks = timedOutTasks;
	}

}
					
				

Generated documentation for all custom JFR events

Periodic events

					
@Category("MyEvents")
@Label("Cache Stats")
@Description("Simple cache statistics")
@Period("10s")
@StackTrace(false)
public class CacheStatsEvent extends Event {

    @Label("Cache Name")
    public String cacheName;

    @Label("Cache Size")
    public int cacheSize;

	public static void enableStatsRecording(String cacheName, Map cache) {
		final WeakReference> ref = new WeakReference>(cache);
		final CacheStatsEvent event = new CacheStatsEvent();
		event.cacheName = cacheName;
		FlightRecorder.addPeriodicEvent(CacheStatsEvent.class, new Runnable() {
			@Override
			public void run() {
				Map cache = ref.get();
				if (cache == null) {
					FlightRecorder.removePeriodicEvent(this);
				} else {
					event.begin();
					event.cacheSize = cache.size();
					event.commit();
				}
			}
		});
	}
}
					
				

Grafana

On demand recording

Instrumenting unaware classes

🕵🏼‍♂️ JMC Agent | JMC Agent preset

Event filtering / configuration

					
@Label("CustomEvent")
public class CustomEvent extends Event {
    @Label("Message")
    private String message;

    @SettingDefinition
    public static MessageLengthThresholdControl messageLengthThreshold
                                            = new MessageLengthThresholdControl();

    public CustomEvent(String message) {
        this.message = message;
    }

    public static void recordEvent(String message) {
        // Use the threshold in the logic
        if (message != null && message.length() > messageLengthThreshold.getValue()) {
            CustomEvent event = new CustomEvent(message);
            if (event.isEnabled()) {
                event.commit();
            }
        }
    }
}
					
				
					
public class MessageLengthThresholdControl extends SettingControl {
    private static int threshold = 10;  // Default threshold

    @Override
    public Integer combine(Integer currentValue, Integer newValue) {
        return newValue;  // Replace the current value with the new one
    }

    @Override
    public Integer parse(String value) {
        return Integer.parseInt(value);  // Parse the string setting into an integer
    }

    @Override
    public Integer getValue() {
        return threshold;  // Get the current threshold
    }

    @Override
    public void setValue(Integer value) {
        threshold = value;  // Update the threshold dynamically
    }
}
				

Profiles

Using `jfr configure`:

					
# sets messageLengthThreshold to 20 for CustomEvent in Java process with PID 12345
$ jcmd 12345 JFR.configure jdk.jfr.CustomEvent#messageLengthThreshold=20
# starts recording with custom profile
java -XX:StartFlightRecording:filename=recording.jfr,settings=path/to/custom_profile.jfc -cp yourapp.jar com.example.Main
					
				

With particular JFC configuration:

					

  
  

    
    
       
                    
                 
    

    
  

					
				

Where you can encounter it?

Grafana - Pyroscope

Happy post-mortems!

Resources

Follow us on:

Contact me @Novoj or novotnaci@gmail.com