• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package main
6
7import (
8	"fmt"
9	"math"
10	"os"
11	"runtime"
12	"runtime/debug"
13	"runtime/metrics"
14	"sync"
15	"sync/atomic"
16	"time"
17	"unsafe"
18)
19
20func init() {
21	register("GCFairness", GCFairness)
22	register("GCFairness2", GCFairness2)
23	register("GCSys", GCSys)
24	register("GCPhys", GCPhys)
25	register("DeferLiveness", DeferLiveness)
26	register("GCZombie", GCZombie)
27	register("GCMemoryLimit", GCMemoryLimit)
28	register("GCMemoryLimitNoGCPercent", GCMemoryLimitNoGCPercent)
29}
30
31func GCSys() {
32	runtime.GOMAXPROCS(1)
33	memstats := new(runtime.MemStats)
34	runtime.GC()
35	runtime.ReadMemStats(memstats)
36	sys := memstats.Sys
37
38	runtime.MemProfileRate = 0 // disable profiler
39
40	itercount := 100000
41	for i := 0; i < itercount; i++ {
42		workthegc()
43	}
44
45	// Should only be using a few MB.
46	// We allocated 100 MB or (if not short) 1 GB.
47	runtime.ReadMemStats(memstats)
48	if sys > memstats.Sys {
49		sys = 0
50	} else {
51		sys = memstats.Sys - sys
52	}
53	if sys > 16<<20 {
54		fmt.Printf("using too much memory: %d bytes\n", sys)
55		return
56	}
57	fmt.Printf("OK\n")
58}
59
60var sink []byte
61
62func workthegc() []byte {
63	sink = make([]byte, 1029)
64	return sink
65}
66
67func GCFairness() {
68	runtime.GOMAXPROCS(1)
69	f, err := os.Open("/dev/null")
70	if os.IsNotExist(err) {
71		// This test tests what it is intended to test only if writes are fast.
72		// If there is no /dev/null, we just don't execute the test.
73		fmt.Println("OK")
74		return
75	}
76	if err != nil {
77		fmt.Println(err)
78		os.Exit(1)
79	}
80	for i := 0; i < 2; i++ {
81		go func() {
82			for {
83				f.Write([]byte("."))
84			}
85		}()
86	}
87	time.Sleep(10 * time.Millisecond)
88	fmt.Println("OK")
89}
90
91func GCFairness2() {
92	// Make sure user code can't exploit the GC's high priority
93	// scheduling to make scheduling of user code unfair. See
94	// issue #15706.
95	runtime.GOMAXPROCS(1)
96	debug.SetGCPercent(1)
97	var count [3]int64
98	var sink [3]any
99	for i := range count {
100		go func(i int) {
101			for {
102				sink[i] = make([]byte, 1024)
103				atomic.AddInt64(&count[i], 1)
104			}
105		}(i)
106	}
107	// Note: If the unfairness is really bad, it may not even get
108	// past the sleep.
109	//
110	// If the scheduling rules change, this may not be enough time
111	// to let all goroutines run, but for now we cycle through
112	// them rapidly.
113	//
114	// OpenBSD's scheduler makes every usleep() take at least
115	// 20ms, so we need a long time to ensure all goroutines have
116	// run. If they haven't run after 30ms, give it another 1000ms
117	// and check again.
118	time.Sleep(30 * time.Millisecond)
119	var fail bool
120	for i := range count {
121		if atomic.LoadInt64(&count[i]) == 0 {
122			fail = true
123		}
124	}
125	if fail {
126		time.Sleep(1 * time.Second)
127		for i := range count {
128			if atomic.LoadInt64(&count[i]) == 0 {
129				fmt.Printf("goroutine %d did not run\n", i)
130				return
131			}
132		}
133	}
134	fmt.Println("OK")
135}
136
137func GCPhys() {
138	// This test ensures that heap-growth scavenging is working as intended.
139	//
140	// It attempts to construct a sizeable "swiss cheese" heap, with many
141	// allocChunk-sized holes. Then, it triggers a heap growth by trying to
142	// allocate as much memory as would fit in those holes.
143	//
144	// The heap growth should cause a large number of those holes to be
145	// returned to the OS.
146
147	const (
148		// The total amount of memory we're willing to allocate.
149		allocTotal = 32 << 20
150
151		// The page cache could hide 64 8-KiB pages from the scavenger today.
152		maxPageCache = (8 << 10) * 64
153	)
154
155	// How big the allocations are needs to depend on the page size.
156	// If the page size is too big and the allocations are too small,
157	// they might not be aligned to the physical page size, so the scavenger
158	// will gloss over them.
159	pageSize := os.Getpagesize()
160	var allocChunk int
161	if pageSize <= 8<<10 {
162		allocChunk = 64 << 10
163	} else {
164		allocChunk = 512 << 10
165	}
166	allocs := allocTotal / allocChunk
167
168	// Set GC percent just so this test is a little more consistent in the
169	// face of varying environments.
170	debug.SetGCPercent(100)
171
172	// Set GOMAXPROCS to 1 to minimize the amount of memory held in the page cache,
173	// and to reduce the chance that the background scavenger gets scheduled.
174	defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
175
176	// Allocate allocTotal bytes of memory in allocChunk byte chunks.
177	// Alternate between whether the chunk will be held live or will be
178	// condemned to GC to create holes in the heap.
179	saved := make([][]byte, allocs/2+1)
180	condemned := make([][]byte, allocs/2)
181	for i := 0; i < allocs; i++ {
182		b := make([]byte, allocChunk)
183		if i%2 == 0 {
184			saved = append(saved, b)
185		} else {
186			condemned = append(condemned, b)
187		}
188	}
189
190	// Run a GC cycle just so we're at a consistent state.
191	runtime.GC()
192
193	// Drop the only reference to all the condemned memory.
194	condemned = nil
195
196	// Clear the condemned memory.
197	runtime.GC()
198
199	// At this point, the background scavenger is likely running
200	// and could pick up the work, so the next line of code doesn't
201	// end up doing anything. That's fine. What's important is that
202	// this test fails somewhat regularly if the runtime doesn't
203	// scavenge on heap growth, and doesn't fail at all otherwise.
204
205	// Make a large allocation that in theory could fit, but won't
206	// because we turned the heap into swiss cheese.
207	saved = append(saved, make([]byte, allocTotal/2))
208
209	// heapBacked is an estimate of the amount of physical memory used by
210	// this test. HeapSys is an estimate of the size of the mapped virtual
211	// address space (which may or may not be backed by physical pages)
212	// whereas HeapReleased is an estimate of the amount of bytes returned
213	// to the OS. Their difference then roughly corresponds to the amount
214	// of virtual address space that is backed by physical pages.
215	//
216	// heapBacked also subtracts out maxPageCache bytes of memory because
217	// this is memory that may be hidden from the scavenger per-P. Since
218	// GOMAXPROCS=1 here, subtracting it out once is fine.
219	var stats runtime.MemStats
220	runtime.ReadMemStats(&stats)
221	heapBacked := stats.HeapSys - stats.HeapReleased - maxPageCache
222	// If heapBacked does not exceed the heap goal by more than retainExtraPercent
223	// then the scavenger is working as expected; the newly-created holes have been
224	// scavenged immediately as part of the allocations which cannot fit in the holes.
225	//
226	// Since the runtime should scavenge the entirety of the remaining holes,
227	// theoretically there should be no more free and unscavenged memory. However due
228	// to other allocations that happen during this test we may still see some physical
229	// memory over-use.
230	overuse := (float64(heapBacked) - float64(stats.HeapAlloc)) / float64(stats.HeapAlloc)
231	// Check against our overuse threshold, which is what the scavenger always reserves
232	// to encourage allocation of memory that doesn't need to be faulted in.
233	//
234	// Add additional slack in case the page size is large and the scavenger
235	// can't reach that memory because it doesn't constitute a complete aligned
236	// physical page. Assume the worst case: a full physical page out of each
237	// allocation.
238	threshold := 0.1 + float64(pageSize)/float64(allocChunk)
239	if overuse <= threshold {
240		fmt.Println("OK")
241		return
242	}
243	// Physical memory utilization exceeds the threshold, so heap-growth scavenging
244	// did not operate as expected.
245	//
246	// In the context of this test, this indicates a large amount of
247	// fragmentation with physical pages that are otherwise unused but not
248	// returned to the OS.
249	fmt.Printf("exceeded physical memory overuse threshold of %3.2f%%: %3.2f%%\n"+
250		"(alloc: %d, goal: %d, sys: %d, rel: %d, objs: %d)\n", threshold*100, overuse*100,
251		stats.HeapAlloc, stats.NextGC, stats.HeapSys, stats.HeapReleased, len(saved))
252	runtime.KeepAlive(saved)
253	runtime.KeepAlive(condemned)
254}
255
256// Test that defer closure is correctly scanned when the stack is scanned.
257func DeferLiveness() {
258	var x [10]int
259	escape(&x)
260	fn := func() {
261		if x[0] != 42 {
262			panic("FAIL")
263		}
264	}
265	defer fn()
266
267	x[0] = 42
268	runtime.GC()
269	runtime.GC()
270	runtime.GC()
271}
272
273//go:noinline
274func escape(x any) { sink2 = x; sink2 = nil }
275
276var sink2 any
277
278// Test zombie object detection and reporting.
279func GCZombie() {
280	// Allocate several objects of unusual size (so free slots are
281	// unlikely to all be re-allocated by the runtime).
282	const size = 190
283	const count = 8192 / size
284	keep := make([]*byte, 0, (count+1)/2)
285	free := make([]uintptr, 0, (count+1)/2)
286	zombies := make([]*byte, 0, len(free))
287	for i := 0; i < count; i++ {
288		obj := make([]byte, size)
289		p := &obj[0]
290		if i%2 == 0 {
291			keep = append(keep, p)
292		} else {
293			free = append(free, uintptr(unsafe.Pointer(p)))
294		}
295	}
296
297	// Free the unreferenced objects.
298	runtime.GC()
299
300	// Bring the free objects back to life.
301	for _, p := range free {
302		zombies = append(zombies, (*byte)(unsafe.Pointer(p)))
303	}
304
305	// GC should detect the zombie objects.
306	runtime.GC()
307	println("failed")
308	runtime.KeepAlive(keep)
309	runtime.KeepAlive(zombies)
310}
311
312func GCMemoryLimit() {
313	gcMemoryLimit(100)
314}
315
316func GCMemoryLimitNoGCPercent() {
317	gcMemoryLimit(-1)
318}
319
320// Test SetMemoryLimit functionality.
321//
322// This test lives here instead of runtime/debug because the entire
323// implementation is in the runtime, and testprog gives us a more
324// consistent testing environment to help avoid flakiness.
325func gcMemoryLimit(gcPercent int) {
326	if oldProcs := runtime.GOMAXPROCS(4); oldProcs < 4 {
327		// Fail if the default GOMAXPROCS isn't at least 4.
328		// Whatever invokes this should check and do a proper t.Skip.
329		println("insufficient CPUs")
330		return
331	}
332	debug.SetGCPercent(gcPercent)
333
334	const myLimit = 256 << 20
335	if limit := debug.SetMemoryLimit(-1); limit != math.MaxInt64 {
336		print("expected MaxInt64 limit, got ", limit, " bytes instead\n")
337		return
338	}
339	if limit := debug.SetMemoryLimit(myLimit); limit != math.MaxInt64 {
340		print("expected MaxInt64 limit, got ", limit, " bytes instead\n")
341		return
342	}
343	if limit := debug.SetMemoryLimit(-1); limit != myLimit {
344		print("expected a ", myLimit, "-byte limit, got ", limit, " bytes instead\n")
345		return
346	}
347
348	target := make(chan int64)
349	var wg sync.WaitGroup
350	wg.Add(1)
351	go func() {
352		defer wg.Done()
353
354		sinkSize := int(<-target / memLimitUnit)
355		for {
356			if len(memLimitSink) != sinkSize {
357				memLimitSink = make([]*[memLimitUnit]byte, sinkSize)
358			}
359			for i := 0; i < len(memLimitSink); i++ {
360				memLimitSink[i] = new([memLimitUnit]byte)
361				// Write to this memory to slow down the allocator, otherwise
362				// we get flaky behavior. See #52433.
363				for j := range memLimitSink[i] {
364					memLimitSink[i][j] = 9
365				}
366			}
367			// Again, Gosched to slow down the allocator.
368			runtime.Gosched()
369			select {
370			case newTarget := <-target:
371				if newTarget == math.MaxInt64 {
372					return
373				}
374				sinkSize = int(newTarget / memLimitUnit)
375			default:
376			}
377		}
378	}()
379	var m [2]metrics.Sample
380	m[0].Name = "/memory/classes/total:bytes"
381	m[1].Name = "/memory/classes/heap/released:bytes"
382
383	// Don't set this too high, because this is a *live heap* target which
384	// is not directly comparable to a total memory limit.
385	maxTarget := int64((myLimit / 10) * 8)
386	increment := int64((myLimit / 10) * 1)
387	for i := increment; i < maxTarget; i += increment {
388		target <- i
389
390		// Check to make sure the memory limit is maintained.
391		// We're just sampling here so if it transiently goes over we might miss it.
392		// The internal accounting is inconsistent anyway, so going over by a few
393		// pages is certainly possible. Just make sure we're within some bound.
394		// Note that to avoid flakiness due to #52433 (especially since we're allocating
395		// somewhat heavily here) this bound is kept loose. In practice the Go runtime
396		// should do considerably better than this bound.
397		bound := int64(myLimit + 16<<20)
398		start := time.Now()
399		for time.Since(start) < 200*time.Millisecond {
400			metrics.Read(m[:])
401			retained := int64(m[0].Value.Uint64() - m[1].Value.Uint64())
402			if retained > bound {
403				print("retained=", retained, " limit=", myLimit, " bound=", bound, "\n")
404				panic("exceeded memory limit by more than bound allows")
405			}
406			runtime.Gosched()
407		}
408	}
409
410	if limit := debug.SetMemoryLimit(math.MaxInt64); limit != myLimit {
411		print("expected a ", myLimit, "-byte limit, got ", limit, " bytes instead\n")
412		return
413	}
414	println("OK")
415}
416
417// Pick a value close to the page size. We want to m
418const memLimitUnit = 8000
419
420var memLimitSink []*[memLimitUnit]byte
421