Have you ever wanted to bring your JFR events into context? Adding information on sessions, user IDs, and more can improve your ability to make sense of all the events in your profile. Currently, we can only add context by creating custom JFR events, as I presented in my Profiling Talks:
We can use these custom events (see Custom JFR Events: A Short Introduction and Custom Events in the Blocky World: Using JFR in Minecraft) to store away the information and later relate them to all the other events by using the event’s time, duration, and thread. This works out-of-the-box but has one major problem: Relating events is quite fuzzy, as time stamps are not as accurate (see JFR Timestamps and System.nanoTime), and we do all of this in post-processing.
But couldn’t we just attach some context to every JFR event we’re interested in? Not yet, but Jaroslav Bachorik from DataDog is working on it. Recently, he wrote three blog posts (1, 2, 3). The following is a different take on his idea, showing how to use it in a small file server example.
The main idea of Jaroslav’s approach is to store a context in thread-local memory and attach it to every JFR event as configured. But before I dive into the custom context, I want to show you the example program, which you can find, as always, MIT-licensed on GitHub.
Example
We create a simple file server via Javalin, which allows a user to
- Register (URL schema
register/{user}
)
- Store data in a file (
store/{user}/{file}/{content}
)
- Retrieve file content (
load/{user}/{file}
)
- Delete files (
delete/{user}/{file}
)
The URLs are simple to use, and we don’t bother about error handling, user authentication, or large files, as this would complicate our example. I leave it as an exercise for the inclined reader. The following is the most essential part of the application: the server declaration:
FileStorage storage = new FileStorage();
try (Javalin lin = Javalin.create(conf -> {
conf.jetty.server(() ->
new Server(new QueuedThreadPool(4))
);
})
.exception(Exception.class, (e, ctx) -> {
ctx.status(500);
ctx.result("Error: " + e.getMessage());
e.printStackTrace();
})
.get("/register/{user}", ctx -> {
String user = ctx.pathParam("user");
storage.register(user);
ctx.result("registered");
})
.get("/store/{user}/{file}/{content}", ctx -> {
String user = ctx.pathParam("user");
String file = ctx.pathParam("file");
storage.store(user, file, ctx.pathParam("content"));
ctx.result("stored");
})
.get("/load/{user}/{file}", ctx -> {
String user = ctx.pathParam("user");
String file = ctx.pathParam("file");
ctx.result(storage.load(user, file));
})
.get("/delete/{user}/{file}", ctx -> {
String user = ctx.pathParam("user");
String file = ctx.pathParam("file");
storage.delete(user, file);
ctx.result("deleted");
})) {
lin.start(port);
Thread.sleep(100000000);
} catch (InterruptedException ignored) {
}
This example runs on Jaroslav’s OpenJDK fork (commit 6ea2b4f), so if you want to run it in its complete form, please build the fork and make sure that you’re PATH
and JAVA_HOME
environment variables are set accordingly.
You can build the server using mvn package
and
start it, listening on the port 1000
, via:
java -jar target/jfr-context-example.jar 1000
You can then use it via your browser or curl
:
# start the server
java -XX:StartFlightRecording=filename=flight.jfr,settings=config.jfc \
-jar target/jfr-context-example.jar 1000 &
pid=$!
# register a user
curl http://localhost:1000/register/moe
# store a file
curl http://localhost:1000/store/moe/hello_file/Hello
# load the file
curl http://localhost:1000/load/moe/hello_file
-> Hello
# delete the file
curl http://localhost:1000/delete/moe/hello_file
kill $pid
# this results in the flight.jfr file
To make testing easier, I created the test.sh script, which starts the server, registers a few users and stores, loads, and deletes a few files, creating a JFR file along the way. We're using a custom JFR configuration to enable the IO events without any threshold. This is not recommended for production
but is required in our toy example to get any such event:
<?xml version="1.0" encoding="UTF-8"?>
<configuration version="2.0" label="Custom" description="Custom config for the example"
provider="Johannes Bechberger">
<event name="jdk.FileRead" withContext="true">
<setting name="enabled">true</setting>
<setting name="stackTrace">true</setting>
<setting name="threshold" control="file-threshold">0 ms</setting>
</event>
<event name="jdk.FileWrite" withContext="true">
<setting name="enabled">true</setting>
<setting name="stackTrace">true</setting>
<setting name="threshold" control="file-threshold">0 ms</setting>
</event>
</configuration>
We can use the