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 = 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. 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} 84 85func 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) { 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 136// 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 150// checkAllocations validates that the profile records collected for 151// the named function are consistent with count contiguous allocations 152// of the specified sizes. 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. 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]) { 166 if firstLine == 0 || firstLine > ln { 167 firstLine = ln 168 } 169 } 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 } 182 for i, w := range size { 183 ln := firstLine + i 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 } 190 } 191 return checkValue(frames[0], 0, "total", count*int64(len(size)), totalCount) 192} 193 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. 198func checkValue(fname string, ln int, testName string, want int64, got []int64) error { 199 if got == nil { 200 return fmt.Errorf("Unexpected empty result") 201 } 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 216} 217 218func getMemProfileRecords() []runtime.MemProfileRecord { 219 // Force the runtime to update the object and byte counts. 220 // This can take up to two GC cycles to get a complete 221 // snapshot of the current point in time. 222 runtime.GC() 223 runtime.GC() 224 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 248type allocStat struct { 249 bytes, objects int64 250} 251 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). 255func allocObjects(records []runtime.MemProfileRecord, function string) map[int]allocStat { 256 a := make(map[int]allocStat) 257 for _, r := range records { 258 var pcs []uintptr 259 for _, s := range r.Stack0 { 260 if s == 0 { 261 break 262 } 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 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 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} 309