Building a Flame Graph Generator in Java: Visualizing Performance Profiling Data

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:

  1. Stack Trace Parser: Processes profiling data
  2. Flame Graph Model: Builds the hierarchical structure
  3. SVG Generator: Creates the visual representation
  4. 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("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;");
}
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

  1. Sample Appropriately: Balance between overhead and accuracy in sampling profilers
  2. Filter Noise: Exclude system threads and uninteresting frames
  3. Handle Large Profiles: Use efficient data structures for large profile datasets
  4. Provide Interactivity: Zoom, search, and tooltips in SVG output
  5. Support Multiple Formats: Collapsed stacks, JFR, async-profiler, etc.
  6. Optimize Rendering: Use level-of-detail rendering for large flame graphs

Conclusion

Building a Flame Graph Generator in Java involves:

  1. Data Collection: Parsing stack traces from various sources
  2. Hierarchical Modeling: Building a tree structure of frame nodes
  3. Layout Algorithm: Calculating positions and sizes based on sample counts
  4. SVG Generation: Creating interactive visualizations with proper styling
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper