mirror of
https://github.com/golang/go
synced 2024-11-18 08:14:41 -07: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:
parent
edd3c8e9e2
commit
95780ea8b3
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -24,6 +24,7 @@ const concurrentAnalyses = 1
|
||||
func NewServer(session source.Session, client protocol.Client) *Server {
|
||||
return &Server{
|
||||
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{}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{
|
||||
|
121
internal/lsp/source/gc_annotations.go
Normal file
121
internal/lsp/source/gc_annotations.go
Normal 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
|
||||
}
|
@ -102,6 +102,7 @@ func DefaultOptions() Options {
|
||||
CommandGenerate.Name: true,
|
||||
CommandUpgradeDependency.Name: true,
|
||||
CommandRegenerateCgo.Name: true,
|
||||
CommandToggleDetails.Name: false,
|
||||
},
|
||||
},
|
||||
DebuggingOptions: DebuggingOptions{
|
||||
|
Loading…
Reference in New Issue
Block a user