Skip to content

Commit db54983

Browse files
committed
bench/benchprof: add benchprof package
The benchprof package has been created to provide utilities for creating CPU and memory profiles in benchmarks. Release note: None
1 parent e465745 commit db54983

File tree

5 files changed

+232
-163
lines changed

5 files changed

+232
-163
lines changed

pkg/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,7 @@ GO_TARGETS = [
874874
"//pkg/base/serverident:serverident",
875875
"//pkg/base:base",
876876
"//pkg/base:base_test",
877+
"//pkg/bench/benchprof:benchprof",
877878
"//pkg/bench/cmd/pgbenchsetup:pgbenchsetup",
878879
"//pkg/bench/cmd/pgbenchsetup:pgbenchsetup_lib",
879880
"//pkg/bench/hashbench:hashbench_test",

pkg/bench/benchprof/BUILD.bazel

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
load("@io_bazel_rules_go//go:def.bzl", "go_library")
2+
3+
go_library(
4+
name = "benchprof",
5+
srcs = ["benchprof.go"],
6+
importpath = "github.com/cockroachdb/cockroach/pkg/bench/benchprof",
7+
visibility = ["//visibility:public"],
8+
deps = [
9+
"//pkg/testutils/sniffarg",
10+
"@com_github_google_pprof//profile",
11+
],
12+
)

pkg/bench/benchprof/benchprof.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
6+
package benchprof
7+
8+
import (
9+
"bytes"
10+
"os"
11+
"path/filepath"
12+
"regexp"
13+
"runtime"
14+
runtimepprof "runtime/pprof"
15+
"strings"
16+
"testing"
17+
18+
"github.com/cockroachdb/cockroach/pkg/testutils/sniffarg"
19+
"github.com/google/pprof/profile"
20+
)
21+
22+
// StopFn is a function that stops profiling.
23+
type StopFn func(testing.TB)
24+
25+
// Stop stops profiling.
26+
func (f StopFn) Stop(b testing.TB) {
27+
f(b)
28+
}
29+
30+
// StartCPUProfile starts collection of a CPU profile if the "-test.cpuprofile"
31+
// flag has been set. The profile will omit CPU samples collected prior to
32+
// calling StartCPUProfile, and include all allocations made until the returned
33+
// StopFn is called.
34+
//
35+
// Example usage:
36+
//
37+
// func BenchmarkFoo(b *testing.B) {
38+
// // ..
39+
// b.Run("case", func(b *testing.B) {
40+
// defer benchprof.StartCPUProfile(b).Stop(b)
41+
// // Benchmark loop.
42+
// })
43+
// }
44+
//
45+
// The file name of the profile will include the prefix of the profile flags and
46+
// the benchmark names. For example, "foo_benchmark_thing_1.cpu" would be
47+
// created for a "BenchmarkThing/1" benchmark if the "-test.cpuprofile=foo.cpu"
48+
// flag is set.
49+
//
50+
// NB: The "foo.cpu" file will not be created because we must stop the global
51+
// CPU profiler in order to collect profiles that omit setup samples.
52+
func StartCPUProfile(tb testing.TB) StopFn {
53+
var cpuProfFile string
54+
if err := sniffarg.DoEnv("test.cpuprofile", &cpuProfFile); err != nil {
55+
tb.Fatal(err)
56+
}
57+
if cpuProfFile == "" {
58+
// Not CPU profile requested.
59+
return func(b testing.TB) {}
60+
}
61+
62+
prefix := profilePrefix(cpuProfFile)
63+
64+
// Hijack the harness's profile to make a clean profile.
65+
// The flag is set, so likely a CPU profile started by the Go harness is
66+
// running (unless -count is specified, but StopCPUProfile is idempotent).
67+
runtimepprof.StopCPUProfile()
68+
69+
var outputDir string
70+
if err := sniffarg.DoEnv("test.outputdir", &outputDir); err != nil {
71+
tb.Fatal(err)
72+
}
73+
if outputDir != "" {
74+
cpuProfFile = filepath.Join(outputDir, cpuProfFile)
75+
}
76+
77+
// Remove the harness's profile file to avoid confusion.
78+
_ = os.Remove(cpuProfFile)
79+
80+
// Create a new profile file.
81+
fileName := profileFileName(tb, outputDir, prefix, "cpu")
82+
f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
83+
if err != nil {
84+
tb.Fatal(err)
85+
}
86+
87+
// Start profiling.
88+
if err := runtimepprof.StartCPUProfile(f); err != nil {
89+
tb.Fatal(err)
90+
}
91+
92+
return func(b testing.TB) {
93+
runtimepprof.StopCPUProfile()
94+
if err := f.Close(); err != nil {
95+
b.Fatal(err)
96+
}
97+
}
98+
}
99+
100+
// StartMemProfile starts collection of a heap profile if the "-test.memprofile"
101+
// flag has been set. The profile will omit memory allocations prior to calling
102+
// StartMemProfile, and include all allocations made until the returned StopFn
103+
// is called.
104+
//
105+
// Example usage:
106+
//
107+
// func BenchmarkFoo(b *testing.B) {
108+
// // ..
109+
// b.Run("case", func(b *testing.B) {
110+
// defer benchprof.StartMemProfile(b).Stop(b)
111+
// // Benchmark loop.
112+
// })
113+
// }
114+
//
115+
// The file name of the profile will include the prefix of the profile flags and
116+
// the benchmark names. For example, "foo_benchmark_thing_1.mem" would be
117+
// created for a "BenchmarkThing/1" benchmark if the "-test.memprofile=foo.mem"
118+
// flag is set.
119+
//
120+
// NB: The "foo.mem" file will still be created and include all allocations made
121+
// during the entire duration of the benchmark.
122+
func StartMemProfile(tb testing.TB) StopFn {
123+
var memProfFile string
124+
if err := sniffarg.DoEnv("test.memprofile", &memProfFile); err != nil {
125+
tb.Fatal(err)
126+
}
127+
if memProfFile == "" {
128+
// No heap profile requested.
129+
return func(b testing.TB) {}
130+
}
131+
132+
prefix := profilePrefix(memProfFile)
133+
134+
var outputDir string
135+
if err := sniffarg.DoEnv("test.outputdir", &outputDir); err != nil {
136+
tb.Fatal(err)
137+
}
138+
if outputDir != "" {
139+
memProfFile = filepath.Join(outputDir, memProfFile)
140+
}
141+
142+
// Create a new profile file.
143+
fileName := profileFileName(tb, outputDir, prefix, "mem")
144+
diffAllocs := diffProfile(func() []byte {
145+
p := runtimepprof.Lookup("allocs")
146+
var buf bytes.Buffer
147+
runtime.GC()
148+
if err := p.WriteTo(&buf, 0); err != nil {
149+
tb.Fatal(err)
150+
}
151+
return buf.Bytes()
152+
})
153+
154+
return func(b testing.TB) {
155+
if sl := diffAllocs(b); len(sl) > 0 {
156+
if err := os.WriteFile(fileName, sl, 0644); err != nil {
157+
b.Fatal(err)
158+
}
159+
}
160+
}
161+
}
162+
163+
func diffProfile(take func() []byte) func(testing.TB) []byte {
164+
// The below is essentially cribbed from pprof.go in net/http/pprof.
165+
166+
baseBytes := take()
167+
if baseBytes == nil {
168+
return func(tb testing.TB) []byte { return nil }
169+
}
170+
return func(b testing.TB) []byte {
171+
newBytes := take()
172+
pBase, err := profile.ParseData(baseBytes)
173+
if err != nil {
174+
b.Fatal(err)
175+
}
176+
pNew, err := profile.ParseData(newBytes)
177+
if err != nil {
178+
b.Fatal(err)
179+
}
180+
pBase.Scale(-1)
181+
pMerged, err := profile.Merge([]*profile.Profile{pBase, pNew})
182+
if err != nil {
183+
b.Fatal(err)
184+
}
185+
pMerged.TimeNanos = pNew.TimeNanos
186+
pMerged.DurationNanos = pNew.TimeNanos - pBase.TimeNanos
187+
188+
buf := bytes.Buffer{}
189+
if err := pMerged.Write(&buf); err != nil {
190+
b.Fatal(err)
191+
}
192+
return buf.Bytes()
193+
}
194+
}
195+
196+
func profilePrefix(profileArg string) string {
197+
i := strings.Index(profileArg, ".")
198+
if i == -1 {
199+
return profileArg
200+
}
201+
return profileArg[:i]
202+
}
203+
204+
func profileFileName(tb testing.TB, outputDir, prefix, suffix string) string {
205+
saniRE := regexp.MustCompile(`\W+`)
206+
testName := strings.TrimPrefix(tb.Name(), "Benchmark")
207+
testName = strings.ToLower(testName)
208+
testName = saniRE.ReplaceAllString(testName, "_")
209+
210+
fileName := prefix + "_" + testName + "." + suffix
211+
if outputDir != "" {
212+
fileName = filepath.Join(outputDir, fileName)
213+
}
214+
return fileName
215+
}

pkg/bench/tpcc/BUILD.bazel

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ go_test(
99
data = glob(["testdata/**"]),
1010
deps = [
1111
"//pkg/base",
12+
"//pkg/bench/benchprof",
1213
"//pkg/ccl/workloadccl",
1314
"//pkg/rpc/nodedialer",
1415
"//pkg/security/securityassets",
@@ -17,7 +18,6 @@ go_test(
1718
"//pkg/settings/cluster",
1819
"//pkg/sql/stats",
1920
"//pkg/testutils/serverutils",
20-
"//pkg/testutils/sniffarg",
2121
"//pkg/testutils/sqlutils",
2222
"//pkg/testutils/testcluster",
2323
"//pkg/ts",
@@ -27,6 +27,5 @@ go_test(
2727
"//pkg/workload/histogram",
2828
"//pkg/workload/tpcc",
2929
"//pkg/workload/workloadsql",
30-
"@com_github_google_pprof//profile",
3130
],
3231
)

0 commit comments

Comments
 (0)