1// Unless explicitly stated otherwise all files in this repository are licensed
2// under the Apache License Version 2.0.
3// This product includes software developed at Datadog (https://www.datadoghq.com/).
4// Copyright 2016 Datadog, Inc.
5
6package profiler
7
8import (
9	"bytes"
10	"io"
11	"io/ioutil"
12	"testing"
13	"time"
14
15	pprofile "github.com/google/pprof/profile"
16	"github.com/stretchr/testify/assert"
17	"github.com/stretchr/testify/require"
18)
19
20func TestRunProfile(t *testing.T) {
21	t.Run("heap", func(t *testing.T) {
22		defer func(old func(_ io.Writer) error) { writeHeapProfile = old }(writeHeapProfile)
23		writeHeapProfile = func(w io.Writer) error {
24			_, err := w.Write([]byte("my-heap-profile"))
25			return err
26		}
27		p, err := unstartedProfiler()
28		prof, err := p.runProfile(HeapProfile)
29		require.NoError(t, err)
30		assert.Equal(t, "heap.pprof", prof.name)
31		assert.Equal(t, []byte("my-heap-profile"), prof.data)
32	})
33
34	t.Run("cpu", func(t *testing.T) {
35		defer func(old func(_ io.Writer) error) { startCPUProfile = old }(startCPUProfile)
36		startCPUProfile = func(w io.Writer) error {
37			_, err := w.Write([]byte("my-cpu-profile"))
38			return err
39		}
40		defer func(old func()) { stopCPUProfile = old }(stopCPUProfile)
41		stopCPUProfile = func() {}
42
43		p, err := unstartedProfiler(CPUDuration(10 * time.Millisecond))
44		start := time.Now()
45		prof, err := p.runProfile(CPUProfile)
46		end := time.Now()
47		require.NoError(t, err)
48		assert.True(t, end.Sub(start) > 10*time.Millisecond)
49		assert.Equal(t, "cpu.pprof", prof.name)
50		assert.Equal(t, []byte("my-cpu-profile"), prof.data)
51	})
52
53	t.Run("mutex", func(t *testing.T) {
54		defer func(old func(_ string, _ io.Writer, _ int) error) { lookupProfile = old }(lookupProfile)
55		lookupProfile = func(name string, w io.Writer, _ int) error {
56			_, err := w.Write([]byte(name))
57			return err
58		}
59
60		p, err := unstartedProfiler()
61		prof, err := p.runProfile(MutexProfile)
62		require.NoError(t, err)
63		assert.Equal(t, "mutex.pprof", prof.name)
64		assert.Equal(t, []byte("mutex"), prof.data)
65	})
66
67	t.Run("block", func(t *testing.T) {
68		defer func(old func(_ string, _ io.Writer, _ int) error) { lookupProfile = old }(lookupProfile)
69		lookupProfile = func(name string, w io.Writer, _ int) error {
70			_, err := w.Write([]byte(name))
71			return err
72		}
73
74		p, err := unstartedProfiler()
75		prof, err := p.runProfile(BlockProfile)
76		require.NoError(t, err)
77		assert.Equal(t, "block.pprof", prof.name)
78		assert.Equal(t, []byte("block"), prof.data)
79	})
80
81	t.Run("goroutine", func(t *testing.T) {
82		defer func(old func(_ string, _ io.Writer, _ int) error) { lookupProfile = old }(lookupProfile)
83		lookupProfile = func(name string, w io.Writer, _ int) error {
84			_, err := w.Write([]byte(name))
85			return err
86		}
87
88		p, err := unstartedProfiler()
89		prof, err := p.runProfile(GoroutineProfile)
90		require.NoError(t, err)
91		assert.Equal(t, "goroutines.pprof", prof.name)
92		assert.Equal(t, []byte("goroutine"), prof.data)
93	})
94
95	t.Run("goroutinewait", func(t *testing.T) {
96		const sample = `
97goroutine 1 [running]:
98main.main()
99	/example/main.go:152 +0x3d2
100
101goroutine 2 [running]:
102badFunctionCall)(
103
104goroutine 3 [sleep, 1 minutes]:
105time.Sleep(0x3b9aca00)
106	/usr/local/Cellar/go/1.15.6/libexec/src/runtime/time.go:188 +0xbf
107created by main.indirectShortSleepLoop2
108	/example/main.go:185 +0x35
109
110goroutine 4 [running]:
111main.stackDump(0x62)
112	/example/max_frames.go:20 +0x131
113main.main()
114	/example/max_frames.go:11 +0x2a
115...additional frames elided...
116`
117
118		defer func(old func(_ string, _ io.Writer, _ int) error) { lookupProfile = old }(lookupProfile)
119		lookupProfile = func(name string, w io.Writer, _ int) error {
120			_, err := w.Write([]byte(sample))
121			return err
122		}
123
124		p, err := unstartedProfiler()
125		prof, err := p.runProfile(expGoroutineWaitProfile)
126		require.NoError(t, err)
127		require.Equal(t, "goroutineswait.pprof", prof.name)
128
129		// pro tip: enable line below to inspect the pprof output using cli tools
130		// ioutil.WriteFile(prof.name, prof.data, 0644)
131
132		requireFunctions := func(t *testing.T, s *pprofile.Sample, want []string) {
133			t.Helper()
134			var got []string
135			for _, loc := range s.Location {
136				got = append(got, loc.Line[0].Function.Name)
137			}
138			require.Equal(t, want, got)
139		}
140
141		pp, err := pprofile.Parse(bytes.NewReader(prof.data))
142		require.NoError(t, err)
143		// timestamp
144		require.NotEqual(t, int64(0), pp.TimeNanos)
145		// 1 sample type
146		require.Equal(t, 1, len(pp.SampleType))
147		// 3 valid samples, 1 invalid sample (added as comment)
148		require.Equal(t, 3, len(pp.Sample))
149		require.Equal(t, 1, len(pp.Comments))
150		// Wait duration
151		require.Equal(t, []int64{time.Minute.Nanoseconds()}, pp.Sample[1].Value)
152		// Labels
153		require.Equal(t, []string{"running"}, pp.Sample[0].Label["state"])
154		require.Equal(t, []string{"false"}, pp.Sample[0].Label["lockedm"])
155		require.Equal(t, []int64{3}, pp.Sample[1].NumLabel["goid"])
156		require.Equal(t, []string{"id"}, pp.Sample[1].NumUnit["goid"])
157		// Virtual frame for "frames elided" goroutine
158		requireFunctions(t, pp.Sample[2], []string{
159			"main.stackDump",
160			"main.main",
161			"...additional frames elided...",
162		})
163		// Virtual frame go "created by" frame
164		requireFunctions(t, pp.Sample[1], []string{
165			"time.Sleep",
166			"main.indirectShortSleepLoop2",
167		})
168	})
169}
170
171func Test_goroutineDebug2ToPprof_CrashSafety(t *testing.T) {
172	err := goroutineDebug2ToPprof(panicReader{}, ioutil.Discard, time.Time{})
173	require.NotNil(t, err)
174	require.Equal(t, "panic: 42", err.Error())
175}
176
177// panicReader is used to create a panic inside of stackparse.Parse()
178type panicReader struct{}
179
180func (c panicReader) Read(_ []byte) (int, error) {
181	panic("42")
182}
183