Skip to content

Commit 4c914b8

Browse files
aclementsbradfitz
authored andcommitted
[release-branch.go1.14] runtime/debug: add SetMaxHeap API
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 [email protected]. Updates golang#29696. Change-Id: I82e6df42ada6bd77f0a998a8ac021058996a8d65
1 parent c650c2d commit 4c914b8

File tree

5 files changed

+459
-37
lines changed

5 files changed

+459
-37
lines changed

api/go1.14.txt

+6
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,12 @@ pkg net/http, method (Header) Values(string) []string
174174
pkg net/http, type Transport struct, DialTLSContext func(context.Context, string, string) (net.Conn, error)
175175
pkg net/http/httptest, type Server struct, EnableHTTP2 bool
176176
pkg net/textproto, method (MIMEHeader) Values(string) []string
177+
pkg runtime/debug, func ReadGCPolicy(*GCPolicy)
178+
pkg runtime/debug, func SetMaxHeap(uintptr, chan<- struct) uintptr
179+
pkg runtime/debug, type GCPolicy struct
180+
pkg runtime/debug, type GCPolicy struct, AvailGCPercent int
181+
pkg runtime/debug, type GCPolicy struct, GCPercent int
182+
pkg runtime/debug, type GCPolicy struct, MaxHeapBytes uintptr
177183
pkg strconv, method (*NumError) Unwrap() error
178184
pkg syscall (windows-386), const CTRL_CLOSE_EVENT = 2
179185
pkg syscall (windows-386), const CTRL_CLOSE_EVENT ideal-int

src/runtime/debug/garbage.go

+115-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,10 @@ func ReadGCStats(stats *GCStats) {
8787
// SetGCPercent returns the previous setting.
8888
// The initial setting is the value of the GOGC environment variable
8989
// at startup, or 100 if the variable is not set.
90-
// A negative percentage disables garbage collection.
90+
// A negative percentage disables triggering garbage collection
91+
// based on the ratio of fresh allocation to previously live heap.
92+
// However, GC can still be explicitly triggered by runtime.GC and
93+
// similar functions, or by the maximum heap size set by SetMaxHeap.
9194
func SetGCPercent(percent int) int {
9295
return int(setGCPercent(int32(percent)))
9396
}
@@ -166,3 +169,114 @@ func WriteHeapDump(fd uintptr)
166169
// If SetTraceback is called with a level lower than that of the
167170
// environment variable, the call is ignored.
168171
func SetTraceback(level string)
172+
173+
// GCPolicy reports the garbage collector's policy for controlling the
174+
// heap size and scheduling garbage collection work.
175+
type GCPolicy struct {
176+
// GCPercent is the current value of GOGC, as set by the GOGC
177+
// environment variable or SetGCPercent.
178+
//
179+
// If triggering GC by relative heap growth is disabled, this
180+
// will be -1.
181+
GCPercent int
182+
183+
// MaxHeapBytes is the current soft heap limit set by
184+
// SetMaxHeap, in bytes.
185+
//
186+
// If there is no heap limit set, this will be ^uintptr(0).
187+
MaxHeapBytes uintptr
188+
189+
// AvailGCPercent is the heap space available for allocation
190+
// before the next GC, as a percent of the heap used at the
191+
// end of the previous garbage collection. It measures memory
192+
// pressure and how hard the garbage collector must work to
193+
// achieve the heap size goals set by GCPercent and
194+
// MaxHeapBytes.
195+
//
196+
// For example, if AvailGCPercent is 100, then at the end of
197+
// the previous garbage collection, the space available for
198+
// allocation before the next GC was the same as the space
199+
// used. If AvailGCPercent is 20, then the space available is
200+
// only a 20% of the space used.
201+
//
202+
// AvailGCPercent is directly comparable with GCPercent.
203+
//
204+
// If AvailGCPercent >= GCPercent, the garbage collector is
205+
// not under pressure and can amortize the cost of garbage
206+
// collection by allowing the heap to grow in proportion to
207+
// how much is used.
208+
//
209+
// If AvailGCPercent < GCPercent, the garbage collector is
210+
// under pressure and must run more frequently to keep the
211+
// heap size under MaxHeapBytes. Smaller values of
212+
// AvailGCPercent indicate greater pressure. In this case, the
213+
// application should shed load and reduce its live heap size
214+
// to relieve memory pressure.
215+
//
216+
// AvailGCPercent is always >= 0.
217+
AvailGCPercent int
218+
}
219+
220+
// SetMaxHeap sets a soft limit on the size of the Go heap and returns
221+
// the previous setting. By default, there is no limit.
222+
//
223+
// If a max heap is set, the garbage collector will endeavor to keep
224+
// the heap size under the specified size, even if this is lower than
225+
// would normally be determined by GOGC (see SetGCPercent).
226+
//
227+
// Whenever the garbage collector's scheduling policy changes as a
228+
// result of this heap limit (that is, the result that would be
229+
// returned by ReadGCPolicy changes), the garbage collector will send
230+
// to the notify channel. This is a non-blocking send, so this should
231+
// be a single-element buffered channel, though this is not required.
232+
// Only a single channel may be registered for notifications at a
233+
// time; SetMaxHeap replaces any previously registered channel.
234+
//
235+
// The application is strongly encouraged to respond to this
236+
// notification by calling ReadGCPolicy and, if AvailGCPercent is less
237+
// than GCPercent, shedding load to reduce its live heap size. Setting
238+
// a maximum heap size limits the garbage collector's ability to
239+
// amortize the cost of garbage collection when the heap reaches the
240+
// heap size limit. This is particularly important in
241+
// request-processing systems, where increasing pressure on the
242+
// garbage collector reduces CPU time available to the application,
243+
// making it less able to complete work, leading to even more pressure
244+
// on the garbage collector. The application must shed load to avoid
245+
// this "GC death spiral".
246+
//
247+
// The limit set by SetMaxHeap is soft. If the garbage collector would
248+
// consume too much CPU to keep the heap under this limit (leading to
249+
// "thrashing"), it will allow the heap to grow larger than the
250+
// specified max heap.
251+
//
252+
// The heap size does not include everything in the process's memory
253+
// footprint. Notably, it does not include stacks, C-allocated memory,
254+
// or many runtime-internal structures.
255+
//
256+
// To disable the heap limit, pass ^uintptr(0) for the bytes argument.
257+
// In this case, notify can be nil.
258+
//
259+
// To depend only on the heap limit to trigger garbage collection,
260+
// call SetGCPercent(-1) after setting a heap limit.
261+
func SetMaxHeap(bytes uintptr, notify chan<- struct{}) uintptr {
262+
if bytes == ^uintptr(0) {
263+
return gcSetMaxHeap(bytes, nil)
264+
}
265+
if notify == nil {
266+
panic("SetMaxHeap requires a non-nil notify channel")
267+
}
268+
return gcSetMaxHeap(bytes, notify)
269+
}
270+
271+
// gcSetMaxHeap is provided by package runtime.
272+
func gcSetMaxHeap(bytes uintptr, notify chan<- struct{}) uintptr
273+
274+
// ReadGCPolicy reads the garbage collector's current policy for
275+
// managing the heap size. This includes static settings controlled by
276+
// the application and dynamic policy determined by heap usage.
277+
func ReadGCPolicy(gcp *GCPolicy) {
278+
gcp.GCPercent, gcp.MaxHeapBytes, gcp.AvailGCPercent = gcReadPolicy()
279+
}
280+
281+
// gcReadPolicy is provided by package runtime.
282+
func gcReadPolicy() (gogc int, maxHeap uintptr, egogc int)

src/runtime/debug/garbage_test.go

+108
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,114 @@ func abs64(a int64) int64 {
179179
return a
180180
}
181181

182+
func TestSetMaxHeap(t *testing.T) {
183+
defer func() {
184+
setGCPercentBallast = nil
185+
setGCPercentSink = nil
186+
SetMaxHeap(^uintptr(0), nil)
187+
runtime.GC()
188+
}()
189+
190+
runtime.GC()
191+
var m1 runtime.MemStats
192+
runtime.ReadMemStats(&m1)
193+
194+
// Create 50 MB of live heap as a baseline.
195+
const baseline = 50 << 20
196+
setGCPercentBallast = make([]byte, baseline-m1.Alloc)
197+
// Disable GOGC-based policy.
198+
defer SetGCPercent(SetGCPercent(-1))
199+
// Set max heap to 2x baseline.
200+
const limit = 2 * baseline
201+
notify := make(chan struct{}, 1)
202+
prev := SetMaxHeap(limit, notify)
203+
204+
// Check that that notified us of heap pressure.
205+
select {
206+
case <-notify:
207+
default:
208+
t.Errorf("missing GC pressure notification")
209+
}
210+
211+
// Test return value.
212+
if prev != ^uintptr(0) {
213+
t.Errorf("want previous limit %d, got %d", ^uintptr(0), prev)
214+
}
215+
prev = SetMaxHeap(limit, notify)
216+
if prev != limit {
217+
t.Errorf("want previous limit %d, got %d", limit, prev)
218+
}
219+
220+
// Allocate a bunch and check that we stay under the limit.
221+
runtime.ReadMemStats(&m1)
222+
var m2 runtime.MemStats
223+
var gcp GCPolicy
224+
for i := 0; i < 200; i++ {
225+
setGCPercentSink = make([]byte, 1<<20)
226+
runtime.ReadMemStats(&m2)
227+
if m2.HeapAlloc > limit {
228+
t.Fatalf("HeapAlloc %d exceeds heap limit %d", m2.HeapAlloc, limit)
229+
}
230+
ReadGCPolicy(&gcp)
231+
if gcp.GCPercent != -1 {
232+
t.Fatalf("want GCPercent %d, got policy %+v", -1, gcp)
233+
}
234+
if gcp.MaxHeapBytes != limit {
235+
t.Fatalf("want MaxHeapBytes %d, got policy %+v", limit, gcp)
236+
}
237+
const availErr = 10
238+
availLow := 100*(limit-baseline)/baseline - availErr
239+
availHigh := 100*(limit-baseline)/baseline + availErr
240+
if !(availLow <= gcp.AvailGCPercent && gcp.AvailGCPercent <= availHigh) {
241+
t.Fatalf("AvailGCPercent %d out of range [%d, %d]", gcp.AvailGCPercent, availLow, availHigh)
242+
}
243+
}
244+
if m1.NumGC == m2.NumGC {
245+
t.Fatalf("failed to trigger GC")
246+
}
247+
}
248+
249+
func TestAvailGCPercent(t *testing.T) {
250+
defer func() {
251+
SetMaxHeap(^uintptr(0), nil)
252+
runtime.GC()
253+
}()
254+
255+
runtime.GC()
256+
257+
// Set GOGC=100.
258+
defer SetGCPercent(SetGCPercent(100))
259+
// Set max heap to 100MB.
260+
const limit = 100 << 20
261+
SetMaxHeap(limit, make(chan struct{}))
262+
263+
// Allocate a bunch and monitor AvailGCPercent.
264+
var m runtime.MemStats
265+
var gcp GCPolicy
266+
sl := [][]byte{}
267+
for i := 0; i < 200; i++ {
268+
sl = append(sl, make([]byte, 1<<20))
269+
runtime.GC()
270+
runtime.ReadMemStats(&m)
271+
ReadGCPolicy(&gcp)
272+
// Use int64 to avoid overflow on 32-bit.
273+
avail := int(100 * (limit - int64(m.HeapAlloc)) / int64(m.HeapAlloc))
274+
if avail > 100 {
275+
avail = 100
276+
}
277+
if avail < 10 {
278+
avail = 10
279+
}
280+
const availErr = 2 // This is more controlled than the test above.
281+
availLow := avail - availErr
282+
availHigh := avail + availErr
283+
if !(availLow <= gcp.AvailGCPercent && gcp.AvailGCPercent <= availHigh) {
284+
t.Logf("MemStats: %+v\nGCPolicy: %+v\n", m, gcp)
285+
t.Fatalf("AvailGCPercent %d out of range [%d, %d]", gcp.AvailGCPercent, availLow, availHigh)
286+
}
287+
}
288+
}
289+
182290
func TestSetMaxThreadsOvf(t *testing.T) {
183291
// Verify that a big threads count will not overflow the int32
184292
// maxmcount variable, causing a panic (see Issue 16076).

0 commit comments

Comments
 (0)