← Back to blog
Product 4 min read 炬鲸团队

Continuous Profiling: Pinpoint CPU and Memory Hotspots Down to the Function

Metrics tell you a service is slow; flame graphs tell you which line is slow. This post covers OBSERVE continuous profiling: reading flame graphs, low-overhead sampling config, and three troubleshooting scenarios for CPU, memory, and lock contention.

Why Metrics and Traces Aren't Enough

CPU hits 90%, and your endpoint's p99 jumps from 50 ms to 300 ms. Metrics tell you something is wrong; traces tell you it's the order-service; but to answer "which function is burning CPU, which object is eating memory," you need profiling. Metrics and traces are great at narrowing the blast radius. Profiling is the step that names the culprit.

The old approach is to attach pprof or async-profiler when something breaks — except by the time you connect, the traffic spike has passed and you've missed the scene. OBSERVE's continuous profiling turns that one-shot capture into an always-on recorder: by default it samples once every 60 seconds for about 10 seconds each time, keeping CPU overhead under 3% while continuously collecting stack traces from every process. When something goes wrong, you can rewind through the last 15 minutes — or longer — of stack snapshots. No reproduction required, and no "it didn't reproduce on my machine" during the postmortem.

How to Read a Flame Graph

The horizontal axis is sample share, the vertical axis is the call stack, and the wider a bar, the more it costs. Read it in this order:

  1. Start at the top level and find the widest "plateaus" of CPU usage;
  2. Click through to drill down to a specific function;
  3. Diff the abnormal window against a normal one and look for bars that suddenly got wider.

A common mistake is looking only at total width. What you actually want to optimize is self time — the time a function spends in its own code, excluding the children it calls. Some functions are narrow themselves but are invoked from tens of thousands of call sites; those add up too, and you find them with the "callers" view rather than the "callees" view. Two more pitfalls: recursion flattens a call chain into a confusing wide bar, and JIT inlining can hide a function entirely — so if a frame you expect isn't there, check whether the JIT inlined it rather than assuming it's cheap.

Onboarding and Sampling Configuration

Install a collection agent on the host and label the process — no code changes needed. A typical config:

profiling:
  enabled: true
  interval: 60s
  duration: 10s
  targets:
    - service: order-service
      process: java
      lang: jvm
      cpu: true
      alloc: true

For JVM services enable both CPU and allocation sampling; for Go use pprof's CPU and heap profiles; for Node.js watch event-loop blocking and GC. Keep sampling overhead under 3% and you can leave it on in production. You can also configure it to raise the sampling frequency in low-traffic windows and dial it back during peaks automatically, so the overhead tracks actual load. If a host is already running hot, disable allocation sampling there and keep only CPU — allocation tracing is usually the more expensive of the two.

Three Troubleshooting Scenarios

  • CPU pegged: the flame graph points at a regex-plus-serialization path; after precompiling the pattern and reusing objects, CPU drops from 90% to 30%.
  • Memory creeping up: diffing two heap flame graphs shows a cache Map that only ever grows; adding a TTL flattens the curve.
  • Lock contention: a high lock-wait share means threads are fighting over one lock; switching to striped locks doubles throughput.

Tie profiling to alerting: when a CPU or memory alert fires, automatically capture a 5-minute flame-graph snapshot around the incident so you skip manual reproduction and walk into the postmortem with concrete evidence. That one change alone often cuts the time from "alert" to "understood" by more than half.

A final note worth internalizing: profiling data is cheap to collect and expensive to ignore. Teams that turn it on during a quiet week usually find a CPU sink or a slow-growing leak that would have surfaced during the next peak. The value also compounds — flame graphs from a healthy window become the reference you diff against when things break, which is far easier than reconstructing what "normal" looked like from memory. Start with CPU and heap on your three busiest services and expand from there; you'll likely have answers before the next incident, not after.