blob: 0f44575381ed57f5efe1abba9faad314ca9f9ea4 [file] [log] [blame]
// Copyright 2015 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.
package main
import (
"fmt"
"math"
"os"
"runtime"
"runtime/debug"
"runtime/metrics"
"sync"
"sync/atomic"
"time"
"unsafe"
)
func init() {
register("GCFairness", GCFairness)
register("GCFairness2", GCFairness2)
register("GCSys", GCSys)
register("GCPhys", GCPhys)
register("DeferLiveness", DeferLiveness)
register("GCZombie", GCZombie)
register("GCMemoryLimit", GCMemoryLimit)
register("GCMemoryLimitNoGCPercent", GCMemoryLimitNoGCPercent)
}
func GCSys() {
runtime.GOMAXPROCS(1)
memstats := new(runtime.MemStats)
runtime.GC()
runtime.ReadMemStats(memstats)
sys := memstats.Sys
runtime.MemProfileRate = 0 // disable profiler
itercount := 100000
for i := 0; i < itercount; i++ {
workthegc()
}
// Should only be using a few MB.
// We allocated 100 MB or (if not short) 1 GB.
runtime.ReadMemStats(memstats)
if sys > memstats.Sys {
sys = 0
} else {
sys = memstats.Sys - sys
}
if sys > 16<<20 {
fmt.Printf("using too much memory: %d bytes\n", sys)
return
}
fmt.Printf("OK\n")
}
var sink []byte
func workthegc() []byte {
sink = make([]byte, 1029)
return sink
}
func GCFairness() {
runtime.GOMAXPROCS(1)
f, err := os.Open("/dev/null")
if os.IsNotExist(err) {
// This test tests what it is intended to test only if writes are fast.
// If there is no /dev/null, we just don't execute the test.
fmt.Println("OK")
return
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
for i := 0; i < 2; i++ {
go func() {
for {
f.Write([]byte("."))
}
}()
}
time.Sleep(10 * time.Millisecond)
fmt.Println("OK")
}
func GCFairness2() {
// Make sure user code can't exploit the GC's high priority
// scheduling to make scheduling of user code unfair. See
// issue #15706.
runtime.GOMAXPROCS(1)
debug.SetGCPercent(1)
var count [3]int64
var sink [3]any
for i := range count {
go func(i int) {
for {
sink[i] = make([]byte, 1024)
atomic.AddInt64(&count[i], 1)
}
}(i)
}
// Note: If the unfairness is really bad, it may not even get
// past the sleep.
//
// If the scheduling rules change, this may not be enough time
// to let all goroutines run, but for now we cycle through
// them rapidly.
//
// OpenBSD's scheduler makes every usleep() take at least
// 20ms, so we need a long time to ensure all goroutines have
// run. If they haven't run after 30ms, give it another 1000ms
// and check again.
time.Sleep(30 * time.Millisecond)
var fail bool
for i := range count {
if atomic.LoadInt64(&count[i]) == 0 {
fail = true
}
}
if fail {
time.Sleep(1 * time.Second)
for i := range count {
if atomic.LoadInt64(&count[i]) == 0 {
fmt.Printf("goroutine %d did not run\n", i)
return
}
}
}
fmt.Println("OK")
}
func GCPhys() {
// This test ensures that heap-growth scavenging is working as intended.
//
// 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 (
// The total amount of memory we're willing to allocate.
allocTotal = 32 << 20
// The page cache could hide 64 8-KiB pages from the scavenger today.
maxPageCache = (8 << 10) * 64
)
// How big the allocations are needs to depend on the page size.
// If the page size is too big and the allocations are too small,
// they might not be aligned to the physical page size, so the scavenger
// will gloss over them.
pageSize := os.Getpagesize()
var allocChunk int
if pageSize <= 8<<10 {
allocChunk = 64 << 10
} else {
allocChunk = 512 << 10
}
allocs := allocTotal / allocChunk
// Set GC percent just so this test is a little more consistent in the
// face of varying environments.
debug.SetGCPercent(100)
// 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, b)
} else {
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
// 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, subtracting it out once is fine.
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
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.
//
// Since the runtime should scavenge the entirety of the remaining holes,
// theoretically there should be no more free and unscavenged memory. However due
// 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)
// 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.
//
// Add additional slack in case the page size is large and the scavenger
// can't reach that memory because it doesn't constitute a complete aligned
// physical page. Assume the worst case: a full physical page out of each
// allocation.
threshold := 0.1 + float64(pageSize)/float64(allocChunk)
if overuse <= threshold {
fmt.Println("OK")
return
}
// Physical memory utilization exceeds the threshold, so heap-growth scavenging
// did not operate as expected.
//
// In the context of this test, this indicates a large amount of
// fragmentation with physical pages that are otherwise unused but not
// returned to the OS.
fmt.Printf("exceeded physical memory overuse threshold of %3.2f%%: %3.2f%%\n"+
"(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.
func DeferLiveness() {
var x [10]int
escape(&x)
fn := func() {
if x[0] != 42 {
panic("FAIL")
}
}
defer fn()
x[0] = 42
runtime.GC()
runtime.GC()
runtime.GC()
}
//go:noinline
func escape(x any) { sink2 = x; sink2 = nil }
var sink2 any
// Test zombie object detection and reporting.
func GCZombie() {
// Allocate several objects of unusual size (so free slots are
// unlikely to all be re-allocated by the runtime).
const size = 190
const count = 8192 / size
keep := make([]*byte, 0, (count+1)/2)
free := make([]uintptr, 0, (count+1)/2)
zombies := make([]*byte, 0, len(free))
for i := 0; i < count; i++ {
obj := make([]byte, size)
p := &obj[0]
if i%2 == 0 {
keep = append(keep, p)
} else {
free = append(free, uintptr(unsafe.Pointer(p)))
}
}
// Free the unreferenced objects.
runtime.GC()
// Bring the free objects back to life.
for _, p := range free {
zombies = append(zombies, (*byte)(unsafe.Pointer(p)))
}
// GC should detect the zombie objects.
runtime.GC()
println("failed")
runtime.KeepAlive(keep)
runtime.KeepAlive(zombies)
}
func GCMemoryLimit() {
gcMemoryLimit(100)
}
func GCMemoryLimitNoGCPercent() {
gcMemoryLimit(-1)
}
// Test SetMemoryLimit functionality.
//
// This test lives here instead of runtime/debug because the entire
// implementation is in the runtime, and testprog gives us a more
// consistent testing environment to help avoid flakiness.
func gcMemoryLimit(gcPercent int) {
if oldProcs := runtime.GOMAXPROCS(4); oldProcs < 4 {
// Fail if the default GOMAXPROCS isn't at least 4.
// Whatever invokes this should check and do a proper t.Skip.
println("insufficient CPUs")
return
}
debug.SetGCPercent(gcPercent)
const myLimit = 256 << 20
if limit := debug.SetMemoryLimit(-1); limit != math.MaxInt64 {
print("expected MaxInt64 limit, got ", limit, " bytes instead\n")
return
}
if limit := debug.SetMemoryLimit(myLimit); limit != math.MaxInt64 {
print("expected MaxInt64 limit, got ", limit, " bytes instead\n")
return
}
if limit := debug.SetMemoryLimit(-1); limit != myLimit {
print("expected a ", myLimit, "-byte limit, got ", limit, " bytes instead\n")
return
}
target := make(chan int64)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
sinkSize := int(<-target / memLimitUnit)
for {
if len(memLimitSink) != sinkSize {
memLimitSink = make([]*[memLimitUnit]byte, sinkSize)
}
for i := 0; i < len(memLimitSink); i++ {
memLimitSink[i] = new([memLimitUnit]byte)
// Write to this memory to slow down the allocator, otherwise
// we get flaky behavior. See #52433.
for j := range memLimitSink[i] {
memLimitSink[i][j] = 9
}
}
// Again, Gosched to slow down the allocator.
runtime.Gosched()
select {
case newTarget := <-target:
if newTarget == math.MaxInt64 {
return
}
sinkSize = int(newTarget / memLimitUnit)
default:
}
}
}()
var m [2]metrics.Sample
m[0].Name = "/memory/classes/total:bytes"
m[1].Name = "/memory/classes/heap/released:bytes"
// Don't set this too high, because this is a *live heap* target which
// is not directly comparable to a total memory limit.
maxTarget := int64((myLimit / 10) * 8)
increment := int64((myLimit / 10) * 1)
for i := increment; i < maxTarget; i += increment {
target <- i
// Check to make sure the memory limit is maintained.
// We're just sampling here so if it transiently goes over we might miss it.
// The internal accounting is inconsistent anyway, so going over by a few
// pages is certainly possible. Just make sure we're within some bound.
// Note that to avoid flakiness due to #52433 (especially since we're allocating
// somewhat heavily here) this bound is kept loose. In practice the Go runtime
// should do considerably better than this bound.
bound := int64(myLimit + 16<<20)
start := time.Now()
for time.Now().Sub(start) < 200*time.Millisecond {
metrics.Read(m[:])
retained := int64(m[0].Value.Uint64() - m[1].Value.Uint64())
if retained > bound {
print("retained=", retained, " limit=", myLimit, " bound=", bound, "\n")
panic("exceeded memory limit by more than bound allows")
}
runtime.Gosched()
}
}
if limit := debug.SetMemoryLimit(math.MaxInt64); limit != myLimit {
print("expected a ", myLimit, "-byte limit, got ", limit, " bytes instead\n")
return
}
println("OK")
}
// Pick a value close to the page size. We want to m
const memLimitUnit = 8000
var memLimitSink []*[memLimitUnit]byte