Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 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 |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 21 | var a16k *[16 * 1024]byte |
| 22 | var a17k *[17 * 1024]byte |
| 23 | var a18k *[18 * 1024]byte |
Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 24 | |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 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. |
Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 29 | func main() { |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 30 | // Sample at 16K instead of default 512K to exercise sampling more heavily. |
| 31 | runtime.MemProfileRate = 16 * 1024 |
Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 32 | |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 33 | if err := testInterleavedAllocations(); err != nil { |
| 34 | panic(err.Error()) |
| 35 | } |
| 36 | if err := testSmallAllocations(); err != nil { |
| 37 | panic(err.Error()) |
| 38 | } |
Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 39 | } |
| 40 | |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 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 = 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 | |
| 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. |
Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 72 | func allocInterleaved(n int) { |
| 73 | for i := 0; i < n; i++ { |
| 74 | // Test verification depends on these lines being contiguous. |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 75 | a17k = new([17 * 1024]byte) |
Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 76 | a1k = new([1024]byte) |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 77 | a18k = new([18 * 1024]byte) |
Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 78 | a512 = new([512]byte) |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 79 | a16k = new([16 * 1024]byte) |
Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 80 | a256 = new([256]byte) |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 81 | // Test verification depends on these lines being contiguous. |
Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 82 | } |
| 83 | } |
| 84 | |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 85 | func allocInterleaved1(n int) { |
| 86 | allocInterleaved(n) |
| 87 | } |
| 88 | |
| 89 | func allocInterleaved2(n int) { |
| 90 | allocInterleaved(n) |
| 91 | } |
| 92 | |
| 93 | func 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. |
| 103 | func 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. |
| 127 | func allocSmall(n int) { |
Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 128 | 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 Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 136 | // Three separate instances of testing to avoid flakes. Will report an error |
| 137 | // only if they all consistently report failures. |
| 138 | func allocSmall1(n int) { |
| 139 | allocSmall(n) |
| 140 | } |
| 141 | |
| 142 | func allocSmall2(n int) { |
| 143 | allocSmall(n) |
| 144 | } |
| 145 | |
| 146 | func allocSmall3(n int) { |
| 147 | allocSmall(n) |
| 148 | } |
| 149 | |
Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 150 | // checkAllocations validates that the profile records collected for |
| 151 | // the named function are consistent with count contiguous allocations |
| 152 | // of the specified sizes. |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 153 | // 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. |
| 158 | func 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 Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 166 | if firstLine == 0 || firstLine > ln { |
| 167 | firstLine = ln |
| 168 | } |
| 169 | } |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 170 | 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 Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 182 | for i, w := range size { |
| 183 | ln := firstLine + i |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 184 | 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 Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 190 | } |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 191 | return checkValue(frames[0], 0, "total", count*int64(len(size)), totalCount) |
Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 192 | } |
| 193 | |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 194 | // 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. |
| 198 | func checkValue(fname string, ln int, testName string, want int64, got []int64) error { |
| 199 | if got == nil { |
| 200 | return fmt.Errorf("Unexpected empty result") |
Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 201 | } |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 202 | 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 Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 216 | } |
| 217 | |
| 218 | func getMemProfileRecords() []runtime.MemProfileRecord { |
Austin Clements | b5a0c67 | 2015-11-12 11:30:26 -0500 | [diff] [blame] | 219 | // Force the runtime to update the object and byte counts. |
Austin Clements | a9ca213 | 2015-11-12 12:24:36 -0500 | [diff] [blame] | 220 | // This can take up to two GC cycles to get a complete |
| 221 | // snapshot of the current point in time. |
| 222 | runtime.GC() |
Austin Clements | b5a0c67 | 2015-11-12 11:30:26 -0500 | [diff] [blame] | 223 | runtime.GC() |
| 224 | |
Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 225 | // 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 | |
| 248 | type allocStat struct { |
| 249 | bytes, objects int64 |
| 250 | } |
| 251 | |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 252 | // 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 Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 255 | func allocObjects(records []runtime.MemProfileRecord, function string) map[int]allocStat { |
| 256 | a := make(map[int]allocStat) |
| 257 | for _, r := range records { |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 258 | var pcs []uintptr |
Raul Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 259 | for _, s := range r.Stack0 { |
| 260 | if s == 0 { |
| 261 | break |
| 262 | } |
Raul Silvera | 2dd066d | 2019-01-18 19:06:16 +0000 | [diff] [blame] | 263 | 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 Silvera | 27ee719 | 2015-09-14 14:03:45 -0700 | [diff] [blame] | 281 | } |
| 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 |
| 293 | func 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 | } |