Created
July 22, 2026 01:00
-
-
Save mohashari/0c967de4d7b9ff06f0cdfd3f8f9f0444 to your computer and use it in GitHub Desktop.
Profiling JVM Safepoint Delays at Scale with JDK Flight Recorder and eBPF — code snippets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.mycompany.observability; | |
| import jdk.jfr.Configuration; | |
| import jdk.jfr.Recording; | |
| import java.io.IOException; | |
| import java.nio.file.Paths; | |
| import java.text.ParseException; | |
| import java.time.Duration; | |
| public class JfrSafepointProfiler { | |
| public static void startProfiling(String outputPath, Duration duration) throws IOException, ParseException { | |
| // Load the default profile configuration as a baseline | |
| Configuration config = Configuration.getConfiguration("profile"); | |
| Recording recording = new Recording(config); | |
| // Enable and adjust thresholds for specific safepoint lifecycle events | |
| recording.enable("jdk.SafepointBegin").withThreshold(Duration.ofMillis(1)); | |
| recording.enable("jdk.SafepointStateSynchronization").withThreshold(Duration.ofMillis(1)); | |
| recording.enable("jdk.SafepointWaitBlocked").withThreshold(Duration.ofMillis(1)); | |
| recording.enable("jdk.SafepointCleanup").withThreshold(Duration.ofMillis(1)); | |
| recording.enable("jdk.SafepointEnd"); | |
| recording.setDestination(Paths.get(outputPath)); | |
| recording.setMaxAge(Duration.ofHours(1)); | |
| recording.start(); | |
| System.out.printf("JFR Safepoint profiling started. Writing output to: %s%n", outputPath); | |
| // Schedule clean termination | |
| new Thread(() -> { | |
| try { | |
| Thread.sleep(duration.toMillis()); | |
| recording.stop(); | |
| recording.close(); | |
| System.out.println("JFR Safepoint profiling completed successfully."); | |
| } catch (Exception e) { | |
| System.err.println("Failed to stop JFR recording: " + e.getMessage()); | |
| } | |
| }).start(); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?xml version="1.0" encoding="UTF-8"?> | |
| <configuration version="2.0" label="Aggressive Safepoint Profiling" description="Low overhead profile targeting JVM Safepoints and TTSP anomalies" provider="Moh Ashari Muklis"> | |
| <event name="jdk.SafepointBegin"> | |
| <setting name="enabled">true</setting> | |
| <setting name="threshold">0 ms</setting> | |
| </event> | |
| <event name="jdk.SafepointStateSynchronization"> | |
| <setting name="enabled">true</setting> | |
| <setting name="threshold">0 ms</setting> | |
| </event> | |
| <event name="jdk.SafepointWaitBlocked"> | |
| <setting name="enabled">true</setting> | |
| <setting name="threshold">0 ms</setting> | |
| </event> | |
| <event name="jdk.SafepointCleanup"> | |
| <setting name="enabled">true</setting> | |
| <setting name="threshold">0 ms</setting> | |
| </event> | |
| <event name="jdk.SafepointEnd"> | |
| <setting name="enabled">true</setting> | |
| </event> | |
| </configuration> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Extract and format all safepoint events from a JFR recording file. | |
| # Sorts the output by the duration of the state synchronization phase (TTSP). | |
| jfr print --events "jdk.SafepointBegin,jdk.SafepointStateSynchronization" production_dump.jfr | \ | |
| awk ' | |
| /jdk.SafepointBegin/ { | |
| split($0, t_arr, " "); | |
| time_stamp = t_arr[2]; | |
| } | |
| /jdk.SafepointStateSynchronization/ { | |
| in_sync = 1; | |
| } | |
| /duration =/ && in_sync { | |
| match($0, /[0-9]+(\.[0-9]+)?\s*(ms|s|us)/); | |
| duration_str = substr($0, RSTART, RLENGTH); | |
| print time_stamp " | TTSP: " duration_str; | |
| in_sync = 0; | |
| } | |
| ' | sort -t':' -k3 -n |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package com.mycompany.workload; | |
| public class SafepointBlockingTask implements Runnable { | |
| private static final int ITERATIONS = 2_000_000_000; | |
| private final double[] data = new double[1024]; | |
| public SafepointBlockingTask() { | |
| for (int i = 0; i < data.length; i++) { | |
| data[i] = Math.random(); | |
| } | |
| } | |
| @Override | |
| public void run() { | |
| double sum = 0; | |
| // Counted loop: JIT optimizes out safepoint checks in the compiled loop body | |
| for (int i = 0; i < ITERATIONS; i++) { | |
| int index = i & 1023; | |
| // Calculations to prevent compiler dead-code elimination | |
| sum += Math.sin(data[index]) * Math.cos(i); | |
| } | |
| System.out.println("Computation output: " + sum); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #pragma init_uprobes | |
| /* | |
| * bpftrace script to monitor JVM TTSP latency. | |
| * Hooks into the HotSpot symbol for safepoint synchronization startup. | |
| * Usage: bpftrace jvm_ttsp.bt -p <PID> | |
| */ | |
| uprobe:/usr/lib/jvm/java-17-openjdk/lib/server/libjvm.so:_ZN20SafepointSynchronize5beginEv { | |
| @start[tid] = nsecs; | |
| @safepoint_requested = nsecs; | |
| } | |
| uretprobe:/usr/lib/jvm/java-17-openjdk/lib/server/libjvm.so:_ZN20SafepointSynchronize5beginEv { | |
| $s = @start[tid]; | |
| if ($s) { | |
| $duration_us = (nsecs - $s) / 1000; | |
| @ttsp_duration_us = lhist($duration_us, 0, 100000, 5000); // 0 to 100ms range | |
| delete(@start[tid]); | |
| } | |
| @safepoint_requested = 0; | |
| } | |
| // Trace scheduling switches while a safepoint request is active | |
| tracepoint:sched:sched_switch /@safepoint_requested != 0/ { | |
| // Record the on-CPU duration of threads running during the sync phase | |
| @running_tids[args->prev_pid] = nsecs - @safepoint_requested; | |
| } | |
| interval:s:10 { | |
| printf("--- TTSP Latency Distribution (microseconds) ---\n"); | |
| print(@ttsp_duration_us); | |
| printf("\n--- Threads running during active Safepoint sync (TID & Duration ns) ---\n"); | |
| print(@running_tids); | |
| clear(@running_tids); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from bcc import BPF | |
| import time | |
| import sys | |
| # BCC script to trace user-space stacks of threads blocking JVM safepoint entry. | |
| # Hooks JVM's libjvm.so internal symbols. | |
| if len(sys.argv) < 3: | |
| print("Usage: python ttsp_stack_tracer.py <PID> <PATH_TO_LIBJVM>") | |
| sys.exit(1) | |
| pid = sys.argv[1] | |
| libjvm_path = sys.argv[2] | |
| ebpf_program = """ | |
| #include <uapi/linux/ptrace.h> | |
| #include <linux/sched.h> | |
| BPF_HASH(start_ns, u32, u64); | |
| BPF_STACK_TRACE(stack_traces, 2048); | |
| BPF_HASH(blocked_stacks, u32, int); | |
| int trace_safepoint_begin(struct pt_regs *ctx) { | |
| u32 tid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; | |
| u64 ts = bpf_ktime_get_ns(); | |
| start_ns.update(&tid, &ts); | |
| return 0; | |
| } | |
| int trace_safepoint_end(struct pt_regs *ctx) { | |
| u32 tid = bpf_get_current_pid_tgid() & 0xFFFFFFFF; | |
| u64 *ts = start_ns.lookup(&tid); | |
| if (ts) { | |
| u64 delta = bpf_ktime_get_ns() - *ts; | |
| // Threshold: If sync takes more than 15ms (15,000,000 ns) | |
| if (delta > 15000000) { | |
| int stack_id = stack_traces.get_stackid(ctx, BPF_F_USER_STACK); | |
| if (stack_id >= 0) { | |
| blocked_stacks.update(&tid, &stack_id); | |
| } | |
| } | |
| start_ns.delete(&tid); | |
| } | |
| return 0; | |
| } | |
| """ | |
| b = BPF(text=ebpf_program) | |
| # Attach uprobes using the target JVM Process ID and absolute library path | |
| b.attach_uprobe(name=libjvm_path, sym="_ZN20SafepointSynchronize5beginEv", fn_name="trace_safepoint_begin", pid=int(pid)) | |
| b.attach_uretprobe(name=libjvm_path, sym="_ZN20SafepointSynchronize5beginEv", fn_name="trace_safepoint_end", pid=int(pid)) | |
| print(f"Tracing JVM PID {pid} for safepoint delays > 15ms. Press Ctrl+C to stop.") | |
| try: | |
| while True: | |
| time.sleep(1) | |
| except KeyboardInterrupt: | |
| pass | |
| print("\n--- Blocked Stack Traces Detected During TTSP Windows ---") | |
| blocked_stacks = b["blocked_stacks"] | |
| stack_traces = b["stack_traces"] | |
| for k, v in blocked_stacks.items(): | |
| print(f"\nThread OS ID (TID): {k.value} was running when VM Thread was waiting.") | |
| for addr in stack_traces.walk(v.value): | |
| print(f" [Instruction Address] {hex(addr)}") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment