Flame graphs are mostly runtime
Open a flame graph of a long-running Java program and the surprise is how much of it is the JVM. Garbage collection. JIT compilation. Class loading. Deoptimization. Lock contention. Class metadata lookups. In a heap-tight or freshly-warmed service, fifteen to thirty percent of the wall clock can be the runtime doing its job.
Open the equivalent Rust program and the surprise is how little of the graph is the runtime. The panic unwinder is there. The async runtime, if you opted in, is there. Maybe an allocator path. Otherwise the stack is your application code from main down to the syscall.
The same workload, drawn as two flame graphs:
Bottom-up flame graphs. Amber = application code. Purple = runtime infrastructure. Red = GC and JIT. The Rust graph has one thin purple band; the JVM graph has a purple slab plus red frames totaling about thirty percent.
This is not an attack on the JVM. The JVM's runtime is doing real work: escape analysis, branch prediction shaped by history, garbage collection that may be sub-millisecond paused these days. The work shows up in the flame graph because the work is real. Rust does the same work, or skips it, at compile time. Compile-time decisions don't show up in a flame graph.
The orchestrator's read on a flame graph depends on which substrate. For a Rust flame graph, application frames dominate; if you spent ten percent in allocation, that's a real ten percent and you can chase it. For a JVM flame graph, you need to know which frames are the runtime doing its job and which are the program doing yours. The HotSpot Glossary names the frames you can usually ignore; Brendan Gregg's flame graph guides work on both substrates.
Read the substrate before you read the chart.