Ever wondered how the views of the jfr
tool are implemented? There are views like hot-methods
which gives the most used methods, or cpu-load-samples
that gives you the system load over time that you can directly use on the command line:
> jfr view cpu-load-samples recording.jfr CPU Load Time JVM User JVM System Machine Total ------------------ ------------------ -------------------- ----------------------- 14:33:29 8,25% 0,08% 29,65% 14:33:30 8,25% 0,00% 29,69% 14:33:31 8,33% 0,08% 25,42% 14:33:32 8,25% 0,08% 27,71% 14:33:33 8,25% 0,08% 24,64% 14:33:34 8,33% 0,00% 30,67% ...
This is helpful when glancing at JFR files and trying to roughly understand their contents, without loading the files directly into more powerful, but also more resource-hungry, JFR viewers.
In this short blog post, I’ll show you how the views work under the hood using JFR queries and how to use the queries with my new experimental JFR query tool.
I didn’t forget the promised blog post on implementing the new CPU-time profiler in JDK 25; it’ll come soon.
Under the hood, JFR views use a built-in query language to define all views in the view.ini file. The above is, for example, defined as:
[environment.cpu-load-samples] label = "CPU Load" table = "SELECT startTime, jvmUser, jvmSystem, machineTotal FROM CPULoad"
With my new query tool (GitHub), we can plot this as:

The query language is a subset of SQL, tailored explicitly to defining these simple views. A more complex view is the hot-methods
view:
[application.hot-methods] label = "Java Methods that Executes the Most" table = "COLUMN 'Method', 'Samples', 'Percent' FORMAT none, none, normalized SELECT stackTrace.topFrame AS T, COUNT(*), COUNT(*) FROM ExecutionSample GROUP BY T LIMIT 25"
Which first names the columns, then gives some formatting info, which is followed by a query that groups and counts the top frame methods.
This query language is really versatile. I’m focusing on the following on the implementation of the latest LTS release, JDK 21. This defines the grammar of these queries as follows:
Grammar
The grammar is similar to SQL:
query ::= [column] [format] select from [where] [groupBy] [orderBy] [limit]
column ::= “COLUMN” text (“,” text)*
format ::= “FORMAT” formatter (“,” formatter)*
formatter ::= property (“;” property)*
select ::= “SELECT” “” | expression (“,” expression)
expression ::= (aggregator | field) [alias]
aggregator ::= function “(” (field | ““) “)” alias ::= “AS” symbol from ::= “FROM” source (“,” source)
source ::= type [alias]
where ::= condition (“AND” condition)*
condition ::= field “=” text
groupBy ::= “GROUP BY” field (“,” field)*
orderBy ::= “ORDER BY” orderField (“,” orderField)*
orderField ::= field [sortOrder]
sortOrder ::= “ASC” | “DESC”
limit ::= “LIMIT”
- text, characters surrounded by single quotes
- symbol, alphabetic characters
- type, the event type name, for example SystemGC. To avoid ambiguity,
the name may be qualified, for example jdk.SystemGC- field, the event field name, for example stackTrace.
To avoid ambiguity, the name may be qualified, for example
jdk.SystemGC.stackTrace. A type alias declared in a FROM clause
can be used instead of the type, for example S.eventThread- function, determines how fields are aggregated when using GROUP BY.
Aggregate functions are:
AVG: The numeric average
COUNT: The number of values
DIFF: The numeric difference between the last and first value
FIRST: The first value
LAST: The last value
LAST_BATCH: The last set of values with the same end timestamp
LIST: All values in a comma-separated list
MAX: The numeric maximum
MEDIAN: The numeric median
MIN: The numeric minimum
P90, P95, P99, P999: The numeric percentile, 90%, 95%, 99% or 99.9%
STDEV: The numeric standard deviation
SUM: The numeric sum
UNIQUE: The unique number of occurrences of a value
Null values are included, but ignored for numeric functions. If no
aggregator function is specified, the first non-null value is used.- property, any of the following:
cell-height: Maximum height of a table cell
missing:whitespace Replace missing values (N/A) with blank space
normalized Normalize values between 0 and 1.0 for the column
truncate-beginning if value can’t fit a table cell, remove the first characters
truncate-end if value can’t fit a table cell, remove the last characters If no value exist, or a numeric value can’t be aggregated, the result is ‘N/A’,
unless missing:whitespace is used. The top frame of a stack trace can be referred’
to as stackTrace.topFrame. When multiple event types are specified in a FROM clause,
the union of the event types are used (not the cartesian product) To see all available events, use the query ‘”SHOW EVENTS”‘. To see all fields for
a particular event type, use the query ‘”SHOW FIELDS “‘.
This, of course, is not as powerful as using full SQL queries, but it is somewhat standardized in the OpenJDK. An alternative is Gunnar Morling’s JFR Analytics tool:
This tool allows you to analyze JFR recordings using standard SQL (leveraging Apache Calcite under the hood). In JFR Analytics, each event type is represented by its own “table”. Finding thread start events without matching end events is as simple as running a
Finding Java Thread Leaks With JDK Flight Recorder and a Bit Of SQLLEFT JOIN
on the two event types and keeping only those start events which don’t have a join partner.
Back to the JFR queries.
Using JFR Queries and the Query Tool
Sadly, JFR queries can’t be used directly, as only JFR views are available via the jfr
tool (see JDK-8352648). But OpenJDK gladly is open-source, so I created my (highly experimental) JFR query tool, which contains a fork of JDK 21’s query parser and executor. You can download the query.jar
from the GitHub releases page.
The tool has a few commands (java -jar query.jar --help
):
Commands: help, --help, -h, -? Display all available commands, or help about a specific command view Display event values in a recording file (.jfr) in predefined views query Execute JFR queries against recording files web Start a web server to query JFR files
But the most interesting is the web
command as it offers you a tiny web frontend to explore JFR recording using the query language:
Usage: query web [-hV] [--verbose] [--cell-height=<cellHeight>] [--host=<host>] [--maxage=<maxAge>] [--port=<port>] [--truncate=<truncate>] [--width=<width>] <file> Start a web server to query JFR files <file> The JFR file to serve --cell-height=<cellHeight> Maximum height for cells -h, --help Show this help message and exit. --host=<host> Host to bind the web server to (default: localhost) --maxage=<maxAge> Length of time for the query to span, in (s)econds, (m)inutes, (h)ours, or (d)ays, e.g. 60m, or 0 for no limit --port=<port> Port to run the web server on (default: 8080) --truncate=<truncate> Truncate mode (BEGINNING or END). --width=<width> Maximum number of horizontal characters Examples: $ jfr web recording.jfr # starts a webserver at localhost:8080
So java -jar query.jar web recording.jfr
launches a web server at localhost:8080:

You can use it to run arbitrary queries like the CPU load related query from before:

I support parsing the raw JFR printout and more. It has syntax highlighting for queries and limited auto-completion:

You can zoom in the graph, which also filters the rows shown:

And selecting specific rows:

The graph is presented if the first column in the table is a time column.
You can view the existing views via the views
command:

Conclusion
This tool is still an early prototype, but I hope it’s useful if you want to explore the JFR query language and look into your recordings differently, using customizable queries. Please try it out. I’m happy to hear any feedback, and maybe we could use it to create new JFR views that find their way back into the OpenJDK.
This short blog post gave you an insight into the tiny tools I build at work to explore profiling tooling. You can hopefully expect more in the future, including a blog post on the implementation of the new CPU-time profiler in JDK 25.
This blog post is part of my work in the SapMachine team at SAP, making profiling easier for everyone.