Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Parse and compare async-profiler CPU flamegraphs from Camunda/Zeebe brokers — benchmark nodes or production. Use when investigating a CPU regression or outlier with .html flamegraphs (e.g. from dashboard.benchmark.camunda.cloud daily runs, or pulled from a live cluster/customer incident) — attribute a node's CPU to Zeebe subsystems and diff a suspect run against a healthy baseline.
.claude/skills/camunda-zeebe-flamegraph-diff/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | -2% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 35% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 83% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 64% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 53% | 0% |
Tooling to read async-profiler HTML flamegraphs from Camunda 8 / Zeebe brokers and answer: "where is this node burning CPU, and what changed vs a healthy run?" Works on any pair of comparable flamegraphs — daily load-test captures (per-node, per-protocol) from dashboard.benchmark.camunda.cloud, or ad-hoc captures pulled from a production/customer cluster during an incident.
(backpressure, dropped requests, low throughput, high CPU) and you have per-node .html flamegraphs.
sizing change, or just load — on a benchmark run or in production.
If you don't have a profile yet: benchmark nodes emit one automatically per daily run; on production you need async-profiler attached to the JVM (or an equivalent continuous-profiling agent) and must capture one yourself — this skill only covers reading and diffing .html output, not attaching/capturing.
Pair with Grafana (prometheus datasource) for the metrics half of the story — the flamegraph tells you where CPU goes; metrics tell you whether it matters (see step 4). For benchmark runs, also pair with the load-test-ops skill (triggering/monitoring runs).
TWO flamegraphs per node — one while the gateway serves gRPC, one for REST. Their request-handling stacks are completely different (REST = Tomcat/coyote + Spring Security filter chain + BearerTokenAuthenticationFilter; gRPC = Netty event loop, no Tomcat). Diffing across protocols produces garbage. Filenames do NOT say which is which — detect it (step 1).
different totals. Only compare percentages (all scripts output %).
.so files carry per-run random ids($$Lambda.0x…7b91000.run, librocksdbjni<random>.so). Raw they look like huge diffs for identical code. fg_diff.py normalizes these; grep over the raw HTML will also miss compressed names — always parse, don't grep.
All scripts live in scripts/ and share fg_common.py. Run them from that dir (or cd there) so the import resolves.
bashcd scripts python3 fg_top.py <file.html> 40 | grep -iE "coyote|tomcat|BearerToken|SingleThreadIoEventLoop|FrameworkServlet"
Tomcat/coyote/FrameworkServlet/BearerToken present → REST. Only Netty (SingleThreadIoEventLoop) with no Tomcat → gRPC. Pick matching protocols on both sides of a diff.
bashpython3 fg_subsystems.py <file.html>
Leaf-based split across Zeebe subsystems (exporter, replay, processing, rocksdb-state, journal-flush, raft-netty, grpc, gc). ~70-75% coverage is normal (native/syscall scatter → other). See references/zeebe-contributors.md for what each subsystem is and which frames feed it.
bashpython3 fg_diff.py <baseline.html> <candidate.html> [min_delta_pct]
Inclusive-% diff, volatile ids normalized. "Grew in candidate" = where the regressed run spends more; "Shrank" = where the healthy run spent more. Ignore generic roots that move together (thread_native_entry, start_thread, Thread::call_run, Executors$RunnableAdapter.call, the renamed .so) — they are attribution shuffles, not signal. Focus on named Camunda/RocksDB/journal frames.
A flamegraph shows CPU distribution, not efficiency. Two runs can have an almost identical profile shape yet very different throughput. Pull the metrics half from Grafana (prometheus datasource). Namespace/datasource depends on where the profile came from: daily benchmarks live under c8-medic-daily-<date>-<hash>-test; a production or customer cluster will be on a different namespace/datasource — confirm you can actually reach it (customer envs are often not scrapeable from the same Grafana) before assuming the metric half is available at all.
Metric cheat-sheet (all rate/histogram over the steady window):
| question | metric | |---|---| | CPU per pod / saturated? / outlier? | container_cpu_usage_seconds_total | | backpressure / shed load | zeebe_dropped_request_count_total, zeebe_backpressure_* | | processing throughput | zeebe_stream_processor_records_total{action="processed"} | | bytes persisted / append rate | atomix_journal_append_data_rate_total, atomix_journal_append_rate_total | | flush count + latency | atomix_journal_flush_time_seconds_{count,sum,bucket} | | per-record cost by type | zeebe_stream_processor_processing_duration_seconds_bucket (group by valueType,intent) | | state size (rule out data growth) | zeebe_rocksdb_live_estimate_live_data_size, _num_keys, zeebe_rocksdb_sst_total_sst_files_size | | GC | jvm_gc_pause_seconds_* (STW) vs the gc flamegraph bucket (concurrent, off-pause) |
The key derived signal is work-per-core. Compute CPU/record and CPU/MB-persisted on both runs. Interpreting the combination:
up as lower throughput at flat CPU, NOT higher CPU. Don't expect the CPU line to move — divide it by throughput.
regression (more CPU per unit work), not more load.
queueing symptom, not a per-record slowdown; and the extra CPU lives outside processCommand (replay/commit/flush/state access, which that metric excludes). Break the histogram down by valueType,intent to see if one record type regressed vs a uniform shift.
Before blaming a commit, separate "code got slower" from "code runs more often" from "config/infra changed":
git log -1 --format=%ad -- <path/to/hot/file> for the top grown frames. If thehot file hasn't changed in the regression window, the code didn't get slower — it's being called more per record, or a config/flag/sizing changed. Chase the caller or the config, not the frame.
state-access CPU = more ops per record, not more data.
binary-search across days (is the run between healthy and broken already broken?) to shrink the commit range before diffing 100s of commits. For production, narrow by deploy history instead — which version/config was live at each past-good vs first-bad capture.
-e cpu)These bite every flamegraph read, not just regressions:
(blocked on disk, lock, socket, park). So a slower disk / slower ES backend does not raise CPU samples — it shows as lower throughput with threads parked. Never infer "X got slower" from more CPU in X; infer "X did more on-CPU work".
perf_events + perf_event_paranoid<=1 + kernel symbols), kernel time (e.g. msync/fsync dirty-page scan, socket writes) is attributed to the glibc syscall trampoline (__syscall_cancel_arch) or the JNI leaf. You then can't split user vs kernel from that file — re-profile with kernel stacks, or add a wall-clock/off-CPU profile, to see the real split.
gc bucket (G1 concurrent marking) burns CPU on GCthreads without showing as STW pause; cross-check jvm_gc_pause_seconds.
| Diff signature | Likely direction | |---|---| | rocksdb-state / TransactionalColumnFamily up, state size flat | more state ops per record (caller change / config), not bigger data | | journal-flush / msync up, flush-rate flat or down, p99 flat | heavier work per flush (larger scanned region / segment sizing), not disk stalls | | exporter-es-client up, ES health degraded | exporter blocked/retrying on a slow backend | | gc up, allocation metrics up | allocation-pressure regression | | whole profile shape ~unchanged, throughput down at flat CPU | efficiency regression — find it by ratio, not by eye | | one valueType,intent tail explodes, others flat | a specific processor/path, not a global slowdown |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 4,132 | 4,038 | -2% | 1 | 1 | 0% | 250 | 2,675 | +970% | 0 | 0 | — |
case-02 | fail→fail | 25,848 | 7,316 | -72% | 1 | 1 | 0% | 4,650 | 2,662 | -43% | 0 | 0 | — |
case-03 | fail→pass | 25,943 | 11,989 | -54% | 1 | 1 | 0% | 4,764 | 4,653 | -2% | 0 | 0 | — |
case-04 | fail→fail | 12,896 | 9,561 | -26% | 1 | 1 | 0% | 2,544 | 4,228 | +66% | 0 | 0 | — |
case-05 | fail→fail | 10,774 | 8,371 | -22% | 1 | 1 | 0% | 2,118 | 3,923 | +85% | 0 | 0 | — |
case-06 | fail→fail | 11,424 | 7,828 | -31% | 1 | 1 | 0% | 2,297 | 3,911 | +70% | 0 | 0 | — |
case-07 | pass→pass | 10,065 | 3,617 | -64% | 1 | 1 | 0% | 1,790 | 3,091 | +73% | 0 | 0 | — |
case-08 | pass→pass | 10,518 | 5,832 | -45% | 1 | 1 | 0% | 2,077 | 3,516 | +69% | 0 | 0 | — |
case-09 | pass→pass | 12,597 | 5,900 | -53% | 1 | 1 | 0% | 2,374 | 3,393 | +43% | 0 | 0 | — |
case-10 | pass→pass | 11,542 | 3,606 | -69% | 1 | 1 | 0% | 2,058 | 2,980 | +45% | 0 | 0 | — |
case-11 | fail→pass | 16,265 | 6,456 | -60% | 1 | 1 | 0% | 2,691 | 3,631 | +35% | 0 | 0 | — |
case-12 | fail→pass | 10,827 | 5,977 | -45% | 1 | 1 | 0% | 1,934 | 3,536 | +83% | 0 | 0 | — |
case-13 | fail→fail | 10,317 | 7,003 | -32% | 1 | 1 | 0% | 1,706 | 3,606 | +111% | 0 | 0 | — |
case-14 | pass→pass | 10,779 | 6,980 | -35% | 1 | 1 | 0% | 1,835 | 3,599 | +96% | 0 | 0 | — |
case-15 | pass→pass | 15,394 | 9,930 | -35% | 1 | 1 | 0% | 2,817 | 4,223 | +50% | 0 | 0 | — |
case-16 | pass→pass | 11,393 | 6,851 | -40% | 1 | 1 | 0% | 2,131 | 3,617 | +70% | 0 | 0 | — |
case-17 | fail→pass | 10,735 | 5,070 | -53% | 1 | 1 | 0% | 2,006 | 3,298 | +64% | 0 | 0 | — |
case-18 | pass→pass | 14,249 | 13,873 | -3% | 1 | 1 | 0% | 2,456 | 4,972 | +102% | 0 | 0 | — |
case-19 | pass→pass | 10,751 | 6,511 | -39% | 1 | 1 | 0% | 1,738 | 3,367 | +94% | 0 | 0 | — |
case-20 | fail→pass | 13,330 | 6,578 | -51% | 1 | 1 | 0% | 2,337 | 3,568 | +53% | 0 | 0 | — |
case-21 | fail→pass | 6,834 | 2,605 | -62% | 1 | 1 | 0% | 1,377 | 2,866 | +108% | 0 | 0 | — |
case-22 | fail→pass | 5,635 | 2,502 | -56% | 1 | 1 | 0% | 1,167 | 2,803 | +140% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 20 counted toward the lift figure. The other 2 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +32 percentage points is the difference between those two pass rates over the 20 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.