Skip to content

Commit

Permalink
[release-branch.go1.14] runtime/debug: add SetMaxHeap API
Browse files Browse the repository at this point in the history
DO NOT SUBMIT. This is an experiment to get some experience with the
API and figure out if this is even a reasonable primitive.

This adds an API to set a soft limit on the heap size. This augments
the existing GOGC-based GC policy by using the lower of the
GOGC-computed GC target and the heap limit.

When the garbage collector is bounded by the heap limit, it can no
longer amortize the cost of garbage collection against the cost of
growing the heap. Hence, callers of this API are required to register
for notifications of when the garbage collector is under pressure and
are strongly encouraged/expected to use this signal to shed load.

This CL incorporates fixes from CL 151540, CL 156917, and CL 183317 by
mknyszek@google.com.

Updates golang#29696.

Change-Id: I82e6df42ada6bd77f0a998a8ac021058996a8d65
  • Loading branch information
aclements authored and bradfitz committed Jun 14, 2020
1 parent c650c2d commit 4c914b8
Show file tree
Hide file tree
Showing 5 changed files with 459 additions and 37 deletions.
6 changes: 6 additions & 0 deletions api/go1.14.txt
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ pkg net/http, method (Header) Values(string) []string
pkg net/http, type Transport struct, DialTLSContext func(context.Context, string, string) (net.Conn, error)
pkg net/http/httptest, type Server struct, EnableHTTP2 bool
pkg net/textproto, method (MIMEHeader) Values(string) []string
pkg runtime/debug, func ReadGCPolicy(*GCPolicy)
pkg runtime/debug, func SetMaxHeap(uintptr, chan<- struct) uintptr
pkg runtime/debug, type GCPolicy struct
pkg runtime/debug, type GCPolicy struct, AvailGCPercent int
pkg runtime/debug, type GCPolicy struct, GCPercent int
pkg runtime/debug, type GCPolicy struct, MaxHeapBytes uintptr
pkg strconv, method (*NumError) Unwrap() error
pkg syscall (windows-386), const CTRL_CLOSE_EVENT = 2
pkg syscall (windows-386), const CTRL_CLOSE_EVENT ideal-int
Expand Down
116 changes: 115 additions & 1 deletion src/runtime/debug/garbage.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ func ReadGCStats(stats *GCStats) {
// SetGCPercent returns the previous setting.
// The initial setting is the value of the GOGC environment variable
// at startup, or 100 if the variable is not set.
// A negative percentage disables garbage collection.
// A negative percentage disables triggering garbage collection
// based on the ratio of fresh allocation to previously live heap.
// However, GC can still be explicitly triggered by runtime.GC and
// similar functions, or by the maximum heap size set by SetMaxHeap.
func SetGCPercent(percent int) int {
return int(setGCPercent(int32(percent)))
}
Expand Down Expand Up @@ -166,3 +169,114 @@ func WriteHeapDump(fd uintptr)
// If SetTraceback is called with a level lower than that of the
// environment variable, the call is ignored.
func SetTraceback(level string)

// GCPolicy reports the garbage collector's policy for controlling the
// heap size and scheduling garbage collection work.
type GCPolicy struct {
// GCPercent is the current value of GOGC, as set by the GOGC
// environment variable or SetGCPercent.
//
// If triggering GC by relative heap growth is disabled, this
// will be -1.
GCPercent int

// MaxHeapBytes is the current soft heap limit set by
// SetMaxHeap, in bytes.
//
// If there is no heap limit set, this will be ^uintptr(0).
MaxHeapBytes uintptr

// AvailGCPercent is the heap space available for allocation
// before the next GC, as a percent of the heap used at the
// end of the previous garbage collection. It measures memory
// pressure and how hard the garbage collector must work to
// achieve the heap size goals set by GCPercent and
// MaxHeapBytes.
//
// For example, if AvailGCPercent is 100, then at the end of
// the previous garbage collection, the space available for
// allocation before the next GC was the same as the space
// used. If AvailGCPercent is 20, then the space available is
// only a 20% of the space used.
//
// AvailGCPercent is directly comparable with GCPercent.
//
// If AvailGCPercent >= GCPercent, the garbage collector is
// not under pressure and can amortize the cost of garbage
// collection by allowing the heap to grow in proportion to
// how much is used.
//
// If AvailGCPercent < GCPercent, the garbage collector is
// under pressure and must run more frequently to keep the
// heap size under MaxHeapBytes. Smaller values of
// AvailGCPercent indicate greater pressure. In this case, the
// application should shed load and reduce its live heap size
// to relieve memory pressure.
//
// AvailGCPercent is always >= 0.
AvailGCPercent int
}

// SetMaxHeap sets a soft limit on the size of the Go heap and returns
// the previous setting. By default, there is no limit.
//
// If a max heap is set, the garbage collector will endeavor to keep
// the heap size under the specified size, even if this is lower than
// would normally be determined by GOGC (see SetGCPercent).
//
// Whenever the garbage collector's scheduling policy changes as a
// result of this heap limit (that is, the result that would be
// returned by ReadGCPolicy changes), the garbage collector will send
// to the notify channel. This is a non-blocking send, so this should
// be a single-element buffered channel, though this is not required.
// Only a single channel may be registered for notifications at a
// time; SetMaxHeap replaces any previously registered channel.
//
// The application is strongly encouraged to respond to this
// notification by calling ReadGCPolicy and, if AvailGCPercent is less
// than GCPercent, shedding load to reduce its live heap size. Setting
// a maximum heap size limits the garbage collector's ability to
// amortize the cost of garbage collection when the heap reaches the
// heap size limit. This is particularly important in
// request-processing systems, where increasing pressure on the
// garbage collector reduces CPU time available to the application,
// making it less able to complete work, leading to even more pressure
// on the garbage collector. The application must shed load to avoid
// this "GC death spiral".
//
// The limit set by SetMaxHeap is soft. If the garbage collector would
// consume too much CPU to keep the heap under this limit (leading to
// "thrashing"), it will allow the heap to grow larger than the
// specified max heap.
//
// The heap size does not include everything in the process's memory
// footprint. Notably, it does not include stacks, C-allocated memory,
// or many runtime-internal structures.
//
// To disable the heap limit, pass ^uintptr(0) for the bytes argument.
// In this case, notify can be nil.
//
// To depend only on the heap limit to trigger garbage collection,
// call SetGCPercent(-1) after setting a heap limit.
func SetMaxHeap(bytes uintptr, notify chan<- struct{}) uintptr {
if bytes == ^uintptr(0) {
return gcSetMaxHeap(bytes, nil)
}
if notify == nil {
panic("SetMaxHeap requires a non-nil notify channel")
}
return gcSetMaxHeap(bytes, notify)
}

// gcSetMaxHeap is provided by package runtime.
func gcSetMaxHeap(bytes uintptr, notify chan<- struct{}) uintptr

// ReadGCPolicy reads the garbage collector's current policy for
// managing the heap size. This includes static settings controlled by
// the application and dynamic policy determined by heap usage.
func ReadGCPolicy(gcp *GCPolicy) {
gcp.GCPercent, gcp.MaxHeapBytes, gcp.AvailGCPercent = gcReadPolicy()
}

// gcReadPolicy is provided by package runtime.
func gcReadPolicy() (gogc int, maxHeap uintptr, egogc int)
108 changes: 108 additions & 0 deletions src/runtime/debug/garbage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,114 @@ func abs64(a int64) int64 {
return a
}

func TestSetMaxHeap(t *testing.T) {
defer func() {
setGCPercentBallast = nil
setGCPercentSink = nil
SetMaxHeap(^uintptr(0), nil)
runtime.GC()
}()

runtime.GC()
var m1 runtime.MemStats
runtime.ReadMemStats(&m1)

// Create 50 MB of live heap as a baseline.
const baseline = 50 << 20
setGCPercentBallast = make([]byte, baseline-m1.Alloc)
// Disable GOGC-based policy.
defer SetGCPercent(SetGCPercent(-1))
// Set max heap to 2x baseline.
const limit = 2 * baseline
notify := make(chan struct{}, 1)
prev := SetMaxHeap(limit, notify)

// Check that that notified us of heap pressure.
select {
case <-notify:
default:
t.Errorf("missing GC pressure notification")
}

// Test return value.
if prev != ^uintptr(0) {
t.Errorf("want previous limit %d, got %d", ^uintptr(0), prev)
}
prev = SetMaxHeap(limit, notify)
if prev != limit {
t.Errorf("want previous limit %d, got %d", limit, prev)
}

// Allocate a bunch and check that we stay under the limit.
runtime.ReadMemStats(&m1)
var m2 runtime.MemStats
var gcp GCPolicy
for i := 0; i < 200; i++ {
setGCPercentSink = make([]byte, 1<<20)
runtime.ReadMemStats(&m2)
if m2.HeapAlloc > limit {
t.Fatalf("HeapAlloc %d exceeds heap limit %d", m2.HeapAlloc, limit)
}
ReadGCPolicy(&gcp)
if gcp.GCPercent != -1 {
t.Fatalf("want GCPercent %d, got policy %+v", -1, gcp)
}
if gcp.MaxHeapBytes != limit {
t.Fatalf("want MaxHeapBytes %d, got policy %+v", limit, gcp)
}
const availErr = 10
availLow := 100*(limit-baseline)/baseline - availErr
availHigh := 100*(limit-baseline)/baseline + availErr
if !(availLow <= gcp.AvailGCPercent && gcp.AvailGCPercent <= availHigh) {
t.Fatalf("AvailGCPercent %d out of range [%d, %d]", gcp.AvailGCPercent, availLow, availHigh)
}
}
if m1.NumGC == m2.NumGC {
t.Fatalf("failed to trigger GC")
}
}

func TestAvailGCPercent(t *testing.T) {
defer func() {
SetMaxHeap(^uintptr(0), nil)
runtime.GC()
}()

runtime.GC()

// Set GOGC=100.
defer SetGCPercent(SetGCPercent(100))
// Set max heap to 100MB.
const limit = 100 << 20
SetMaxHeap(limit, make(chan struct{}))

// Allocate a bunch and monitor AvailGCPercent.
var m runtime.MemStats
var gcp GCPolicy
sl := [][]byte{}
for i := 0; i < 200; i++ {
sl = append(sl, make([]byte, 1<<20))
runtime.GC()
runtime.ReadMemStats(&m)
ReadGCPolicy(&gcp)
// Use int64 to avoid overflow on 32-bit.
avail := int(100 * (limit - int64(m.HeapAlloc)) / int64(m.HeapAlloc))
if avail > 100 {
avail = 100
}
if avail < 10 {
avail = 10
}
const availErr = 2 // This is more controlled than the test above.
availLow := avail - availErr
availHigh := avail + availErr
if !(availLow <= gcp.AvailGCPercent && gcp.AvailGCPercent <= availHigh) {
t.Logf("MemStats: %+v\nGCPolicy: %+v\n", m, gcp)
t.Fatalf("AvailGCPercent %d out of range [%d, %d]", gcp.AvailGCPercent, availLow, availHigh)
}
}
}

func TestSetMaxThreadsOvf(t *testing.T) {
// Verify that a big threads count will not overflow the int32
// maxmcount variable, causing a panic (see Issue 16076).
Expand Down
Loading

0 comments on commit 4c914b8

Please sign in to comment.