Skip to content
This repository was archived by the owner on Oct 12, 2022. It is now read-only.

Commit 91dfd74

Browse files
author
Stephen Gutekanst
committed
Add -editor=true flag & invoke FreeOSMemory periodically (lower memory consumption)
To try this change out: 1. `git checkout sg/mem` to the exact commit. 2. `go install github.com/sourcegraph/go-langserver` 3. Open some Go code and hover over a symbol, then make an edit, then hover, repeat... (each edit and hover triggers type-checking again). 4. For now, you can also set `"go.languageServerFlags": ["-editor=false"]` in VS Code settings to try without this change (to compare memory usage). With `code github.com/docker/docker/cmd/dockerd/docker.go`, and making 10 edits/hovers, the change is: | Real Before | Real After | Real Change | Go Before | Go After | Go Change | |-------------|------------|-------------|-----------|----------|-----------| | 7.61GB | 4.12GB | -45.86% | 3.92GB | 3.33GB | -15.05% | Where `Real` means real memory reported by OS X Activity Monitor, and `Go` means memory reported by Go as being in use. TL;DR: 46% less memory consumption for users running with the vscode-go extension.
1 parent dad0588 commit 91dfd74

File tree

4 files changed

+58
-6
lines changed

4 files changed

+58
-6
lines changed

langserver/handler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import (
2020
)
2121

2222
// NewHandler creates a Go language server handler.
23-
func NewHandler() jsonrpc2.Handler {
23+
func NewHandler(editor bool) jsonrpc2.Handler {
2424
return lspHandler{jsonrpc2.HandlerWithError((&LangHandler{
25-
HandlerShared: &HandlerShared{},
25+
HandlerShared: &HandlerShared{editor: editor},
2626
}).handle)}
2727
}
2828

langserver/handler_shared.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type HandlerShared struct {
1414
Mu sync.Mutex // guards all fields
1515
Shared bool // true if this struct is shared with a build server
1616
FS *AtomicFS // full filesystem (mounts both deps and overlay)
17+
editor bool // running in local editor environment
1718

1819
// FindPackage if non-nil is used by our typechecker. See
1920
// loader.Config.FindPackage. We use this in production to lazily

langserver/loader.go

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ import (
1111
"log"
1212
"path/filepath"
1313
"reflect"
14+
"runtime/debug"
1415
"strings"
16+
"sync"
17+
"time"
1518

1619
opentracing "github.com/opentracing/opentracing-go"
1720

@@ -204,7 +207,7 @@ func (h *LangHandler) cachedTypecheck(ctx context.Context, bctx *build.Context,
204207
res := &typecheckResult{
205208
fset: token.NewFileSet(),
206209
}
207-
res.prog, diags, res.err = typecheck(ctx, res.fset, bctx, bpkg, h.getFindPackageFunc())
210+
res.prog, diags, res.err = typecheck(ctx, res.fset, bctx, bpkg, h.getFindPackageFunc(), h.HandlerShared.editor)
208211
return res
209212
})
210213
if r == nil {
@@ -216,7 +219,10 @@ func (h *LangHandler) cachedTypecheck(ctx context.Context, bctx *build.Context,
216219
}
217220

218221
// TODO(sqs): allow typechecking just a specific file not in a package, too
219-
func typecheck(ctx context.Context, fset *token.FileSet, bctx *build.Context, bpkg *build.Package, findPackage FindPackageFunc) (*loader.Program, diagnostics, error) {
222+
func typecheck(ctx context.Context, fset *token.FileSet, bctx *build.Context, bpkg *build.Package, findPackage FindPackageFunc, editor bool) (*loader.Program, diagnostics, error) {
223+
if editor {
224+
freeOSMemory()
225+
}
220226
var typeErrs []error
221227
conf := loader.Config{
222228
Fset: fset,
@@ -306,3 +312,47 @@ func isMultiplePackageError(err error) bool {
306312
_, ok := err.(*build.MultiplePackageError)
307313
return ok
308314
}
315+
316+
var freeOSMemoryOnce sync.Once
317+
318+
// freeOSMemory spawns a goroutine (only once) which invokes
319+
// runtime/debug.FreeOSMemory() more aggressively than the runtime default of
320+
// 5 minutes after GC.
321+
//
322+
// There is a long-standing known issue with Go in which memory is not returned
323+
// to the OS aggressively enough[1], which coincidently harms our application
324+
// quite a lot because we perform so many short-burst heap allocations during
325+
// the type-checking phase.
326+
//
327+
// This function should only be invoked in editor mode, not in sourcegraph.com
328+
// mode, because users running the language server as part of their editor
329+
// generally expect much lower memory usage. In contrast, on sourcegraph.com we
330+
// can give our servers plenty of RAM and allow Go to consume as much as it
331+
// wants. Go does reuse the memory not free'd to the OS, and as such enabling
332+
// this does _technically_ make our application perform less optimally -- but
333+
// in practice this has no observable affect in editor mode.
334+
//
335+
// The end effect of performing this is that repeating "hover over code" -> "make an edit"
336+
// 10 times inside a large package like github.com/docker/docker/cmd/dockerd:
337+
//
338+
//
339+
// | Real Before | Real After | Real Change | Go Before | Go After | Go Change |
340+
// |-------------|------------|-------------|-----------|----------|-----------|
341+
// | 7.61GB | 4.12GB | -45.86% | 3.92GB | 3.33GB | -15.05% |
342+
//
343+
// Where `Real` means real memory reported by OS X Activity Monitor, and `Go`
344+
// means memory reported by Go as being in use.
345+
//
346+
// TL;DR: 46% less memory consumption for users running with the vscode-go extension.
347+
//
348+
// [1] https://github.com/golang/go/issues/14735#issuecomment-194470114
349+
func freeOSMemory() {
350+
freeOSMemoryOnce.Do(func() {
351+
go func() {
352+
for {
353+
time.Sleep(1 * time.Second)
354+
debug.FreeOSMemory()
355+
}
356+
}()
357+
})
358+
}

main.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var (
2323
logfile = flag.String("logfile", "", "also log to this file (in addition to stderr)")
2424
printVersion = flag.Bool("version", false, "print version and exit")
2525
pprof = flag.String("pprof", ":6060", "start a pprof http server (https://golang.org/pkg/net/http/pprof/)")
26+
editor = flag.Bool("editor", true, "run the server in an editor-based local environment")
2627
)
2728

2829
// version is the version field we report back. If you are releasing a new version:
@@ -87,12 +88,12 @@ func run() error {
8788
if err != nil {
8889
return err
8990
}
90-
jsonrpc2.NewConn(context.Background(), jsonrpc2.NewBufferedStream(conn, jsonrpc2.VSCodeObjectCodec{}), langserver.NewHandler(), connOpt...)
91+
jsonrpc2.NewConn(context.Background(), jsonrpc2.NewBufferedStream(conn, jsonrpc2.VSCodeObjectCodec{}), langserver.NewHandler(*editor), connOpt...)
9192
}
9293

9394
case "stdio":
9495
log.Println("langserver-go: reading on stdin, writing on stdout")
95-
<-jsonrpc2.NewConn(context.Background(), jsonrpc2.NewBufferedStream(stdrwc{}, jsonrpc2.VSCodeObjectCodec{}), langserver.NewHandler(), connOpt...).DisconnectNotify()
96+
<-jsonrpc2.NewConn(context.Background(), jsonrpc2.NewBufferedStream(stdrwc{}, jsonrpc2.VSCodeObjectCodec{}), langserver.NewHandler(*editor), connOpt...).DisconnectNotify()
9697
log.Println("connection closed")
9798
return nil
9899

0 commit comments

Comments
 (0)