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.
7 // Test heap sampling logic.
21 var a16k *[16 * 1024]byte
22 var a17k *[17 * 1024]byte
23 var a18k *[18 * 1024]byte
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.
30 // Sample at 16K instead of default 512K to exercise sampling more heavily.
31 runtime.MemProfileRate = 16 * 1024
33 if err := testInterleavedAllocations(); err != nil {
36 if err := testSmallAllocations(); err != nil {
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.
47 func testInterleavedAllocations() error {
49 // Sizes of the allocations performed by each experiment.
50 frames := []string{"main.allocInterleaved1", "main.allocInterleaved2", "main.allocInterleaved3"}
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.
59 allocInterleaved2(iters)
60 if checkAllocations(getMemProfileRecords(), frames[0:2], iters, allocInterleavedSizes) == nil {
61 // Passed on second try, report no error.
64 allocInterleaved3(iters)
65 // If it fails a third time, we may be onto something.
66 return checkAllocations(getMemProfileRecords(), frames[0:3], iters, allocInterleavedSizes)
69 var allocInterleavedSizes = []int64{17 * 1024, 1024, 18 * 1024, 512, 16 * 1024, 256}
71 // allocInterleaved stress-tests the heap sampling logic by interleaving large and small allocations.
72 func 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)
77 a18k = new([18 * 1024]byte)
79 a16k = new([16 * 1024]byte)
81 // Test verification depends on these lines being contiguous.
83 // Slow down the allocation rate to avoid #52433.
88 func allocInterleaved1(n int) {
92 func allocInterleaved2(n int) {
96 func allocInterleaved3(n int) {
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.
106 func testSmallAllocations() error {
108 // Sizes of the allocations performed by each experiment.
109 sizes := []int64{1024, 512, 256}
110 frames := []string{"main.allocSmall1", "main.allocSmall2", "main.allocSmall3"}
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.
115 if checkAllocations(getMemProfileRecords(), frames[0:1], iters, sizes) == nil {
116 // Passed on first try, report no error.
120 if checkAllocations(getMemProfileRecords(), frames[0:2], iters, sizes) == nil {
121 // Passed on second try, report no error.
125 // If it fails a third time, we may be onto something.
126 return checkAllocations(getMemProfileRecords(), frames[0:3], iters, sizes)
129 // allocSmall performs only small allocations for sanity testing.
130 func 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)
137 // Slow down the allocation rate to avoid #52433.
142 // Three separate instances of testing to avoid flakes. Will report an error
143 // only if they all consistently report failures.
144 func allocSmall1(n int) {
148 func allocSmall2(n int) {
152 func allocSmall3(n int) {
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
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.
164 func 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.
171 for ln := range allocObjects(records, frames[0]) {
172 if firstLine == 0 || firstLine > ln {
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.
182 objectsPerLine[ln] = append(objectsPerLine[ln], a[ln].objects)
183 bytesPerLine[ln] = append(bytesPerLine[ln], a[ln].bytes)
184 objectCount += a[ln].objects
186 totalCount = append(totalCount, objectCount)
188 for i, w := range size {
190 if err := checkValue(frames[0], ln, "objects", count, objectsPerLine[ln]); err != nil {
193 if err := checkValue(frames[0], ln, "bytes", count*w, bytesPerLine[ln]); err != nil {
197 return checkValue(frames[0], 0, "total", count*int64(len(size)), totalCount)
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.
204 func checkValue(fname string, ln int, testName string, want int64, got []int64) error {
206 return fmt.Errorf("Unexpected empty result")
208 min, max := got[0], got[0]
209 for _, g := range got[1:] {
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)
224 func 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.
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)
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)
249 // Profile grew; try again.
254 type allocStat struct {
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).
261 func allocObjects(records []runtime.MemProfileRecord, function string) map[int]allocStat {
262 a := make(map[int]allocStat)
263 for _, r := range records {
265 for _, s := range r.Stack0 {
271 frames := runtime.CallersFrames(pcs)
274 frame, more := frames.Next()
275 name := frame.Function
279 if name == function {
281 allocStat.bytes += r.AllocBytes
282 allocStat.objects += r.AllocObjects
290 for line, stats := range a {
291 objects, bytes := scaleHeapSample(stats.objects, stats.bytes, int64(runtime.MemProfileRate))
292 a[line] = allocStat{bytes, objects}
297 // scaleHeapSample unsamples heap allocations.
298 // Taken from src/cmd/pprof/internal/profile/legacy_profile.go
299 func scaleHeapSample(count, size, rate int64) (int64, int64) {
300 if count == 0 || size == 0 {
305 // if rate==1 all samples were collected so no adjustment is needed.
306 // if rate<1 treat as unknown and skip scaling.
310 avgSize := float64(size) / float64(count)
311 scale := 1 / (1 - math.Exp(-avgSize/float64(rate)))
313 return int64(float64(count) * scale), int64(float64(size) * scale)