Skip to content

Commit e1c0349

Browse files
committed
internal/profile: fully decode proto even if there are no samples
This is a partial revert of CL 483137. CL 483137 started checking errors in postDecode, which is good. Now we can catch more malformed pprof protos. However this made TestEmptyProfile fail, so an early return was added when the profile was "empty" (no samples). Unfortunately, this was problematic. Profiles with no samples can still be valid, but skipping postDecode meant that the resulting Profile was missing values from the string table. In particular, net/http/pprof needs to parse empty profiles in order to pass through the sample and period types to a final output proto. CL 483137 broke this behavior. internal/profile.Parse is only used in two places: in cmd/compile to parse PGO pprof profiles, and in net/http/pprof to parse before/after pprof profiles for delta profiles. In both cases, the input is never literally empty (0 bytes). Even a pprof proto with no samples still contains some header fields, such as sample and period type. Upstream github.com/google/pprof/profile even has an explicit error on 0 byte input, so `go tool pprof` will not support such an input. Thus TestEmptyProfile was misleading; this profile doesn't need to support empty input at all. Resolve this by removing TestEmptyProfile and replacing it with an explicit error on empty input, as upstream github.com/google/pprof/profile has. For non-empty input, always run postDecode to ensure the string table is processed. TestConvertCPUProfileEmpty is reverted back to assert the values from before CL 483137. Note that in this case "Empty" means no samples, not a 0 byte input. Continue to allow empty files for PGO in order to minimize the chance of last minute breakage if some users have empty files. Fixes #64566. Change-Id: I83a1f0200ae225ac6da0009d4b2431fe215b283f Reviewed-on: https://go-review.googlesource.com/c/go/+/547996 Reviewed-by: Michael Knyszek <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Cherry Mui <[email protected]>
1 parent c71eedf commit e1c0349

File tree

7 files changed

+136
-30
lines changed

7 files changed

+136
-30
lines changed

src/cmd/compile/internal/pgo/irgraph.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import (
4646
"cmd/compile/internal/pgo/internal/graph"
4747
"cmd/compile/internal/typecheck"
4848
"cmd/compile/internal/types"
49+
"errors"
4950
"fmt"
5051
"internal/profile"
5152
"os"
@@ -145,18 +146,22 @@ func New(profileFile string) (*Profile, error) {
145146
return nil, fmt.Errorf("error opening profile: %w", err)
146147
}
147148
defer f.Close()
148-
profile, err := profile.Parse(f)
149-
if err != nil {
149+
p, err := profile.Parse(f)
150+
if errors.Is(err, profile.ErrNoData) {
151+
// Treat a completely empty file the same as a profile with no
152+
// samples: nothing to do.
153+
return nil, nil
154+
} else if err != nil {
150155
return nil, fmt.Errorf("error parsing profile: %w", err)
151156
}
152157

153-
if len(profile.Sample) == 0 {
158+
if len(p.Sample) == 0 {
154159
// We accept empty profiles, but there is nothing to do.
155160
return nil, nil
156161
}
157162

158163
valueIndex := -1
159-
for i, s := range profile.SampleType {
164+
for i, s := range p.SampleType {
160165
// Samples count is the raw data collected, and CPU nanoseconds is just
161166
// a scaled version of it, so either one we can find is fine.
162167
if (s.Type == "samples" && s.Unit == "count") ||
@@ -170,7 +175,7 @@ func New(profileFile string) (*Profile, error) {
170175
return nil, fmt.Errorf(`profile does not contain a sample index with value/type "samples/count" or cpu/nanoseconds"`)
171176
}
172177

173-
g := graph.NewGraph(profile, &graph.Options{
178+
g := graph.NewGraph(p, &graph.Options{
174179
SampleValue: func(v []int64) int64 { return v[valueIndex] },
175180
})
176181

src/internal/profile/encode.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,6 @@ var profileDecoder = []decoder{
207207
// suffix X) and populates the corresponding exported fields.
208208
// The unexported fields are cleared up to facilitate testing.
209209
func (p *Profile) postDecode() error {
210-
if p.Empty() {
211-
return nil
212-
}
213210
var err error
214211

215212
mappings := make(map[uint64]*Mapping)

src/internal/profile/profile.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,10 +141,14 @@ func Parse(r io.Reader) (*Profile, error) {
141141
}
142142
orig = data
143143
}
144-
if p, err = parseUncompressed(orig); err != nil {
145-
if p, err = parseLegacy(orig); err != nil {
146-
return nil, fmt.Errorf("parsing profile: %v", err)
147-
}
144+
145+
var lErr error
146+
p, pErr := parseUncompressed(orig)
147+
if pErr != nil {
148+
p, lErr = parseLegacy(orig)
149+
}
150+
if pErr != nil && lErr != nil {
151+
return nil, fmt.Errorf("parsing profile: not a valid proto profile (%w) or legacy profile (%w)", pErr, lErr)
148152
}
149153

150154
if err := p.CheckValid(); err != nil {
@@ -155,6 +159,7 @@ func Parse(r io.Reader) (*Profile, error) {
155159

156160
var errUnrecognized = fmt.Errorf("unrecognized profile format")
157161
var errMalformed = fmt.Errorf("malformed profile format")
162+
var ErrNoData = fmt.Errorf("empty input file")
158163

159164
func parseLegacy(data []byte) (*Profile, error) {
160165
parsers := []func([]byte) (*Profile, error){
@@ -180,6 +185,10 @@ func parseLegacy(data []byte) (*Profile, error) {
180185
}
181186

182187
func parseUncompressed(data []byte) (*Profile, error) {
188+
if len(data) == 0 {
189+
return nil, ErrNoData
190+
}
191+
183192
p := &Profile{}
184193
if err := unmarshal(data, p); err != nil {
185194
return nil, err

src/internal/profile/profile_test.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,9 @@
55
package profile
66

77
import (
8-
"bytes"
98
"testing"
109
)
1110

12-
func TestEmptyProfile(t *testing.T) {
13-
var buf bytes.Buffer
14-
p, err := Parse(&buf)
15-
if err != nil {
16-
t.Error("Want no error, got", err)
17-
}
18-
if p == nil {
19-
t.Fatal("Want a valid profile, got <nil>")
20-
}
21-
if !p.Empty() {
22-
t.Errorf("Profile should be empty, got %#v", p)
23-
}
24-
}
25-
2611
func TestParseContention(t *testing.T) {
2712
tests := []struct {
2813
name string

src/net/http/pprof/pprof_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ package pprof
66

77
import (
88
"bytes"
9+
"encoding/base64"
910
"fmt"
1011
"internal/profile"
1112
"internal/testenv"
1213
"io"
1314
"net/http"
1415
"net/http/httptest"
16+
"path/filepath"
1517
"runtime"
1618
"runtime/pprof"
1719
"strings"
@@ -261,3 +263,64 @@ func seen(p *profile.Profile, fname string) bool {
261263
}
262264
return false
263265
}
266+
267+
// TestDeltaProfileEmptyBase validates that we still receive a valid delta
268+
// profile even if the base contains no samples.
269+
//
270+
// Regression test for https://go.dev/issue/64566.
271+
func TestDeltaProfileEmptyBase(t *testing.T) {
272+
if testing.Short() {
273+
// Delta profile collection has a 1s minimum.
274+
t.Skip("skipping in -short mode")
275+
}
276+
277+
testenv.MustHaveGoRun(t)
278+
279+
gotool, err := testenv.GoTool()
280+
if err != nil {
281+
t.Fatalf("error finding go tool: %v", err)
282+
}
283+
284+
out, err := testenv.Command(t, gotool, "run", filepath.Join("testdata", "delta_mutex.go")).CombinedOutput()
285+
if err != nil {
286+
t.Fatalf("error running profile collection: %v\noutput: %s", err, out)
287+
}
288+
289+
// Log the binary output for debugging failures.
290+
b64 := make([]byte, base64.StdEncoding.EncodedLen(len(out)))
291+
base64.StdEncoding.Encode(b64, out)
292+
t.Logf("Output in base64.StdEncoding: %s", b64)
293+
294+
p, err := profile.Parse(bytes.NewReader(out))
295+
if err != nil {
296+
t.Fatalf("Parse got err %v want nil", err)
297+
}
298+
299+
t.Logf("Output as parsed Profile: %s", p)
300+
301+
if len(p.SampleType) != 2 {
302+
t.Errorf("len(p.SampleType) got %d want 2", len(p.SampleType))
303+
}
304+
if p.SampleType[0].Type != "contentions" {
305+
t.Errorf(`p.SampleType[0].Type got %q want "contentions"`, p.SampleType[0].Type)
306+
}
307+
if p.SampleType[0].Unit != "count" {
308+
t.Errorf(`p.SampleType[0].Unit got %q want "count"`, p.SampleType[0].Unit)
309+
}
310+
if p.SampleType[1].Type != "delay" {
311+
t.Errorf(`p.SampleType[1].Type got %q want "delay"`, p.SampleType[1].Type)
312+
}
313+
if p.SampleType[1].Unit != "nanoseconds" {
314+
t.Errorf(`p.SampleType[1].Unit got %q want "nanoseconds"`, p.SampleType[1].Unit)
315+
}
316+
317+
if p.PeriodType == nil {
318+
t.Fatal("p.PeriodType got nil want not nil")
319+
}
320+
if p.PeriodType.Type != "contentions" {
321+
t.Errorf(`p.PeriodType.Type got %q want "contentions"`, p.PeriodType.Type)
322+
}
323+
if p.PeriodType.Unit != "count" {
324+
t.Errorf(`p.PeriodType.Unit got %q want "count"`, p.PeriodType.Unit)
325+
}
326+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// This binary collects a 1s delta mutex profile and dumps it to os.Stdout.
6+
//
7+
// This is in a subprocess because we want the base mutex profile to be empty
8+
// (as a regression test for https://go.dev/issue/64566) and the only way to
9+
// force reset the profile is to create a new subprocess.
10+
//
11+
// This manually collects the HTTP response and dumps to stdout in order to
12+
// avoid any flakiness around port selection for a real HTTP server.
13+
package main
14+
15+
import (
16+
"bytes"
17+
"fmt"
18+
"log"
19+
"net/http"
20+
"net/http/pprof"
21+
"net/http/httptest"
22+
"runtime"
23+
)
24+
25+
func main() {
26+
// Disable the mutex profiler. This is the default, but that default is
27+
// load-bearing for this test, which needs the base profile to be empty.
28+
runtime.SetMutexProfileFraction(0)
29+
30+
h := pprof.Handler("mutex")
31+
32+
req := httptest.NewRequest("GET", "/debug/pprof/mutex?seconds=1", nil)
33+
rec := httptest.NewRecorder()
34+
rec.Body = new(bytes.Buffer)
35+
36+
h.ServeHTTP(rec, req)
37+
resp := rec.Result()
38+
if resp.StatusCode != http.StatusOK {
39+
log.Fatalf("Request failed: %s\n%s", resp.Status, rec.Body)
40+
}
41+
42+
fmt.Print(rec.Body)
43+
}

src/runtime/pprof/proto_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func fmtJSON(x any) string {
4545
return string(js)
4646
}
4747

48-
func TestConvertCPUProfileEmpty(t *testing.T) {
48+
func TestConvertCPUProfileNoSamples(t *testing.T) {
4949
// A test server with mock cpu profile data.
5050
var buf bytes.Buffer
5151

@@ -64,9 +64,13 @@ func TestConvertCPUProfileEmpty(t *testing.T) {
6464
}
6565

6666
// Expected PeriodType and SampleType.
67-
sampleType := []*profile.ValueType{{}, {}}
67+
periodType := &profile.ValueType{Type: "cpu", Unit: "nanoseconds"}
68+
sampleType := []*profile.ValueType{
69+
{Type: "samples", Unit: "count"},
70+
{Type: "cpu", Unit: "nanoseconds"},
71+
}
6872

69-
checkProfile(t, p, 2000*1000, nil, sampleType, nil, "")
73+
checkProfile(t, p, 2000*1000, periodType, sampleType, nil, "")
7074
}
7175

7276
func f1() { f1() }

0 commit comments

Comments
 (0)