An Experimental Front-End for JFR Queries

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 LEFT JOIN on the two event types and keeping only those start events which don’t have a join partner.

Finding Java Thread Leaks With JDK Flight Recorder and a Bit Of SQL

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.

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. He started at SAP in 2022 after two years of research studies at the KIT in the field of Java security analyses. His work today is comprised of many open-source contributions and his blog, where he writes regularly on in-depth profiling and debugging topics, and of working on his JEP Candidate 435 to add a new profiling API to the OpenJDK.

    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 *