| // Copyright 2016 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package pprof | |
| import ( | |
| "bytes" | |
| "fmt" | |
| "internal/asan" | |
| "internal/profile" | |
| "internal/profilerecord" | |
| "internal/testenv" | |
| "runtime" | |
| "slices" | |
| "strings" | |
| "testing" | |
| ) | |
| func TestConvertMemProfile(t *testing.T) { | |
| addr1, addr2, map1, map2 := testPCs(t) | |
| // MemProfileRecord stacks are return PCs, so add one to the | |
| // addresses recorded in the "profile". The proto profile | |
| // locations are call PCs, so conversion will subtract one | |
| // from these and get back to addr1 and addr2. | |
| a1, a2 := uintptr(addr1)+1, uintptr(addr2)+1 | |
| rate := int64(512 * 1024) | |
| rec := []profilerecord.MemProfileRecord{ | |
| {ObjectSize: 1024, AllocObjects: 4, FreeObjects: 1, Stack: []uintptr{a1, a2}}, | |
| {ObjectSize: 512 * 1024, AllocObjects: 1, FreeObjects: 0, Stack: []uintptr{a2 + 1, a2 + 2}}, | |
| {ObjectSize: 512 * 1024, AllocObjects: 1, FreeObjects: 1, Stack: []uintptr{a1 + 1, a1 + 2, a2 + 3}}, | |
| } | |
| periodType := &profile.ValueType{Type: "space", Unit: "bytes"} | |
| sampleType := []*profile.ValueType{ | |
| {Type: "alloc_objects", Unit: "count"}, | |
| {Type: "alloc_space", Unit: "bytes"}, | |
| {Type: "inuse_objects", Unit: "count"}, | |
| {Type: "inuse_space", Unit: "bytes"}, | |
| } | |
| samples := []*profile.Sample{ | |
| { | |
| Value: []int64{2050, 2099200, 1537, 1574400}, | |
| Location: []*profile.Location{ | |
| {ID: 1, Mapping: map1, Address: addr1}, | |
| {ID: 2, Mapping: map2, Address: addr2}, | |
| }, | |
| NumLabel: map[string][]int64{"bytes": {1024}}, | |
| }, | |
| { | |
| Value: []int64{1, 829411, 1, 829411}, | |
| Location: []*profile.Location{ | |
| {ID: 3, Mapping: map2, Address: addr2 + 1}, | |
| {ID: 4, Mapping: map2, Address: addr2 + 2}, | |
| }, | |
| NumLabel: map[string][]int64{"bytes": {512 * 1024}}, | |
| }, | |
| { | |
| Value: []int64{1, 829411, 0, 0}, | |
| Location: []*profile.Location{ | |
| {ID: 5, Mapping: map1, Address: addr1 + 1}, | |
| {ID: 6, Mapping: map1, Address: addr1 + 2}, | |
| {ID: 7, Mapping: map2, Address: addr2 + 3}, | |
| }, | |
| NumLabel: map[string][]int64{"bytes": {512 * 1024}}, | |
| }, | |
| } | |
| for _, tc := range []struct { | |
| name string | |
| defaultSampleType string | |
| }{ | |
| {"heap", ""}, | |
| {"allocs", "alloc_space"}, | |
| } { | |
| t.Run(tc.name, func(t *testing.T) { | |
| var buf bytes.Buffer | |
| if err := writeHeapProto(&buf, rec, rate, tc.defaultSampleType); err != nil { | |
| t.Fatalf("writing profile: %v", err) | |
| } | |
| p, err := profile.Parse(&buf) | |
| if err != nil { | |
| t.Fatalf("profile.Parse: %v", err) | |
| } | |
| checkProfile(t, p, rate, periodType, sampleType, samples, tc.defaultSampleType) | |
| }) | |
| } | |
| } | |
| func genericAllocFunc[T interface{ uint32 | uint64 }](n int) []T { | |
| return make([]T, n) | |
| } | |
| func profileToStrings(p *profile.Profile) []string { | |
| var res []string | |
| for _, s := range p.Sample { | |
| res = append(res, sampleToString(s)) | |
| } | |
| return res | |
| } | |
| func sampleToString(s *profile.Sample) string { | |
| var funcs []string | |
| for i := len(s.Location) - 1; i >= 0; i-- { | |
| loc := s.Location[i] | |
| funcs = locationToStrings(loc, funcs) | |
| } | |
| return fmt.Sprintf("%s %v", strings.Join(funcs, ";"), s.Value) | |
| } | |
| func locationToStrings(loc *profile.Location, funcs []string) []string { | |
| for j := range loc.Line { | |
| line := loc.Line[len(loc.Line)-1-j] | |
| funcs = append(funcs, line.Function.Name) | |
| } | |
| return funcs | |
| } | |
| // This is a regression test for https://go.dev/issue/64528. | |
| func TestGenericsHashKeyInPprofBuilder(t *testing.T) { | |
| if asan.Enabled { | |
| t.Skip("extra allocations with -asan throw off the test; see #70079") | |
| } | |
| previousRate := runtime.MemProfileRate | |
| runtime.MemProfileRate = 1 | |
| defer func() { | |
| runtime.MemProfileRate = previousRate | |
| }() | |
| for _, sz := range []int{128, 256} { | |
| genericAllocFunc[uint32](sz / 4) | |
| } | |
| for _, sz := range []int{64, 128} { | |
| genericAllocFunc[uint64](sz / 8) | |
| } | |
| runtime.GC() | |
| buf := bytes.NewBuffer(nil) | |
| if err := WriteHeapProfile(buf); err != nil { | |
| t.Fatalf("writing profile: %v", err) | |
| } | |
| p, err := profile.Parse(buf) | |
| if err != nil { | |
| t.Fatalf("profile.Parse: %v", err) | |
| } | |
| actual := profileToStrings(p) | |
| expected := []string{ | |
| "testing.tRunner;runtime/pprof.TestGenericsHashKeyInPprofBuilder;runtime/pprof.genericAllocFunc[go.shape.uint32] [1 128 0 0]", | |
| "testing.tRunner;runtime/pprof.TestGenericsHashKeyInPprofBuilder;runtime/pprof.genericAllocFunc[go.shape.uint32] [1 256 0 0]", | |
| "testing.tRunner;runtime/pprof.TestGenericsHashKeyInPprofBuilder;runtime/pprof.genericAllocFunc[go.shape.uint64] [1 64 0 0]", | |
| "testing.tRunner;runtime/pprof.TestGenericsHashKeyInPprofBuilder;runtime/pprof.genericAllocFunc[go.shape.uint64] [1 128 0 0]", | |
| } | |
| for _, l := range expected { | |
| if !slices.Contains(actual, l) { | |
| t.Errorf("profile = %v\nwant = %v", strings.Join(actual, "\n"), l) | |
| } | |
| } | |
| } | |
| type opAlloc struct { | |
| buf [128]byte | |
| } | |
| type opCall struct { | |
| } | |
| var sink []byte | |
| func storeAlloc() { | |
| sink = make([]byte, 16) | |
| } | |
| func nonRecursiveGenericAllocFunction[CurrentOp any, OtherOp any](alloc bool) { | |
| if alloc { | |
| storeAlloc() | |
| } else { | |
| nonRecursiveGenericAllocFunction[OtherOp, CurrentOp](true) | |
| } | |
| } | |
| func TestGenericsInlineLocations(t *testing.T) { | |
| if asan.Enabled { | |
| t.Skip("extra allocations with -asan throw off the test; see #70079") | |
| } | |
| if testenv.OptimizationOff() { | |
| t.Skip("skipping test with optimizations disabled") | |
| } | |
| previousRate := runtime.MemProfileRate | |
| runtime.MemProfileRate = 1 | |
| defer func() { | |
| runtime.MemProfileRate = previousRate | |
| sink = nil | |
| }() | |
| nonRecursiveGenericAllocFunction[opAlloc, opCall](true) | |
| nonRecursiveGenericAllocFunction[opCall, opAlloc](false) | |
| runtime.GC() | |
| buf := bytes.NewBuffer(nil) | |
| if err := WriteHeapProfile(buf); err != nil { | |
| t.Fatalf("writing profile: %v", err) | |
| } | |
| p, err := profile.Parse(buf) | |
| if err != nil { | |
| t.Fatalf("profile.Parse: %v", err) | |
| } | |
| const expectedSample = "testing.tRunner;runtime/pprof.TestGenericsInlineLocations;runtime/pprof.nonRecursiveGenericAllocFunction[go.shape.struct {},go.shape.struct { runtime/pprof.buf [128]uint8 }];runtime/pprof.nonRecursiveGenericAllocFunction[go.shape.struct { runtime/pprof.buf [128]uint8 },go.shape.struct {}];runtime/pprof.storeAlloc [1 16 1 16]" | |
| const expectedLocation = "runtime/pprof.nonRecursiveGenericAllocFunction[go.shape.struct {},go.shape.struct { runtime/pprof.buf [128]uint8 }];runtime/pprof.nonRecursiveGenericAllocFunction[go.shape.struct { runtime/pprof.buf [128]uint8 },go.shape.struct {}];runtime/pprof.storeAlloc" | |
| const expectedLocationNewInliner = "runtime/pprof.TestGenericsInlineLocations;" + expectedLocation | |
| var s *profile.Sample | |
| for _, sample := range p.Sample { | |
| if sampleToString(sample) == expectedSample { | |
| s = sample | |
| break | |
| } | |
| } | |
| if s == nil { | |
| t.Fatalf("expected \n%s\ngot\n%s", expectedSample, strings.Join(profileToStrings(p), "\n")) | |
| } | |
| loc := s.Location[0] | |
| actual := strings.Join(locationToStrings(loc, nil), ";") | |
| if expectedLocation != actual && expectedLocationNewInliner != actual { | |
| t.Errorf("expected a location with at least 3 functions\n%s\ngot\n%s\n", expectedLocation, actual) | |
| } | |
| } | |
| func growMap() { | |
| m := make(map[int]int) | |
| for i := range 512 { | |
| m[i] = i | |
| } | |
| } | |
| // Runtime frames are hidden in heap profiles. | |
| // This is a regression test for https://go.dev/issue/71174. | |
| func TestHeapRuntimeFrames(t *testing.T) { | |
| previousRate := runtime.MemProfileRate | |
| runtime.MemProfileRate = 1 | |
| defer func() { | |
| runtime.MemProfileRate = previousRate | |
| }() | |
| growMap() | |
| runtime.GC() | |
| buf := bytes.NewBuffer(nil) | |
| if err := WriteHeapProfile(buf); err != nil { | |
| t.Fatalf("writing profile: %v", err) | |
| } | |
| p, err := profile.Parse(buf) | |
| if err != nil { | |
| t.Fatalf("profile.Parse: %v", err) | |
| } | |
| actual := profileToStrings(p) | |
| // We must see growMap at least once. | |
| foundGrowMap := false | |
| for _, l := range actual { | |
| if !strings.Contains(l, "runtime/pprof.growMap") { | |
| continue | |
| } | |
| foundGrowMap = true | |
| // Runtime frames like mapassign and map internals should be hidden. | |
| if strings.Contains(l, "runtime.") { | |
| t.Errorf("Sample got %s, want no runtime frames", l) | |
| } | |
| if strings.Contains(l, "internal/runtime/") { | |
| t.Errorf("Sample got %s, want no runtime frames", l) | |
| } | |
| if strings.Contains(l, "mapassign") { // in case mapassign moves to a package not matching above paths. | |
| t.Errorf("Sample got %s, want no mapassign frames", l) | |
| } | |
| } | |
| if !foundGrowMap { | |
| t.Errorf("Profile got:\n%s\nwant sample in runtime/pprof.growMap", strings.Join(actual, "\n")) | |
| } | |
| } |