Profiler Tool

Exercise: Profiler Tool

Difficulty - Advanced

Learning Objectives

  • Use runtime/pprof for profiling
  • Collect CPU and memory profiles
  • Analyze goroutine stacks
  • Track memory allocations
  • Generate flame graphs
  • Integrate with pprof visualization

Problem Statement

Create a profiling tool that collects and analyzes runtime performance data with support for CPU, memory, and goroutine profiling.

Core Components

 1package profiler
 2
 3import (
 4    "context"
 5    "time"
 6)
 7
 8type ProfileType int
 9
10const (
11    CPUProfile ProfileType = iota
12    MemProfile
13    GoroutineProfile
14    BlockProfile
15    MutexProfile
16)
17
18type Profiler struct {
19    types    []ProfileType
20    interval time.Duration
21    output   string
22}
23
24func New(types []ProfileType, interval time.Duration) *Profiler
25func Start(ctx context.Context) error
26func Stop() error
27func WriteProfile(typ ProfileType, filename string) error
28func Stats() *ProfileStats

Solution

Click to see the solution
  1package profiler
  2
  3import (
  4    "context"
  5    "fmt"
  6    "os"
  7    "runtime"
  8    "runtime/pprof"
  9    "runtime/trace"
 10    "sync"
 11    "time"
 12)
 13
 14type ProfileType int
 15
 16const (
 17    CPUProfile ProfileType = iota
 18    MemProfile
 19    HeapProfile
 20    GoroutineProfile
 21    BlockProfile
 22    MutexProfile
 23    ThreadProfile
 24    AllocProfile
 25)
 26
 27func String() string {
 28    switch pt {
 29    case CPUProfile:
 30        return "cpu"
 31    case MemProfile:
 32        return "mem"
 33    case HeapProfile:
 34        return "heap"
 35    case GoroutineProfile:
 36        return "goroutine"
 37    case BlockProfile:
 38        return "block"
 39    case MutexProfile:
 40        return "mutex"
 41    case ThreadProfile:
 42        return "threadcreate"
 43    case AllocProfile:
 44        return "allocs"
 45    default:
 46        return "unknown"
 47    }
 48}
 49
 50type ProfileStats struct {
 51    NumGoroutines  int
 52    MemAllocated   uint64
 53    MemTotal       uint64
 54    NumGC          uint32
 55    CPUSamples     int
 56    BlockingSamples int
 57}
 58
 59type Profiler struct {
 60    types      []ProfileType
 61    interval   time.Duration
 62    outputDir  string
 63    cpuFile    *os.File
 64    traceFile  *os.File
 65    mu         sync.Mutex
 66    collecting bool
 67    stats      ProfileStats
 68}
 69
 70func New(types []ProfileType, interval time.Duration, outputDir string) *Profiler {
 71    return &Profiler{
 72        types:     types,
 73        interval:  interval,
 74        outputDir: outputDir,
 75    }
 76}
 77
 78func Start(ctx context.Context) error {
 79    p.mu.Lock()
 80    if p.collecting {
 81        p.mu.Unlock()
 82        return fmt.Errorf("profiler already running")
 83    }
 84    p.collecting = true
 85    p.mu.Unlock()
 86
 87    // Enable profiling for specific types
 88    for _, typ := range p.types {
 89        switch typ {
 90        case BlockProfile:
 91            runtime.SetBlockProfileRate(1)
 92        case MutexProfile:
 93            runtime.SetMutexProfileFraction(1)
 94        }
 95    }
 96
 97    // Start CPU profiling if requested
 98    if p.hasProfileType(CPUProfile) {
 99        if err := p.startCPUProfile(); err != nil {
100            return err
101        }
102    }
103
104    // Start periodic collection
105    go p.periodicCollection(ctx)
106
107    return nil
108}
109
110func Stop() error {
111    p.mu.Lock()
112    if !p.collecting {
113        p.mu.Unlock()
114        return fmt.Errorf("profiler not running")
115    }
116    p.collecting = false
117    p.mu.Unlock()
118
119    // Stop CPU profiling
120    if p.cpuFile != nil {
121        pprof.StopCPUProfile()
122        p.cpuFile.Close()
123        p.cpuFile = nil
124    }
125
126    // Stop trace
127    if p.traceFile != nil {
128        trace.Stop()
129        p.traceFile.Close()
130        p.traceFile = nil
131    }
132
133    return nil
134}
135
136func startCPUProfile() error {
137    filename := fmt.Sprintf("%s/cpu_%d.prof", p.outputDir, time.Now().Unix())
138    f, err := os.Create(filename)
139    if err != nil {
140        return err
141    }
142
143    if err := pprof.StartCPUProfile(f); err != nil {
144        f.Close()
145        return err
146    }
147
148    p.cpuFile = f
149    return nil
150}
151
152func periodicCollection(ctx context.Context) {
153    ticker := time.NewTicker(p.interval)
154    defer ticker.Stop()
155
156    for {
157        select {
158        case <-ctx.Done():
159            return
160        case <-ticker.C:
161            p.collectStats()
162            p.writeSnapshots()
163        }
164    }
165}
166
167func collectStats() {
168    var m runtime.MemStats
169    runtime.ReadMemStats(&m)
170
171    p.mu.Lock()
172    p.stats.NumGoroutines = runtime.NumGoroutine()
173    p.stats.MemAllocated = m.Alloc
174    p.stats.MemTotal = m.TotalAlloc
175    p.stats.NumGC = m.NumGC
176    p.mu.Unlock()
177}
178
179func writeSnapshots() {
180    timestamp := time.Now().Unix()
181
182    for _, typ := range p.types {
183        if typ == CPUProfile {
184            continue // CPU profile is written separately
185        }
186
187        filename := fmt.Sprintf("%s/%s_%d.prof",
188            p.outputDir, typ.String(), timestamp)
189        if err := p.WriteProfile(typ, filename); err != nil {
190            // Log error
191        }
192    }
193}
194
195func WriteProfile(typ ProfileType, filename string) error {
196    f, err := os.Create(filename)
197    if err != nil {
198        return err
199    }
200    defer f.Close()
201
202    var profile *pprof.Profile
203
204    switch typ {
205    case HeapProfile, MemProfile:
206        profile = pprof.Lookup("heap")
207    case GoroutineProfile:
208        profile = pprof.Lookup("goroutine")
209    case BlockProfile:
210        profile = pprof.Lookup("block")
211    case MutexProfile:
212        profile = pprof.Lookup("mutex")
213    case ThreadProfile:
214        profile = pprof.Lookup("threadcreate")
215    case AllocProfile:
216        profile = pprof.Lookup("allocs")
217    default:
218        return fmt.Errorf("unknown profile type: %v", typ)
219    }
220
221    if profile == nil {
222        return fmt.Errorf("profile not found: %v", typ)
223    }
224
225    return profile.WriteTo(f, 0)
226}
227
228func Stats() ProfileStats {
229    p.mu.Lock()
230    defer p.mu.Unlock()
231    return p.stats
232}
233
234func hasProfileType(typ ProfileType) bool {
235    for _, t := range p.types {
236        if t == typ {
237            return true
238        }
239    }
240    return false
241}
242
243// StartTrace starts execution tracing
244func StartTrace(filename string) error {
245    f, err := os.Create(filename)
246    if err != nil {
247        return err
248    }
249
250    if err := trace.Start(f); err != nil {
251        f.Close()
252        return err
253    }
254
255    p.traceFile = f
256    return nil
257}
258
259// Snapshot takes an immediate snapshot of all profiles
260func Snapshot() error {
261    p.writeSnapshots()
262    return nil
263}
264
265// GetGoroutineStacks returns stack traces of all goroutines
266func GetGoroutineStacks() []byte {
267    buf := make([]byte, 1<<20) // 1MB buffer
268    n := runtime.Stack(buf, true)
269    return buf[:n]
270}

Usage Example

 1package main
 2
 3import (
 4    "context"
 5    "fmt"
 6    "log"
 7    "os"
 8    "os/signal"
 9    "syscall"
10    "time"
11)
12
13func main() {
14    // Create output directory
15    os.MkdirAll("profiles", 0755)
16
17    // Create profiler
18    profiler := profiler.New(
19        []profiler.ProfileType{
20            profiler.CPUProfile,
21            profiler.HeapProfile,
22            profiler.GoroutineProfile,
23            profiler.BlockProfile,
24        },
25        30*time.Second, // Snapshot every 30 seconds
26        "profiles",
27    )
28
29    // Start profiling
30    ctx, cancel := context.WithCancel(context.Background())
31    defer cancel()
32
33    if err := profiler.Start(ctx); err != nil {
34        log.Fatal(err)
35    }
36    defer profiler.Stop()
37
38    // Optional: Start execution trace
39    if err := profiler.StartTrace("profiles/trace.out"); err != nil {
40        log.Printf("Failed to start trace: %v", err)
41    }
42
43    // Run application
44    go runApplication()
45
46    // Monitor stats
47    go func() {
48        ticker := time.NewTicker(10 * time.Second)
49        defer ticker.Stop()
50
51        for {
52            select {
53            case <-ctx.Done():
54                return
55            case <-ticker.C:
56                stats := profiler.Stats()
57                fmt.Printf("Stats: Goroutines=%d MemAlloc=%d MB NumGC=%d\n",
58                    stats.NumGoroutines,
59                    stats.MemAllocated/(1024*1024),
60                    stats.NumGC)
61            }
62        }
63    }()
64
65    // Wait for interrupt
66    sigCh := make(chan os.Signal, 1)
67    signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
68    <-sigCh
69
70    fmt.Println("Shutting down...")
71}
72
73func runApplication() {
74    // Your application logic here
75    for {
76        time.Sleep(time.Second)
77    }
78}

Analyze profiles:

 1# CPU profile
 2go tool pprof profiles/cpu_*.prof
 3
 4# Memory profile
 5go tool pprof profiles/heap_*.prof
 6
 7# Goroutine profile
 8go tool pprof profiles/goroutine_*.prof
 9
10# View in web browser
11go tool pprof -http=:8080 profiles/cpu_*.prof
12
13# Execution trace
14go tool trace profiles/trace.out

Key Takeaways

  • runtime/pprof provides profiling capabilities
  • CPU profiling shows where time is spent
  • Memory profiling identifies allocation hotspots
  • Goroutine profiling detects leaks
  • Block profiling finds contention points
  • Execution traces show detailed runtime behavior
  • pprof tool visualizes profile data
  • Regular snapshots track performance over time