mirror of https://github.com/golang/go.git
290 lines
8.4 KiB
Go
290 lines
8.4 KiB
Go
// 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{
|
|
{AllocBytes: 4096, FreeBytes: 1024, AllocObjects: 4, FreeObjects: 1, Stack: []uintptr{a1, a2}},
|
|
{AllocBytes: 512 * 1024, FreeBytes: 0, AllocObjects: 1, FreeObjects: 0, Stack: []uintptr{a2 + 1, a2 + 2}},
|
|
{AllocBytes: 512 * 1024, FreeBytes: 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{32, 64} {
|
|
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 32 0 0]",
|
|
"testing.tRunner;runtime/pprof.TestGenericsHashKeyInPprofBuilder;runtime/pprof.genericAllocFunc[go.shape.uint64] [1 64 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, "runtime/internal/") {
|
|
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"))
|
|
}
|
|
}
|