Flame Graphs are powerful visualizations for profiling software performance, invented by Brendan Gregg. They help identify the most frequent code-paths in your application by showing stack traces in a compact, hierarchical format. This article explores how to build a Flame Graph Generator in Java, from parsing stack traces to generating interactive SVG visualizations.
Understanding Flame Graphs
Key Concepts:
- Width: Represents the frequency or time spent in a function
- Height: Represents the depth of the call stack
- Colors: Typically indicate different modules or libraries (warm colors for application code, cool colors for system libraries)
Sample Input Format (Collapsed Stacks):
main;functionA;functionB 150 main;functionA;functionC 200 main;functionD 100
Architecture Overview
The Flame Graph Generator consists of several components:
- Stack Trace Parser: Processes profiling data
- Flame Graph Model: Builds the hierarchical structure
- SVG Generator: Creates the visual representation
- Profile Data Collector: Optional component for live profiling
Core Data Structures
1. Stack Frame Node:
import java.util.*; import java.util.concurrent.atomic.AtomicLong; class FrameNode { private String name; private AtomicLong value; private Map<String, FrameNode> children; private FrameNode parent; public FrameNode(String name) { this.name = name; this.value = new AtomicLong(0); this.children = new HashMap<>(); } public FrameNode getOrCreateChild(String frameName) { return children.computeIfAbsent(frameName, k -> { FrameNode child = new FrameNode(frameName); child.parent = this; return child; }); } public void addValue(long value) { this.value.addAndGet(value); } // Getters public String getName() { return name; } public long getValue() { return value.get(); } public Map<String, FrameNode> getChildren() { return children; } public FrameNode getParent() { return parent; } public long getTotalValue() { long total = value.get(); for (FrameNode child : children.values()) { total += child.getTotalValue(); } return total; } } 2. Flame Graph Model:
class FlameGraph { private FrameNode root; private long totalSamples; public FlameGraph() { this.root = new FrameNode("root"); this.totalSamples = 0; } public void addStackTrace(List<String> stackTrace, long value) { if (stackTrace == null || stackTrace.isEmpty()) return; FrameNode current = root; // Reverse stack trace (root at bottom, leaf at top) List<String> reversed = new ArrayList<>(stackTrace); Collections.reverse(reversed); for (String frame : reversed) { current = current.getOrCreateChild(frame); } current.addValue(value); totalSamples += value; } public void addCollapsedStack(String collapsedStack, long value) { String[] frames = collapsedStack.split(";"); addStackTrace(Arrays.asList(frames), value); } // Getters public FrameNode getRoot() { return root; } public long getTotalSamples() { return totalSamples; } } Stack Trace Parser
1. Collapsed Stack Format Parser:
class CollapsedStackParser { public static FlameGraph parseCollapsedStacks(List<String> lines) { FlameGraph flameGraph = new FlameGraph(); for (String line : lines) { if (line.trim().isEmpty()) continue; String[] parts = line.split("\\s+"); if (parts.length < 2) continue; String stack = parts[0]; long value; try { value = Long.parseLong(parts[1]); } catch (NumberFormatException e) { continue; // Skip malformed lines } flameGraph.addCollapsedStack(stack, value); } return flameGraph; } public static FlameGraph parseFromFile(String filename) throws IOException { List<String> lines = Files.readAllLines(Paths.get(filename)); return parseCollapsedStacks(lines); } } 2. Java Stack Trace Parser (for thread dumps):
class JavaStackTraceParser { public static FlameGraph parseThreadDumps(List<String> threadDumps) { FlameGraph flameGraph = new FlameGraph(); List<String> currentStack = new ArrayList<>(); boolean inStackTrace = false; for (String line : threadDumps) { line = line.trim(); if (line.startsWith("\"")) { // Thread header line if (!currentStack.isEmpty()) { flameGraph.addStackTrace(currentStack, 1); currentStack.clear(); } inStackTrace = true; } else if (line.startsWith("at ")) { // Stack frame line if (inStackTrace) { String frame = extractMethodName(line); if (frame != null) { currentStack.add(frame); } } } else if (line.equals("") && !currentStack.isEmpty()) { // Empty line indicates end of stack trace flameGraph.addStackTrace(currentStack, 1); currentStack.clear(); inStackTrace = false; } } // Don't forget the last stack trace if (!currentStack.isEmpty()) { flameGraph.addStackTrace(currentStack, 1); } return flameGraph; } private static String extractMethodName(String stackLine) { // Example: "at com.example.Class.method(Class.java:123)" if (stackLine.startsWith("at ")) { String methodPart = stackLine.substring(3); int parenIndex = methodPart.indexOf('('); if (parenIndex > 0) { return methodPart.substring(0, parenIndex); } } return null; } } SVG Flame Graph Generator
1. SVG Frame Representation:
class SVGFrame { private String name; private double x; private double y; private double width; private double height; private String color; private long value; public SVGFrame(String name, double x, double y, double width, double height, String color, long value) { this.name = name; this.x = x; this.y = y; this.width = width; this.height = height; this.color = color; this.value = value; } // Getters public String getName() { return name; } public double getX() { return x; } public double getY() { return y; } public double getWidth() { return width; } public double getHeight() { return height; } public String getColor() { return color; } public long getValue() { return value; } } 2. SVG Generator:
class SVGFlameGraphGenerator { private static final int FRAME_HEIGHT = 20; private static final int CANVAS_WIDTH = 1200; private static final int CANVAS_HEIGHT = 800; private static final int FONT_SIZE = 12; public static String generateSVG(FlameGraph flameGraph) { StringBuilder svg = new StringBuilder(); // SVG header svg.append("<?xml version=\"1.0\" standalone=\"no\"?>\n"); svg.append("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" "); svg.append("\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n"); svg.append(String.format( "<svg version=\"1.1\" width=\"%d\" height=\"%d\" " + "onload=\"init(evt)\" xmlns=\"http://www.w3.org/2000/svg\">\n", CANVAS_WIDTH, CANVAS_HEIGHT )); // Styles svg.append("<defs>\n"); svg.append(" <linearGradient id=\"background\" y1=\"0\" y2=\"1\" x1=\"0\" x2=\"0\">\n"); svg.append(" <stop stop-color=\"#eeeeee\" offset=\"5%\" />\n"); svg.append(" <stop stop-color=\"#eeeeb0\" offset=\"95%\" />\n"); svg.append(" </linearGradient>\n"); svg.append("</defs>\n"); // Background svg.append(String.format( "<rect x=\"0\" y=\"0\" width=\"%d\" height=\"%d\" fill=\"url(#background)\"/>\n", CANVAS_WIDTH, CANVAS_HEIGHT )); // Generate frames List<SVGFrame> frames = layoutFrames(flameGraph); for (SVGFrame frame : frames) { svg.append(generateFrameSVG(frame)); } // JavaScript for interactivity svg.append(generateJavaScript()); svg.append("</svg>"); return svg.toString(); } private static List<SVGFrame> layoutFrames(FlameGraph flameGraph) { List<SVGFrame> frames = new ArrayList<>(); long totalSamples = flameGraph.getTotalSamples(); double scale = (double) CANVAS_WIDTH / totalSamples; layoutNode(frames, flameGraph.getRoot(), 0, 0, scale, 0); return frames; } private static void layoutNode(List<SVGFrame> frames, FrameNode node, double x, int y, double scale, int depth) { if (node == null) return; long nodeValue = node.getValue(); double width = nodeValue * scale; if (width > 1.0 && nodeValue > 0) { // Only render frames with significant width String color = generateColor(node.getName(), depth); SVGFrame frame = new SVGFrame( node.getName(), x, y, width, FRAME_HEIGHT, color, nodeValue ); frames.add(frame); // Layout children double childX = x; for (FrameNode child : node.getChildren().values()) { double childWidth = child.getTotalValue() * scale; if (childWidth > 1.0) { layoutNode(frames, child, childX, y + FRAME_HEIGHT, scale, depth + 1); childX += childWidth; } } } } private static String generateColor(String frameName, int depth) { // Generate colors based on package/method name and depth int hue; if (frameName.contains("java.")) { hue = 240; // Blue for Java core } else if (frameName.contains("com.sun.") || frameName.contains("sun.")) { hue = 200; // Light blue for Sun classes } else if (frameName.contains("org.springframework")) { hue = 300; // Magenta for Spring } else if (frameName.contains("org.apache")) { hue = 30; // Orange for Apache } else if (frameName.contains("io.netty")) { hue = 120; // Green for Netty } else { // Hash the frame name for consistent color hue = Math.abs(frameName.hashCode()) % 360; } // Vary saturation based on depth int saturation = 35 + Math.min(depth * 10, 40); int lightness = 85 - Math.min(depth * 3, 30); return String.format("hsl(%d, %d%%, %d%%)", hue, saturation, lightness); } private static String generateFrameSVG(SVGFrame frame) { StringBuilder frameSvg = new StringBuilder(); // Frame rectangle frameSvg.append(String.format( "<rect x=\"%.2f\" y=\"%.2f\" width=\"%.2f\" height=\"%.2f\" " + "fill=\"%s\" stroke=\"#000000\" stroke-width=\"0.5\" " + "data-name=\"%s\" data-value=\"%d\"/>\n", frame.getX(), frame.getY(), frame.getWidth(), frame.getHeight(), frame.getColor(), escapeXml(frame.getName()), frame.getValue() )); // Frame label (only if there's enough space) if (frame.getWidth() > 50) { frameSvg.append(String.format( "<text x=\"%.2f\" y=\"%.2f\" font-size=\"%d\" " + "font-family=\"Verdana\" fill=\"#000000\">%s</text>\n", frame.getX() + 2, frame.getY() + FONT_SIZE + 2, FONT_SIZE, escapeXml(truncateLabel(frame.getName(), frame.getWidth())) )); } return frameSvg.toString(); } private static String truncateLabel(String name, double width) { int maxChars = (int) (width / (FONT_SIZE * 0.6)); if (name.length() > maxChars && maxChars > 5) { return name.substring(0, maxChars - 3) + "..."; } return name; } private static String escapeXml(String text) { return text.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """) .replace("'", "'"); } private static String generateJavaScript() { return """ <script type="text/ecmascript"> <![CDATA[ function init(evt) { if ( window.svgDocument == null ) { svgDocument = evt.target.ownerDocument; } } function showDetails(evt) { var frame = evt.target; var name = frame.getAttribute('data-name'); var value = frame.getAttribute('data-value'); var tooltip = svgDocument.getElementById('tooltip'); if (tooltip) { tooltip.setAttribute('x', evt.clientX + 10); tooltip.setAttribute('y', evt.clientY + 10); tooltip.textContent = name + ' (' + value + ' samples)'; tooltip.setAttribute('visibility', 'visible'); } } function hideDetails(evt) { var tooltip = svgDocument.getElementById('tooltip'); if (tooltip) { tooltip.setAttribute('visibility', 'hidden'); } } // Add event listeners to all frames var frames = document.querySelectorAll('rect[data-name]'); for (var i = 0; i < frames.length; i++) { frames[i].addEventListener('mouseover', showDetails); frames[i].addEventListener('mouseout', hideDetails); frames[i].style.cursor = 'pointer'; } ]]> </script> <!-- Tooltip --> <rect id="tooltip" x="0" y="0" width="300" height="30" fill="white" stroke="black" stroke-width="1" visibility="hidden"/> <text id="tooltip-text" x="5" y="20" font-size="12" font-family="Verdana" visibility="hidden"/> """; } } Live Profiling Integration
1. Simple Sampling Profiler:
class SamplingProfiler { private final Map<Thread, StackTraceElement[]> previousTraces = new HashMap<>(); private final FlameGraph flameGraph = new FlameGraph(); private volatile boolean running = false; private Thread profilingThread; public void startProfiling(long samplingIntervalMs) { running = true; profilingThread = new Thread(() -> { while (running) { try { Thread.sleep(samplingIntervalMs); captureStackTraces(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } }); profilingThread.setDaemon(true); profilingThread.start(); } public void stopProfiling() { running = false; if (profilingThread != null) { profilingThread.interrupt(); } } private void captureStackTraces() { Map<Thread, StackTraceElement[]> allStackTraces = Thread.getAllStackTraces(); for (Map.Entry<Thread, StackTraceElement[]> entry : allStackTraces.entrySet()) { Thread thread = entry.getKey(); StackTraceElement[] stackTrace = entry.getValue(); if (shouldProfileThread(thread) && stackTrace.length > 0) { List<String> frames = Arrays.stream(stackTrace) .map(frame -> frame.getClassName() + "." + frame.getMethodName()) .collect(Collectors.toList()); flameGraph.addStackTrace(frames, 1); } } } private boolean shouldProfileThread(Thread thread) { return thread != Thread.currentThread() && !thread.isDaemon() && thread.getState() == Thread.State.RUNNABLE; } public FlameGraph getFlameGraph() { return flameGraph; } } Usage Examples
1. From Collapsed Stack File:
public class FlameGraphDemo { public static void main(String[] args) throws IOException { // Parse collapsed stack format FlameGraph flameGraph = CollapsedStackParser.parseFromFile("profile.txt"); // Generate SVG String svg = SVGFlameGraphGenerator.generateSVG(flameGraph); // Save to file Files.write(Paths.get("flamegraph.svg"), svg.getBytes()); System.out.println("Flame graph generated: flamegraph.svg"); } } 2. Live Profiling:
public class LiveProfilingDemo { public static void main(String[] args) throws InterruptedException { SamplingProfiler profiler = new SamplingProfiler(); System.out.println("Starting profiling..."); profiler.startProfiling(10); // Sample every 10ms // Run your application code here performWork(); Thread.sleep(5000); // Profile for 5 seconds profiler.stopProfiling(); System.out.println("Profiling stopped"); // Generate flame graph FlameGraph flameGraph = profiler.getFlameGraph(); String svg = SVGFlameGraphGenerator.generateSVG(flameGraph); Files.write(Paths.get("live_profile.svg"), svg.getBytes()); } private static void performWork() { // Simulate application work for (int i = 0; i < 1000; i++) { processData(i); } } private static void processData(int value) { // Simulate processing try { Thread.sleep(1); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } Advanced Features
1. Differential Flame Graphs:
class DifferentialFlameGraph { public static String generateDiffSVG(FlameGraph before, FlameGraph after) { FlameGraph diff = computeDifference(before, after); return SVGFlameGraphGenerator.generateDiffSVG(diff); } private static FlameGraph computeDifference(FlameGraph before, FlameGraph after) { // Implementation for computing differences between two profiles FlameGraph diff = new FlameGraph(); // Compare frames and highlight differences return diff; } } 2. Frame Filtering:
class FrameFilter { public static FlameGraph filterFrames(FlameGraph original, Predicate<String> frameFilter) { FlameGraph filtered = new FlameGraph(); // Implementation for filtering frames based on package, method, etc. return filtered; } } Integration with Existing Tools
1. Async Profiler Integration:
class AsyncProfilerIntegration { public static FlameGraph parseAsyncProfilerOutput(String filePath) throws IOException { // Parse output from async-profiler (collapsed stacks) List<String> lines = Files.readAllLines(Paths.get(filePath)); return CollapsedStackParser.parseCollapsedStacks(lines); } } 2. JFR (Java Flight Recorder) Integration:
class JFRParser { public static FlameGraph parseJFRFile(String jfrFile) throws IOException { // Parse JFR file and extract execution samples // This would use JMC libraries or custom parsing return new FlameGraph(); } } Best Practices
- Sample Appropriately: Balance between overhead and accuracy in sampling profilers
- Filter Noise: Exclude system threads and uninteresting frames
- Handle Large Profiles: Use efficient data structures for large profile datasets
- Provide Interactivity: Zoom, search, and tooltips in SVG output
- Support Multiple Formats: Collapsed stacks, JFR, async-profiler, etc.
- Optimize Rendering: Use level-of-detail rendering for large flame graphs
Conclusion
Building a Flame Graph Generator in Java involves:
- Data Collection: Parsing stack traces from various sources
- Hierarchical Modeling: Building a tree structure of frame nodes
- Layout Algorithm: Calculating positions and sizes based on sample counts
- SVG Generation: Creating interactive visualizations with proper styling
- Advanced Features: Differential views, filtering, and integration with profiling tools
This implementation provides a solid foundation that can be extended with features like:
- Real-time profiling visualization
- Integration with application performance monitoring (APM) tools
- Support for other output formats (PNG, PDF)
- Advanced filtering and analysis capabilities
- Cloud-native profiling for distributed systems
By understanding and implementing these concepts, you can create powerful performance analysis tools tailored to your specific Java application needs.