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