JEP draft: JFR Method Timing & Tracing

OwnerErik Gahlin
TypeFeature
ScopeJDK
StatusSubmitted
Componenthotspot / jfr
EffortS
DurationS
Reviewed byMarkus Grönlund, Vladimir Kozlov
Created2024/03/20 14:10
Updated2025/03/24 20:32
Issue8328610

Summary

Extend JDK Flight Recorder (JFR) to support bytecode-based method timing and tracing for quick and easy use.

Goals

Non-Goals

Motivation

Tracing and timing method invocations help developers understand which methods are being called, how long they take to execute, and how they interact with each other. This information is useful for identifying performance bottlenecks, diagnosing the root causes of bugs, and optimizing code. For instance, if an application takes an unusually long time to start, tracing static initializers can reveal class loading that could be deferred to a later stage. Another example is when optimizing a method; timing its execution can confirm whether the code change has actually improved performance. An application may fail due to resource exhaustion, such as running out of database connections. By tracing the method responsible for opening a connection, developers can identify all points where the database is being accessed and take steps to manage resources more efficiently.

During development, developers use various tools to analyze method execution, including:

Tracing and timing in production and testing environments outside of the developer machine pose challenges. Current approaches include:

The JDK should provide means to time and trace method invocations with low overhead, allowing developers to easily and efficiently analyze and optimize their applications.

Description

The feature will be implemented as two new events, jdk.MethodTrace and jdk.MethodTiming, which both accept a filter to select the methods to trace and time. For example, to see what triggered a HashMap resize, the filter setting of the MethodTrace event could be set like this:

$ java -XX:StartFlightRecording:jdk.MethodTrace#filter=java.util.HashMap::resize,filename=recording.jfr ...
$ jfr print --events jdk.MethodTrace --stack-depth 20 recording.jfr
$ jdk.MethodTrace {
    startTime = 00:39:26.379 (2025-03-05)
    duration = 0.00113 ms
    method = java.util.HashMap.resize()
    eventThread = "main" (javaThreadId = 3)
    stackTrace = [
      java.util.HashMap.putVal(int, Object, Object, boolean, boolean) line: 636
      java.util.HashMap.put(Object, Object) line: 619
      ...
      javax.swing.SwingUtilities.invokeLater(Runnable) line: 1415
      java2d.J2Ddemo.main(String[]) line: 674
    ]
}

To simplify configuration, default.jfc and profile.jfc files will be enhanced with two .jfc options: method-trace and method-timing. These options will control the filter setting of the MethodTrace and MethodTiming events. The method-trace option records a stack trace for methods that match the filter. The method-timing option counts the number of invocations and the average execution time for methods that match the filter. To display the results, the 'jfr view' and 'jcmd JFR.view' commands will be extended with views for both method tracing and timing.

For example, if an application suffers from slow startup, timing the execution of all static initializers may give an idea of where lazy initialization could be beneficial. By omitting the class name and using <clinit> as the method name in the filter, all static initializers are traced.

$ java '-XX:StartFlightRecording:method-timing=::<clinit>,report-on-exit=method-timing' ...

                              Method Timing

Timed Method                                                 Invocations Average Time
------------------------------------------------------------ ----------- ------------
sun.font.HBShaper.<clinit>()                                           1 32.500000 ms
java.awt.GraphicsEnvironment$LocalGE.<clinit>()                        1 32.400000 ms
java2d.DemoFonts.<clinit>()                                            1 21.200000 ms
java.nio.file.TempFileHelper.<clinit>()                                1 17.100000 ms
sun.security.util.SecurityProviderConstants.<clinit>()                 1  9.860000 ms
...

If the constructor of a class needs to be traced or timed, <init> is used as the method name. Besides a method, the filter can be a class, in which case all the methods in that class are timed or traced. The filter can also be an annotation, in which case the annotated method is traced or timed. For example, to see the number of times a REST endpoint is invoked and measure the approximate execution time:

$ jcmd <pid> JFR.start method-timing=@jakarta.ws.rs.GET

Multiple filters can be specified by separating them with a semicolon. In the following example, the creation and destruction of file descriptors are traced. By looking at where the events do not match up, it is possible to find where file descriptors are leaking.

$ java '-XX:StartFlightRecording:filename=fd.jfr,method-trace=java.io.FileDescriptor::<init>;java.io.FileDescriptor::close' ...
$ jfr view --cell-height 10 MethodTrace fd.jfr

                                                  Method Trace

Start Time    Duration Event Thread     Stack Trace                                     Method                         
---------- ----------- ---------------- ----------------------------------------------- -------------------------------                             
10:50:11   0.000167 ms AWT-EventQueue-0 java.io.FileInputStream.<init>(File)            java.io.FileDescriptor.<init>()
                                        sun.security.provider.FileInputStreamPool.ge...                                
                                        sun.security.provider.NativePRNG$RandomIO.<i...                                
                                        sun.security.provider.NativePRNG.initIO(...)                                   
                                        sun.security.provider.NativePRNG.<clinit>()                                    
                                        sun.security.provider.SunEntries.<clinit>()                                    
                                        sun.security.provider.Sun.<init>()                                             
                                        sun.security.jca.ProviderConfig.getProvider()                                  
                                        sun.security.jca.ProviderList.getProvider(int)                                 
                                        sun.security.jca.ProviderList$2.get(int)                                                                                                                                
10:50:20    0.00113 ms AWT-EventQueue-0 java.io.FileInputStream.<init>(File)            java.io.FileDescriptor.<init>()
                                        java.io.FileInputStream.<init>(String)                                         
                                        java.io.FileReader.<init>(String)                                              
                                        java2d.demos.Mix.BezierScroller.getFile()                                      
                                        java2d.demos.Mix.BezierScroller.step(int, int)                                 
                                        java2d.Surface.paint(Graphics)                                                 
                                        javax.swing.JComponent.paintChildren(Graphics)                                 
                                        javax.swing.JComponent.paint(Graphics)                                         
                                        javax.swing.JComponent.paintChildren(Graphics)                                 
                                        javax.swing.JComponent.paint(Graphics)                         
...

The grammar for the new filter setting is:

filter ::= target (";" target)*
 target ::= class | class-method | method | annotation
 class ::= identifier ("." identifier)*
 class-method ::= class method
 method ::= "::" method-name
 method-name ::= identifier | "<clinit>" | "<init>"
 annotation ::= "@" class
 identifier ::= see JLS 3.8

Users who do not want to add JFR events to their code, perhaps because they think it is noisy or because they want to avoid a dependency on the jdk.jfr module, can create custom annotations, for example:

@Retention(RUNTIME)
@Target({ TYPE, METHOD })
public @interface StopWatch {
}

@Retention(RUNTIME)
@Target({ TYPE, METHOD })
public @interface Initialization {
}

@Retention(RUNTIME)
@Target({ TYPE, METHOD })
public @interface Debug {
}

These annotations can be added to classes or methods that are worth investigating. When there is a need to troubleshoot the application, the annotation can be specified, and all the annotated methods will be timed or traced, for example:

$ java -XX:StartFlightRecording:method-trace=@example.Debug
$ java -XX:StartFlightRecording:method-timing=@example.Initialization,@example.StopWatch

The resulting MethodTrace events can be correlated with other events that JFR emits, such as events for lock contention, method sampling, or I/O, to understand why the annotated parts of the program were slow.

For configuration over the network, JMX and the RemoteRecordingStream class in the jdk.management.jfr module can be used. In the following example, a non-secure connection is made to a remote host where timing and tracing are enabled for 60 seconds.

JMXServiceURL url = new JMXServiceURL(“service:jmx:rmi:///jndi/rmi://example.com:7091/jmxrmi”);
 JMXConnector connector = JMXConnectorFactory.connect(url);
 try (var r = new RemoteRecordingStream(connector.getMBeanServerConnection())) {
     Map<String, String> settings = new HashMap<>();
     // Trace methods in class com.foo.Bar that takes more than 1 ms to execute
     settings.put("jdk.MethodTrace#enabled", "true");
     settings.put("jdk.MethodTrace#stackTrace", "true");
     settings.put("jdk.MethodTrace#threshold", "1 ms");
     settings.put("jdk.MethodTrace#filter", "com.foo.Bar");
     r.onEvent("jdk.MethodTrace", event -> ...);
     // Measure execution time and invocation count for methods with 
     // the jakarta.ws.rs.GET annotation and emit the result every 10 s.
     settings.put("jdk.MethodTiming#enabled", "true");
     settings.put("jdk.MethodTiming#filter", "@jakarta.ws.rs.GET");
     settings.put("jdk.MethodTiming#period", "10 s");
     r.onEvent("jdk.MethodTiming", event -> ...);    
     r.setSettings(settings);
     r.startAsync();
     Thread.sleep(60_000);
     r.stop();
}
connector.close();

Once the 60 seconds have passed, the RemoteRecordingStream is stopped, and the added bytecode for the MethodTrace and MethodTiming events are removed. The RemoteRecordingStream then waits for events that happened during the interval to be consumed. Finally, the stream is closed, and the recording data on the remote host is removed, leaving the system as it was before the RemoteRecordingStream was started. For in-process use, such as a Java Agent, the RecordingStream class can be used to consume MethodTrace and MethodTiming events in a similar manner.

In case multiple recordings are running at the same time, either started remotely or locally, the union of all the filters will be applied.

In scenarios where more than a few methods should be traced, a separate .jfc file can be created:

<?xml version="1.0" encoding="UTF-8"?>
<configuration version="2.0">
  <event name="jdk.MethodTiming">
    <setting name="enabled">true</setting>
    <setting name="filter">
      com.example.Foo::method1;
      com.example.Bar::method2;
      ...
      com.example.Baz::method17
    </setting>
  </event>
</configuration>

It can then be used in conjunction with the default settings:

$ java -XX:StartFlightRecording:settings=timing.jfc,settings=default ...

Alternatives