1// run
2
3// Copyright 2009 The Go Authors. All rights reserved.
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file.
6
7// Test heap sampling logic.
8
9package main
10
11import (
12	"fmt"
13	"math"
14	"runtime"
15)
16
17var a16 *[16]byte
18var a512 *[512]byte
19var a256 *[256]byte
20var a1k *[1024]byte
21var a16k *[16 * 1024]byte
22var a17k *[17 * 1024]byte
23var a18k *[18 * 1024]byte
24
25// This test checks that heap sampling produces reasonable results.
26// Note that heap sampling uses randomization, so the results vary for
27// run to run. To avoid flakes, this test performs multiple
28// experiments and only complains if all of them consistently fail.
29func main() {
30	// Sample at 16K instead of default 512K to exercise sampling more heavily.
31	runtime.MemProfileRate = 16 * 1024
32
33	if err := testInterleavedAllocations(); err != nil {
34		panic(err.Error())
35	}
36	if err := testSmallAllocations(); err != nil {
37		panic(err.Error())
38	}
39}
40
41// Repeatedly exercise a set of allocations and check that the heap
42// profile collected by the runtime unsamples to a reasonable
43// value. Because sampling is based on randomization, there can be
44// significant variability on the unsampled data. To account for that,
45// the testcase allows for a 10% margin of error, but only fails if it
46// consistently fails across three experiments, avoiding flakes.
47func testInterleavedAllocations() error {
48	const iters = 50000
49	// Sizes of the allocations performed by each experiment.
50	frames := []string{"main.allocInterleaved1", "main.allocInterleaved2", "main.allocInterleaved3"}
51
52	// Pass if at least one of three experiments has no errors. Use a separate
53	// function for each experiment to identify each experiment in the profile.
54	allocInterleaved1(iters)
55	if checkAllocations(getMemProfileRecords(), frames[0:1], iters, allocInterleavedSizes) == nil {
56		// Passed on first try, report no error.
57		return nil
58	}
59	allocInterleaved2(iters)
60	if checkAllocations(getMemProfileRecords(), frames[0:2], iters, allocInterleavedSizes) == nil {
61		// Passed on second try, report no error.
62		return nil
63	}
64	allocInterleaved3(iters)
65	// If it fails a third time, we may be onto something.
66	return checkAllocations(getMemProfileRecords(), frames[0:3], iters, allocInterleavedSizes)
67}
68
69var allocInterleavedSizes = []int64{17 * 1024, 1024, 18 * 1024, 512, 16 * 1024, 256}
70
71// allocInterleaved stress-tests the heap sampling logic by interleaving large and small allocations.
72func allocInterleaved(n int) {
73	for i := 0; i < n; i++ {
74		// Test verification depends on these lines being contiguous.
75		a17k = new([17 * 1024]byte)
76		a1k = new([1024]byte)
77		a18k = new([18 * 1024]byte)
78		a512 = new([512]byte)
79		a16k = new([16 * 1024]byte)
80		a256 = new([256]byte)
81		// Test verification depends on these lines being contiguous.
82
83		// Slow down the allocation rate to avoid #52433.
84		runtime.Gosched()
85	}
86}
87
88func allocInterleaved1(n int) {
89	allocInterleaved(n)
90}
91
92func allocInterleaved2(n int) {
93	allocInterleaved(n)
94}
95
96func allocInterleaved3(n int) {
97	allocInterleaved(n)
98}
99
100// Repeatedly exercise a set of allocations and check that the heap
101// profile collected by the runtime unsamples to a reasonable
102// value. Because sampling is based on randomization, there can be
103// significant variability on the unsampled data. To account for that,
104// the testcase allows for a 10% margin of error, but only fails if it
105// consistently fails across three experiments, avoiding flakes.
106func testSmallAllocations() error {
107	const iters = 50000
108	// Sizes of the allocations performed by each experiment.
109	sizes := []int64{1024, 512, 256}
110	frames := []string{"main.allocSmall1", "main.allocSmall2", "main.allocSmall3"}
111
112	// Pass if at least one of three experiments has no errors. Use a separate
113	// function for each experiment to identify each experiment in the profile.
114	allocSmall1(iters)
115	if checkAllocations(getMemProfileRecords(), frames[0:1], iters, sizes) == nil {
116		// Passed on first try, report no error.
117		return nil
118	}
119	allocSmall2(iters)
120	if checkAllocations(getMemProfileRecords(), frames[0:2], iters, sizes) == nil {
121		// Passed on second try, report no error.
122		return nil
123	}
124	allocSmall3(iters)
125	// If it fails a third time, we may be onto something.
126	return checkAllocations(getMemProfileRecords(), frames[0:3], iters, sizes)
127}
128
129// allocSmall performs only small allocations for sanity testing.
130func allocSmall(n int) {
131	for i := 0; i < n; i++ {
132		// Test verification depends on these lines being contiguous.
133		a1k = new([1024]byte)
134		a512 = new([512]byte)
135		a256 = new([256]byte)
136
137		// Slow down the allocation rate to avoid #52433.
138		runtime.Gosched()
139	}
140}
141
142// Three separate instances of testing to avoid flakes. Will report an error
143// only if they all consistently report failures.
144func allocSmall1(n int) {
145	allocSmall(n)
146}
147
148func allocSmall2(n int) {
149	allocSmall(n)
150}
151
152func allocSmall3(n int) {
153	allocSmall(n)
154}
155
156// checkAllocations validates that the profile records collected for
157// the named function are consistent with count contiguous allocations
158// of the specified sizes.
159// Check multiple functions and only report consistent failures across
160// multiple tests.
161// Look only at samples that include the named frames, and group the
162// allocations by their line number. All these allocations are done from
163// the same leaf function, so their line numbers are the same.
164func checkAllocations(records []runtime.MemProfileRecord, frames []string, count int64, size []int64) error {
165	objectsPerLine := map[int][]int64{}
166	bytesPerLine := map[int][]int64{}
167	totalCount := []int64{}
168	// Compute the line number of the first allocation. All the
169	// allocations are from the same leaf, so pick the first one.
170	var firstLine int
171	for ln := range allocObjects(records, frames[0]) {
172		if firstLine == 0 || firstLine > ln {
173			firstLine = ln
174		}
175	}
176	for _, frame := range frames {
177		var objectCount int64
178		a := allocObjects(records, frame)
179		for s := range size {
180			// Allocations of size size[s] should be on line firstLine + s.
181			ln := firstLine + s
182			objectsPerLine[ln] = append(objectsPerLine[ln], a[ln].objects)
183			bytesPerLine[ln] = append(bytesPerLine[ln], a[ln].bytes)
184			objectCount += a[ln].objects
185		}
186		totalCount = append(totalCount, objectCount)
187	}
188	for i, w := range size {
189		ln := firstLine + i
190		if err := checkValue(frames[0], ln, "objects", count, objectsPerLine[ln]); err != nil {
191			return err
192		}
193		if err := checkValue(frames[0], ln, "bytes", count*w, bytesPerLine[ln]); err != nil {
194			return err
195		}
196	}
197	return checkValue(frames[0], 0, "total", count*int64(len(size)), totalCount)
198}
199
200// checkValue checks an unsampled value against its expected value.
201// Given that this is a sampled value, it will be unexact and will change
202// from run to run. Only report it as a failure if all the values land
203// consistently far from the expected value.
204func checkValue(fname string, ln int, testName string, want int64, got []int64) error {
205	if got == nil {
206		return fmt.Errorf("Unexpected empty result")
207	}
208	min, max := got[0], got[0]
209	for _, g := range got[1:] {
210		if g < min {
211			min = g
212		}
213		if g > max {
214			max = g
215		}
216	}
217	margin := want / 10 // 10% margin.
218	if min > want+margin || max < want-margin {
219		return fmt.Errorf("%s:%d want %s in [%d: %d], got %v", fname, ln, testName, want-margin, want+margin, got)
220	}
221	return nil
222}
223
224func getMemProfileRecords() []runtime.MemProfileRecord {
225	// Force the runtime to update the object and byte counts.
226	// This can take up to two GC cycles to get a complete
227	// snapshot of the current point in time.
228	runtime.GC()
229	runtime.GC()
230
231	// Find out how many records there are (MemProfile(nil, true)),
232	// allocate that many records, and get the data.
233	// There's a race—more records might be added between
234	// the two calls—so allocate a few extra records for safety
235	// and also try again if we're very unlucky.
236	// The loop should only execute one iteration in the common case.
237	var p []runtime.MemProfileRecord
238	n, ok := runtime.MemProfile(nil, true)
239	for {
240		// Allocate room for a slightly bigger profile,
241		// in case a few more entries have been added
242		// since the call to MemProfile.
243		p = make([]runtime.MemProfileRecord, n+50)
244		n, ok = runtime.MemProfile(p, true)
245		if ok {
246			p = p[0:n]
247			break
248		}
249		// Profile grew; try again.
250	}
251	return p
252}
253
254type allocStat struct {
255	bytes, objects int64
256}
257
258// allocObjects examines the profile records for samples including the
259// named function and returns the allocation stats aggregated by
260// source line number of the allocation (at the leaf frame).
261func allocObjects(records []runtime.MemProfileRecord, function string) map[int]allocStat {
262	a := make(map[int]allocStat)
263	for _, r := range records {
264		var pcs []uintptr
265		for _, s := range r.Stack0 {
266			if s == 0 {
267				break
268			}
269			pcs = append(pcs, s)
270		}
271		frames := runtime.CallersFrames(pcs)
272		line := 0
273		for {
274			frame, more := frames.Next()
275			name := frame.Function
276			if line == 0 {
277				line = frame.Line
278			}
279			if name == function {
280				allocStat := a[line]
281				allocStat.bytes += r.AllocBytes
282				allocStat.objects += r.AllocObjects
283				a[line] = allocStat
284			}
285			if !more {
286				break
287			}
288		}
289	}
290	for line, stats := range a {
291		objects, bytes := scaleHeapSample(stats.objects, stats.bytes, int64(runtime.MemProfileRate))
292		a[line] = allocStat{bytes, objects}
293	}
294	return a
295}
296
297// scaleHeapSample unsamples heap allocations.
298// Taken from src/cmd/pprof/internal/profile/legacy_profile.go
299func scaleHeapSample(count, size, rate int64) (int64, int64) {
300	if count == 0 || size == 0 {
301		return 0, 0
302	}
303
304	if rate <= 1 {
305		// if rate==1 all samples were collected so no adjustment is needed.
306		// if rate<1 treat as unknown and skip scaling.
307		return count, size
308	}
309
310	avgSize := float64(size) / float64(count)
311	scale := 1 / (1 - math.Exp(-avgSize/float64(rate)))
312
313	return int64(float64(count) * scale), int64(float64(size) * scale)
314}
315