| // 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.Since(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 |