1// Copyright 2014 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
5//go:build !js
6
7package pprof
8
9import (
10	"bytes"
11	"fmt"
12	"internal/profile"
13	"reflect"
14	"regexp"
15	"runtime"
16	"testing"
17	"unsafe"
18)
19
20var memSink any
21
22func allocateTransient1M() {
23	for i := 0; i < 1024; i++ {
24		memSink = &struct{ x [1024]byte }{}
25	}
26}
27
28//go:noinline
29func allocateTransient2M() {
30	memSink = make([]byte, 2<<20)
31}
32
33func allocateTransient2MInline() {
34	memSink = make([]byte, 2<<20)
35}
36
37type Obj32 struct {
38	link *Obj32
39	pad  [32 - unsafe.Sizeof(uintptr(0))]byte
40}
41
42var persistentMemSink *Obj32
43
44func allocatePersistent1K() {
45	for i := 0; i < 32; i++ {
46		// Can't use slice because that will introduce implicit allocations.
47		obj := &Obj32{link: persistentMemSink}
48		persistentMemSink = obj
49	}
50}
51
52// Allocate transient memory using reflect.Call.
53
54func allocateReflectTransient() {
55	memSink = make([]byte, 2<<20)
56}
57
58func allocateReflect() {
59	rv := reflect.ValueOf(allocateReflectTransient)
60	rv.Call(nil)
61}
62
63var memoryProfilerRun = 0
64
65func TestMemoryProfiler(t *testing.T) {
66	// Disable sampling, otherwise it's difficult to assert anything.
67	oldRate := runtime.MemProfileRate
68	runtime.MemProfileRate = 1
69	defer func() {
70		runtime.MemProfileRate = oldRate
71	}()
72
73	// Allocate a meg to ensure that mcache.nextSample is updated to 1.
74	for i := 0; i < 1024; i++ {
75		memSink = make([]byte, 1024)
76	}
77
78	// Do the interesting allocations.
79	allocateTransient1M()
80	allocateTransient2M()
81	allocateTransient2MInline()
82	allocatePersistent1K()
83	allocateReflect()
84	memSink = nil
85
86	runtime.GC() // materialize stats
87
88	memoryProfilerRun++
89
90	tests := []struct {
91		stk    []string
92		legacy string
93	}{{
94		stk: []string{"runtime/pprof.allocatePersistent1K", "runtime/pprof.TestMemoryProfiler"},
95		legacy: fmt.Sprintf(`%v: %v \[%v: %v\] @ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+
96#	0x[0-9,a-f]+	runtime/pprof\.allocatePersistent1K\+0x[0-9,a-f]+	.*runtime/pprof/mprof_test\.go:47
97#	0x[0-9,a-f]+	runtime/pprof\.TestMemoryProfiler\+0x[0-9,a-f]+	.*runtime/pprof/mprof_test\.go:82
98`, 32*memoryProfilerRun, 1024*memoryProfilerRun, 32*memoryProfilerRun, 1024*memoryProfilerRun),
99	}, {
100		stk: []string{"runtime/pprof.allocateTransient1M", "runtime/pprof.TestMemoryProfiler"},
101		legacy: fmt.Sprintf(`0: 0 \[%v: %v\] @ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+
102#	0x[0-9,a-f]+	runtime/pprof\.allocateTransient1M\+0x[0-9,a-f]+	.*runtime/pprof/mprof_test.go:24
103#	0x[0-9,a-f]+	runtime/pprof\.TestMemoryProfiler\+0x[0-9,a-f]+	.*runtime/pprof/mprof_test.go:79
104`, (1<<10)*memoryProfilerRun, (1<<20)*memoryProfilerRun),
105	}, {
106		stk: []string{"runtime/pprof.allocateTransient2M", "runtime/pprof.TestMemoryProfiler"},
107		legacy: fmt.Sprintf(`0: 0 \[%v: %v\] @ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+
108#	0x[0-9,a-f]+	runtime/pprof\.allocateTransient2M\+0x[0-9,a-f]+	.*runtime/pprof/mprof_test.go:30
109#	0x[0-9,a-f]+	runtime/pprof\.TestMemoryProfiler\+0x[0-9,a-f]+	.*runtime/pprof/mprof_test.go:80
110`, memoryProfilerRun, (2<<20)*memoryProfilerRun),
111	}, {
112		stk: []string{"runtime/pprof.allocateTransient2MInline", "runtime/pprof.TestMemoryProfiler"},
113		legacy: fmt.Sprintf(`0: 0 \[%v: %v\] @ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+ 0x[0-9,a-f]+
114#	0x[0-9,a-f]+	runtime/pprof\.allocateTransient2MInline\+0x[0-9,a-f]+	.*runtime/pprof/mprof_test.go:34
115#	0x[0-9,a-f]+	runtime/pprof\.TestMemoryProfiler\+0x[0-9,a-f]+	.*runtime/pprof/mprof_test.go:81
116`, memoryProfilerRun, (2<<20)*memoryProfilerRun),
117	}, {
118		stk: []string{"runtime/pprof.allocateReflectTransient"},
119		legacy: fmt.Sprintf(`0: 0 \[%v: %v\] @( 0x[0-9,a-f]+)+
120#	0x[0-9,a-f]+	runtime/pprof\.allocateReflectTransient\+0x[0-9,a-f]+	.*runtime/pprof/mprof_test.go:55
121`, memoryProfilerRun, (2<<20)*memoryProfilerRun),
122	}}
123
124	t.Run("debug=1", func(t *testing.T) {
125		var buf bytes.Buffer
126		if err := Lookup("heap").WriteTo(&buf, 1); err != nil {
127			t.Fatalf("failed to write heap profile: %v", err)
128		}
129
130		for _, test := range tests {
131			if !regexp.MustCompile(test.legacy).Match(buf.Bytes()) {
132				t.Fatalf("The entry did not match:\n%v\n\nProfile:\n%v\n", test.legacy, buf.String())
133			}
134		}
135	})
136
137	t.Run("proto", func(t *testing.T) {
138		var buf bytes.Buffer
139		if err := Lookup("heap").WriteTo(&buf, 0); err != nil {
140			t.Fatalf("failed to write heap profile: %v", err)
141		}
142		p, err := profile.Parse(&buf)
143		if err != nil {
144			t.Fatalf("failed to parse heap profile: %v", err)
145		}
146		t.Logf("Profile = %v", p)
147
148		stks := stacks(p)
149		for _, test := range tests {
150			if !containsStack(stks, test.stk) {
151				t.Fatalf("No matching stack entry for %q\n\nProfile:\n%v\n", test.stk, p)
152			}
153		}
154
155		if !containsInlinedCall(TestMemoryProfiler, 4<<10) {
156			t.Logf("Can't determine whether allocateTransient2MInline was inlined into TestMemoryProfiler.")
157			return
158		}
159
160		// Check the inlined function location is encoded correctly.
161		for _, loc := range p.Location {
162			inlinedCaller, inlinedCallee := false, false
163			for _, line := range loc.Line {
164				if line.Function.Name == "runtime/pprof.allocateTransient2MInline" {
165					inlinedCallee = true
166				}
167				if inlinedCallee && line.Function.Name == "runtime/pprof.TestMemoryProfiler" {
168					inlinedCaller = true
169				}
170			}
171			if inlinedCallee != inlinedCaller {
172				t.Errorf("want allocateTransient2MInline after TestMemoryProfiler in one location, got separate location entries:\n%v", loc)
173			}
174		}
175	})
176}
177