You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
309 lines
9.5 KiB
309 lines
9.5 KiB
// run
|
|
|
|
// Copyright 2009 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Test heap sampling logic.
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"runtime"
|
|
)
|
|
|
|
var a16 *[16]byte
|
|
var a512 *[512]byte
|
|
var a256 *[256]byte
|
|
var a1k *[1024]byte
|
|
var a16k *[16 * 1024]byte
|
|
var a17k *[17 * 1024]byte
|
|
var a18k *[18 * 1024]byte
|
|
|
|
// This test checks that heap sampling produces reasonable results.
|
|
// Note that heap sampling uses randomization, so the results vary for
|
|
// run to run. To avoid flakes, this test performs multiple
|
|
// experiments and only complains if all of them consistently fail.
|
|
func main() {
|
|
// Sample at 16K instead of default 512K to exercise sampling more heavily.
|
|
runtime.MemProfileRate = 16 * 1024
|
|
|
|
if err := testInterleavedAllocations(); err != nil {
|
|
panic(err.Error())
|
|
}
|
|
if err := testSmallAllocations(); err != nil {
|
|
panic(err.Error())
|
|
}
|
|
}
|
|
|
|
// Repeatedly exercise a set of allocations and check that the heap
|
|
// profile collected by the runtime unsamples to a reasonable
|
|
// value. Because sampling is based on randomization, there can be
|
|
// significant variability on the unsampled data. To account for that,
|
|
// the testcase allows for a 10% margin of error, but only fails if it
|
|
// consistently fails across three experiments, avoiding flakes.
|
|
func testInterleavedAllocations() error {
|
|
const iters = 100000
|
|
// Sizes of the allocations performed by each experiment.
|
|
frames := []string{"main.allocInterleaved1", "main.allocInterleaved2", "main.allocInterleaved3"}
|
|
|
|
// Pass if at least one of three experiments has no errors. Use a separate
|
|
// function for each experiment to identify each experiment in the profile.
|
|
allocInterleaved1(iters)
|
|
if checkAllocations(getMemProfileRecords(), frames[0:1], iters, allocInterleavedSizes) == nil {
|
|
// Passed on first try, report no error.
|
|
return nil
|
|
}
|
|
allocInterleaved2(iters)
|
|
if checkAllocations(getMemProfileRecords(), frames[0:2], iters, allocInterleavedSizes) == nil {
|
|
// Passed on second try, report no error.
|
|
return nil
|
|
}
|
|
allocInterleaved3(iters)
|
|
// If it fails a third time, we may be onto something.
|
|
return checkAllocations(getMemProfileRecords(), frames[0:3], iters, allocInterleavedSizes)
|
|
}
|
|
|
|
var allocInterleavedSizes = []int64{17 * 1024, 1024, 18 * 1024, 512, 16 * 1024, 256}
|
|
|
|
// allocInterleaved stress-tests the heap sampling logic by interleaving large and small allocations.
|
|
func allocInterleaved(n int) {
|
|
for i := 0; i < n; i++ {
|
|
// Test verification depends on these lines being contiguous.
|
|
a17k = new([17 * 1024]byte)
|
|
a1k = new([1024]byte)
|
|
a18k = new([18 * 1024]byte)
|
|
a512 = new([512]byte)
|
|
a16k = new([16 * 1024]byte)
|
|
a256 = new([256]byte)
|
|
// Test verification depends on these lines being contiguous.
|
|
}
|
|
}
|
|
|
|
func allocInterleaved1(n int) {
|
|
allocInterleaved(n)
|
|
}
|
|
|
|
func allocInterleaved2(n int) {
|
|
allocInterleaved(n)
|
|
}
|
|
|
|
func allocInterleaved3(n int) {
|
|
allocInterleaved(n)
|
|
}
|
|
|
|
// Repeatedly exercise a set of allocations and check that the heap
|
|
// profile collected by the runtime unsamples to a reasonable
|
|
// value. Because sampling is based on randomization, there can be
|
|
// significant variability on the unsampled data. To account for that,
|
|
// the testcase allows for a 10% margin of error, but only fails if it
|
|
// consistently fails across three experiments, avoiding flakes.
|
|
func testSmallAllocations() error {
|
|
const iters = 100000
|
|
// Sizes of the allocations performed by each experiment.
|
|
sizes := []int64{1024, 512, 256}
|
|
frames := []string{"main.allocSmall1", "main.allocSmall2", "main.allocSmall3"}
|
|
|
|
// Pass if at least one of three experiments has no errors. Use a separate
|
|
// function for each experiment to identify each experiment in the profile.
|
|
allocSmall1(iters)
|
|
if checkAllocations(getMemProfileRecords(), frames[0:1], iters, sizes) == nil {
|
|
// Passed on first try, report no error.
|
|
return nil
|
|
}
|
|
allocSmall2(iters)
|
|
if checkAllocations(getMemProfileRecords(), frames[0:2], iters, sizes) == nil {
|
|
// Passed on second try, report no error.
|
|
return nil
|
|
}
|
|
allocSmall3(iters)
|
|
// If it fails a third time, we may be onto something.
|
|
return checkAllocations(getMemProfileRecords(), frames[0:3], iters, sizes)
|
|
}
|
|
|
|
// allocSmall performs only small allocations for sanity testing.
|
|
func allocSmall(n int) {
|
|
for i := 0; i < n; i++ {
|
|
// Test verification depends on these lines being contiguous.
|
|
a1k = new([1024]byte)
|
|
a512 = new([512]byte)
|
|
a256 = new([256]byte)
|
|
}
|
|
}
|
|
|
|
// Three separate instances of testing to avoid flakes. Will report an error
|
|
// only if they all consistently report failures.
|
|
func allocSmall1(n int) {
|
|
allocSmall(n)
|
|
}
|
|
|
|
func allocSmall2(n int) {
|
|
allocSmall(n)
|
|
}
|
|
|
|
func allocSmall3(n int) {
|
|
allocSmall(n)
|
|
}
|
|
|
|
// checkAllocations validates that the profile records collected for
|
|
// the named function are consistent with count contiguous allocations
|
|
// of the specified sizes.
|
|
// Check multiple functions and only report consistent failures across
|
|
// multiple tests.
|
|
// Look only at samples that include the named frames, and group the
|
|
// allocations by their line number. All these allocations are done from
|
|
// the same leaf function, so their line numbers are the same.
|
|
func checkAllocations(records []runtime.MemProfileRecord, frames []string, count int64, size []int64) error {
|
|
objectsPerLine := map[int][]int64{}
|
|
bytesPerLine := map[int][]int64{}
|
|
totalCount := []int64{}
|
|
// Compute the line number of the first allocation. All the
|
|
// allocations are from the same leaf, so pick the first one.
|
|
var firstLine int
|
|
for ln := range allocObjects(records, frames[0]) {
|
|
if firstLine == 0 || firstLine > ln {
|
|
firstLine = ln
|
|
}
|
|
}
|
|
for _, frame := range frames {
|
|
var objectCount int64
|
|
a := allocObjects(records, frame)
|
|
for s := range size {
|
|
// Allocations of size size[s] should be on line firstLine + s.
|
|
ln := firstLine + s
|
|
objectsPerLine[ln] = append(objectsPerLine[ln], a[ln].objects)
|
|
bytesPerLine[ln] = append(bytesPerLine[ln], a[ln].bytes)
|
|
objectCount += a[ln].objects
|
|
}
|
|
totalCount = append(totalCount, objectCount)
|
|
}
|
|
for i, w := range size {
|
|
ln := firstLine + i
|
|
if err := checkValue(frames[0], ln, "objects", count, objectsPerLine[ln]); err != nil {
|
|
return err
|
|
}
|
|
if err := checkValue(frames[0], ln, "bytes", count*w, bytesPerLine[ln]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return checkValue(frames[0], 0, "total", count*int64(len(size)), totalCount)
|
|
}
|
|
|
|
// checkValue checks an unsampled value against its expected value.
|
|
// Given that this is a sampled value, it will be unexact and will change
|
|
// from run to run. Only report it as a failure if all the values land
|
|
// consistently far from the expected value.
|
|
func checkValue(fname string, ln int, testName string, want int64, got []int64) error {
|
|
if got == nil {
|
|
return fmt.Errorf("Unexpected empty result")
|
|
}
|
|
min, max := got[0], got[0]
|
|
for _, g := range got[1:] {
|
|
if g < min {
|
|
min = g
|
|
}
|
|
if g > max {
|
|
max = g
|
|
}
|
|
}
|
|
margin := want / 10 // 10% margin.
|
|
if min > want+margin || max < want-margin {
|
|
return fmt.Errorf("%s:%d want %s in [%d: %d], got %v", fname, ln, testName, want-margin, want+margin, got)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getMemProfileRecords() []runtime.MemProfileRecord {
|
|
// Force the runtime to update the object and byte counts.
|
|
// This can take up to two GC cycles to get a complete
|
|
// snapshot of the current point in time.
|
|
runtime.GC()
|
|
runtime.GC()
|
|
|
|
// Find out how many records there are (MemProfile(nil, true)),
|
|
// allocate that many records, and get the data.
|
|
// There's a race—more records might be added between
|
|
// the two calls—so allocate a few extra records for safety
|
|
// and also try again if we're very unlucky.
|
|
// The loop should only execute one iteration in the common case.
|
|
var p []runtime.MemProfileRecord
|
|
n, ok := runtime.MemProfile(nil, true)
|
|
for {
|
|
// Allocate room for a slightly bigger profile,
|
|
// in case a few more entries have been added
|
|
// since the call to MemProfile.
|
|
p = make([]runtime.MemProfileRecord, n+50)
|
|
n, ok = runtime.MemProfile(p, true)
|
|
if ok {
|
|
p = p[0:n]
|
|
break
|
|
}
|
|
// Profile grew; try again.
|
|
}
|
|
return p
|
|
}
|
|
|
|
type allocStat struct {
|
|
bytes, objects int64
|
|
}
|
|
|
|
// allocObjects examines the profile records for samples including the
|
|
// named function and returns the allocation stats aggregated by
|
|
// source line number of the allocation (at the leaf frame).
|
|
func allocObjects(records []runtime.MemProfileRecord, function string) map[int]allocStat {
|
|
a := make(map[int]allocStat)
|
|
for _, r := range records {
|
|
var pcs []uintptr
|
|
for _, s := range r.Stack0 {
|
|
if s == 0 {
|
|
break
|
|
}
|
|
pcs = append(pcs, s)
|
|
}
|
|
frames := runtime.CallersFrames(pcs)
|
|
line := 0
|
|
for {
|
|
frame, more := frames.Next()
|
|
name := frame.Function
|
|
if line == 0 {
|
|
line = frame.Line
|
|
}
|
|
if name == function {
|
|
allocStat := a[line]
|
|
allocStat.bytes += r.AllocBytes
|
|
allocStat.objects += r.AllocObjects
|
|
a[line] = allocStat
|
|
}
|
|
if !more {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
for line, stats := range a {
|
|
objects, bytes := scaleHeapSample(stats.objects, stats.bytes, int64(runtime.MemProfileRate))
|
|
a[line] = allocStat{bytes, objects}
|
|
}
|
|
return a
|
|
}
|
|
|
|
// scaleHeapSample unsamples heap allocations.
|
|
// Taken from src/cmd/pprof/internal/profile/legacy_profile.go
|
|
func scaleHeapSample(count, size, rate int64) (int64, int64) {
|
|
if count == 0 || size == 0 {
|
|
return 0, 0
|
|
}
|
|
|
|
if rate <= 1 {
|
|
// if rate==1 all samples were collected so no adjustment is needed.
|
|
// if rate<1 treat as unknown and skip scaling.
|
|
return count, size
|
|
}
|
|
|
|
avgSize := float64(size) / float64(count)
|
|
scale := 1 / (1 - math.Exp(-avgSize/float64(rate)))
|
|
|
|
return int64(float64(count) * scale), int64(float64(size) * scale)
|
|
}
|