Skip to content

Commit 120f187

Browse files
committed
runtime: add more precise test of assist credit handling for runtime.freegc
This CL is part of a set of CLs that attempt to reduce how much work the GC must do. See the design in https://go.dev/design/74299-runtime-freegc This CL adds a better test of assist credit handling when heap objects are being reused after a runtime.freegc call. The main approach is bracketing alloc/free pairs with measurements of the assist credit before and after, and hoping to see a net zero change in the assist credit. However, validating the desired behavior is perhaps a bit subtle. To help stabilize the measurements, we do acquirem in the test code to avoid being preempted during the measurements to reduce other code's ability to adjust the assist credit while we are measuring, and we also reduce GOMAXPROCS to 1. This test currently does fail if we deliberately introduce bugs in the runtime.freegc implementation such as if we: - never adjust the assist credit when reusing an object, or - always adjust the assist credit when reusing an object, or - deliberately mishandle internal fragmentation. The two main cases of current interest for testing runtime.freegc are when over the course of our bracketed measurements gcBlackenEnable is either true or false. The test attempts to exercise both of those case by running the GC continually in the background (which we can see seems effective based on logging and by how our deliberate bugs fail). This passes ~10K test executions locally via stress. A small note to the future: a previous incarnation of this test (circa patchset 11 of this CL) did not do acquirem but had an approach of ignoring certain measurements, which also was able to pass ~10K runs via stress. The current version in this CL is simpler, but recording the existence of the prior version here in case it is useful in the future. (Hopefully not.) Updates #74299 Change-Id: I46c7e0295d125f5884fee0cc3d3d31aedc7e5ff4 Reviewed-on: https://go-review.googlesource.com/c/go/+/717520 Reviewed-by: Michael Knyszek <[email protected]> Reviewed-by: Junyang Shao <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent fecfcaa commit 120f187

File tree

2 files changed

+117
-4
lines changed

2 files changed

+117
-4
lines changed

src/runtime/export_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,25 @@ func Freegc(p unsafe.Pointer, size uintptr, noscan bool) {
644644
freegc(p, size, noscan)
645645
}
646646

647+
// Expose gcAssistBytes for the current g for testing.
648+
func AssistCredit() int64 {
649+
assistG := getg()
650+
if assistG.m.curg != nil {
651+
assistG = assistG.m.curg
652+
}
653+
return assistG.gcAssistBytes
654+
}
655+
656+
// Expose gcBlackenEnabled for testing.
657+
func GcBlackenEnable() bool {
658+
// Note we do a non-atomic load here.
659+
// Some checks against gcBlackenEnabled (e.g., in mallocgc)
660+
// are currently done via non-atomic load for performance reasons,
661+
// but other checks are done via atomic load (e.g., in mgcmark.go),
662+
// so interpreting this value in a test may be subtle.
663+
return gcBlackenEnabled != 0
664+
}
665+
647666
const SizeSpecializedMallocEnabled = sizeSpecializedMallocEnabled
648667

649668
const RuntimeFreegcEnabled = runtimeFreegcEnabled
@@ -1487,6 +1506,15 @@ func Releasem() {
14871506
releasem(getg().m)
14881507
}
14891508

1509+
// GoschedIfBusy is an explicit preemption check to call back
1510+
// into the scheduler. This is useful for tests that run code
1511+
// which spend most of their time as non-preemptible, as it
1512+
// can be placed right after becoming preemptible again to ensure
1513+
// that the scheduler gets a chance to preempt the goroutine.
1514+
func GoschedIfBusy() {
1515+
goschedIfBusy()
1516+
}
1517+
14901518
type PIController struct {
14911519
piController
14921520
}

src/runtime/malloc_test.go

Lines changed: 89 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ func TestFreegc(t *testing.T) {
249249
{"size=500", testFreegc[[500]byte], true},
250250
{"size=512", testFreegc[[512]byte], true},
251251
{"size=4096", testFreegc[[4096]byte], true},
252+
{"size=20000", testFreegc[[20000]byte], true}, // not power of 2 or spc boundary
252253
{"size=32KiB-8", testFreegc[[1<<15 - 8]byte], true}, // max noscan small object for 64-bit
253254
}
254255

@@ -300,7 +301,7 @@ func testFreegc[T comparable](noscan bool) func(*testing.T) {
300301
t.Helper()
301302
var zero T
302303
if *p != zero {
303-
t.Fatalf("found non-zero memory before freeing (tests do not modify memory): %v", *p)
304+
t.Fatalf("found non-zero memory before freegc (tests do not modify memory): %v", *p)
304305
}
305306
runtime.Freegc(unsafe.Pointer(p), unsafe.Sizeof(*p), noscan)
306307
}
@@ -405,7 +406,7 @@ func testFreegc[T comparable](noscan bool) func(*testing.T) {
405406
// Confirm we are graceful if we have more freed elements at once
406407
// than the max free list size.
407408
s := make([]*T, 0, 1000)
408-
iterations := stressMultiple * stressMultiple // currently 1 or 100 depending on -short
409+
iterations := stressMultiple * stressMultiple // currently 1 (-short) or 100
409410
for range iterations {
410411
s = s[:0]
411412
for range 1000 {
@@ -431,7 +432,7 @@ func testFreegc[T comparable](noscan bool) func(*testing.T) {
431432
p := alloc()
432433
uptr := uintptr(unsafe.Pointer(p))
433434
if live[uptr] {
434-
t.Fatalf("TestFreeLive: found duplicate pointer (0x%x). i: %d j: %d", uptr, i, j)
435+
t.Fatalf("found duplicate pointer (0x%x). i: %d j: %d", uptr, i, j)
435436
}
436437
live[uptr] = true
437438
s = append(s, p)
@@ -451,7 +452,7 @@ func testFreegc[T comparable](noscan bool) func(*testing.T) {
451452
// Use explicit free, but the free happens on a different goroutine than the alloc.
452453
// This also lightly simulates how the free code sees P migration or flushing
453454
// the mcache, assuming we have > 1 P. (Not using testing.AllocsPerRun here).
454-
iterations := 10 * stressMultiple * stressMultiple // currently 10 or 1000 depending on -short
455+
iterations := 10 * stressMultiple * stressMultiple // currently 10 (-short) or 1000
455456
for _, capacity := range []int{2} {
456457
for range iterations {
457458
ch := make(chan *T, capacity)
@@ -501,6 +502,90 @@ func testFreegc[T comparable](noscan bool) func(*testing.T) {
501502
wg.Wait()
502503
}
503504
})
505+
506+
t.Run("assist-credit", func(t *testing.T) {
507+
// Allocate and free using the same span class repeatedly while
508+
// verifying it results in a net zero change in assist credit.
509+
// This helps double-check our manipulation of the assist credit
510+
// during mallocgc/freegc, including in cases when there is
511+
// internal fragmentation when the requested mallocgc size is
512+
// smaller than the size class.
513+
//
514+
// See https://go.dev/cl/717520 for some additional discussion,
515+
// including how we can deliberately cause the test to fail currently
516+
// if we purposefully introduce some assist credit bugs.
517+
if SizeSpecializedMallocEnabled {
518+
// TODO(thepudds): skip this test at this point in the stack; later CL has
519+
// integration with sizespecializedmalloc.
520+
t.Skip("temporarily skip assist credit test for GOEXPERIMENT=sizespecializedmalloc")
521+
}
522+
if !RuntimeFreegcEnabled {
523+
t.Skip("skipping assist credit test with runtime.freegc disabled")
524+
}
525+
526+
// Use a background goroutine to continuously run the GC.
527+
done := make(chan struct{})
528+
defer close(done)
529+
go func() {
530+
for {
531+
select {
532+
case <-done:
533+
return
534+
default:
535+
runtime.GC()
536+
}
537+
}
538+
}()
539+
540+
// If making changes related to this test, consider testing locally with
541+
// larger counts, like 100K or 1M.
542+
counts := []int{1, 2, 10, 100 * stressMultiple}
543+
// Dropping down to GOMAXPROCS=1 might help reduce noise.
544+
defer GOMAXPROCS(GOMAXPROCS(1))
545+
size := int64(unsafe.Sizeof(*new(T)))
546+
for _, count := range counts {
547+
// Start by forcing a GC to reset this g's assist credit
548+
// and perhaps help us get a cleaner measurement of GC cycle count.
549+
runtime.GC()
550+
for i := range count {
551+
// We disable preemption to reduce other code's ability to adjust this g's
552+
// assist credit or otherwise change things while we are measuring.
553+
Acquirem()
554+
555+
// We do two allocations per loop, with the second allocation being
556+
// the one we measure. The first allocation tries to ensure at least one
557+
// reusable object on the mspan's free list when we do our measured allocation.
558+
p := alloc()
559+
free(p)
560+
561+
// Now do our primary allocation of interest, bracketed by measurements.
562+
// We measure more than we strictly need (to log details in case of a failure).
563+
creditStart := AssistCredit()
564+
blackenStart := GcBlackenEnable()
565+
p = alloc()
566+
blackenAfterAlloc := GcBlackenEnable()
567+
creditAfterAlloc := AssistCredit()
568+
free(p)
569+
blackenEnd := GcBlackenEnable()
570+
creditEnd := AssistCredit()
571+
572+
Releasem()
573+
GoschedIfBusy()
574+
575+
delta := creditEnd - creditStart
576+
if delta != 0 {
577+
t.Logf("assist credit non-zero delta: %d", delta)
578+
t.Logf("\t| size: %d i: %d count: %d", size, i, count)
579+
t.Logf("\t| credit before: %d credit after: %d", creditStart, creditEnd)
580+
t.Logf("\t| alloc delta: %d free delta: %d",
581+
creditAfterAlloc-creditStart, creditEnd-creditAfterAlloc)
582+
t.Logf("\t| gcBlackenEnable (start / after alloc / end): %v/%v/%v",
583+
blackenStart, blackenAfterAlloc, blackenEnd)
584+
t.FailNow()
585+
}
586+
}
587+
}
588+
})
504589
}
505590
}
506591

0 commit comments

Comments
 (0)