Reading and Writing JFR Files Programmatically

Last week, I showed you the Fastest Way to get the Version of a Java Installation. This week, I’ll show you something completely different: how to interact with JFR data programmatically, showcasing a new library called basic-jfr-processor in the process.

While JFR is a great tool for profiling your application and gaining insights, the file format is, on purpose, not well documented or specified. One of the best sources of information is Gunnar Morling’s blog post on the topic, and of course, the OpenJDK source code.

But of course, there are ready-made APIs for reading JFR files and OpenJDK-adjacent libraries to write them. In this overview blog post, I’ll showcase the built-in Java JFR API, Jaroslav Bachorik’s jafar API, and the JMC JFR writer API, as well as my own basic-jfr-processor library based on the latter.

We start with the built-in API:

Reading JFR Files using Java’s API

The API is pretty simple: it consists of a RecordingFile class that allows us to parse the events in the file as RecordingEvent instances with properties that can be a primitive, a special type like RecordedStackTrace or RecordedClass, or a complex RecordingObject:

The following is an example from documentation: It prints a histogram of all sampled methods in a file:

public static void main(String[] args) throws IOException {
    if (args.length != 1) {
        System.err.println("Must specify a recording file.");
        return;
    }

    RecordingFile.readAllEvents(Path.of(args[0])).stream()
        .filter(e -> e.getEventType().getName().equals("jdk.ExecutionSample"))
        .map(e -> e.getStackTrace())
        .filter(s -> s != null)
        .map(s -> s.getFrames().getFirst())
        .filter(f -> f.isJavaFrame())
        .map(f -> f.getMethod())
        .collect(
            Collectors.groupingBy(m -> m.getType().getName() + "." + m.getName() + " " + m.getDescriptor(),
            Collectors.counting()))
        .entrySet()
        .stream()
        .sorted((a, b) -> b.getValue().compareTo(a.getValue()))
        .forEach(e -> System.out.printf("%8d %s\n", e.getValue(), e.getKey()));
    // there is also an iterator-like API via new RecordingFile(outputFile)
}

You can access fields of an event or object by using one of the many accessor functions, like getBoolean(String name). The main advantage of this API is that it’s already built in and maintained by the JFR authors themselves. The main disadvantage is that it’s a pull-based API (we ask JFR for the next event) and that it’s rather slow.

An alternative API is jafar by Jaroslav Bachorik:

Reading JFR Files using Jafar

Jafar allows us to directly parse JFR files into objects, removing the need for slow object-getter requests like in the previous API and making the code far more readable.

The following is an example from the documentation:

import io.jafar.parser.api.*;
import java.nio.file.Paths;

@JfrType("custom.MyEvent")
public interface MyEvent { // no base interface required
  String myfield();
}

try (TypedJafarParser p = JafarParser.newTypedParser(Paths.get("/path/to/recording.jfr"))) {
  HandlerRegistration<MyEvent> reg = p.handle(MyEvent.class, (e, ctl) -> {
    System.out.println(e.myfield());
    long pos = ctl.stream().position(); // current byte position while in handler
    // ctl.abort(); // optionally stop parsing immediately without throwing
  });
  p.run();
  reg.destroy(p); // deregister
}

This API is especially great when parsing large JFR files with known events, as jafar uses compile-time code generation to improve speed and drastically reduce memory footprint (more on this in the excellent README). But jafar also has an untyped API (again, the example is from the documentation):

import io.jafar.parser.api.*;
import java.nio.file.Paths;

try (UntypedJafarParser p = JafarParser.newUntypedParser(Paths.get("/path/to/recording.jfr"))) {
  HandlerRegistration<?> reg = p.handle((type, value) -> {
    if ("jdk.ExecutionSample".equals(type.getName())) {
      // You can retrieve the value by providing 'path' -> "eventThread", "javaThreadId"
      Object threadId = Values.get(value, "eventThread", "javaThreadId");
      // You can also get the value conveniently typed - for primitive values you need to use the boxed type in the call
      long threadIdLong = Values.as(value, Long.class, "eventThread", "javaThreadId");
      // use threadId ...
    }
  });
  p.run();
  reg.destroy(p);
}

It’s impressive what Jaroslav built, and I recommend that you take a closer look at this relatively new library.

Being able to read JFR files is great, but you might wonder how to write them ourselves.

Writing JFR Events

Sadly, there is no built-in writer API in Java, but there is the JMC JFR writer API, which is developed by the OpenJDK project, albeit not by the current primary JFR file format maintainers.

To start writing a file, we first create a Recording object using the Recordings class:

Recording recording = Recordings.newRecording(outputStream, 
       r -> { /* configure settings */ });

The Recording class allows us to register event types and object types, and write events.

In the following, an example that writes two instances of the custom event MyEvent from above, into a file and reads it back using the built-in JFR reader API (GitHub):

import jdk.jfr.consumer.RecordingFile;
import org.openjdk.jmc.flightrecorder.writer.TypesImpl;
import org.openjdk.jmc.flightrecorder.writer.api.*;

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;

/**
 * Example demonstrating how to write a JFR file using JMC Writer API.
 */
public class JMCWriterExample {

    public static void main(String[] args) throws IOException {
        Path outputFile = Path.of("example.jfr");

        final long startTicks = 1;

        try (FileOutputStream fos = 
                     new FileOutputStream(outputFile.toFile())) {
            // Initialize a new recording
            Recording recording = Recordings.newRecording(fos, r -> {
                // Optional: configure recording settings
                // Ensure JDK type initialization
                r.withJdkTypeInitialization();
                // Set start ticks for timestamp consistency in nanoseconds
                // 1 seems to work best
                r.withStartTicks(startTicks);
            });

            // Register the event type
            Type myEventType = recording.registerType(
                "com.example.MyEvent",
                "jdk.jfr.Event",
                typeBuilder -> {
                    // Add the implicit startTime field
                    // the implicit fields must come first
                    typeBuilder.addField("startTime", 
                            TypesImpl.Builtin.LONG,
                            field ->
                                    field.addAnnotation(
                                            Types.JDK.ANNOTATION_TIMESTAMP, 
                                            "TICKS"));
                    // Add the custom field
                    typeBuilder.addField("myfield", 
                            Types.Builtin.STRING);
                }
            );

            // Write an event instance
            recording.writeEvent(myEventType.asValue(eventBuilder -> {
                // the fields have to be set in order of declaration
                eventBuilder.putField("startTime", System.nanoTime() - startTicks);
                eventBuilder.putField("myfield", "Hello from JMC!");
            }));

            // Write another event
            recording.writeEvent(myEventType.asValue(eventBuilder -> {
                eventBuilder.putField("startTime", System.nanoTime() - startTicks);
                eventBuilder.putField("myfield", "Another event");
            }));

            // Close the recording to finalize the file
            recording.close();
        }

        // Printing file contents for demonstration
        RecordingFile.readAllEvents(outputFile)
                .forEach(System.out::println);

        System.out.println("JFR file written to: " + outputFile);
    }

When we run this, we get something like:

com.example.MyEvent {
  startTime = 19:08:19.704 (2026-01-14)
  myfield = "Hello from JMC!"
}


com.example.MyEvent {
  startTime = 19:08:19.704 (2026-01-14)
  myfield = "Another event"
}


JFR file written to: example.jfr

The API is rather complicated to use, and the predefined complex types (e.g., stack traces, …) are only approximations of the real types. The RecordingImpl class is a good starting point to see how it can be used.

But what if you only want to modify existing events in an existing file?

Modifying Files with Basic-JFR-Processor

My new basic-jfr-processor library allows you to do precisely this. It’s built on top of the JMC writer API. It supports writing RecordedEvents from the built-in JFR reader API, including support for an event modifier class to allow you to drop events or modify values.

Removing a specific type of event from a file is as simple as:

// Create a modifier that drops events
JFREventModifier modifier = new JFREventModifier() {
    @Override
    public boolean shouldRemoveEvent(RecordedEvent event) {
        return event.getEventType().getName().equals("example.UserLogin");
    }
};

// Process the recording
JFRProcessor processor = new JFRProcessor(modifier, inputFile);
try (FileOutputStream out = new FileOutputStream(outputFile.toFile())) {
    processor.process(out).close();
}

The JFRProcessor code is also a good example of how to use the JMC writer API to create a file as close as possible to a JFR-generated file. Feel free to reuse the code in your own projects.

Conclusion

The JFR file format is not formally specified; however, there is the possibility of reading and writing JFR files using both built-in and external libraries. This leads to a tool that redacts JFR files, including individual properties. In the last example of the blog post, you saw a basic version of such a tool. In the next blog post, I’ll cover a more complete version.

Thanks for coming along with me and see you next week.

This article is part of my work in the SapMachine team at SAP, making profiling and debugging easier for everyone.

Author

  • Johannes Bechberger

    Johannes Bechberger is a JVM developer working on profilers and their underlying technology in the SapMachine team at SAP. This includes improvements to async-profiler and its ecosystem, a website to view the different JFR event types, and improvements to the FirefoxProfiler, making it usable in the Java world. His work today comprises many open-source contributions and his blog, where he regularly writes on in-depth profiling and debugging topics. He also works on hello-ebpf, the first eBPF library for Java. His most recent contribution is the new CPU Time Profiler in JDK 25.

    View all posts

New posts like these come out at least every two weeks, to get notified about new posts, follow me on BlueSky, Twitter, Mastodon, or LinkedIn, or join the newsletter:

Leave a Reply

Your email address will not be published. Required fields are marked *