1
0
mirror of https://github.com/golang/go synced 2024-09-30 14:08:32 -06:00

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>
This commit is contained in:
Peter Weinbergr 2020-07-16 10:45:30 -04:00 committed by Peter Weinberger
parent edd3c8e9e2
commit 95780ea8b3
8 changed files with 221 additions and 11 deletions

View File

@ -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.

View File

@ -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
}

View File

@ -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.

View File

@ -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{}

View File

@ -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
}

View File

@ -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{

View File

@ -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
}

View File

@ -102,6 +102,7 @@ func DefaultOptions() Options {
CommandGenerate.Name: true,
CommandUpgradeDependency.Name: true,
CommandRegenerateCgo.Name: true,
CommandToggleDetails.Name: false,
},
},
DebuggingOptions: DebuggingOptions{