blob: cc72832ab48d4680eccb1bd24eacd818fda7b9b9 [file] [log] [blame]
Raul Silvera27ee7192015-09-14 14:03:45 -07001// 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
Raul Silvera2dd066d2019-01-18 19:06:16 +000021var a16k *[16 * 1024]byte
22var a17k *[17 * 1024]byte
23var a18k *[18 * 1024]byte
Raul Silvera27ee7192015-09-14 14:03:45 -070024
Raul Silvera2dd066d2019-01-18 19:06:16 +000025// 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.
Raul Silvera27ee7192015-09-14 14:03:45 -070029func main() {
Raul Silvera2dd066d2019-01-18 19:06:16 +000030 // Sample at 16K instead of default 512K to exercise sampling more heavily.
31 runtime.MemProfileRate = 16 * 1024
Raul Silvera27ee7192015-09-14 14:03:45 -070032
Raul Silvera2dd066d2019-01-18 19:06:16 +000033 if err := testInterleavedAllocations(); err != nil {
34 panic(err.Error())
35 }
36 if err := testSmallAllocations(); err != nil {
37 panic(err.Error())
38 }
Raul Silvera27ee7192015-09-14 14:03:45 -070039}
40
Raul Silvera2dd066d2019-01-18 19:06:16 +000041// 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 = 100000
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.
Raul Silvera27ee7192015-09-14 14:03:45 -070072func allocInterleaved(n int) {
73 for i := 0; i < n; i++ {
74 // Test verification depends on these lines being contiguous.
Raul Silvera2dd066d2019-01-18 19:06:16 +000075 a17k = new([17 * 1024]byte)
Raul Silvera27ee7192015-09-14 14:03:45 -070076 a1k = new([1024]byte)
Raul Silvera2dd066d2019-01-18 19:06:16 +000077 a18k = new([18 * 1024]byte)
Raul Silvera27ee7192015-09-14 14:03:45 -070078 a512 = new([512]byte)
Raul Silvera2dd066d2019-01-18 19:06:16 +000079 a16k = new([16 * 1024]byte)
Raul Silvera27ee7192015-09-14 14:03:45 -070080 a256 = new([256]byte)
Raul Silvera2dd066d2019-01-18 19:06:16 +000081 // Test verification depends on these lines being contiguous.
Raul Silvera27ee7192015-09-14 14:03:45 -070082 }
83}
84
Raul Silvera2dd066d2019-01-18 19:06:16 +000085func allocInterleaved1(n int) {
86 allocInterleaved(n)
87}
88
89func allocInterleaved2(n int) {
90 allocInterleaved(n)
91}
92
93func allocInterleaved3(n int) {
94 allocInterleaved(n)
95}
96
97// Repeatedly exercise a set of allocations and check that the heap
98// profile collected by the runtime unsamples to a reasonable
99// value. Because sampling is based on randomization, there can be
100// significant variability on the unsampled data. To account for that,
101// the testcase allows for a 10% margin of error, but only fails if it
102// consistently fails across three experiments, avoiding flakes.
103func testSmallAllocations() error {
104 const iters = 100000
105 // Sizes of the allocations performed by each experiment.
106 sizes := []int64{1024, 512, 256}
107 frames := []string{"main.allocSmall1", "main.allocSmall2", "main.allocSmall3"}
108
109 // Pass if at least one of three experiments has no errors. Use a separate
110 // function for each experiment to identify each experiment in the profile.
111 allocSmall1(iters)
112 if checkAllocations(getMemProfileRecords(), frames[0:1], iters, sizes) == nil {
113 // Passed on first try, report no error.
114 return nil
115 }
116 allocSmall2(iters)
117 if checkAllocations(getMemProfileRecords(), frames[0:2], iters, sizes) == nil {
118 // Passed on second try, report no error.
119 return nil
120 }
121 allocSmall3(iters)
122 // If it fails a third time, we may be onto something.
123 return checkAllocations(getMemProfileRecords(), frames[0:3], iters, sizes)
124}
125
126// allocSmall performs only small allocations for sanity testing.
127func allocSmall(n int) {
Raul Silvera27ee7192015-09-14 14:03:45 -0700128 for i := 0; i < n; i++ {
129 // Test verification depends on these lines being contiguous.
130 a1k = new([1024]byte)
131 a512 = new([512]byte)
132 a256 = new([256]byte)
133 }
134}
135
Raul Silvera2dd066d2019-01-18 19:06:16 +0000136// Three separate instances of testing to avoid flakes. Will report an error
137// only if they all consistently report failures.
138func allocSmall1(n int) {
139 allocSmall(n)
140}
141
142func allocSmall2(n int) {
143 allocSmall(n)
144}
145
146func allocSmall3(n int) {
147 allocSmall(n)
148}
149
Raul Silvera27ee7192015-09-14 14:03:45 -0700150// checkAllocations validates that the profile records collected for
151// the named function are consistent with count contiguous allocations
152// of the specified sizes.
Raul Silvera2dd066d2019-01-18 19:06:16 +0000153// Check multiple functions and only report consistent failures across
154// multiple tests.
155// Look only at samples that include the named frames, and group the
156// allocations by their line number. All these allocations are done from
157// the same leaf function, so their line numbers are the same.
158func checkAllocations(records []runtime.MemProfileRecord, frames []string, count int64, size []int64) error {
159 objectsPerLine := map[int][]int64{}
160 bytesPerLine := map[int][]int64{}
161 totalCount := []int64{}
162 // Compute the line number of the first allocation. All the
163 // allocations are from the same leaf, so pick the first one.
164 var firstLine int
165 for ln := range allocObjects(records, frames[0]) {
Raul Silvera27ee7192015-09-14 14:03:45 -0700166 if firstLine == 0 || firstLine > ln {
167 firstLine = ln
168 }
169 }
Raul Silvera2dd066d2019-01-18 19:06:16 +0000170 for _, frame := range frames {
171 var objectCount int64
172 a := allocObjects(records, frame)
173 for s := range size {
174 // Allocations of size size[s] should be on line firstLine + s.
175 ln := firstLine + s
176 objectsPerLine[ln] = append(objectsPerLine[ln], a[ln].objects)
177 bytesPerLine[ln] = append(bytesPerLine[ln], a[ln].bytes)
178 objectCount += a[ln].objects
179 }
180 totalCount = append(totalCount, objectCount)
181 }
Raul Silvera27ee7192015-09-14 14:03:45 -0700182 for i, w := range size {
183 ln := firstLine + i
Raul Silvera2dd066d2019-01-18 19:06:16 +0000184 if err := checkValue(frames[0], ln, "objects", count, objectsPerLine[ln]); err != nil {
185 return err
186 }
187 if err := checkValue(frames[0], ln, "bytes", count*w, bytesPerLine[ln]); err != nil {
188 return err
189 }
Raul Silvera27ee7192015-09-14 14:03:45 -0700190 }
Raul Silvera2dd066d2019-01-18 19:06:16 +0000191 return checkValue(frames[0], 0, "total", count*int64(len(size)), totalCount)
Raul Silvera27ee7192015-09-14 14:03:45 -0700192}
193
Raul Silvera2dd066d2019-01-18 19:06:16 +0000194// checkValue checks an unsampled value against its expected value.
195// Given that this is a sampled value, it will be unexact and will change
196// from run to run. Only report it as a failure if all the values land
197// consistently far from the expected value.
198func checkValue(fname string, ln int, testName string, want int64, got []int64) error {
199 if got == nil {
200 return fmt.Errorf("Unexpected empty result")
Raul Silvera27ee7192015-09-14 14:03:45 -0700201 }
Raul Silvera2dd066d2019-01-18 19:06:16 +0000202 min, max := got[0], got[0]
203 for _, g := range got[1:] {
204 if g < min {
205 min = g
206 }
207 if g > max {
208 max = g
209 }
210 }
211 margin := want / 10 // 10% margin.
212 if min > want+margin || max < want-margin {
213 return fmt.Errorf("%s:%d want %s in [%d: %d], got %v", fname, ln, testName, want-margin, want+margin, got)
214 }
215 return nil
Raul Silvera27ee7192015-09-14 14:03:45 -0700216}
217
218func getMemProfileRecords() []runtime.MemProfileRecord {
Austin Clementsb5a0c672015-11-12 11:30:26 -0500219 // Force the runtime to update the object and byte counts.
Austin Clementsa9ca2132015-11-12 12:24:36 -0500220 // This can take up to two GC cycles to get a complete
221 // snapshot of the current point in time.
222 runtime.GC()
Austin Clementsb5a0c672015-11-12 11:30:26 -0500223 runtime.GC()
224
Raul Silvera27ee7192015-09-14 14:03:45 -0700225 // Find out how many records there are (MemProfile(nil, true)),
226 // allocate that many records, and get the data.
227 // There's a race—more records might be added between
228 // the two calls—so allocate a few extra records for safety
229 // and also try again if we're very unlucky.
230 // The loop should only execute one iteration in the common case.
231 var p []runtime.MemProfileRecord
232 n, ok := runtime.MemProfile(nil, true)
233 for {
234 // Allocate room for a slightly bigger profile,
235 // in case a few more entries have been added
236 // since the call to MemProfile.
237 p = make([]runtime.MemProfileRecord, n+50)
238 n, ok = runtime.MemProfile(p, true)
239 if ok {
240 p = p[0:n]
241 break
242 }
243 // Profile grew; try again.
244 }
245 return p
246}
247
248type allocStat struct {
249 bytes, objects int64
250}
251
Raul Silvera2dd066d2019-01-18 19:06:16 +0000252// allocObjects examines the profile records for samples including the
253// named function and returns the allocation stats aggregated by
254// source line number of the allocation (at the leaf frame).
Raul Silvera27ee7192015-09-14 14:03:45 -0700255func allocObjects(records []runtime.MemProfileRecord, function string) map[int]allocStat {
256 a := make(map[int]allocStat)
257 for _, r := range records {
Raul Silvera2dd066d2019-01-18 19:06:16 +0000258 var pcs []uintptr
Raul Silvera27ee7192015-09-14 14:03:45 -0700259 for _, s := range r.Stack0 {
260 if s == 0 {
261 break
262 }
Raul Silvera2dd066d2019-01-18 19:06:16 +0000263 pcs = append(pcs, s)
264 }
265 frames := runtime.CallersFrames(pcs)
266 line := 0
267 for {
268 frame, more := frames.Next()
269 name := frame.Function
270 if line == 0 {
271 line = frame.Line
272 }
273 if name == function {
274 allocStat := a[line]
275 allocStat.bytes += r.AllocBytes
276 allocStat.objects += r.AllocObjects
277 a[line] = allocStat
278 }
279 if !more {
280 break
Raul Silvera27ee7192015-09-14 14:03:45 -0700281 }
282 }
283 }
284 for line, stats := range a {
285 objects, bytes := scaleHeapSample(stats.objects, stats.bytes, int64(runtime.MemProfileRate))
286 a[line] = allocStat{bytes, objects}
287 }
288 return a
289}
290
291// scaleHeapSample unsamples heap allocations.
292// Taken from src/cmd/pprof/internal/profile/legacy_profile.go
293func scaleHeapSample(count, size, rate int64) (int64, int64) {
294 if count == 0 || size == 0 {
295 return 0, 0
296 }
297
298 if rate <= 1 {
299 // if rate==1 all samples were collected so no adjustment is needed.
300 // if rate<1 treat as unknown and skip scaling.
301 return count, size
302 }
303
304 avgSize := float64(size) / float64(count)
305 scale := 1 / (1 - math.Exp(-avgSize/float64(rate)))
306
307 return int64(float64(count) * scale), int64(float64(size) * scale)
308}