1// Copyright 2016 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
5package pprof
6
7import (
8	"bytes"
9	"fmt"
10	"internal/profile"
11	"internal/profilerecord"
12	"internal/testenv"
13	"runtime"
14	"slices"
15	"strings"
16	"testing"
17)
18
19func TestConvertMemProfile(t *testing.T) {
20	addr1, addr2, map1, map2 := testPCs(t)
21
22	// MemProfileRecord stacks are return PCs, so add one to the
23	// addresses recorded in the "profile". The proto profile
24	// locations are call PCs, so conversion will subtract one
25	// from these and get back to addr1 and addr2.
26	a1, a2 := uintptr(addr1)+1, uintptr(addr2)+1
27	rate := int64(512 * 1024)
28	rec := []profilerecord.MemProfileRecord{
29		{AllocBytes: 4096, FreeBytes: 1024, AllocObjects: 4, FreeObjects: 1, Stack: []uintptr{a1, a2}},
30		{AllocBytes: 512 * 1024, FreeBytes: 0, AllocObjects: 1, FreeObjects: 0, Stack: []uintptr{a2 + 1, a2 + 2}},
31		{AllocBytes: 512 * 1024, FreeBytes: 512 * 1024, AllocObjects: 1, FreeObjects: 1, Stack: []uintptr{a1 + 1, a1 + 2, a2 + 3}},
32	}
33
34	periodType := &profile.ValueType{Type: "space", Unit: "bytes"}
35	sampleType := []*profile.ValueType{
36		{Type: "alloc_objects", Unit: "count"},
37		{Type: "alloc_space", Unit: "bytes"},
38		{Type: "inuse_objects", Unit: "count"},
39		{Type: "inuse_space", Unit: "bytes"},
40	}
41	samples := []*profile.Sample{
42		{
43			Value: []int64{2050, 2099200, 1537, 1574400},
44			Location: []*profile.Location{
45				{ID: 1, Mapping: map1, Address: addr1},
46				{ID: 2, Mapping: map2, Address: addr2},
47			},
48			NumLabel: map[string][]int64{"bytes": {1024}},
49		},
50		{
51			Value: []int64{1, 829411, 1, 829411},
52			Location: []*profile.Location{
53				{ID: 3, Mapping: map2, Address: addr2 + 1},
54				{ID: 4, Mapping: map2, Address: addr2 + 2},
55			},
56			NumLabel: map[string][]int64{"bytes": {512 * 1024}},
57		},
58		{
59			Value: []int64{1, 829411, 0, 0},
60			Location: []*profile.Location{
61				{ID: 5, Mapping: map1, Address: addr1 + 1},
62				{ID: 6, Mapping: map1, Address: addr1 + 2},
63				{ID: 7, Mapping: map2, Address: addr2 + 3},
64			},
65			NumLabel: map[string][]int64{"bytes": {512 * 1024}},
66		},
67	}
68	for _, tc := range []struct {
69		name              string
70		defaultSampleType string
71	}{
72		{"heap", ""},
73		{"allocs", "alloc_space"},
74	} {
75		t.Run(tc.name, func(t *testing.T) {
76			var buf bytes.Buffer
77			if err := writeHeapProto(&buf, rec, rate, tc.defaultSampleType); err != nil {
78				t.Fatalf("writing profile: %v", err)
79			}
80
81			p, err := profile.Parse(&buf)
82			if err != nil {
83				t.Fatalf("profile.Parse: %v", err)
84			}
85
86			checkProfile(t, p, rate, periodType, sampleType, samples, tc.defaultSampleType)
87		})
88	}
89}
90
91func genericAllocFunc[T interface{ uint32 | uint64 }](n int) []T {
92	return make([]T, n)
93}
94
95func profileToStrings(p *profile.Profile) []string {
96	var res []string
97	for _, s := range p.Sample {
98		res = append(res, sampleToString(s))
99	}
100	return res
101}
102
103func sampleToString(s *profile.Sample) string {
104	var funcs []string
105	for i := len(s.Location) - 1; i >= 0; i-- {
106		loc := s.Location[i]
107		funcs = locationToStrings(loc, funcs)
108	}
109	return fmt.Sprintf("%s %v", strings.Join(funcs, ";"), s.Value)
110}
111
112func locationToStrings(loc *profile.Location, funcs []string) []string {
113	for j := range loc.Line {
114		line := loc.Line[len(loc.Line)-1-j]
115		funcs = append(funcs, line.Function.Name)
116	}
117	return funcs
118}
119
120// This is a regression test for https://go.dev/issue/64528 .
121func TestGenericsHashKeyInPprofBuilder(t *testing.T) {
122	previousRate := runtime.MemProfileRate
123	runtime.MemProfileRate = 1
124	defer func() {
125		runtime.MemProfileRate = previousRate
126	}()
127	for _, sz := range []int{128, 256} {
128		genericAllocFunc[uint32](sz / 4)
129	}
130	for _, sz := range []int{32, 64} {
131		genericAllocFunc[uint64](sz / 8)
132	}
133
134	runtime.GC()
135	buf := bytes.NewBuffer(nil)
136	if err := WriteHeapProfile(buf); err != nil {
137		t.Fatalf("writing profile: %v", err)
138	}
139	p, err := profile.Parse(buf)
140	if err != nil {
141		t.Fatalf("profile.Parse: %v", err)
142	}
143
144	actual := profileToStrings(p)
145	expected := []string{
146		"testing.tRunner;runtime/pprof.TestGenericsHashKeyInPprofBuilder;runtime/pprof.genericAllocFunc[go.shape.uint32] [1 128 0 0]",
147		"testing.tRunner;runtime/pprof.TestGenericsHashKeyInPprofBuilder;runtime/pprof.genericAllocFunc[go.shape.uint32] [1 256 0 0]",
148		"testing.tRunner;runtime/pprof.TestGenericsHashKeyInPprofBuilder;runtime/pprof.genericAllocFunc[go.shape.uint64] [1 32 0 0]",
149		"testing.tRunner;runtime/pprof.TestGenericsHashKeyInPprofBuilder;runtime/pprof.genericAllocFunc[go.shape.uint64] [1 64 0 0]",
150	}
151
152	for _, l := range expected {
153		if !slices.Contains(actual, l) {
154			t.Errorf("profile = %v\nwant = %v", strings.Join(actual, "\n"), l)
155		}
156	}
157}
158
159type opAlloc struct {
160	buf [128]byte
161}
162
163type opCall struct {
164}
165
166var sink []byte
167
168func storeAlloc() {
169	sink = make([]byte, 16)
170}
171
172func nonRecursiveGenericAllocFunction[CurrentOp any, OtherOp any](alloc bool) {
173	if alloc {
174		storeAlloc()
175	} else {
176		nonRecursiveGenericAllocFunction[OtherOp, CurrentOp](true)
177	}
178}
179
180func TestGenericsInlineLocations(t *testing.T) {
181	if testenv.OptimizationOff() {
182		t.Skip("skipping test with optimizations disabled")
183	}
184
185	previousRate := runtime.MemProfileRate
186	runtime.MemProfileRate = 1
187	defer func() {
188		runtime.MemProfileRate = previousRate
189		sink = nil
190	}()
191
192	nonRecursiveGenericAllocFunction[opAlloc, opCall](true)
193	nonRecursiveGenericAllocFunction[opCall, opAlloc](false)
194
195	runtime.GC()
196
197	buf := bytes.NewBuffer(nil)
198	if err := WriteHeapProfile(buf); err != nil {
199		t.Fatalf("writing profile: %v", err)
200	}
201	p, err := profile.Parse(buf)
202	if err != nil {
203		t.Fatalf("profile.Parse: %v", err)
204	}
205
206	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]"
207	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"
208	const expectedLocationNewInliner = "runtime/pprof.TestGenericsInlineLocations;" + expectedLocation
209	var s *profile.Sample
210	for _, sample := range p.Sample {
211		if sampleToString(sample) == expectedSample {
212			s = sample
213			break
214		}
215	}
216	if s == nil {
217		t.Fatalf("expected \n%s\ngot\n%s", expectedSample, strings.Join(profileToStrings(p), "\n"))
218	}
219	loc := s.Location[0]
220	actual := strings.Join(locationToStrings(loc, nil), ";")
221	if expectedLocation != actual && expectedLocationNewInliner != actual {
222		t.Errorf("expected a location with at least 3 functions\n%s\ngot\n%s\n", expectedLocation, actual)
223	}
224}
225