From 95780ea8b324b37f8cf08967d36b372d5ce4bb01 Mon Sep 17 00:00:00 2001 From: Peter Weinbergr Date: Thu, 16 Jul 2020 10:45:30 -0400 Subject: [PATCH] internal/lsp: show compiler optimization decisions The gc compiler will report its decisions about inlining, escapes, etc. This can be turned on and off with a new optional code lens gc_details. When enabled, the code lens will be displayed above the package statement. The compiler's decisions are shown as information diagnostics. (Other diagnostics have been errors and warnings.) Change-Id: I7d1d5b5b5cf8acd7ff08f683e537ea618e269547 Reviewed-on: https://go-review.googlesource.com/c/tools/+/243119 Run-TryBot: Peter Weinberger Reviewed-by: Rebecca Stambler --- gopls/doc/settings.md | 18 +++- internal/lsp/command.go | 29 +++++- internal/lsp/diagnostics.go | 20 ++++- internal/lsp/server.go | 13 ++- internal/lsp/source/code_lens.go | 24 +++++ internal/lsp/source/command.go | 6 ++ internal/lsp/source/gc_annotations.go | 121 ++++++++++++++++++++++++++ internal/lsp/source/options.go | 1 + 8 files changed, 221 insertions(+), 11 deletions(-) create mode 100644 internal/lsp/source/gc_annotations.go diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 793369659e..fe0191a879 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -86,14 +86,24 @@ Example Usage: ### **codelens** *map[string]bool* Overrides the enabled/disabled state of various code lenses. Currently, we -support two code lenses: +support several code lenses: * `generate`: [default: enabled] run `go generate` as specified by a `//go:generate` directive. -* `upgrade.dependency`: [default: enabled] upgrade a dependency listed in a `go.mod` file. +* `upgrade_dependency`: [default: enabled] upgrade a dependency listed in a `go.mod` file. * `test`: [default: disabled] run `go test -run` for a test func. +* `gc_details`: [default: disabled] Show the gc compiler's choices for inline analysis and escaping. -By default, both of these code lenses are enabled. - +Example Usage: +```json5 +"gopls": { +... + "codelens": { + "generate": false, // Don't run `go generate`. + "gc_details": true // Show a code lens toggling the display of gc's choices. + } +... +} +``` ### **completionDocumentation** *boolean* If false, indicates that the user does not want documentation with completion results. diff --git a/internal/lsp/command.go b/internal/lsp/command.go index 218df2cd77..a9cbcd9c94 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -8,6 +8,7 @@ import ( "context" "fmt" "io" + "path" "strings" "golang.org/x/tools/internal/event" @@ -50,8 +51,9 @@ func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCom } } if unsaved { - switch command { - case source.CommandTest, source.CommandGenerate: + switch params.Command { + case source.CommandTest.Name, source.CommandGenerate.Name, source.CommandToggleDetails.Name: + // TODO(PJW): for Toggle, not an error if it is being disabled return nil, s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ Type: protocol.Error, Message: fmt.Sprintf("cannot run command %s: unsaved files in the view", params.Command), @@ -145,6 +147,29 @@ func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCom return nil, err default: return nil, fmt.Errorf("unknown command: %s", params.Command) + case source.CommandToggleDetails: + var fileURI span.URI + if err := source.UnmarshalArgs(params.Arguments, &fileURI); err != nil { + return nil, err + } + pkgDir := span.URI(path.Dir(fileURI.Filename())) + s.deliveredMu.Lock() + if s.gcOptimizatonDetails[pkgDir] { + delete(s.gcOptimizatonDetails, pkgDir) + } else { + s.gcOptimizatonDetails[pkgDir] = true + } + event.Log(ctx, fmt.Sprintf("PJW details %s now %v %v", pkgDir, s.gcOptimizatonDetails[pkgDir], + s.gcOptimizatonDetails)) + s.deliveredMu.Unlock() + // need to recompute diagnostics. + // so find the snapshot + sv, err := s.session.ViewOf(fileURI) + if err != nil { + return nil, err + } + s.diagnoseSnapshot(sv.Snapshot()) + return nil, nil } return nil, nil } diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go index 54911783d1..e3f2d754af 100644 --- a/internal/lsp/diagnostics.go +++ b/internal/lsp/diagnostics.go @@ -8,6 +8,7 @@ import ( "context" "crypto/sha1" "fmt" + "path/filepath" "strings" "sync" @@ -16,6 +17,7 @@ import ( "golang.org/x/tools/internal/lsp/mod" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/source" + "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/xcontext" "golang.org/x/xerrors" ) @@ -115,16 +117,32 @@ If you believe this is a mistake, please file an issue: https://github.com/golan go func(pkg source.Package) { defer wg.Done() + detailsDir := "" // Only run analyses for packages with open files. withAnalysis := alwaysAnalyze for _, pgf := range pkg.CompiledGoFiles() { if snapshot.IsOpen(pgf.URI) { withAnalysis = true - break + } + if detailsDir == "" { + dir := filepath.Dir(pgf.URI.Filename()) + if s.gcOptimizatonDetails[span.URI(dir)] { + detailsDir = dir + } } } pkgReports, warn, err := source.Diagnostics(ctx, snapshot, pkg, withAnalysis) + if detailsDir != "" { + var more map[source.FileIdentity][]*source.Diagnostic + more, err = source.DoGcDetails(ctx, snapshot, detailsDir) + if err != nil { + event.Error(ctx, "warning: gcdetails", err, tag.Snapshot.Of(snapshot.ID())) + } + for k, v := range more { + pkgReports[k] = append(pkgReports[k], v...) + } + } // Check if might want to warn the user about their build configuration. // Our caller decides whether to send the message. diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 5b9f71f221..a362c03f58 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -23,10 +23,11 @@ const concurrentAnalyses = 1 // messages on on the supplied stream. func NewServer(session source.Session, client protocol.Client) *Server { return &Server{ - delivered: make(map[span.URI]sentDiagnostics), - session: session, - client: client, - diagnosticsSema: make(chan struct{}, concurrentAnalyses), + delivered: make(map[span.URI]sentDiagnostics), + gcOptimizatonDetails: make(map[span.URI]bool), + session: session, + client: client, + diagnosticsSema: make(chan struct{}, concurrentAnalyses), } } @@ -73,6 +74,10 @@ type Server struct { deliveredMu sync.Mutex delivered map[span.URI]sentDiagnostics + // gcOptimizationDetails describes which packages we want optimization details + // included in the diagnostics. The key is the directory of the package. + gcOptimizatonDetails map[span.URI]bool + // diagnosticsSema limits the concurrency of diagnostics runs, which can be expensive. diagnosticsSema chan struct{} diff --git a/internal/lsp/source/code_lens.go b/internal/lsp/source/code_lens.go index d3cfb4b8c4..b0f146fed1 100644 --- a/internal/lsp/source/code_lens.go +++ b/internal/lsp/source/code_lens.go @@ -23,6 +23,7 @@ var lensFuncs = map[string]lensFunc{ CommandGenerate.Name: goGenerateCodeLens, CommandTest.Name: runTestCodeLens, CommandRegenerateCgo.Name: regenerateCgoLens, + CommandToggleDetails.Name: toggleDetailsCodeLens, } // CodeLens computes code lens for Go source code. @@ -221,3 +222,26 @@ func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([ }, }, nil } + +func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { + _, pgf, err := getParsedFile(ctx, snapshot, fh, WidestPackage) + fset := snapshot.View().Session().Cache().FileSet() + rng, err := newMappedRange(fset, pgf.Mapper, pgf.File.Package, pgf.File.Package).Range() + if err != nil { + return nil, err + } + jsonArgs, err := MarshalArgs(fh.URI()) + if err != nil { + return nil, err + } + return []protocol.CodeLens{ + { + Range: rng, + Command: protocol.Command{ + Title: "Toggle gc annotation details", + Command: CommandToggleDetails.Name, + Arguments: jsonArgs, + }, + }, + }, nil +} diff --git a/internal/lsp/source/command.go b/internal/lsp/source/command.go index 211f562329..ff52f3e786 100644 --- a/internal/lsp/source/command.go +++ b/internal/lsp/source/command.go @@ -53,6 +53,7 @@ var Commands = []*Command{ CommandVendor, CommandExtractVariable, CommandExtractFunction, + CommandToggleDetails, } var ( @@ -86,6 +87,11 @@ var ( Name: "regenerate_cgo", } + // CommandToggleDetails controls calculation of gc annotations. + CommandToggleDetails = &Command{ + Name: "gc_details", + } + // CommandFillStruct is a gopls command to fill a struct with default // values. CommandFillStruct = &Command{ diff --git a/internal/lsp/source/gc_annotations.go b/internal/lsp/source/gc_annotations.go new file mode 100644 index 0000000000..4d6e028271 --- /dev/null +++ b/internal/lsp/source/gc_annotations.go @@ -0,0 +1,121 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package source + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "golang.org/x/tools/internal/lsp/protocol" + "golang.org/x/tools/internal/span" +) + +func DoGcDetails(ctx context.Context, snapshot Snapshot, pkgDir string) (map[FileIdentity][]*Diagnostic, error) { + outDir := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.details", os.Getpid())) + if err := os.MkdirAll(outDir, 0700); err != nil { + return nil, err + } + args := []string{fmt.Sprintf("-gcflags=-json=0,%s", outDir), pkgDir} + err := snapshot.RunGoCommandDirect(ctx, "build", args) + if err != nil { + return nil, err + } + files, err := findJSONFiles(outDir) + if err != nil { + return nil, err + } + reports := make(map[FileIdentity][]*Diagnostic) + var parseError error + for _, fn := range files { + fname, v, err := parseDetailsFile(fn) + if err != nil { + // expect errors for all the files, save 1 + parseError = err + } + if !strings.HasSuffix(fname, ".go") { + continue // + } + uri := span.URIFromPath(fname) + x := snapshot.FindFile(uri) + if x == nil { + continue + } + k := x.Identity() + reports[k] = v + } + return reports, parseError +} + +func parseDetailsFile(fn string) (string, []*Diagnostic, error) { + buf, err := ioutil.ReadFile(fn) + if err != nil { + return "", nil, err // This is an internal error. Likely ever file will fail. + } + var fname string + var ans []*Diagnostic + lines := bytes.Split(buf, []byte{'\n'}) + for i, l := range lines { + if len(l) == 0 { + continue + } + if i == 0 { + x := make(map[string]interface{}) + if err := json.Unmarshal(l, &x); err != nil { + return "", nil, fmt.Errorf("internal error (%v) parsing first line of json file %s", + err, fn) + } + fname = x["file"].(string) + continue + } + y := protocol.Diagnostic{} + if err := json.Unmarshal(l, &y); err != nil { + return "", nil, fmt.Errorf("internal error (%#v) parsing json file for %s", err, fname) + } + y.Range.Start.Line-- // change from 1-based to 0-based + y.Range.Start.Character-- + y.Range.End.Line-- + y.Range.End.Character-- + msg := y.Code.(string) + if y.Message != "" { + msg = fmt.Sprintf("%s(%s)", msg, y.Message) + } + x := Diagnostic{ + Range: y.Range, + Message: msg, + Source: y.Source, + Severity: y.Severity, + } + for _, ri := range y.RelatedInformation { + x.Related = append(x.Related, RelatedInformation{ + URI: ri.Location.URI.SpanURI(), + Range: ri.Location.Range, + Message: ri.Message, + }) + } + ans = append(ans, &x) + } + return fname, ans, nil +} + +func findJSONFiles(dir string) ([]string, error) { + ans := []string{} + f := func(path string, fi os.FileInfo, err error) error { + if fi.IsDir() { + return nil + } + if strings.HasSuffix(path, ".json") { + ans = append(ans, path) + } + return nil + } + err := filepath.Walk(dir, f) + return ans, err +} diff --git a/internal/lsp/source/options.go b/internal/lsp/source/options.go index f6bf891879..7755e6aeac 100644 --- a/internal/lsp/source/options.go +++ b/internal/lsp/source/options.go @@ -102,6 +102,7 @@ func DefaultOptions() Options { CommandGenerate.Name: true, CommandUpgradeDependency.Name: true, CommandRegenerateCgo.Name: true, + CommandToggleDetails.Name: false, }, }, DebuggingOptions: DebuggingOptions{