Easy Profiling Go Benchmark Tests with Bash

Update: This idea got popular enough that I decided to turn it into a tool. You can grab it from http://pprof.sh.

There are a lot of great articles on the interwebs about profiling entire Go applications for CPU and memory usage, however I can never remember the syntax for doing the same thing for specific Benchmark-prefixed tests. Editors have great support for running these tests, but sometimes I want to be able to produce binary and profile output files to open in the visualizer for further inspection. So like any lazy programmer I made a few scripts to do all the work for me.

bench.sh

#!/bin/sh

if [ $# -eq 0 ]
    then
        printf "Runs a specified benchmark test func and opens the profile in Chrome.\n\n"
        printf "Usage:\n"
        printf "\t./bench.sh <cpu|mem> <dir> <function>\n\n"
        printf "Example:\n"
        printf "\t./bench.sh cpu ./internal BenchmarkBinaryEncoding\n\n"
        exit 0
fi

go test -bench=${3} -benchmem -${1}profile profile.out -o bench.test ${2}
go tool pprof -http=:6060 bench.test profile.out

trace.sh

#!/bin/sh

if [ $# -eq 0 ]
    then
        printf "Runs a specified benchmark test func and opens the trace profile in Chrome.\n\n"
        printf "Usage:\n"
        printf "\t./trace.sh <dir> <function>\n\n"
        printf "Example:\n"
        printf "\t./trace.sh ./internal BenchmarkBinaryEncoding\n\n"
        exit 0
fi

go test -bench=${2} -trace trace.out -o trace.test ${1}
go tool trace -http=:6060 trace.test trace.out

Lets consider a sample Go project layout that looks something like the following:

my-go-project/
├─ cmd/
│  ├─ server/
│  │  ├─ main.go
├─ internal/
│  ├─ encoding.go
│  ├─ encoding_bench_test.go
│  ├─ encoding_test.go
├─ scripts/
│  ├─ bench.sh
│  ├─ tests.sh
│  ├─ trace.sh
├─ go.mod
├─ go.sum
├─ README.md

Inside the internal directory there is encoding_bench_test.go which has a benchmark function called BenchmarkBinaryEncoding that we want to profile.

./internal/encoding_bench_test.go

package internal_test
...
func BenchmarkBinaryEncoding(b *testing.B) {
    ...
}
...

You can see we placed bench.sh and trace.sh inside the scripts folder so we can run it from the root of the project and see the help menu.

my-go-project on master via Go v1.16
> ./scripts/bench.sh
Runs a specified benchmark test in app's package and opens the profile in Chrome.

Usage:
	./bench.sh <cpu|mem> <dir> <function>

Example:
	./bench.sh cpu ./internal BenchmarkBinaryEncoding

Running it with the correct arguments will perform the following:

  1. Run the benchmark test BenchmarkBinaryEncoding within the internal package and capture the cpu|memory profile
  2. Write a test binary and profile file to disk
  3. Launch the profiler visualizer on port :6060 with test binary and profile

Now let's run the BenchmarkBinaryEncoding function in the internal package.

my-go-project on master via Go v1.16
> ./scripts/bench.sh cpu ./internal BenchmarkBinaryEncoding
goos: linux
goarch: amd64
pkg: my-go-project/internal
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkBinaryEncoding-8   	  567564	      1916 ns/op	     979 B/op	       3 allocs/op
PASS
ok  	my-go-project/internal	2.047s
Serving web UI on http://localhost:6060

At this point the browser should open automatically so you can poke around the CPU profile. Once you are done you can close the browser and quit the bash process and go about your business.

The trace.sh script operates the same way as bench.sh, but it opens the trace visualizer in the browser.

my-go-project on master via Go v1.16
> ./scripts/trace.sh ./internal BenchmarkBinaryEncoding
goos: linux
goarch: amd64
pkg: my-go-project/internal
cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz
BenchmarkBinaryEncoding-8   	  567564	      1916 ns/op	     979 B/op	       3 allocs/op
PASS
ok  	my-go-project/internal	2.047s
2021/02/18 13:17:50 Parsing trace...
2021/02/18 13:17:51 Splitting trace...
2021/02/18 13:17:51 Opening browser. Trace viewer is listening on http://[::]:6060

I found these scripts to be super helpful to reduce the feedback loop of changing code and running a benchmark to see if things changed. I hope this helps you as much as it has helped me in my day-to-day work and if there are things that can be improved please reach out and let me know.

Happy pprof-ing!