Flame Graphs

Visualize where your application spends CPU time with interactive flame graphs.

What Is a Flame Graph?#

A flame graph is a visualization of profiled software, allowing you to see which code paths consume the most CPU time. Originally invented by Brendan Gregg, flame graphs have become the standard way to understand application performance at the function level.

In JustAnalytics, flame graphs are generated from CPU profiles collected by the SDK's continuous profiler. They show the call stack hierarchy and the relative time spent in each function.

Reading a Flame Graph#

Axes#

  • X-axis -- represents the proportion of total time (or sample count). Wider bars mean more time spent. The x-axis does not represent the passage of time -- it's sorted alphabetically for consistent layout.
  • Y-axis -- represents stack depth. The bottom of the graph is the entry point (e.g., main() or the HTTP handler). Each layer above is a function called by the one below it.

Anatomy of a Bar#

Each horizontal bar (called a "frame") represents a single function:

┌──────────────────────────────────────────────────────┐
│                    processOrder                       │  ← function name
│               src/orders.ts:42                        │  ← file:line
│           self: 12ms  total: 245ms                    │  ← timing
└──────────────────────────────────────────────────────┘
     ↑ width = proportion of total profile time
  • Self time -- time spent executing code in this function directly (not in functions it calls)
  • Total time -- time spent in this function and all functions it calls (its subtree)
  • Width -- proportional to total time. A function that takes 50% of all samples will be half the width of the graph.

Color Coding#

JustAnalytics uses semantic colors to help you identify different types of code:

| Color | Meaning | |-------|---------| | Blue | Your application code | | Green | Node.js standard library | | Orange | Third-party npm packages | | Red | Functions with high self time (hot spots) | | Gray | V8 internal / native code |

Reading Top to Bottom#

Start from the bottom and read upward:

│                    handleRequest                      │  ← Entry point
│   ┌──────────────────────┬────────────────────┐      │
│   │    processOrder      │   sendResponse     │      │
│   │  ┌────────┬────────┐ │                    │      │
│   │  │validate│ charge │ │                    │      │
│   │  │Order   │Payment │ │                    │      │
│   │  │        │┌──────┐│ │                    │      │
│   │  │        ││stripe││ │                    │      │
│   │  │        │└──────┘│ │                    │      │

In this example:

  • handleRequest calls processOrder and sendResponse
  • processOrder calls validateOrder and chargePayment
  • chargePayment calls into the stripe library
  • The widths tell you that processOrder takes most of the time, and within it, chargePayment (specifically the stripe call) dominates

Identifying Hot Spots#

Look for:

  1. Wide frames at the top -- functions with high self time (doing significant work themselves)
  2. Wide towers -- deep call stacks where time is concentrated in a single path
  3. Plateaus -- a function that is wide but has no children (all time is self time)

A plateau at the top of the graph is the clearest signal: that function is consuming significant CPU and is a prime optimization target.

Aggregated Flame Graphs#

Individual profiles show a 10-second snapshot. Aggregated flame graphs merge profiles across a time range to show a holistic view.

How Aggregation Works#

  1. Select a time range (e.g., "last 1 hour" or "last 24 hours")
  2. All profiles from that range are collected
  3. Matching call stacks are merged -- identical stack frames are combined, and their sample counts are summed
  4. The result is a single flame graph representing the aggregate behavior

When to Use Aggregated Views#

  • Baseline understanding -- "Where does my service spend CPU on a typical day?"
  • Capacity planning -- "Which functions will become bottlenecks as traffic grows?"
  • Optimization prioritization -- "Which function, if optimized, would save the most CPU across the fleet?"

Filtering Aggregated Graphs#

Narrow the aggregated view:

  • By service -- see profiles from a specific service only
  • By environment -- production vs. staging
  • By endpoint -- filter to profiles captured during specific HTTP routes
  • By instance -- compare behavior across different instances/pods

Differential Flame Graphs#

Differential flame graphs compare two sets of profiles and highlight the differences. This is invaluable for understanding the performance impact of a deploy.

How It Works#

  1. Select a baseline time range (e.g., "yesterday 2-4 PM")
  2. Select a comparison time range (e.g., "today 2-4 PM")
  3. JustAnalytics merges each set into an aggregated flame graph
  4. The differential view shows frames colored by their change:

| Color | Meaning | |-------|---------| | Red | Function takes more time in the comparison (regression) | | Blue | Function takes less time in the comparison (improvement) | | Gray | No significant change |

Reading a Differential Graph#

┌──────────────────────────────────────────────────────┐
│               processOrder (gray - no change)         │
│   ┌──────────────────────┬────────────────────┐      │
│   │ validateOrder (blue) │ chargePayment (red) │      │
│   │   -15% faster        │   +40% slower       │      │
│   │                      │ ┌──────────────────┐│      │
│   │                      │ │ newFraudCheck     ││      │
│   │                      │ │ (red, new frame)  ││      │
│   │                      │ └──────────────────┘│      │

In this example:

  • validateOrder got faster (optimization landed)
  • chargePayment got slower due to a new newFraudCheck function
  • processOrder overall is roughly the same (the speedup and slowdown balanced out)

Common Differential Use Cases#

  • Deploy comparison -- compare the hour before and after a deploy
  • Release comparison -- compare two releases by their time ranges
  • Incident investigation -- compare the incident window with a healthy baseline
  • A/B testing -- compare performance of two code paths

Interactive Features#

Click to Zoom#

Click any frame to zoom in. The selected frame expands to full width, and only its subtree is shown. This lets you focus on a specific part of the call stack without visual noise from unrelated code.

Click the Reset Zoom button or click the root frame to zoom back out.

Press Ctrl+F (or Cmd+F) to search for a function name, file path, or package name. Matching frames are highlighted; non-matching frames are dimmed.

Search: "stripe"
→ All frames containing "stripe" highlighted in yellow
→ Shows what percentage of total time is spent in stripe-related code

Hover Details#

Hover over any frame to see:

  • Function name and full file path
  • Line number
  • Self time (ms and %)
  • Total time (ms and %)
  • Sample count
  • Package name (for third-party code)

Invert#

Click Invert to flip the flame graph upside down (sometimes called an "icicle graph"). This puts the entry point at the top and leaf functions at the bottom. Some people find this orientation more intuitive.

Sandwich View#

The sandwich view focuses on a single function and shows:

  • Callers (above) -- all functions that call the selected function
  • Selected function (middle) -- the function you clicked
  • Callees (below) -- all functions called by the selected function

This is useful for understanding a function's context: who calls it and what it calls.

Common Patterns and What They Mean#

Pattern: Wide Plateau at the Top#

│          ████████████████████████████████████          │
│          │          JSON.parse()              │         │

Meaning: A single function is consuming a large share of CPU. This is your highest-impact optimization target.

Action: Look at what data is being parsed. Can you reduce the payload size? Cache the parsed result? Use a streaming parser?

Pattern: Tall Narrow Tower#

│ ██ │
│ ██ │
│ ██ │
│ ██ │
│ ██ │
│ ██ │
│ ██ │
│ ██ │

Meaning: A deep call stack with each function calling only one child. This is typical of recursive algorithms or deeply nested middleware chains.

Action: Check for unnecessary recursion. If it's middleware, see if some layers can be skipped for certain routes.

Pattern: Many Thin Towers#

│ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ │

Meaning: CPU time is spread across many small operations. No single function dominates.

Action: This is often healthy -- it means your code is well-distributed. Look at the aggregated view over a longer time range to see if any function consistently appears.

Pattern: Regex Execution Dominating#

│              RegExp.exec()                           │
│         ┌────────────────────────┐                   │
│         │  validateEmailFormat    │                   │

Meaning: Regular expression execution is consuming significant CPU. This can indicate a catastrophic backtracking regex (ReDoS).

Action: Review the regex pattern. Use a regex analyzer to check for exponential backtracking. Consider using a simple string operation instead.

Pattern: GC (Garbage Collection) Spikes#

│        GC (Scavenge)        │        GC (Mark-Sweep)          │

Meaning: A significant portion of CPU time is spent in garbage collection. This indicates high memory allocation pressure.

Action: Look at the functions below the GC frames -- they're the ones creating the objects that GC has to clean up. Reduce allocations by reusing objects, using object pools, or reducing intermediate data structures.

Pattern: Crypto Operations#

│               crypto.pbkdf2                          │
│         ┌────────────────────────┐                   │
│         │     hashPassword        │                   │

Meaning: Cryptographic operations are inherently CPU-intensive. This is expected for auth-heavy workloads.

Action: Ensure crypto operations are running on worker threads (not blocking the event loop). Consider caching authenticated sessions to reduce hash operations per request.

Exporting Profiles#

Download as JSON#

Click Export > JSON to download the raw profile data. This produces a V8 CPU profile format that can be loaded into Chrome DevTools or other analysis tools.

Download as SVG#

Click Export > SVG to download the flame graph as a vector image. Useful for sharing in postmortems, documentation, or presentations.

Click Share to generate a permalink to the current flame graph view, including your zoom level and search filters. Anyone with access to the project can view the shared link.

Tips for Effective Profiling#

Profile the Right Thing#

  • Profile in production or a production-like environment -- development environments have different performance characteristics
  • Profile under realistic load -- an idle service looks very different from one handling 1000 req/s
  • Use aggregated views for general optimization -- use single profiles for investigating specific incidents

Compare, Don't Guess#

  • Always compare against a baseline before and after optimization
  • Use differential flame graphs to verify that your change had the expected effect
  • A function might look expensive in absolute terms but be perfectly fine relative to the work it does

Look at Self Time, Not Just Total Time#

A function with high total time but low self time isn't the bottleneck -- its children are. Drill down into the callees to find the actual hot spot.

Don't Over-Optimize#

If a function takes 0.1% of total CPU time, optimizing it won't meaningfully improve performance. Focus on the widest frames -- that's where the impact is.