heapcheck

module
v1.0.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 15, 2025 License: MIT

README

heapcheck

CI Go Reference Go Report Card DOI

heapcheck is a unified memory analysis tool for Go that combines static escape analysis with test-time leak detection.

Features

  • Static escape analysis - transforms cryptic compiler output into human-readable reports with actionable suggestions
  • Runtime leak detection - detects goroutine and heap leaks during test execution
  • Test integration - simple API to add memory checks to your tests (goleak-compatible)
  • Multiple output formats - text, JSON, HTML, and SARIF for CI/CD integration

Quick Reference

heapcheck provides two types of analysis:

Analysis Type Command What it does
Static escape analysis heapcheck ./... Shows heap allocations at compile time
Runtime leak detection go test ./... Detects goroutine/memory leaks during tests

Important: Runtime leak detection only works if your tests use guard.VerifyNone(t). Regular tests without guard do not detect leaks.

// Without guard - NO leak detection
func TestMyFunction(t *testing.T) {
    result := myFunction()  // Could leak goroutines, you'd never know!
}

// With guard - leak detection ENABLED
func TestMyFunction(t *testing.T) {
    defer guard.VerifyNone(t)  // ← Enables leak detection
    result := myFunction()     // Leaks will fail the test
}

The Problem

Go's compiler escape analysis is powerful but cryptic:

$ go build -gcflags="-m" ./...
./main.go:15:6: can inline square
./main.go:12:2: moved to heap: z
./main.go:8:14: *y escapes to heap
./main.go:11:13: x does not escape

What does this mean? Why did z move to heap? How do I fix it?

The Solution

$ heapcheck ./...

heapcheck - Escape Analysis Report
--------------------------------------------------

Summary:
  Total variables analyzed: 847
  Stack allocated:          792 (93.5%)
  Heap allocated:            55 (6.5%)

Escape Causes:
  1. interface-boxing      23 (41.8%)  -> Use concrete types or generics
  2. return-pointer        15 (27.3%)  -> Return by value if struct <= 64 bytes
  3. closure-capture        9 (16.4%)  -> Pass as parameter instead
  4. goroutine-escape       5 (9.1%)   -> Use worker pools
  5. unknown-size           3 (5.5%)   -> Pre-allocate capacity

Hotspots (files with most escapes):
  pkg/server/handler.go                      12 escapes
  pkg/cache/store.go                          8 escapes
  internal/util/strings.go                    6 escapes

Run with -v for detailed breakdown of all 55 escapes.

Installation

go install github.com/harshakonda/heapcheck/cmd/heapcheck@latest

CLI Usage

Basic Analysis
# Analyze all packages in current module
heapcheck ./...

# Analyze specific package
heapcheck ./pkg/server

# Verbose output with all escape details
heapcheck -v ./...
Output Formats
# Human-readable text (default)
heapcheck ./...

# JSON for CI/CD integration
heapcheck --format=json ./...

# HTML visual report with charts
heapcheck --format=html ./... > report.html

# SARIF for GitHub Code Scanning
heapcheck --format=sarif ./... > results.sarif
Filtering
# Show only heap escapes (hide "does not escape")
heapcheck --escapes-only ./...

# Filter by package path
heapcheck --filter=pkg/server ./...

Test Integration (guard package)

Add leak detection to your tests with the guard package. The API is compatible with goleak.

Note: The guard package is for test-time only (unit tests, integration tests, CI/CD). For production monitoring, use dedicated tools like Prometheus, Datadog, or Pyroscope.

Basic Usage
import "github.com/harshakonda/heapcheck/guard"

func TestMyFunction(t *testing.T) {
    defer guard.VerifyNone(t)
    
    // Your test code here
    result := myHandler.Process(input)
    assert.Equal(t, expected, result)
}
With Thresholds
func TestWithThresholds(t *testing.T) {
    defer guard.VerifyNone(t,
        guard.MaxGoroutines(5),   // Allow up to 5 transient goroutines
        guard.MaxHeapMB(50),      // Allow up to 50MB heap growth
    )
    
    // Your test code here
    processLargeDataset()
}
Ignoring Known Goroutines
func TestWithBackgroundWorker(t *testing.T) {
    defer guard.VerifyNone(t,
        guard.IgnoreTopFunction("github.com/myapp/pkg.backgroundWorker"),
        guard.IgnoreContains("database/sql.(*DB).connectionOpener"),
    )
    
    // Your test code here
}
Package-Level Check
func TestMain(m *testing.M) {
    guard.VerifyTestMain(m)
}
Manual Control with Checkpoints
func TestComplex(t *testing.T) {
    g := guard.Check(t)
    
    // Phase 1
    setupDatabase()
    g.Checkpoint("after setup")
    
    // Phase 2
    runMigrations()
    g.Checkpoint("after migrations")
    
    // Final verification
    g.Verify()
}
Running Tests
# Run all tests
go test ./...

# Run with verbose output (shows checkpoints and leak details)
go test -v ./...
What Failure Looks Like

When a leak is detected, the test fails with details:

--- FAIL: TestWorkerPool (0.31s)
    guard.go:142: heapcheck: goroutine leak detected
      Leaked: 2 (max allowed: 0)
      
      goroutine 25 [running]:
        github.com/myapp/worker.(*Pool).worker(...)
            /app/worker/pool.go:45
        ...

When tests pass, it means no leaks were detected:

--- PASS: TestMyFunction (0.11s)
PASS
ok      github.com/myapp/pkg    1.048s

Runtime Analysis

For custom analysis scenarios, use the runtime package directly:

import "github.com/harshakonda/heapcheck/runtime"

func TestDetailed(t *testing.T) {
    snapshot := runtime.TakeSnapshot()
    
    // Run your code
    myFunction()
    
    // Get detailed diff
    diff := snapshot.Compare()
    
    t.Logf("Goroutine growth: %d", diff.GoroutineGrowth)
    t.Logf("Heap growth: %.2f MB", float64(diff.HeapGrowthBytes)/1024/1024)
    
    for _, g := range diff.LeakedGoroutines {
        t.Logf("Leaked: goroutine %d [%s]", g.ID, g.State)
    }
}

Escape Categories

heapcheck categorizes escapes by their cause and provides optimization suggestions:

Category Description Suggestion
return-pointer Returns pointer to local variable Return by value if struct <= 64 bytes
interface-boxing Assigned to interface{} Use concrete types or generics
closure-capture Captured by closure Pass as parameter instead
goroutine-escape Passed to goroutine Use worker pools
channel-send Sent over channel Consider sync.Pool
slice-grow Slice may grow Pre-allocate capacity
unknown-size Size unknown at compile time Use fixed-size arrays
fmt-call Passed to fmt functions Use strconv in hot paths
reflection Uses reflect package Avoid in hot paths
leaking-param Parameter escapes function Review function signature
map-allocation make(map[K]V) Expected behavior
new-allocation new(T) Expected behavior
too-large Struct too large for stack Expected behavior

CI/CD Integration

GitHub Actions
name: Memory Analysis
on: [push, pull_request]

jobs:
  heapcheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.22'
      
      - name: Install heapcheck
        run: go install github.com/harshakonda/heapcheck/cmd/heapcheck@latest
      
      - name: Run Static Analysis
        run: heapcheck ./...
      
      - name: Run Tests with Leak Detection
        run: go test -v ./...
      
      - name: Upload SARIF
        run: heapcheck --format=sarif ./... > results.sarif
      
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif
GitLab CI
heapcheck:
  stage: lint
  script:
    - go install github.com/harshakonda/heapcheck/cmd/heapcheck@latest
    - heapcheck --format=json ./... > heapcheck.json
  artifacts:
    reports:
      codequality: heapcheck.json
Pre-commit Hook
#!/bin/bash
# .git/hooks/pre-commit

heapcheck ./...
if [ $? -ne 0 ]; then
    echo "heapcheck found issues"
    exit 1
fi

Understanding Escape Analysis

Why Does It Matter?
  • Stack allocations are fast: Just moving a pointer, automatically freed when function returns
  • Heap allocations are slow: Requires GC overhead, causes memory fragmentation
  • GC pressure: More heap allocations = more GC pauses = higher latency
Common Escape Patterns

1. Returning Pointers

// Escapes - pointer to local variable
func newUser() *User {
    u := User{Name: "test"}
    return &u  // escapes!
}

// No escape - return by value
func newUser() User {
    return User{Name: "test"}
}

2. Interface Boxing

// Escapes - interface boxing
func log(msg interface{}) { ... }
log(myStruct)  // escapes!

// No escape - concrete type or generics
func log[T any](msg T) { ... }
log(myStruct)

3. Closure Capture

// Escapes - captured by closure
func process(data []byte) {
    go func() {
        use(data)  // data escapes!
    }()
}

// No escape - pass as parameter
func process(data []byte) {
    go func(d []byte) {
        use(d)
    }(data)
}

For Researchers

heapcheck is designed to be citable in academic work.

Citation

Konda, S. H. (2025). heapcheck: Unified Memory Analysis for Go. Zenodo. https://doi.org/10.5281/zenodo.17895742

BibTeX
@software{konda2025heapcheck,
  author       = {Konda, Sri Harsha},
  title        = {heapcheck: Unified Memory Analysis for Go},
  year         = {2025},
  publisher    = {Zenodo},
  version      = {1.0.2},
  doi          = {10.5281/zenodo.17895742},
  url          = {https://doi.org/10.5281/zenodo.17895742}
}

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

Development
git clone https://github.com/harshakonda/heapcheck
cd heapcheck
go test ./...
go build ./cmd/heapcheck
./heapcheck ./examples/...

License

MIT License - see LICENSE for details.

Directories

Path Synopsis
cmd
heapcheck command
heapcheck - A developer-friendly Go escape analysis reporter
heapcheck - A developer-friendly Go escape analysis reporter
examples
basic-patterns
Package patterns demonstrates common escape analysis patterns in Go.
Package patterns demonstrates common escape analysis patterns in Go.
http-server
Package server demonstrates escape analysis in HTTP handlers.
Package server demonstrates escape analysis in HTTP handlers.
json-processor
Package jsonproc demonstrates escape analysis in JSON processing code.
Package jsonproc demonstrates escape analysis in JSON processing code.
worker-pool
Package worker demonstrates escape analysis in concurrent Go code.
Package worker demonstrates escape analysis in concurrent Go code.
Package guard provides simple test integration for memory leak detection.
Package guard provides simple test integration for memory leak detection.
internal
categorizer
Package categorizer analyzes escape information and categorizes it by the cause of escape, adding actionable suggestions.
Package categorizer analyzes escape information and categorizes it by the cause of escape, adding actionable suggestions.
parser
Package parser handles running the Go compiler with escape analysis flags and parsing the output into structured data.
Package parser handles running the Go compiler with escape analysis flags and parsing the output into structured data.
reporter
Package reporter provides various output formatters for escape analysis results.
Package reporter provides various output formatters for escape analysis results.
Package runtime provides test-time runtime leak detection for Go applications.
Package runtime provides test-time runtime leak detection for Go applications.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL