From dd65955a9a6be8126e38460da1f7cecc6ae0488e Mon Sep 17 00:00:00 2001 From: Dave Collins Date: Mon, 14 Nov 2022 21:43:51 -0600 Subject: [PATCH] main: Tweak runtime GC params for Go 1.19. Go 1.19 introduced the ability to specify a soft upper memory limit to help deal with transient memory spikes without needing to set the GC percent to an artificially low value. This takes advantage of that to impose an upper memory limit that leaves plenty of headroom for the minimum recommended value while taking into account the max utxo cache size configuration option and increase the GC percent to the default value of 100% which allows the mem usage to expand more in between each GC cycle which in turn significantly reduces the number of GC cycles that need to be performed, particularly while performing the initial chain sync. The end result is much less CPU time spent doing garbage collection and thus reduces the amount of time it takes to perform the initial chain sync by about 10%. The update also has the nice side benefit that the `GOGC` environment variable can be used if an advanced sysadmin really wanted to tune it since it is no longer being overridden. --- dcrd.go | 38 +++++++++++++++++++++++++++------ internal/limits/README.md | 2 +- internal/limits/memlimit.go | 20 +++++++++++++++++ internal/limits/memlimit_old.go | 17 +++++++++++++++ log.go | 15 +++++++++++++ 5 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 internal/limits/memlimit.go create mode 100644 internal/limits/memlimit_old.go diff --git a/dcrd.go b/dcrd.go index 2c8d9a290a..5ec101f05a 100644 --- a/dcrd.go +++ b/dcrd.go @@ -73,6 +73,38 @@ func dcrdMain() error { dcrdLog.Info("File logging disabled") } + // Block and transaction processing can cause bursty allocations. This + // limits the garbage collector from excessively overallocating during + // bursts. It does this by tweaking the target GC percent and soft memory + // limit depending on the version of the Go runtime. + // + // Starting with Go 1.19, a soft upper memory limit is imposed that leaves + // plenty of headroom for the minimum recommended value and the target GC + // percentage is left at the default value to significantly reduce the + // number of GC cycles thereby reducing the amount of CPU time spent doing + // garbage collection. + // + // For versions of Go prior to 1.19, the ability to set a soft upper memory + // limit was not available, so the GC percentage is lowered instead which + // has the effect of preventing overallocations at the expense of more + // frequent GC cycles. + // + // These values were arrived at with the help of profiling live usage. + if limits.SupportsMemoryLimit { + // Enforce a soft memory limit for a base amount along with any extra + // utxo cache over and above the default max cache size. + const memLimitBase = (15 * (1 << 30)) / 10 // 1.5 GiB + softMemLimit := int64(memLimitBase) + if cfg.UtxoCacheMaxSize > defaultUtxoCacheMaxSize { + extra := int64(cfg.UtxoCacheMaxSize) - defaultUtxoCacheMaxSize + softMemLimit += extra * (1 << 20) + } + limits.SetMemoryLimit(softMemLimit) + dcrdLog.Infof("Soft memory limit: %s", humanizeBytes(softMemLimit)) + } else { + debug.SetGCPercent(20) + } + // Enable http profiling server if requested. if cfg.Profile != "" { go func() { @@ -228,12 +260,6 @@ func dcrdMain() error { } func main() { - // Block and transaction processing can cause bursty allocations. This - // limits the garbage collector from excessively overallocating during - // bursts. This value was arrived at with the help of profiling live - // usage. - debug.SetGCPercent(20) - // Up some limits. if err := limits.SetLimits(); err != nil { fmt.Fprintf(os.Stderr, "failed to set limits: %v\n", err) diff --git a/internal/limits/README.md b/internal/limits/README.md index a9a2f738ef..e9cb185ce8 100644 --- a/internal/limits/README.md +++ b/internal/limits/README.md @@ -5,7 +5,7 @@ limits [![ISC License](https://img.shields.io/badge/license-ISC-blue.svg)](http://copyfree.org) [![Doc](https://img.shields.io/badge/doc-reference-blue.svg)](https://pkg.go.dev/github.com/decred/dcrd/internal/limits) -Package limits allows some process limits to be raised. +Package limits modifies process limits depending on the OS and Go runtime. ## Installation and Updating diff --git a/internal/limits/memlimit.go b/internal/limits/memlimit.go new file mode 100644 index 0000000000..2c8976d725 --- /dev/null +++ b/internal/limits/memlimit.go @@ -0,0 +1,20 @@ +// Copyright (c) 2022 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +//go:build go1.19 +// +build go1.19 + +package limits + +import "runtime/debug" + +// SupportsMemoryLimit indicates that a runtime enforced soft memory limit is +// supported starting with Go 1.19. +const SupportsMemoryLimit = true + +// SetMemoryLimit configures the runtime to use the provided limit as a soft +// memory limit starting with Go 1.19. +func SetMemoryLimit(limit int64) { + debug.SetMemoryLimit(limit) +} diff --git a/internal/limits/memlimit_old.go b/internal/limits/memlimit_old.go new file mode 100644 index 0000000000..e0f5cf49cf --- /dev/null +++ b/internal/limits/memlimit_old.go @@ -0,0 +1,17 @@ +// Copyright (c) 2022 The Decred developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +//go:build !go1.19 +// +build !go1.19 + +package limits + +// SupportsMemoryLimit indicates that a runtime enforced soft memory limit is +// not supported for versions of Go prior to version 1.19. +const SupportsMemoryLimit = false + +// SetMemoryLimit is a no-op on versions of Go prior to version 1.19 since the +// the ability is not supported on those versions. +func SetMemoryLimit(_ int64) { +} diff --git a/log.go b/log.go index 4149604d30..eb4be6dacc 100644 --- a/log.go +++ b/log.go @@ -194,3 +194,18 @@ func fatalf(str string) { } os.Exit(1) } + +// humanizeBytes returns the provided number of bytes in humanized form with IEC +// units (aka binary prefixes such as KiB and MiB). +func humanizeBytes(b int64) string { + const unit = 1024 + if b < unit { + return fmt.Sprintf("%d B", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.2f %ciB", float64(b)/float64(div), "KMGTPE"[exp]) +}