Source file 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  	leafFrame := "main.allocInterleaved"
    52  
    53  	// Pass if at least one of three experiments has no errors. Use a separate
    54  	// function for each experiment to identify each experiment in the profile.
    55  	allocInterleaved1(iters)
    56  	if checkAllocations(getMemProfileRecords(), leafFrame, frames[0:1], iters, allocInterleavedSizes) == nil {
    57  		// Passed on first try, report no error.
    58  		return nil
    59  	}
    60  	allocInterleaved2(iters)
    61  	if checkAllocations(getMemProfileRecords(), leafFrame, frames[0:2], iters, allocInterleavedSizes) == nil {
    62  		// Passed on second try, report no error.
    63  		return nil
    64  	}
    65  	allocInterleaved3(iters)
    66  	// If it fails a third time, we may be onto something.
    67  	return checkAllocations(getMemProfileRecords(), leafFrame, frames[0:3], iters, allocInterleavedSizes)
    68  }
    69  
    70  var allocInterleavedSizes = []int64{17 * 1024, 1024, 18 * 1024, 512, 16 * 1024, 256}
    71  
    72  // allocInterleaved stress-tests the heap sampling logic by interleaving large and small allocations.
    73  func allocInterleaved(n int) {
    74  	for i := 0; i < n; i++ {
    75  		// Test verification depends on these lines being contiguous.
    76  		a17k = new([17 * 1024]byte)
    77  		a1k = new([1024]byte)
    78  		a18k = new([18 * 1024]byte)
    79  		a512 = new([512]byte)
    80  		a16k = new([16 * 1024]byte)
    81  		a256 = new([256]byte)
    82  		// Test verification depends on these lines being contiguous.
    83  
    84  		// Slow down the allocation rate to avoid #52433.
    85  		runtime.Gosched()
    86  	}
    87  }
    88  
    89  func allocInterleaved1(n int) {
    90  	allocInterleaved(n)
    91  }
    92  
    93  func allocInterleaved2(n int) {
    94  	allocInterleaved(n)
    95  }
    96  
    97  func allocInterleaved3(n int) {
    98  	allocInterleaved(n)
    99  }
   100  
   101  // Repeatedly exercise a set of allocations and check that the heap
   102  // profile collected by the runtime unsamples to a reasonable
   103  // value. Because sampling is based on randomization, there can be
   104  // significant variability on the unsampled data. To account for that,
   105  // the testcase allows for a 10% margin of error, but only fails if it
   106  // consistently fails across three experiments, avoiding flakes.
   107  func testSmallAllocations() error {
   108  	const iters = 50000
   109  	// Sizes of the allocations performed by each experiment.
   110  	sizes := []int64{1024, 512, 256}
   111  	frames := []string{"main.allocSmall1", "main.allocSmall2", "main.allocSmall3"}
   112  	leafFrame := "main.allocSmall"
   113  
   114  	// Pass if at least one of three experiments has no errors. Use a separate
   115  	// function for each experiment to identify each experiment in the profile.
   116  	allocSmall1(iters)
   117  	if checkAllocations(getMemProfileRecords(), leafFrame, frames[0:1], iters, sizes) == nil {
   118  		// Passed on first try, report no error.
   119  		return nil
   120  	}
   121  	allocSmall2(iters)
   122  	if checkAllocations(getMemProfileRecords(), leafFrame, frames[0:2], iters, sizes) == nil {
   123  		// Passed on second try, report no error.
   124  		return nil
   125  	}
   126  	allocSmall3(iters)
   127  	// If it fails a third time, we may be onto something.
   128  	return checkAllocations(getMemProfileRecords(), leafFrame, frames[0:3], iters, sizes)
   129  }
   130  
   131  // allocSmall performs only small allocations for sanity testing.
   132  func allocSmall(n int) {
   133  	for i := 0; i < n; i++ {
   134  		// Test verification depends on these lines being contiguous.
   135  		a1k = new([1024]byte)
   136  		a512 = new([512]byte)
   137  		a256 = new([256]byte)
   138  
   139  		// Slow down the allocation rate to avoid #52433.
   140  		runtime.Gosched()
   141  	}
   142  }
   143  
   144  // Three separate instances of testing to avoid flakes. Will report an error
   145  // only if they all consistently report failures.
   146  func allocSmall1(n int) {
   147  	allocSmall(n)
   148  }
   149  
   150  func allocSmall2(n int) {
   151  	allocSmall(n)
   152  }
   153  
   154  func allocSmall3(n int) {
   155  	allocSmall(n)
   156  }
   157  
   158  // checkAllocations validates that the profile records collected for
   159  // the named function are consistent with count contiguous allocations
   160  // of the specified sizes.
   161  // Check multiple functions and only report consistent failures across
   162  // multiple tests.
   163  // Look only at samples that include the named frames, and group the
   164  // allocations by their line number. All these allocations are done from
   165  // the same leaf function, so their line numbers are the same.
   166  func checkAllocations(records []runtime.MemProfileRecord, leafFrame string, frames []string, count int64, size []int64) error {
   167  	objectsPerLine := map[int][]int64{}
   168  	bytesPerLine := map[int][]int64{}
   169  	totalCount := []int64{}
   170  	// Compute the line number of the first allocation. All the
   171  	// allocations are from the same leaf, so pick the first one.
   172  	var firstLine int
   173  	for ln := range allocObjects(records, leafFrame, frames[0]) {
   174  		if firstLine == 0 || firstLine > ln {
   175  			firstLine = ln
   176  		}
   177  	}
   178  	for _, frame := range frames {
   179  		var objectCount int64
   180  		a := allocObjects(records, leafFrame, frame)
   181  		for s := range size {
   182  			// Allocations of size size[s] should be on line firstLine + s.
   183  			ln := firstLine + s
   184  			objectsPerLine[ln] = append(objectsPerLine[ln], a[ln].objects)
   185  			bytesPerLine[ln] = append(bytesPerLine[ln], a[ln].bytes)
   186  			objectCount += a[ln].objects
   187  		}
   188  		totalCount = append(totalCount, objectCount)
   189  	}
   190  	for i, w := range size {
   191  		ln := firstLine + i
   192  		if err := checkValue(frames[0], ln, "objects", count, objectsPerLine[ln]); err != nil {
   193  			return err
   194  		}
   195  		if err := checkValue(frames[0], ln, "bytes", count*w, bytesPerLine[ln]); err != nil {
   196  			return err
   197  		}
   198  	}
   199  	return checkValue(frames[0], 0, "total", count*int64(len(size)), totalCount)
   200  }
   201  
   202  // checkValue checks an unsampled value against its expected value.
   203  // Given that this is a sampled value, it will be unexact and will change
   204  // from run to run. Only report it as a failure if all the values land
   205  // consistently far from the expected value.
   206  func checkValue(fname string, ln int, testName string, want int64, got []int64) error {
   207  	if got == nil {
   208  		return fmt.Errorf("Unexpected empty result")
   209  	}
   210  	min, max := got[0], got[0]
   211  	for _, g := range got[1:] {
   212  		if g < min {
   213  			min = g
   214  		}
   215  		if g > max {
   216  			max = g
   217  		}
   218  	}
   219  	margin := want / 10 // 10% margin.
   220  	if min > want+margin || max < want-margin {
   221  		return fmt.Errorf("%s:%d want %s in [%d: %d], got %v", fname, ln, testName, want-margin, want+margin, got)
   222  	}
   223  	return nil
   224  }
   225  
   226  func getMemProfileRecords() []runtime.MemProfileRecord {
   227  	// Force the runtime to update the object and byte counts.
   228  	// This can take up to two GC cycles to get a complete
   229  	// snapshot of the current point in time.
   230  	runtime.GC()
   231  	runtime.GC()
   232  
   233  	// Find out how many records there are (MemProfile(nil, true)),
   234  	// allocate that many records, and get the data.
   235  	// There's a race—more records might be added between
   236  	// the two calls—so allocate a few extra records for safety
   237  	// and also try again if we're very unlucky.
   238  	// The loop should only execute one iteration in the common case.
   239  	var p []runtime.MemProfileRecord
   240  	n, ok := runtime.MemProfile(nil, true)
   241  	for {
   242  		// Allocate room for a slightly bigger profile,
   243  		// in case a few more entries have been added
   244  		// since the call to MemProfile.
   245  		p = make([]runtime.MemProfileRecord, n+50)
   246  		n, ok = runtime.MemProfile(p, true)
   247  		if ok {
   248  			p = p[0:n]
   249  			break
   250  		}
   251  		// Profile grew; try again.
   252  	}
   253  	return p
   254  }
   255  
   256  type allocStat struct {
   257  	bytes, objects int64
   258  }
   259  
   260  // allocObjects examines the profile records for samples including the
   261  // named function and returns the allocation stats aggregated by
   262  // source line number of the allocation (at the leaf frame).
   263  func allocObjects(records []runtime.MemProfileRecord, leafFrame, function string) map[int]allocStat {
   264  	a := make(map[int]allocStat)
   265  	for _, r := range records {
   266  		var pcs []uintptr
   267  		for _, s := range r.Stack0 {
   268  			if s == 0 {
   269  				break
   270  			}
   271  			pcs = append(pcs, s)
   272  		}
   273  		frames := runtime.CallersFrames(pcs)
   274  		line := 0
   275  		for {
   276  			frame, more := frames.Next()
   277  			name := frame.Function
   278  			if name == leafFrame && line == 0 {
   279  				line = frame.Line
   280  			}
   281  			if name == function {
   282  				allocStat := a[line]
   283  				allocStat.bytes += r.AllocBytes
   284  				allocStat.objects += r.AllocObjects
   285  				a[line] = allocStat
   286  			}
   287  			if !more {
   288  				break
   289  			}
   290  		}
   291  	}
   292  	for line, stats := range a {
   293  		objects, bytes := scaleHeapSample(stats.objects, stats.bytes, int64(runtime.MemProfileRate))
   294  		a[line] = allocStat{bytes, objects}
   295  	}
   296  	return a
   297  }
   298  
   299  // scaleHeapSample unsamples heap allocations.
   300  // Taken from src/cmd/pprof/internal/profile/legacy_profile.go
   301  func scaleHeapSample(count, size, rate int64) (int64, int64) {
   302  	if count == 0 || size == 0 {
   303  		return 0, 0
   304  	}
   305  
   306  	if rate <= 1 {
   307  		// if rate==1 all samples were collected so no adjustment is needed.
   308  		// if rate<1 treat as unknown and skip scaling.
   309  		return count, size
   310  	}
   311  
   312  	avgSize := float64(size) / float64(count)
   313  	scale := 1 / (1 - math.Exp(-avgSize/float64(rate)))
   314  
   315  	return int64(float64(count) * scale), int64(float64(size) * scale)
   316  }
   317  

View as plain text