]> Cypherpunks.ru repositories - gostls13.git/blob - test/heapsampling.go
cmd/compile/internal/inline: score call sites exposed by inlines
[gostls13.git] / test / heapsampling.go
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
9 package main
10
11 import (
12         "fmt"
13         "math"
14         "runtime"
15 )
16
17 var a16 *[16]byte
18 var a512 *[512]byte
19 var a256 *[256]byte
20 var a1k *[1024]byte
21 var a16k *[16 * 1024]byte
22 var a17k *[17 * 1024]byte
23 var 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.
29 func 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.
47 func 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
69 var 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.
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)
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
88 func allocInterleaved1(n int) {
89         allocInterleaved(n)
90 }
91
92 func allocInterleaved2(n int) {
93         allocInterleaved(n)
94 }
95
96 func 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.
106 func 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.
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)
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.
144 func allocSmall1(n int) {
145         allocSmall(n)
146 }
147
148 func allocSmall2(n int) {
149         allocSmall(n)
150 }
151
152 func 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.
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.
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.
204 func 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
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.
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
254 type 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).
261 func 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
299 func 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 }