flame-graph-analyzer
Reads CPU flame-graph output from py-spy (Python), async-profiler (JVM), Go pprof, or Node.js `perf_hooks` / clinic.js: identifies the hot path (top sample-time frames), classifies the bottleneck (CPU-bound vs lock contention vs allocator pressure), and proposes the next investigation step. Use when a perf regression is bisected to a commit but the hot path inside it is unclear; for tail-latency percentiles use latency-percentile-analyzer, for GC pauses specifically use jvm-gc-tuning, and for a slow SQL hot path use db-query-plan-analyzer.
Install with skills.sh (any agent)
npx skills add testland/qa --skill flame-graph-analyzerflame-graph-analyzer
Overview
Canonical flame-graph reference: brendan-gregg-flame (opens in new window). The widest leaf (bottom of the stack) is the hot path.
Each runtime produces flame graphs from its own profiler - py-spy (Python), async-profiler (JVM), Go's built-in pprof, clinic.js flame (Node.js), and perf / eBPF (native C/C++/Rust). This skill is language-agnostic: it consumes the profiler output (SVG, JSON, or folded-stacks .txt) and surfaces a hypothesis the engineer can act on. Per-runtime capture commands live in references/capturing-and-interpreting-flame-graphs.md.
When to use
How to use
Reading the hot path
Read the folded stacks (or extract them from the SVG / JSON), sort by sample count, and identify the top leaves - the bottom-of-stack frames that are the actual working code, not framework wrappers:
# top 5 leaves by sample count
sort -k2 -n -r folded.txt | head -5Each hot leaf's sample-time signature points to one category:
| Category | Signature in the flame graph |
|---|---|
| CPU-bound (hot algo) | A wide leaf in user code (a regex, a JSON serializer, a hash function). |
| Allocator pressure | Wide GC frames (gc::scavenge, Java GC, gc.collect). |
| Lock contention | Wide synchronization frames (pthread_mutex_lock, Object.wait, parking). |
| I/O wait misclassified | On-CPU profilers don't show I/O blocks; switch to wall-clock profiling. |
| Reflection / dynamic dispatch | Wide reflection.invoke, method_missing, getattr chains. |
| Logging overhead | Wide log.format, Logger.debug, serialization for log lines. |
Worked example
A Node.js endpoint regressed after a bisected commit. Capture under load, then read the hot path end to end.
Capture (Node.js clinic.js flame; other runtimes in the reference):
npx clinic flame -- node app.js
# drive load against the process, then stop it; clinic writes flame.html + folded stacksSort the folded stacks and read the top leaves:
sort -k2 -n -r folded.txt | head -5main;handleRequest;serializeJson;JSON.stringify 4521
main;handleRequest;dbQuery;Array.from 2103
main;handleRequest;dbQuery;parseRows 1832
main;authCheck;jwt.verify;crypto.createHash 904
main;handleRequest;serializeJson;Buffer.from 312Emit the analysis:
## Flame graph analysis - `flame.html`
**Runtime:** node
**Profile duration:** 30s under load
| Rank | Sample share | Stack (leaf) | Category |
|-----:|-------------:|---------------------------------------|----------|
| 1 | 38% | `JSON.stringify` (in `serializeJson`) | CPU-bound hot algo |
| 2 | 17% | `Array.from` (in `dbQuery`) | Allocator pressure |
| 3 | 15% | `parseRows` (in `dbQuery`) | CPU-bound hot algo |
| 4 | 8% | `jwt.verify` (in `authCheck`) | CPU-bound hot algo (crypto) |
| 5 | 3% | `Buffer.from` (in `serializeJson`) | Allocator pressure |
### Hypothesis
The serialization path is load-bearing: rank 1 (`JSON.stringify`, 38%) plus
rank 5 (`Buffer.from`, 3%) account for ~41% of sampled time combined.
### Recommended next step
1. Switch to a streaming JSON serializer (`fast-json-stringify` in Node,
`orjson` in Python, Jackson's `JsonGenerator` in the JVM) to eliminate the
intermediate string allocation.
2. Re-profile; expect rank 1 to drop below 10%.
3. Hand off to `perf-budget-gate` to confirm the regression delta closes.More interpreted cases (GC pressure, lock contention, reflection overhead) and the category-to-fix remediation catalog are in references/capturing-and-interpreting-flame-graphs.md.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Reading the SVG visually only, no quantitative data | Easy to mis-judge widths; biases toward dramatic-looking deep stacks. | Always work from folded stacks; sort by sample count. |
| Profiling under load that is too low | One request per second can't expose contention or allocator pressure. | Profile under realistic load - pair with k6-load-testing. |
| Optimizing rank 5 first because rank 1 looks "structural" | Premature optimization; misses the dominant cost. | Always start with rank 1; only descend if rank 1 is genuinely framework-bound (e.g. event_loop). |
| On-CPU profiler for an I/O-bound workload | I/O wait doesn't appear; the flame graph shows what's running, not what's waiting. | Use wall-clock / off-CPU profiling for I/O-bound workloads. |
| Single 30-second capture under highly variable load | The sample is unrepresentative. | Capture multiple samples across the load-test duration; merge. |
Limitations
References
Capturing and interpreting flame graphs per runtime
View source (opens in new window)Capturing and interpreting flame graphs per runtime
Deep reference for flame-graph-analyzer SKILL.md. Consult for the full per-runtime capture wiring, the category-to-fix remediation catalog, and worked interpretation cases beyond the single inline example.
Per-runtime capture wiring
Each runtime produces flame-graph data from its own profiler. Export folded stacks (.txt, one line per unique stack + sample count) wherever the profiler supports it - that is the machine-readable form the analyzer reads.
Python - py-spy
py-spy record -o flame.svg -d 30 --pid <pid>
# or run-and-record
py-spy record -o flame.svg -d 30 -- python app.py
# raw folded stacks for analysis
py-spy record -f raw -o folded.txt -d 30 --pid <pid>JVM - async-profiler
async-profiler (opens in new window):
java -agentpath:/path/to/libasyncProfiler.so=start,event=cpu,duration=30s,file=flame.html ...
# JFR output
java -agentpath:.../libasyncProfiler.so=start,event=cpu,file=profile.jfr ...Output: HTML flame graph or JFR (Java Flight Recorder) format.
Go - pprof
Go's built-in pprof (opens in new window):
# in-process: import _ "net/http/pprof"; http.ListenAndServe(":6060", nil)
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30Open the served URL, then VIEW -> Flame Graph.
Node.js - clinic.js flame
Clinic.js (opens in new window):
npx clinic flame -- node app.js
# writes flame.html when the process exitsNative (C/C++/Rust) - perf
perf record piped through Brendan Gregg's stackcollapse-perf.pl and flamegraph.pl produces folded stacks and the SVG (bg (opens in new window)).
Folded stacks - the universal format
All major profilers can emit folded stacks: one line per unique stack with its sample count.
main;handleRequest;serializeJson;Buffer.from 4521
main;handleRequest;dbQuery;parseRows 1832
main;handleRequest;authCheck;jwtVerify 904flamegraph.pl consumes this format directly. Folded stacks are the canonical machine-readable input for this skill's analysis.
Category-to-fix remediation catalog
Once a hot leaf is classified, map the category to a typical fix:
| Category | Typical fix |
|---|---|
| CPU-bound hot algo | Cache the result; switch to a faster algorithm; move it out of the hot path. |
| Allocator pressure | Reuse buffers / pools; switch to streaming serialization; escape-analysis fixes for the JVM. |
| Lock contention | Reduce critical-section scope; move to lock-free data structures; per-shard locking. |
| Reflection overhead | Replace dynamic dispatch with cached call-sites or codegen. |
| Logging overhead | Lazy log-message construction; level-check before format. |
Worked interpretation cases
GC pressure
| Rank | Share | Stack (leaf) | Category |
|-----:|------:|------------------|----------|
| 1 | 32% | `gc.collect` | Allocator pressure |
| 2 | 18% | `dict.update` | (callsite) |
| 3 | 14% | `parse_response` | CPU-bound hot algo |GC at 32% of samples means allocator pressure dominates. The fix is not making any one function faster - it is reducing the rate of allocations from dict.update and parse_response (object pooling, streaming parsing).
Lock contention
| Rank | Share | Stack (leaf) | Category |
|-----:|------:|-----------------------|----------|
| 1 | 41% | `pthread_mutex_lock` | Lock contention |
| 2 | 12% | `cache.get` | (callsite) |41% of samples in lock acquisition. The fix is not "make cache.get faster" - it is "reduce the contention" (per-shard locks, lock-free structures, or a lock-free cache such as Caffeine for the JVM).
Reflection / dynamic-dispatch overhead
| Rank | Share | Stack (leaf) | Category |
|-----:|------:|-----------------------------|----------|
| 1 | 28% | `Method.invoke` / `getattr` | Reflection overhead |A common surprise - an ORM's reflective field access dominates the profile of an otherwise simple endpoint. The fix is the ORM-equivalent of "compile the mapping": cached method handles in the JVM, __slots__ in Python, generated SQL in Go.
Related skills
db-query-plan-analyzer
Reads `EXPLAIN` / `EXPLAIN ANALYZE` output from PostgreSQL, MySQL, or SQLite - identifies the dominant cost (sequential scan, nested loop, sort spill, missing index, type-cast preventing index use), proposes the specific index or query rewrite to fix it, and emits the candidate `CREATE INDEX` statement. Use when load testing or production telemetry shows the database as the bottleneck and the team needs targeted query-level remediation.
gatling-load-testing
Authors Gatling simulations in Java / Kotlin / Scala (or JS / TS) using the Simulation class plus http() / scenario() / exec() DSL builders, ramps virtual users via injectOpen (arrival rate) or injectClosed (concurrent count), runs via Maven / Gradle / sbt with the Gatling plugin, and gates CI on assertions defined in setUp(). Use when the project is on the JVM and the team prefers code-first load tests over JMeter's XML or k6's JavaScript-only authoring.
jmeter-load-testing
Authors Apache JMeter `.jmx` test plans (Thread Groups + HTTP samplers + assertions + listeners) in the JMeter GUI, runs them headlessly via `jmeter -n -t plan.jmx -l results.jtl`, generates an HTML dashboard with `-e -o`, and gates CI on JTL parsing. Use when the project has an existing JMeter investment, needs JVM-native load tooling, or works in domains with strong JMeter community support (banking, telecom, enterprise).
jvm-gc-tuning
Diagnoses JVM garbage-collection behaviour under load: reads and interprets unified GC logs (-Xlog:gc*), selects the right collector (G1 vs ZGC vs Parallel vs Serial), tunes heap sizing and pause-time targets, quantifies allocation rate, and traces the GC-pause-to-latency-tail link using GCViewer and Java Flight Recorder (JFR). Use when a load test reveals p99/p999 latency spikes that correlate with GC activity, or when heap sizing and collector selection need justification before a performance baseline is locked.
k6-load-testing
Authors k6 JavaScript load-test scripts (VU loops + checks + sleeps), configures the `options` block with `stages` (ramp-up patterns) and `thresholds` (p(95) latency, error rate), runs via `k6 run script.js` or `--vus / --duration` ad-hoc flags, and uses thresholds as the CI pass/fail signal. Use when the project ships HTTP / WebSocket / gRPC load tests and the team wants developer-friendly JavaScript authoring.
latency-percentile-analyzer
Interprets latency distributions from k6 load tests beyond the p95/p99 gate: reads percentile summaries and JSON exports to identify tail shape, computes the tail ratio (p99/p50) as a distribution-spread signal, detects bimodal distributions, explains coordinated omission and why naive p99 values are optimistic under sustained load, and distinguishes request-rate from concurrency models. Use when a k6 threshold passes but the system still feels slow, when p99 is suspiciously low during ramp-up, or when the team needs to explain why tail latency is high rather than just observing that it is.
lighthouse-budget-author
Drafts a `lighthouserc.js` (or `budget.json`) at design time - picks Web Vitals thresholds (LCP / INP / CLS) per route based on traffic class (cached / dynamic / API-heavy / form-heavy) and the team's NFRs, plus resource-size budgets (JS / CSS / images / total bytes). Emits the config file ready for the lighthouse-perf runner. Use when starting Lighthouse coverage on a project that has no budgets yet, or when the existing budgets need a redesign.
lighthouse-perf
Configures Lighthouse CI (`@lhci/cli`) to audit Web Vitals (LCP, INP, CLS) on every PR, asserts against canonical thresholds (LCP ≤2.5s, INP ≤200ms, CLS ≤0.1 at the 75th percentile), uploads Lighthouse reports as build artifacts, and posts deltas as PR comments. Use when the project ships a web frontend and the team needs continuous Web Vitals monitoring tied to PR gating.
load-testing-overview
Teaches load and performance testing from zero: how to choose between k6, JMeter, Gatling, Locust, and Artillery based on observable project facts (team language, tests-as-code vs GUI authoring, protocols beyond HTTP, CI gating needs); the six load profiles (smoke, average-load, stress, spike, soak, breakpoint) and the question each one answers; the difference between open workload models that hold arrival rate constant and closed models that hold concurrent users constant; why percentiles rather than averages are the unit of measurement; and how to turn a run into a pass/fail CI gate, with a first runnable k6 script. Use when a service needs performance coverage and the tool, the load profile, or the pass/fail threshold has not been decided yet.
locust-load-testing
Authors Locust load tests as Python classes - HttpUser with @task-decorated methods plus on_start hooks and between() wait_time - runs via `locust -f locustfile.py` headless mode (or distributed via `--master` / `--worker`), and exports CSV / JUnit reports for CI gating. Use when the project's primary stack is Python and the team wants load tests in the same language as the application.
perf-budget-gate
Builds a unified release-readiness gate that aggregates verdicts from any combination of k6 / JMeter / Gatling / Locust load runners and Lighthouse CI Web Vitals, applies severity-aware pass/fail thresholds, and emits a single go / no-go decision with per-metric deltas vs the main-branch baseline. Posts the delta as a PR comment when the team has the integration set up. Use when authoring a CI step that gates a deployment on cross-runner perf compatibility.
slo-load-test-plan
Turns a service's SLOs and endpoint traffic mix into a named scenario matrix: one scenario per SLO boundary condition, a load profile (smoke, average-load, stress, soak, spike, breakpoint) per scenario, an open or closed workload injection model, a threshold expression derived from the SLO the scenario guards, and an error-budget calculation that sets the soak run's failure allowance. Stays runner-agnostic and fixes the pass/fail line before any tool is configured. Use when an SLO document and an endpoint list both exist but nobody has decided which load runs to make, what shape of load each carries, or what number would count as a failure.