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	"encoding/json"
10	"fmt"
11	"internal/abi"
12	"internal/profile"
13	"internal/testenv"
14	"os"
15	"os/exec"
16	"reflect"
17	"runtime"
18	"strings"
19	"testing"
20	"unsafe"
21)
22
23// translateCPUProfile parses binary CPU profiling stack trace data
24// generated by runtime.CPUProfile() into a profile struct.
25// This is only used for testing. Real conversions stream the
26// data into the profileBuilder as it becomes available.
27//
28// count is the number of records in data.
29func translateCPUProfile(data []uint64, count int) (*profile.Profile, error) {
30	var buf bytes.Buffer
31	b := newProfileBuilder(&buf)
32	tags := make([]unsafe.Pointer, count)
33	if err := b.addCPUData(data, tags); err != nil {
34		return nil, err
35	}
36	b.build()
37	return profile.Parse(&buf)
38}
39
40// fmtJSON returns a pretty-printed JSON form for x.
41// It works reasonably well for printing protocol-buffer
42// data structures like profile.Profile.
43func fmtJSON(x any) string {
44	js, _ := json.MarshalIndent(x, "", "\t")
45	return string(js)
46}
47
48func TestConvertCPUProfileNoSamples(t *testing.T) {
49	// A test server with mock cpu profile data.
50	var buf bytes.Buffer
51
52	b := []uint64{3, 0, 500} // empty profile at 500 Hz (2ms sample period)
53	p, err := translateCPUProfile(b, 1)
54	if err != nil {
55		t.Fatalf("translateCPUProfile: %v", err)
56	}
57	if err := p.Write(&buf); err != nil {
58		t.Fatalf("writing profile: %v", err)
59	}
60
61	p, err = profile.Parse(&buf)
62	if err != nil {
63		t.Fatalf("profile.Parse: %v", err)
64	}
65
66	// Expected PeriodType and SampleType.
67	periodType := &profile.ValueType{Type: "cpu", Unit: "nanoseconds"}
68	sampleType := []*profile.ValueType{
69		{Type: "samples", Unit: "count"},
70		{Type: "cpu", Unit: "nanoseconds"},
71	}
72
73	checkProfile(t, p, 2000*1000, periodType, sampleType, nil, "")
74}
75
76func f1() { f1() }
77func f2() { f2() }
78
79// testPCs returns two PCs and two corresponding memory mappings
80// to use in test profiles.
81func testPCs(t *testing.T) (addr1, addr2 uint64, map1, map2 *profile.Mapping) {
82	switch runtime.GOOS {
83	case "linux", "android", "netbsd":
84		// Figure out two addresses from /proc/self/maps.
85		mmap, err := os.ReadFile("/proc/self/maps")
86		if err != nil {
87			t.Fatal(err)
88		}
89		var mappings []*profile.Mapping
90		id := uint64(1)
91		parseProcSelfMaps(mmap, func(lo, hi, offset uint64, file, buildID string) {
92			mappings = append(mappings, &profile.Mapping{
93				ID:      id,
94				Start:   lo,
95				Limit:   hi,
96				Offset:  offset,
97				File:    file,
98				BuildID: buildID,
99			})
100			id++
101		})
102		if len(mappings) < 2 {
103			// It is possible for a binary to only have 1 executable
104			// region of memory.
105			t.Skipf("need 2 or more mappings, got %v", len(mappings))
106		}
107		addr1 = mappings[0].Start
108		map1 = mappings[0]
109		addr2 = mappings[1].Start
110		map2 = mappings[1]
111	case "windows", "darwin", "ios":
112		addr1 = uint64(abi.FuncPCABIInternal(f1))
113		addr2 = uint64(abi.FuncPCABIInternal(f2))
114
115		start, end, exe, buildID, err := readMainModuleMapping()
116		if err != nil {
117			t.Fatal(err)
118		}
119
120		map1 = &profile.Mapping{
121			ID:           1,
122			Start:        start,
123			Limit:        end,
124			File:         exe,
125			BuildID:      buildID,
126			HasFunctions: true,
127		}
128		map2 = &profile.Mapping{
129			ID:           1,
130			Start:        start,
131			Limit:        end,
132			File:         exe,
133			BuildID:      buildID,
134			HasFunctions: true,
135		}
136	case "js", "wasip1":
137		addr1 = uint64(abi.FuncPCABIInternal(f1))
138		addr2 = uint64(abi.FuncPCABIInternal(f2))
139	default:
140		addr1 = uint64(abi.FuncPCABIInternal(f1))
141		addr2 = uint64(abi.FuncPCABIInternal(f2))
142		// Fake mapping - HasFunctions will be true because two PCs from Go
143		// will be fully symbolized.
144		fake := &profile.Mapping{ID: 1, HasFunctions: true}
145		map1, map2 = fake, fake
146	}
147	return
148}
149
150func TestConvertCPUProfile(t *testing.T) {
151	addr1, addr2, map1, map2 := testPCs(t)
152
153	b := []uint64{
154		3, 0, 500, // hz = 500
155		5, 0, 10, uint64(addr1 + 1), uint64(addr1 + 2), // 10 samples in addr1
156		5, 0, 40, uint64(addr2 + 1), uint64(addr2 + 2), // 40 samples in addr2
157		5, 0, 10, uint64(addr1 + 1), uint64(addr1 + 2), // 10 samples in addr1
158	}
159	p, err := translateCPUProfile(b, 4)
160	if err != nil {
161		t.Fatalf("translating profile: %v", err)
162	}
163	period := int64(2000 * 1000)
164	periodType := &profile.ValueType{Type: "cpu", Unit: "nanoseconds"}
165	sampleType := []*profile.ValueType{
166		{Type: "samples", Unit: "count"},
167		{Type: "cpu", Unit: "nanoseconds"},
168	}
169	samples := []*profile.Sample{
170		{Value: []int64{20, 20 * 2000 * 1000}, Location: []*profile.Location{
171			{ID: 1, Mapping: map1, Address: addr1},
172			{ID: 2, Mapping: map1, Address: addr1 + 1},
173		}},
174		{Value: []int64{40, 40 * 2000 * 1000}, Location: []*profile.Location{
175			{ID: 3, Mapping: map2, Address: addr2},
176			{ID: 4, Mapping: map2, Address: addr2 + 1},
177		}},
178	}
179	checkProfile(t, p, period, periodType, sampleType, samples, "")
180}
181
182func checkProfile(t *testing.T, p *profile.Profile, period int64, periodType *profile.ValueType, sampleType []*profile.ValueType, samples []*profile.Sample, defaultSampleType string) {
183	t.Helper()
184
185	if p.Period != period {
186		t.Errorf("p.Period = %d, want %d", p.Period, period)
187	}
188	if !reflect.DeepEqual(p.PeriodType, periodType) {
189		t.Errorf("p.PeriodType = %v\nwant = %v", fmtJSON(p.PeriodType), fmtJSON(periodType))
190	}
191	if !reflect.DeepEqual(p.SampleType, sampleType) {
192		t.Errorf("p.SampleType = %v\nwant = %v", fmtJSON(p.SampleType), fmtJSON(sampleType))
193	}
194	if defaultSampleType != p.DefaultSampleType {
195		t.Errorf("p.DefaultSampleType = %v\nwant = %v", p.DefaultSampleType, defaultSampleType)
196	}
197	// Clear line info since it is not in the expected samples.
198	// If we used f1 and f2 above, then the samples will have line info.
199	for _, s := range p.Sample {
200		for _, l := range s.Location {
201			l.Line = nil
202		}
203	}
204	if fmtJSON(p.Sample) != fmtJSON(samples) { // ignore unexported fields
205		if len(p.Sample) == len(samples) {
206			for i := range p.Sample {
207				if !reflect.DeepEqual(p.Sample[i], samples[i]) {
208					t.Errorf("sample %d = %v\nwant = %v\n", i, fmtJSON(p.Sample[i]), fmtJSON(samples[i]))
209				}
210			}
211			if t.Failed() {
212				t.FailNow()
213			}
214		}
215		t.Fatalf("p.Sample = %v\nwant = %v", fmtJSON(p.Sample), fmtJSON(samples))
216	}
217}
218
219var profSelfMapsTests = `
22000400000-0040b000 r-xp 00000000 fc:01 787766                             /bin/cat
2210060a000-0060b000 r--p 0000a000 fc:01 787766                             /bin/cat
2220060b000-0060c000 rw-p 0000b000 fc:01 787766                             /bin/cat
223014ab000-014cc000 rw-p 00000000 00:00 0                                  [heap]
2247f7d76af8000-7f7d7797c000 r--p 00000000 fc:01 1318064                    /usr/lib/locale/locale-archive
2257f7d7797c000-7f7d77b36000 r-xp 00000000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
2267f7d77b36000-7f7d77d36000 ---p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
2277f7d77d36000-7f7d77d3a000 r--p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
2287f7d77d3a000-7f7d77d3c000 rw-p 001be000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
2297f7d77d3c000-7f7d77d41000 rw-p 00000000 00:00 0
2307f7d77d41000-7f7d77d64000 r-xp 00000000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
2317f7d77f3f000-7f7d77f42000 rw-p 00000000 00:00 0
2327f7d77f61000-7f7d77f63000 rw-p 00000000 00:00 0
2337f7d77f63000-7f7d77f64000 r--p 00022000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
2347f7d77f64000-7f7d77f65000 rw-p 00023000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
2357f7d77f65000-7f7d77f66000 rw-p 00000000 00:00 0
2367ffc342a2000-7ffc342c3000 rw-p 00000000 00:00 0                          [stack]
2377ffc34343000-7ffc34345000 r-xp 00000000 00:00 0                          [vdso]
238ffffffffff600000-ffffffffff601000 r-xp 00000090 00:00 0                  [vsyscall]
239->
24000400000 0040b000 00000000 /bin/cat
2417f7d7797c000 7f7d77b36000 00000000 /lib/x86_64-linux-gnu/libc-2.19.so
2427f7d77d41000 7f7d77d64000 00000000 /lib/x86_64-linux-gnu/ld-2.19.so
2437ffc34343000 7ffc34345000 00000000 [vdso]
244ffffffffff600000 ffffffffff601000 00000090 [vsyscall]
245
24600400000-07000000 r-xp 00000000 00:00 0
24707000000-07093000 r-xp 06c00000 00:2e 536754                             /path/to/gobench_server_main
24807093000-0722d000 rw-p 06c92000 00:2e 536754                             /path/to/gobench_server_main
2490722d000-07b21000 rw-p 00000000 00:00 0
250c000000000-c000036000 rw-p 00000000 00:00 0
251->
25207000000 07093000 06c00000 /path/to/gobench_server_main
253`
254
255var profSelfMapsTestsWithDeleted = `
25600400000-0040b000 r-xp 00000000 fc:01 787766                             /bin/cat (deleted)
2570060a000-0060b000 r--p 0000a000 fc:01 787766                             /bin/cat (deleted)
2580060b000-0060c000 rw-p 0000b000 fc:01 787766                             /bin/cat (deleted)
259014ab000-014cc000 rw-p 00000000 00:00 0                                  [heap]
2607f7d76af8000-7f7d7797c000 r--p 00000000 fc:01 1318064                    /usr/lib/locale/locale-archive
2617f7d7797c000-7f7d77b36000 r-xp 00000000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
2627f7d77b36000-7f7d77d36000 ---p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
2637f7d77d36000-7f7d77d3a000 r--p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
2647f7d77d3a000-7f7d77d3c000 rw-p 001be000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
2657f7d77d3c000-7f7d77d41000 rw-p 00000000 00:00 0
2667f7d77d41000-7f7d77d64000 r-xp 00000000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
2677f7d77f3f000-7f7d77f42000 rw-p 00000000 00:00 0
2687f7d77f61000-7f7d77f63000 rw-p 00000000 00:00 0
2697f7d77f63000-7f7d77f64000 r--p 00022000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
2707f7d77f64000-7f7d77f65000 rw-p 00023000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
2717f7d77f65000-7f7d77f66000 rw-p 00000000 00:00 0
2727ffc342a2000-7ffc342c3000 rw-p 00000000 00:00 0                          [stack]
2737ffc34343000-7ffc34345000 r-xp 00000000 00:00 0                          [vdso]
274ffffffffff600000-ffffffffff601000 r-xp 00000090 00:00 0                  [vsyscall]
275->
27600400000 0040b000 00000000 /bin/cat
2777f7d7797c000 7f7d77b36000 00000000 /lib/x86_64-linux-gnu/libc-2.19.so
2787f7d77d41000 7f7d77d64000 00000000 /lib/x86_64-linux-gnu/ld-2.19.so
2797ffc34343000 7ffc34345000 00000000 [vdso]
280ffffffffff600000 ffffffffff601000 00000090 [vsyscall]
281
28200400000-0040b000 r-xp 00000000 fc:01 787766                             /bin/cat with space
2830060a000-0060b000 r--p 0000a000 fc:01 787766                             /bin/cat with space
2840060b000-0060c000 rw-p 0000b000 fc:01 787766                             /bin/cat with space
285014ab000-014cc000 rw-p 00000000 00:00 0                                  [heap]
2867f7d76af8000-7f7d7797c000 r--p 00000000 fc:01 1318064                    /usr/lib/locale/locale-archive
2877f7d7797c000-7f7d77b36000 r-xp 00000000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
2887f7d77b36000-7f7d77d36000 ---p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
2897f7d77d36000-7f7d77d3a000 r--p 001ba000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
2907f7d77d3a000-7f7d77d3c000 rw-p 001be000 fc:01 1180226                    /lib/x86_64-linux-gnu/libc-2.19.so
2917f7d77d3c000-7f7d77d41000 rw-p 00000000 00:00 0
2927f7d77d41000-7f7d77d64000 r-xp 00000000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
2937f7d77f3f000-7f7d77f42000 rw-p 00000000 00:00 0
2947f7d77f61000-7f7d77f63000 rw-p 00000000 00:00 0
2957f7d77f63000-7f7d77f64000 r--p 00022000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
2967f7d77f64000-7f7d77f65000 rw-p 00023000 fc:01 1180217                    /lib/x86_64-linux-gnu/ld-2.19.so
2977f7d77f65000-7f7d77f66000 rw-p 00000000 00:00 0
2987ffc342a2000-7ffc342c3000 rw-p 00000000 00:00 0                          [stack]
2997ffc34343000-7ffc34345000 r-xp 00000000 00:00 0                          [vdso]
300ffffffffff600000-ffffffffff601000 r-xp 00000090 00:00 0                  [vsyscall]
301->
30200400000 0040b000 00000000 /bin/cat with space
3037f7d7797c000 7f7d77b36000 00000000 /lib/x86_64-linux-gnu/libc-2.19.so
3047f7d77d41000 7f7d77d64000 00000000 /lib/x86_64-linux-gnu/ld-2.19.so
3057ffc34343000 7ffc34345000 00000000 [vdso]
306ffffffffff600000 ffffffffff601000 00000090 [vsyscall]
307`
308
309func TestProcSelfMaps(t *testing.T) {
310
311	f := func(t *testing.T, input string) {
312		for tx, tt := range strings.Split(input, "\n\n") {
313			in, out, ok := strings.Cut(tt, "->\n")
314			if !ok {
315				t.Fatal("malformed test case")
316			}
317			if len(out) > 0 && out[len(out)-1] != '\n' {
318				out += "\n"
319			}
320			var buf strings.Builder
321			parseProcSelfMaps([]byte(in), func(lo, hi, offset uint64, file, buildID string) {
322				fmt.Fprintf(&buf, "%08x %08x %08x %s\n", lo, hi, offset, file)
323			})
324			if buf.String() != out {
325				t.Errorf("#%d: have:\n%s\nwant:\n%s\n%q\n%q", tx, buf.String(), out, buf.String(), out)
326			}
327		}
328	}
329
330	t.Run("Normal", func(t *testing.T) {
331		f(t, profSelfMapsTests)
332	})
333
334	t.Run("WithDeletedFile", func(t *testing.T) {
335		f(t, profSelfMapsTestsWithDeleted)
336	})
337}
338
339// TestMapping checks the mapping section of CPU profiles
340// has the HasFunctions field set correctly. If all PCs included
341// in the samples are successfully symbolized, the corresponding
342// mapping entry (in this test case, only one entry) should have
343// its HasFunctions field set true.
344// The test generates a CPU profile that includes PCs from C side
345// that the runtime can't symbolize. See ./testdata/mappingtest.
346func TestMapping(t *testing.T) {
347	testenv.MustHaveGoRun(t)
348	testenv.MustHaveCGO(t)
349
350	prog := "./testdata/mappingtest/main.go"
351
352	// GoOnly includes only Go symbols that runtime will symbolize.
353	// Go+C includes C symbols that runtime will not symbolize.
354	for _, traceback := range []string{"GoOnly", "Go+C"} {
355		t.Run("traceback"+traceback, func(t *testing.T) {
356			cmd := exec.Command(testenv.GoToolPath(t), "run", prog)
357			if traceback != "GoOnly" {
358				cmd.Env = append(os.Environ(), "SETCGOTRACEBACK=1")
359			}
360			cmd.Stderr = new(bytes.Buffer)
361
362			out, err := cmd.Output()
363			if err != nil {
364				t.Fatalf("failed to run the test program %q: %v\n%v", prog, err, cmd.Stderr)
365			}
366
367			prof, err := profile.Parse(bytes.NewReader(out))
368			if err != nil {
369				t.Fatalf("failed to parse the generated profile data: %v", err)
370			}
371			t.Logf("Profile: %s", prof)
372
373			hit := make(map[*profile.Mapping]bool)
374			miss := make(map[*profile.Mapping]bool)
375			for _, loc := range prof.Location {
376				if symbolized(loc) {
377					hit[loc.Mapping] = true
378				} else {
379					miss[loc.Mapping] = true
380				}
381			}
382			if len(miss) == 0 {
383				t.Log("no location with missing symbol info was sampled")
384			}
385
386			for _, m := range prof.Mapping {
387				if miss[m] && m.HasFunctions {
388					t.Errorf("mapping %+v has HasFunctions=true, but contains locations with failed symbolization", m)
389					continue
390				}
391				if !miss[m] && hit[m] && !m.HasFunctions {
392					t.Errorf("mapping %+v has HasFunctions=false, but all referenced locations from this lapping were symbolized successfully", m)
393					continue
394				}
395			}
396
397			if traceback == "Go+C" {
398				// The test code was arranged to have PCs from C and
399				// they are not symbolized.
400				// Check no Location containing those unsymbolized PCs contains multiple lines.
401				for i, loc := range prof.Location {
402					if !symbolized(loc) && len(loc.Line) > 1 {
403						t.Errorf("Location[%d] contains unsymbolized PCs and multiple lines: %v", i, loc)
404					}
405				}
406			}
407		})
408	}
409}
410
411func symbolized(loc *profile.Location) bool {
412	if len(loc.Line) == 0 {
413		return false
414	}
415	l := loc.Line[0]
416	f := l.Function
417	if l.Line == 0 || f == nil || f.Name == "" || f.Filename == "" {
418		return false
419	}
420	return true
421}
422
423// TestFakeMapping tests if at least one mapping exists
424// (including a fake mapping), and their HasFunctions bits
425// are set correctly.
426func TestFakeMapping(t *testing.T) {
427	var buf bytes.Buffer
428	if err := Lookup("heap").WriteTo(&buf, 0); err != nil {
429		t.Fatalf("failed to write heap profile: %v", err)
430	}
431	prof, err := profile.Parse(&buf)
432	if err != nil {
433		t.Fatalf("failed to parse the generated profile data: %v", err)
434	}
435	t.Logf("Profile: %s", prof)
436	if len(prof.Mapping) == 0 {
437		t.Fatal("want profile with at least one mapping entry, got 0 mapping")
438	}
439
440	hit := make(map[*profile.Mapping]bool)
441	miss := make(map[*profile.Mapping]bool)
442	for _, loc := range prof.Location {
443		if symbolized(loc) {
444			hit[loc.Mapping] = true
445		} else {
446			miss[loc.Mapping] = true
447		}
448	}
449	for _, m := range prof.Mapping {
450		if miss[m] && m.HasFunctions {
451			t.Errorf("mapping %+v has HasFunctions=true, but contains locations with failed symbolization", m)
452			continue
453		}
454		if !miss[m] && hit[m] && !m.HasFunctions {
455			t.Errorf("mapping %+v has HasFunctions=false, but all referenced locations from this lapping were symbolized successfully", m)
456			continue
457		}
458	}
459}
460
461// Make sure the profiler can handle an empty stack trace.
462// See issue 37967.
463func TestEmptyStack(t *testing.T) {
464	b := []uint64{
465		3, 0, 500, // hz = 500
466		3, 0, 10, // 10 samples with an empty stack trace
467	}
468	_, err := translateCPUProfile(b, 2)
469	if err != nil {
470		t.Fatalf("translating profile: %v", err)
471	}
472}
473