Span-to-Profile Correlation: Link OpenTelemetry Spans to JFR Samples to Kill P99 Outliers
Aggregated flamegraphs are useless when an isolated P99 request stalls on a single thread due to allocation bursts or lock contention. Staring at 30-minute averaged profiles while your latency SLAs burn is a rookie mistake you need to stop making.
Why Most Developers Get This Wrong
- Treating profiling as aggregate-only: Averaging millions of thread samples completely washes out the transient 400ms lock inflation hit by 0.01% of requests.
- Timestamp alignment guessing: Grepping APM timestamps against JFR recordings fails due to clock drift, thread re-use, and non-deterministic OS thread scheduling.
-
Ignoring JVM-level context propagation: Emitting spans without linking the active
TraceIdandSpanIdinto the thread state makes thread-level JFR events impossible to query deterministically.
The Right Way
Bridge your OpenTelemetry tracing pipeline directly to JVM flight recordings using the OpenTelemetry Profiling Data Model to slice samples strictly within an active span's lifespan.
- Use the OpenTelemetry Java instrumentation agent with continuous profiling extensions to bind
SpanContextinto JFR thread-local state. - Isolate tail latency by filtering
jdk.ExecutionSample,jdk.JavaMonitorEnter, andjdk.ObjectAllocationInNewTLABevents strictly by the targetedtrace_id. - Export via the OTel continuous profiling protocol directly into backends supporting trace-to-profile linking to render an isolated flamegraph representing only that P99 span.
Show Me The Code
Tag your JFR stream directly with the active span context at critical execution boundaries:
@Category("OpenTelemetry")
@StackTrace(false)
public class SpanMarkerEvent extends Event {
@Label("Trace ID") public String traceId;
@Label("Span ID") public String spanId;
}
// In your high-impact path:
Span span = tracer.spanBuilder("order-reconciliation").startSpan();
try (Scope scope = span.makeCurrent()) {
SpanMarkerEvent event = new SpanMarkerEvent();
event.traceId = span.getSpanContext().getTraceId();
event.spanId = span.getSpanContext().getSpanId();
event.begin();
processP99Workload();
event.commit();
} finally {
span.end();
}
Key Takeaways
- Aggregate flamegraphs hide outliers; span-correlated JFR profiles reveal the exact method frames executing during latency spikes.
- The OpenTelemetry Profiling Data Model turns thread dumps and execution samples into queryable trace attributes.
- Stop guessing via timestamps—tag JVM execution events with
trace_idat the thread boundary.
Heads up: if you want to see these patterns applied to real interview problems, javalld.com has full machine coding solutions with traces.
Top comments (0)