gopls/internal/server: add memoryLimit setting This setting imposes a soft limit on memory usage of the gopls process via the runtime/debug.SetMemoryLimit mechanism. This feature is experimental. It may permit users to trade more CPU for lower steady state memory usage; or it may cause GC thrashing if set too low. Thanks to Brian Dillmann at Cockroach labs for contributing it. (See https://github.com/golang/tools/pull/651.) + relnote Change-Id: Idaf6390787a8895336ec27071e67014f4a17982a Reviewed-on: https://go-review.googlesource.com/c/tools/+/794882 Reviewed-by: Hongxiang Jiang <hxjiang@golang.org> Auto-Submit: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/gopls/doc/release/v0.24.0.md b/gopls/doc/release/v0.24.0.md new file mode 100644 index 0000000..4b248c7 --- /dev/null +++ b/gopls/doc/release/v0.24.0.md
@@ -0,0 +1,31 @@ +--- +title: "Gopls release v0.24.0 (expected Sep 2026)" +--- + +In this release: + +- [commits](https://go.googlesource.com/tools/+log/refs/heads/gopls-release-branch.0.23..refs/heads/gopls-release-branch.0.24) +- [issues closed](https://github.com/golang/go/milestone/440?closed=1) + +Key features are described below. + +<!-- TODO: 1-line overview of release --> + +## Configuration changes + +The `memoryLimit` setting allows clients to set a soft memory limit on +the gopls process using +[runtime/debug.SetMemoryLimit](https://pkg.go.dev/runtime/debug#SetMemoryLimit). +This feature is experimental. It may permit users to trade more CPU +for lower steady state memory usage; but it may cause GC thrashing if +set too low. + +## Web-based features + +## Editing features + +## Analysis features + +<!-- TODO Gopls is now using staticcheck [v0.8.0-rc1](https://github.com/dominikh/go-tools/releases/tag/2026.2rc1). --> + +## Code transformation features
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 93e3e87..8e0602c 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md
@@ -683,6 +683,31 @@ Default: `0`. +<a id='memoryLimit'></a> +### `memoryLimit int64` + +**This setting is experimental and may be deleted.** + +memoryLimit sets a soft memory limit (in bytes) for the gopls process, via +runtime/debug.SetMemoryLimit. If non-positive (the default), no limit is set. + +On large workspaces, a single edit that invalidates many +packages (for example a syntax error in a widely-imported +package) can make the heap briefly grow well above the +steady-state working set before the garbage collector +catches up, spiking memory and, on memory-constrained +machines, causing swapping. A soft limit makes the GC work +harder to stay near the limit, trading some CPU for a lower +memory peak. + +The limit is soft and may be exceeded. Set it comfortably above the +steady-state working set, as too low a value causes excessive GC. + +Unlike the GOMEMLIMIT environment variable, this setting is +strictly numeric; SI suffixes are not permitted. + +Default: `0`. + <a id='verboseOutput'></a> ### `verboseOutput bool`
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index 24efa51..c01dbca 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json
@@ -2349,6 +2349,20 @@ "DeprecationMessage": "" }, { + "Name": "memoryLimit", + "Type": "int64", + "Doc": "memoryLimit sets a soft memory limit (in bytes) for the gopls process, via\nruntime/debug.SetMemoryLimit. If non-positive (the default), no limit is set.\n\nOn large workspaces, a single edit that invalidates many\npackages (for example a syntax error in a widely-imported\npackage) can make the heap briefly grow well above the\nsteady-state working set before the garbage collector\ncatches up, spiking memory and, on memory-constrained\nmachines, causing swapping. A soft limit makes the GC work\nharder to stay near the limit, trading some CPU for a lower\nmemory peak.\n\nThe limit is soft and may be exceeded. Set it comfortably above the\nsteady-state working set, as too low a value causes excessive GC.\n\nUnlike the GOMEMLIMIT environment variable, this setting is\nstrictly numeric; SI suffixes are not permitted.\n", + "EnumKeys": { + "ValueType": "", + "Keys": null + }, + "EnumValues": null, + "Default": "0", + "Status": "experimental", + "Hierarchy": "", + "DeprecationMessage": "" + }, + { "Name": "verboseOutput", "Type": "bool", "Doc": "verboseOutput enables additional debug logging.\n",
diff --git a/gopls/internal/protocol/generate/main.go b/gopls/internal/protocol/generate/main.go index a879646..9deeb36 100644 --- a/gopls/internal/protocol/generate/main.go +++ b/gopls/internal/protocol/generate/main.go
@@ -270,8 +270,11 @@ func writejsons() { out := new(bytes.Buffer) fmt.Fprintln(out, fileHdr) - out.WriteString("import \"encoding/json\"\n\n") - out.WriteString("import \"fmt\"\n") + out.WriteString(`import ( + "encoding/json" + "fmt" +) +`) out.WriteString(` // UnmarshalError indicates that a JSON value did not conform to
diff --git a/gopls/internal/server/general.go b/gopls/internal/server/general.go index 420a702..37846a0 100644 --- a/gopls/internal/server/general.go +++ b/gopls/internal/server/general.go
@@ -16,6 +16,7 @@ "os" "path" "path/filepath" + runtimedebug "runtime/debug" "slices" "sort" "strings" @@ -71,9 +72,7 @@ } options.ForClientCapabilities(params.ClientInfo, params.Capabilities) - if options.MaxFileCacheBytes > 0 { - filecache.SetBudget(options.MaxFileCacheBytes) - } + updateGlobalOptions(options) if options.ShowBugReports { // Report the next bug that occurs on the server. @@ -277,6 +276,17 @@ }, nil } +// updateGlobalOptions updates process-wide settings +// received by a DidChangeConfiguration or Initialize request. +func updateGlobalOptions(options *settings.Options) { + if options.MaxFileCacheBytes > 0 { + filecache.SetBudget(options.MaxFileCacheBytes) + } + if options.MemoryLimit > 0 { + runtimedebug.SetMemoryLimit(options.MemoryLimit) + } +} + func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error { ctx, done := event.Start(ctx, "server.Initialized") defer done()
diff --git a/gopls/internal/server/workspace.go b/gopls/internal/server/workspace.go index 66d682a..7e6a7dc 100644 --- a/gopls/internal/server/workspace.go +++ b/gopls/internal/server/workspace.go
@@ -13,7 +13,6 @@ "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/filecache" "golang.org/x/tools/gopls/internal/golang/completion" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" @@ -144,9 +143,7 @@ // An options change may have affected the detected Go version. s.checkViewGoVersions() - if options.MaxFileCacheBytes > 0 { - filecache.SetBudget(options.MaxFileCacheBytes) - } + updateGlobalOptions(options) return nil }
diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index b0a6477..19bf723 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go
@@ -699,6 +699,25 @@ // as measured by du(1) may be significantly higher. MaxFileCacheBytes int64 `status:"experimental"` + // MemoryLimit sets a soft memory limit (in bytes) for the gopls process, via + // runtime/debug.SetMemoryLimit. If non-positive (the default), no limit is set. + // + // On large workspaces, a single edit that invalidates many + // packages (for example a syntax error in a widely-imported + // package) can make the heap briefly grow well above the + // steady-state working set before the garbage collector + // catches up, spiking memory and, on memory-constrained + // machines, causing swapping. A soft limit makes the GC work + // harder to stay near the limit, trading some CPU for a lower + // memory peak. + // + // The limit is soft and may be exceeded. Set it comfortably above the + // steady-state working set, as too low a value causes excessive GC. + // + // Unlike the GOMEMLIMIT environment variable, this setting is + // strictly numeric; SI suffixes are not permitted. + MemoryLimit int64 `status:"experimental"` + // VerboseOutput enables additional debug logging. VerboseOutput bool `status:"debug"` } @@ -1336,6 +1355,9 @@ case "maxFileCacheBytes": return setInt64(&o.MaxFileCacheBytes, value) + case "memoryLimit": + return setInt64(&o.MemoryLimit, value) + case "verboseOutput": return setBool(&o.VerboseOutput, value)