Skip to content

Commit eaa0288

Browse files
committed
server: implement delta profiling for allocs/heap/block/mutex profiles
Previously, when requesting profiles like allocs, heap, block, or mutex through the pprof UI endpoint (/debug/pprof/ui/allocs/?seconds=10), the seconds parameter was ignored and an immediate snapshot was returned. This differed from the behavior of Go's standard pprof handler at /debug/pprof/allocs?seconds=10, which correctly collected a delta profile over the specified duration. The issue was in profileLocal() which handled these profile types in the default case without respecting req.Seconds. Now, when Seconds > 0 is specified, we collect two profile snapshots separated by the requested duration and compute their difference (delta profile) using the same algorithm as Go's net/http/pprof: scale the first profile by -1 and merge with the second. This enables the pprof UI to show "recent allocations" over a specific time window rather than cumulative allocations since process start. Release note (bug fix): Fixed a bug where the pprof UI endpoints for allocs, heap, block, and mutex profiles ignored the seconds parameter and returned immediate snapshots instead of delta profiles.
1 parent e654896 commit eaa0288

File tree

1 file changed

+72
-0
lines changed

1 file changed

+72
-0
lines changed

pkg/server/status_local_file_retrieval.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/cockroachdb/cockroach/pkg/server/srverrors"
2323
"github.com/cockroachdb/cockroach/pkg/settings/cluster"
2424
"github.com/cockroachdb/cockroach/pkg/util/allstacks"
25+
"github.com/google/pprof/profile"
2526
"google.golang.org/grpc/codes"
2627
"google.golang.org/grpc/status"
2728
)
@@ -91,6 +92,14 @@ func profileLocal(
9192
if p == nil {
9293
return nil, status.Errorf(codes.InvalidArgument, "unable to find profile: %s", name)
9394
}
95+
96+
// If seconds is specified, collect a delta profile by taking two snapshots
97+
// and computing the difference. This matches the behavior of Go's standard
98+
// net/http/pprof handler.
99+
if req.Seconds > 0 {
100+
return collectDeltaProfile(ctx, p, time.Duration(req.Seconds)*time.Second)
101+
}
102+
94103
var buf bytes.Buffer
95104
if err := p.WriteTo(&buf, 0); err != nil {
96105
return nil, status.Error(codes.Internal, err.Error())
@@ -99,6 +108,69 @@ func profileLocal(
99108
}
100109
}
101110

111+
// collectDeltaProfile collects a delta profile by taking two snapshots of the
112+
// given profile separated by the specified duration, then computing the
113+
// difference. This matches the behavior of Go's standard net/http/pprof handler
114+
// when the "seconds" parameter is specified.
115+
func collectDeltaProfile(
116+
ctx context.Context, p *pprof.Profile, duration time.Duration,
117+
) (*serverpb.JSONResponse, error) {
118+
// Collect the first profile snapshot.
119+
p0, err := collectProfileSnapshot(p)
120+
if err != nil {
121+
return nil, status.Errorf(codes.Internal, "failed to collect initial profile: %s", err)
122+
}
123+
124+
// Wait for the specified duration.
125+
select {
126+
case <-ctx.Done():
127+
return nil, ctx.Err()
128+
case <-time.After(duration):
129+
}
130+
131+
// Collect the second profile snapshot.
132+
p1, err := collectProfileSnapshot(p)
133+
if err != nil {
134+
return nil, status.Errorf(codes.Internal, "failed to collect final profile: %s", err)
135+
}
136+
137+
// Compute the delta by scaling p0 by -1 and merging with p1.
138+
ts := p1.TimeNanos
139+
dur := p1.TimeNanos - p0.TimeNanos
140+
141+
p0.Scale(-1)
142+
143+
delta, err := profile.Merge([]*profile.Profile{p0, p1})
144+
if err != nil {
145+
return nil, status.Errorf(codes.Internal, "failed to compute delta profile: %s", err)
146+
}
147+
148+
delta.TimeNanos = ts
149+
delta.DurationNanos = dur
150+
151+
var buf bytes.Buffer
152+
if err := delta.Write(&buf); err != nil {
153+
return nil, status.Errorf(codes.Internal, "failed to write delta profile: %s", err)
154+
}
155+
return &serverpb.JSONResponse{Data: buf.Bytes()}, nil
156+
}
157+
158+
// collectProfileSnapshot collects a single snapshot of the given profile and
159+
// parses it into a *profile.Profile.
160+
func collectProfileSnapshot(p *pprof.Profile) (*profile.Profile, error) {
161+
var buf bytes.Buffer
162+
if err := p.WriteTo(&buf, 0); err != nil {
163+
return nil, err
164+
}
165+
ts := time.Now().UnixNano()
166+
prof, err := profile.Parse(&buf)
167+
if err != nil {
168+
return nil, err
169+
}
170+
prof.TimeNanos = ts
171+
return prof, nil
172+
}
173+
102174
// stacksLocal retrieves goroutine stack files on the local node. This method
103175
// returns a gRPC error to the caller.
104176
func stacksLocal(req *serverpb.StacksRequest) (*serverpb.JSONResponse, error) {

0 commit comments

Comments
 (0)