runtime: rewrite TestPhysicalMemoryUtilization

This test changes TestPhysicalMemoryUtilization to be simpler, more
robust, and more honest about what's going on.

Fixes #49411.

Change-Id: I913ef055c6e166c104c62595c1597d44db62018c
Reviewed-on: https://go-review.googlesource.com/c/go/+/362978
Trust: Michael Knyszek <mknyszek@google.com>
Run-TryBot: Michael Knyszek <mknyszek@google.com>
Reviewed-by: David Chase <drchase@google.com>
This commit is contained in:
Michael Anthony Knyszek 2021-11-10 20:14:15 +00:00 committed by Michael Knyszek
parent b2d826c09f
commit a881409960
1 changed files with 53 additions and 68 deletions

View File

@ -132,81 +132,75 @@ func GCFairness2() {
func GCPhys() {
// This test ensures that heap-growth scavenging is working as intended.
//
// It sets up a specific scenario: it allocates two pairs of objects whose
// sizes sum to size. One object in each pair is "small" (though must be
// large enough to be considered a large object by the runtime) and one is
// large. The small objects are kept while the large objects are freed,
// creating two large unscavenged holes in the heap. The heap goal should
// also be small as a result (so size must be at least as large as the
// minimum heap size). We then allocate one large object, bigger than both
// pairs of objects combined. This allocation, because it will tip
// HeapSys-HeapReleased well above the heap goal, should trigger heap-growth
// scavenging and scavenge most, if not all, of the large holes we created
// earlier.
// It attempts to construct a sizeable "swiss cheese" heap, with many
// allocChunk-sized holes. Then, it triggers a heap growth by trying to
// allocate as much memory as would fit in those holes.
//
// The heap growth should cause a large number of those holes to be
// returned to the OS.
const (
// Size must be also large enough to be considered a large
// object (not in any size-segregated span).
size = 4 << 20
split = 64 << 10
objects = 2
allocTotal = 32 << 20
allocChunk = 64 << 10
allocs = allocTotal / allocChunk
// The page cache could hide 64 8-KiB pages from the scavenger today.
maxPageCache = (8 << 10) * 64
// Reduce GOMAXPROCS down to 4 if it's greater. We need to bound the amount
// of memory held in the page cache because the scavenger can't reach it.
// The page cache will hold at most maxPageCache of memory per-P, so this
// bounds the amount of memory hidden from the scavenger to 4*maxPageCache
// at most.
maxProcs = 4
)
// Set GOGC so that this test operates under consistent assumptions.
// Set GC percent just so this test is a little more consistent in the
// face of varying environments.
debug.SetGCPercent(100)
procs := runtime.GOMAXPROCS(-1)
if procs > maxProcs {
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(maxProcs))
procs = runtime.GOMAXPROCS(-1)
}
// Save objects which we want to survive, and condemn objects which we don't.
// Note that we condemn objects in this way and release them all at once in
// order to avoid having the GC start freeing up these objects while the loop
// is still running and filling in the holes we intend to make.
saved := make([][]byte, 0, objects+1)
condemned := make([][]byte, 0, objects)
for i := 0; i < 2*objects; i++ {
// Set GOMAXPROCS to 1 to minimize the amount of memory held in the page cache,
// and to reduce the chance that the background scavenger gets scheduled.
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
// Allocate allocTotal bytes of memory in allocChunk byte chunks.
// Alternate between whether the chunk will be held live or will be
// condemned to GC to create holes in the heap.
saved := make([][]byte, allocs/2+1)
condemned := make([][]byte, allocs/2)
for i := 0; i < allocs; i++ {
b := make([]byte, allocChunk)
if i%2 == 0 {
saved = append(saved, make([]byte, split))
saved = append(saved, b)
} else {
condemned = append(condemned, make([]byte, size-split))
condemned = append(condemned, b)
}
}
// Run a GC cycle just so we're at a consistent state.
runtime.GC()
// Drop the only reference to all the condemned memory.
condemned = nil
// Clean up the heap. This will free up every other object created above
// (i.e. everything in condemned) creating holes in the heap.
// Also, if the condemned objects are still being swept, its possible that
// the scavenging that happens as a result of the next allocation won't see
// the holes at all. We call runtime.GC() twice here so that when we allocate
// our large object there's no race with sweeping.
runtime.GC()
runtime.GC()
// Perform one big allocation which should also scavenge any holes.
//
// The heap goal will rise after this object is allocated, so it's very
// important that we try to do all the scavenging in a single allocation
// that exceeds the heap goal. Otherwise the rising heap goal could foil our
// test.
saved = append(saved, make([]byte, objects*size))
// Clean up the heap again just to put it in a known state.
// Clear the condemned memory.
runtime.GC()
// At this point, the background scavenger is likely running
// and could pick up the work, so the next line of code doesn't
// end up doing anything. That's fine. What's important is that
// this test fails somewhat regularly if the runtime doesn't
// scavenge on heap growth, and doesn't fail at all otherwise.
// Make a large allocation that in theory could fit, but won't
// because we turned the heap into swiss cheese.
saved = append(saved, make([]byte, allocTotal/2))
// heapBacked is an estimate of the amount of physical memory used by
// this test. HeapSys is an estimate of the size of the mapped virtual
// address space (which may or may not be backed by physical pages)
// whereas HeapReleased is an estimate of the amount of bytes returned
// to the OS. Their difference then roughly corresponds to the amount
// of virtual address space that is backed by physical pages.
//
// heapBacked also subtracts out maxPageCache bytes of memory because
// this is memory that may be hidden from the scavenger per-P. Since
// GOMAXPROCS=1 here, that's fine.
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
heapBacked := stats.HeapSys - stats.HeapReleased
heapBacked := stats.HeapSys - stats.HeapReleased - maxPageCache
// If heapBacked does not exceed the heap goal by more than retainExtraPercent
// then the scavenger is working as expected; the newly-created holes have been
// scavenged immediately as part of the allocations which cannot fit in the holes.
@ -216,19 +210,9 @@ func GCPhys() {
// to other allocations that happen during this test we may still see some physical
// memory over-use.
overuse := (float64(heapBacked) - float64(stats.HeapAlloc)) / float64(stats.HeapAlloc)
// Compute the threshold.
//
// In theory, this threshold should just be zero, but that's not possible in practice.
// Firstly, the runtime's page cache can hide up to maxPageCache of free memory from the
// scavenger per P. To account for this, we increase the threshold by the ratio between the
// total amount the runtime could hide from the scavenger to the amount of memory we expect
// to be able to scavenge here, which is (size-split)*objects. This computation is the crux
// GOMAXPROCS above; if GOMAXPROCS is too high the threshold just becomes 100%+ since the
// amount of memory being allocated is fixed. Then we add 5% to account for noise, such as
// other allocations this test may have performed that we don't explicitly account for The
// baseline threshold here is around 11% for GOMAXPROCS=1, capping out at around 30% for
// GOMAXPROCS=4.
threshold := 0.05 + float64(procs)*maxPageCache/float64((size-split)*objects)
// Check against our overuse threshold, which is what the scavenger always reserves
// to encourage allocation of memory that doesn't need to be faulted in.
const threshold = 0.1
if overuse <= threshold {
fmt.Println("OK")
return
@ -243,6 +227,7 @@ func GCPhys() {
"(alloc: %d, goal: %d, sys: %d, rel: %d, objs: %d)\n", threshold*100, overuse*100,
stats.HeapAlloc, stats.NextGC, stats.HeapSys, stats.HeapReleased, len(saved))
runtime.KeepAlive(saved)
runtime.KeepAlive(condemned)
}
// Test that defer closure is correctly scanned when the stack is scanned.