Skip to content
Back to skills

Golang Profiling

ASecurity

Profile Go processes using pprof. Covers CPU, memory, goroutine, and mutex profiles; flamegraph visualization; collapsed-stacks and annotated-call-tree output for LLM analysis (with bundled, tested scripts); and benchmark profiling.

  • 8 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 20, 2026
devopspythongobashsqldockertestingdebugginggitdatabase

Works with

  • terminal

Security analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned September 20, 2026

npx -y skills add tstapler/dotfiles --skill golang-profiling --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Golang Profiling?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Golang Profiling
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tstapler-golang-profiling/badge)](https://www.skillsdirectory.com/skills/tstapler-golang-profiling)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: golang-profiling
description: Profile Go processes using pprof. Covers CPU, memory, goroutine, and mutex profiles; flamegraph visualization; collapsed-stacks and annotated-call-tree output for LLM analysis (with bundled, tested scripts); and benchmark profiling.
---

# Go Profiling with pprof

End-to-end workflow: enable pprof → collect profile → collapsed stacks or annotated call tree (primary analysis formats) → flamegraph (visualization).

## Format Strategy

| Format | Best for | Command |
|--------|----------|---------|
| **Collapsed stacks** | Scripting, CI diffs, awk parsing, max compression | `go tool pprof -raw -output=collapsed` |
| **Annotated call tree** | LLM reasoning about *which call chain* to fix, without full collapsed-stacks token cost | see Step 3a below |
| HTML flamegraph | Interactive human exploration | `go tool pprof -http=:8081` |
| Text top | Quick terminal summary | `go tool pprof -top` |
| Raw `.prof` | Archive, future re-analysis | — |

**Use collapsed stacks when scripting or diffing; use the annotated call tree when handing a profile to an LLM to decide what to fix.** Collapsed-stacks format: `frame1;frame2;leaf N` — one line per unique call stack, count at end. It's the most compact representation but flattens call-path context into thousands of near-duplicate lines; the annotated tree (Step 3a) keeps the tree structure so the LLM can see *which caller* to fix, at roughly the token cost of a top-N table.

## Bundled Scripts

`scripts/` holds stdlib-only Python (no deps to install) with a `tests/` suite (`python3 -m unittest discover -s scripts/tests`):

| Script | Purpose | Used in |
|--------|---------|---------|
| `scripts/annotate_tree.py` | Builds the annotated call tree (self%/cum%, HOTSPOT markers, pruned) directly from `go tool pprof -raw` output | Step 3a |
| `scripts/pct_breakdown.py` | Leaf-frame percentage breakdown from collapsed stacks | Step 3 |

---

## Step 1 — Enable pprof in Your Application

### HTTP Server (long-running apps)

```go
import _ "net/http/pprof"

// In main() or init():
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()
```

Endpoints exposed automatically:
- `http://localhost:6060/debug/pprof/profile?seconds=30` — CPU (30s sample)
- `http://localhost:6060/debug/pprof/heap` — Memory (heap allocations)
- `http://localhost:6060/debug/pprof/goroutine` — All goroutines + stacks
- `http://localhost:6060/debug/pprof/mutex` — Lock contention
- `http://localhost:6060/debug/pprof/block` — Blocking operations (channels, syscalls)

### Enable mutex and block profiling (off by default)

```go
import "runtime"

func init() {
    runtime.SetMutexProfileFraction(1)  // 1 = sample every mutex event
    runtime.SetBlockProfileRate(1)       // 1 = sample every block event (nanoseconds)
}
```

Only enable these in dev/profile builds — they add overhead.

### Benchmark profiling (no HTTP server needed)

```bash
# CPU profile from benchmark
go test -bench=BenchmarkMyFunc -benchmem -cpuprofile=cpu.prof ./pkg -timeout=5m

# Memory profile from benchmark
go test -bench=BenchmarkMyFunc -benchmem -memprofile=mem.prof ./pkg -timeout=5m

# Execution trace (richer than pprof — shows goroutine scheduling)
go test -bench=BenchmarkMyFunc -trace=trace.out ./pkg -timeout=5m
go tool trace trace.out
```

---

## Step 2 — Collect Profiles

### From running HTTP server

```bash
# CPU (30 second sample — app must be under load)
curl -o cpu.prof "http://localhost:6060/debug/pprof/profile?seconds=30"

# Heap (snapshot)
curl -o heap.prof http://localhost:6060/debug/pprof/heap

# Goroutine dump (all goroutines with stacks)
curl -o goroutine.prof http://localhost:6060/debug/pprof/goroutine

# Mutex contention
curl -o mutex.prof http://localhost:6060/debug/pprof/mutex

# Block / channel contention
curl -o block.prof http://localhost:6060/debug/pprof/block
```

### From tests (no server)

```bash
# CPU profile for all tests in a package
go test -cpuprofile=cpu.prof ./pkg

# Memory profile for all tests
go test -memprofile=mem.prof ./pkg

# Specific test function
go test -run=TestHeavyOperation -cpuprofile=cpu.prof ./pkg
```

---

## Step 3 — Analyze Collapsed Stacks

### Generate collapsed stacks

```bash
# CPU collapsed stacks
go tool pprof -raw cpu.prof | awk '/^[^#]/{print}' > cpu.collapsed
# Or use the sample/total-based output:
go tool pprof -output=cpu.collapsed -text cpu.prof

# Better: use Brendan Gregg's stackcollapse-go (most compatible with flamegraph tools)
go tool pprof -raw -output=raw.txt cpu.prof
# Then collapse with: https://github.com/brendangregg/FlameGraph/blob/master/stackcollapse-go.pl
# stackcollapse-go.pl raw.txt > cpu.collapsed
```

### Quick text analysis (no external tools)

```bash
# Top functions by self-sample (CPU)
go tool pprof -top cpu.prof

# Top functions with cumulative cost shown
go tool pprof -top -cum cpu.prof

# Show top 20 with source
go tool pprof -top=20 cpu.prof
```

### awk extraction from collapsed stacks

Collapsed-stacks format is root-first, leaf-last (`root;caller;leaf N`) — the
leaf frame is the *last* `;`-separated field, not the first.

```bash
# Top leaf frames by self-sample count
awk '{n=$NF; sub(/ [0-9]+$/,"",n); split(n,a,";"); leaf=a[length(a)]; count[leaf]+=$NF} END{for(f in count) print count[f],f}' \
  cpu.collapsed | sort -rn | head -20

# Stacks in a specific package
grep "yourpackage" cpu.collapsed | sort -t' ' -k2 -rn | head -10

# Top full stacks (raw, sorted by count)
sort -t' ' -k2 -rn cpu.collapsed | head -20
```

### Percentage breakdown (bundled script)

```bash
python3 scripts/pct_breakdown.py cpu.collapsed --top 20
```

See `scripts/pct_breakdown.py` — leaf-frame self-time ranked by percentage of
total samples, tested in `scripts/tests/test_pct_breakdown.py`.

---

## Step 3a — Annotated Call Tree (LLM format)

The terse-but-context-preserving middle ground between collapsed stacks (all
call-path detail, but thousands of near-duplicate lines) and a flat top-N
table (compact, but loses which caller is responsible). Built with the
bundled `scripts/annotate_tree.py` directly from `go tool pprof -raw` —
no Perl/stackcollapse dependency, stdlib-only.

```bash
go tool pprof -raw cpu.prof > raw.txt
python3 scripts/annotate_tree.py raw.txt
```

Tune pruning/sensitivity for the profile size:

```bash
python3 scripts/annotate_tree.py raw.txt --min-pct 2 --hotspot-pct 8 --max-depth 10 --top-self 15
```

Example output (verified against a real CPU profile with a hot recursive
function, an allocation-heavy path, and a `strings.Builder` growth path):

```
Total samples: 8
Legend: [self% | cum%] function  (pruned below 1.0% cum, HOTSPOT = self% >= 5.0%)

├── [  0.0% |  87.5%] runtime.main
│   ├── [  0.0% |  50.0%] main.main
│   │   └── [  0.0% |  50.0%] main.cpuHeavy
│   │       └── [  0.0% |  50.0%] main.fib
│   ├── [ 25.0% |  25.0%] strings.(*Builder).copyCheck  ◀ HOTSPOT
│   └── [ 12.5% |  12.5%] main.allocHeavy  ◀ HOTSPOT
└── [  0.0% |  12.5%] runtime.systemstack
    └── ...

Top 10 by self-time (flat, across all call sites):
   50.0%  main.fib
   25.0%  strings.(*Builder).copyCheck
   12.5%  main.allocHeavy
```

`--min-pct` prunes any subtree below that cumulative % of total (keeps output
readable on real multi-thousand-sample profiles); `--hotspot-pct` controls
the self% threshold for the `◀ HOTSPOT` marker; `--top-self` adds a flat
leaderboard that catches hotspots split across many call sites (e.g. a shared
helper called from a dozen places, each individually below the tree's
pruning floor). Works on any profile type `-raw` supports (CPU, heap,
goroutine, mutex, block) — self/cum are computed from the sample-count
column, which pprof already normalizes per profile type.

Tested in `scripts/tests/test_annotate_tree.py` against a synthetic
`-raw`-format fixture (shared call-path merging, hotspot marking, pruning,
depth truncation, flat leaderboard).

---

## Step 4 — Flamegraph Visualization

### Interactive browser UI (recommended)

```bash
# CPU flamegraph — opens browser automatically
go tool pprof -http=:8081 cpu.prof

# Memory flamegraph
go tool pprof -http=:8081 heap.prof

# Goroutine flamegraph
go tool pprof -http=:8081 goroutine.prof
```

The browser UI shows: flamegraph, top, source, peek, disassembly tabs.

### SVG flamegraph (shareable)

```bash
# Install Brendan Gregg's tools once
git clone https://github.com/brendangregg/FlameGraph /opt/FlameGraph

# Generate SVG
go tool pprof -raw -output=raw.txt cpu.prof
/opt/FlameGraph/stackcollapse-go.pl raw.txt | /opt/FlameGraph/flamegraph.pl > flamegraph.svg
```

### Differential flamegraph (A/B comparison)

```bash
# Compare two profiles — shows regressions (red) and improvements (blue)
go tool pprof -http=:8081 -diff_base=baseline.prof current.prof
```

---

## Step 5 — Profile Types and When to Use

| Profile | Flag / Endpoint | Use when |
|---------|----------------|----------|
| **CPU** | `-cpuprofile` / `/profile?seconds=N` | High CPU, slow requests, inefficient loops |
| **Heap** | `-memprofile` / `/heap` | High memory, GC pressure, allocation churn |
| **Goroutine** | `/goroutine` | Goroutine leaks, deadlocks, stuck goroutines |
| **Mutex** | `/mutex` | Lock contention, slow shared state access |
| **Block** | `/block` | Channel blocking, syscall starvation |
| **Trace** | `-trace` / `/trace` | Goroutine scheduling, GC pauses, end-to-end latency |

---

> For fixing hotspots identified in the profile, apply the `golang-development` skill for idiomatic optimizations.

## Step 6 — Interpreting Results

### Hotspot patterns and fixes

| Pattern in top/flamegraph | Diagnosis | Fix |
|---------------------------|-----------|-----|
| `runtime.mallocgc` > 10% | Allocation churn (GC pressure) | Object pooling (`sync.Pool`), reuse slices |
| `runtime.gcBgMarkWorker` > 5% | GC running constantly | Reduce allocation rate; tune `GOGC` |
| `sync.(*Mutex).Lock` dominant | Lock contention | Reduce lock scope; use `sync.RWMutex`; shard |
| `syscall.Read` / `syscall.Write` | I/O bound | Buffered I/O (`bufio`); async I/O patterns |
| `encoding/json.Marshal` in hot path | Serialization overhead | `encoding/json` → `jsoniter` or `json.RawMessage` cache |
| `reflect.*` in hot path | Reflection overhead | Code-gen alternatives; avoid reflect in loops |
| `fmt.Sprintf` / `fmt.Fprintf` | String formatting cost | `strings.Builder`; pre-format static strings |
| `regexp.(*Regexp).Find` | Regex re-compilation | Compile once at package level (`var re = regexp.MustCompile(...)`) |
| `database/sql.(*DB).QueryContext` | DB query overhead | Connection pool tuning; query result caching |

### Filter to app-only frames

```bash
# Show only your package in pprof interactive
go tool pprof cpu.prof
(pprof) focus yourpackage/
(pprof) top

# Or from collapsed stacks
grep "github.com/yourorg/yourrepo" cpu.collapsed | sort -t' ' -k2 -rn | head -20
```

### Memory: inuse vs alloc

```bash
# Current live heap (what's consuming memory right now)
go tool pprof -inuse_space heap.prof

# Total allocations (what's driving GC pressure)
go tool pprof -alloc_space heap.prof

# Allocation count (how many objects, not bytes)
go tool pprof -alloc_objects heap.prof
```

---

## Step 7 — CI Integration

```yaml
# .github/workflows/profile.yml
jobs:
  profile:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with: { go-version: stable }

      - name: Run benchmark with CPU profile
        run: |
          go test -bench=BenchmarkCriticalPath -benchmem \
            -cpuprofile=cpu.prof -memprofile=mem.prof \
            -timeout=5m ./pkg/...

      - name: Generate collapsed stacks
        run: |
          go tool pprof -top=50 cpu.prof > cpu-top.txt
          go tool pprof -top=50 mem.prof > mem-top.txt

      - uses: actions/upload-artifact@v4
        with:
          name: profiles-${{ github.sha }}
          path: |
            cpu.prof
            mem.prof
            cpu-top.txt
            mem-top.txt
          retention-days: 30
```

**Store `.prof` files as CI artifacts** — they're binary but small (~1-5 MB), and `go tool pprof -diff_base` can compare them across commits.

---

## Step 8 — Execution Trace (Deeper Analysis)

pprof shows *where* time is spent. `go tool trace` shows *why* — goroutine scheduling, GC pauses, syscall latency.

```bash
# Collect trace from benchmark
go test -bench=BenchmarkHeavy -trace=trace.out ./pkg -timeout=5m

# Collect trace from running app (5 seconds)
curl -o trace.out http://localhost:6060/debug/pprof/trace?seconds=5

# Open interactive viewer
go tool trace trace.out
```

Trace viewer tabs:
- **View trace** — Timeline of all goroutines; zoom into pauses
- **Goroutine analysis** — Time blocked vs running per goroutine
- **Network blocking profile** — Network I/O waits
- **Synchronization blocking** — Mutex/channel wait time
- **Scheduler latency** — Time goroutines waited to be scheduled

---

## Quick Reference

| Goal | Command |
|------|---------|
| CPU profile (live app) | `curl -o cpu.prof "localhost:6060/debug/pprof/profile?seconds=30"` |
| Heap profile | `curl -o heap.prof localhost:6060/debug/pprof/heap` |
| CPU profile (benchmark) | `go test -bench=. -cpuprofile=cpu.prof ./pkg` |
| Top functions | `go tool pprof -top cpu.prof` |
| Interactive flamegraph | `go tool pprof -http=:8081 cpu.prof` |
| Diff two profiles | `go tool pprof -http=:8081 -diff_base=old.prof new.prof` |
| Execution trace | `go tool trace trace.out` |
| Goroutine dump | `curl localhost:6060/debug/pprof/goroutine?debug=2` |

---

## Related Skills

| Skill | When to apply |
|-------|--------------|
| `golang-development` | Apply idiomatic Go fixes after identifying hotspots |
| `github-actions-debugging` | Debug CI failures in profiling or benchmark workflows |
| `infra-docker-build-test` | Profile a Go service running inside a container |

## Common Pitfalls

- **Profiling an idle app** — CPU profiles are useless without load; always profile under realistic traffic or benchmark load
- **Short CPU samples** — Use at least 10-30 seconds; short samples miss infrequent-but-slow paths
- **Forgetting mutex/block rates** — `SetMutexProfileFraction` and `SetBlockProfileRate` are off by default; set them before collecting those profiles
- **`-inuse_space` vs `-alloc_space`** — inuse shows live heap; alloc shows GC pressure; use alloc when diagnosing GC, inuse for OOM
- **`reflect.*` frames masking real callers** — use `-cum` flag in pprof top to see cumulative callers through reflection
- **Benchmarks without `-benchmem`** — always add `-benchmem` to see allocation counts alongside ns/op
- **GC noise in short benchmarks** — use `testing.B.ResetTimer()` after setup code to exclude initialization from measurement

Files in this skill

  • SKILL.md14.6 KB
  • scripts/annotate_tree.py6.2 KB
  • scripts/pct_breakdown.py2.2 KB
  • scripts/tests/test_annotate_tree.py4.4 KB
  • scripts/tests/test_pct_breakdown.py1.9 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…