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 <pjw@google.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md
index 7933696..fe0191a 100644
--- a/gopls/doc/settings.md
+++ b/gopls/doc/settings.md
@@ -86,14 +86,24 @@
### **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 218df2c..a9cbcd9 100644
--- a/internal/lsp/command.go
+++ b/internal/lsp/command.go
@@ -8,6 +8,7 @@
"context"
"fmt"
"io"
+ "path"
"strings"
"golang.org/x/tools/internal/event"
@@ -50,8 +51,9 @@
}
}
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 @@
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 5491178..e3f2d75 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -8,6 +8,7 @@
"context"
"crypto/sha1"
"fmt"
+ "path/filepath"
"strings"
"sync"
@@ -16,6 +17,7 @@
"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 @@
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 5b9f71f..a362c03 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -23,10 +23,11 @@
// 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 @@
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 d3cfb4b..b0f146f 100644
--- a/internal/lsp/source/code_lens.go
+++ b/internal/lsp/source/code_lens.go
@@ -23,6 +23,7 @@
CommandGenerate.Name: goGenerateCodeLens,
CommandTest.Name: runTestCodeLens,
CommandRegenerateCgo.Name: regenerateCgoLens,
+ CommandToggleDetails.Name: toggleDetailsCodeLens,
}
// CodeLens computes code lens for Go source code.
@@ -221,3 +222,26 @@
},
}, 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 211f562..ff52f3e 100644
--- a/internal/lsp/source/command.go
+++ b/internal/lsp/source/command.go
@@ -53,6 +53,7 @@
CommandVendor,
CommandExtractVariable,
CommandExtractFunction,
+ CommandToggleDetails,
}
var (
@@ -86,6 +87,11 @@
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 0000000..4d6e028
--- /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 // <autogenerated>
+ }
+ 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 f6bf891..7755e6a 100644
--- a/internal/lsp/source/options.go
+++ b/internal/lsp/source/options.go
@@ -102,6 +102,7 @@
CommandGenerate.Name: true,
CommandUpgradeDependency.Name: true,
CommandRegenerateCgo.Name: true,
+ CommandToggleDetails.Name: false,
},
},
DebuggingOptions: DebuggingOptions{