blob: 3e7d86aac5ff13abd30a5a1c749d35b4fa70af4d [file] [log] [blame] [edit]
// Copyright 2019 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 logopt
import (
"cmd/internal/obj"
"cmd/internal/src"
"encoding/json"
"fmt"
"internal/buildcfg"
"io"
"log"
"net/url"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"unicode"
)
// This implements (non)optimization logging for -json option to the Go compiler
// The option is -json 0,<destination>.
//
// 0 is the version number; to avoid the need for synchronized updates, if
// new versions of the logging appear, the compiler will support both, for a while,
// and clients will specify what they need.
//
// <destination> is a directory.
// Directories are specified with a leading / or os.PathSeparator,
// or more explicitly with file://directory. The second form is intended to
// deal with corner cases on Windows, and to allow specification of a relative
// directory path (which is normally a bad idea, because the local directory
// varies a lot in a build, especially with modules and/or vendoring, and may
// not be writeable).
//
// For each package pkg compiled, a url.PathEscape(pkg)-named subdirectory
// is created. For each source file.go in that package that generates
// diagnostics (no diagnostics means no file),
// a url.PathEscape(file)+".json"-named file is created and contains the
// logged diagnostics.
//
// For example, "cmd%2Finternal%2Fdwarf/%3Cautogenerated%3E.json"
// for "cmd/internal/dwarf" and <autogenerated> (which is not really a file, but the compiler sees it)
//
// If the package string is empty, it is replaced internally with string(0) which encodes to %00.
//
// Each log file begins with a JSON record identifying version,
// platform, and other context, followed by optimization-relevant
// LSP Diagnostic records, one per line (LSP version 3.15, no difference from 3.14 on the subset used here
// see https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/ )
//
// The fields of a Diagnostic are used in the following way:
// Range: the outermost source position, for now begin and end are equal.
// Severity: (always) SeverityInformation (3)
// Source: (always) "go compiler"
// Code: a string describing the missed optimization, e.g., "nilcheck", "cannotInline", "isInBounds", "escape"
// Message: depending on code, additional information, e.g., the reason a function cannot be inlined.
// RelatedInformation: if the missed optimization actually occurred at a function inlined at Range,
// then the sequence of inlined locations appears here, from (second) outermost to innermost,
// each with message="inlineLoc".
//
// In the case of escape analysis explanations, after any outer inlining locations,
// the lines of the explanation appear, each potentially followed with its own inlining
// location if the escape flow occurred within an inlined function.
//
// For example <destination>/cmd%2Fcompile%2Finternal%2Fssa/prove.json
// might begin with the following line (wrapped for legibility):
//
// {"version":0,"package":"cmd/compile/internal/ssa","goos":"darwin","goarch":"amd64",
// "gc_version":"devel +e1b9a57852 Fri Nov 1 15:07:00 2019 -0400",
// "file":"/Users/drchase/work/go/src/cmd/compile/internal/ssa/prove.go"}
//
// and later contain (also wrapped for legibility):
//
// {"range":{"start":{"line":191,"character":24},"end":{"line":191,"character":24}},
// "severity":3,"code":"nilcheck","source":"go compiler","message":"",
// "relatedInformation":[
// {"location":{"uri":"file:///Users/drchase/work/go/src/cmd/compile/internal/ssa/func.go",
// "range":{"start":{"line":153,"character":16},"end":{"line":153,"character":16}}},
// "message":"inlineLoc"}]}
//
// That is, at prove.go (implicit from context, provided in both filename and header line),
// line 191, column 24, a nilcheck occurred in the generated code.
// The relatedInformation indicates that this code actually came from
// an inlined call to func.go, line 153, character 16.
//
// prove.go:191:
// ft.orderS = f.newPoset()
// func.go:152 and 153:
// func (f *Func) newPoset() *poset {
// if len(f.Cache.scrPoset) > 0 {
//
// In the case that the package is empty, the string(0) package name is also used in the header record, for example
//
// go tool compile -json=0,file://logopt x.go # no -p option to set the package
// head -1 logopt/%00/x.json
// {"version":0,"package":"\u0000","goos":"darwin","goarch":"amd64","gc_version":"devel +86487adf6a Thu Nov 7 19:34:56 2019 -0500","file":"x.go"}
type VersionHeader struct {
Version int `json:"version"`
Package string `json:"package"`
Goos string `json:"goos"`
Goarch string `json:"goarch"`
GcVersion string `json:"gc_version"`
File string `json:"file,omitempty"` // LSP requires an enclosing resource, i.e., a file
}
// DocumentURI, Position, Range, Location, Diagnostic, DiagnosticRelatedInformation all reuse json definitions from gopls.
// See https://github.com/golang/tools/blob/22afafe3322a860fcd3d88448768f9db36f8bc5f/internal/lsp/protocol/tsprotocol.go
type DocumentURI string
type Position struct {
Line uint `json:"line"` // gopls uses float64, but json output is the same for integers
Character uint `json:"character"` // gopls uses float64, but json output is the same for integers
}
// A Range in a text document expressed as (zero-based) start and end positions.
// A range is comparable to a selection in an editor. Therefore the end position is exclusive.
// If you want to specify a range that contains a line including the line ending character(s)
// then use an end position denoting the start of the next line.
type Range struct {
/*Start defined:
* The range's start position
*/
Start Position `json:"start"`
/*End defined:
* The range's end position
*/
End Position `json:"end"` // exclusive
}
// A Location represents a location inside a resource, such as a line inside a text file.
type Location struct {
// URI is
URI DocumentURI `json:"uri"`
// Range is
Range Range `json:"range"`
}
/* DiagnosticRelatedInformation defined:
* Represents a related message and source code location for a diagnostic. This should be
* used to point to code locations that cause or related to a diagnostics, e.g when duplicating
* a symbol in a scope.
*/
type DiagnosticRelatedInformation struct {
/*Location defined:
* The location of this related diagnostic information.
*/
Location Location `json:"location"`
/*Message defined:
* The message of this related diagnostic information.
*/
Message string `json:"message"`
}
// DiagnosticSeverity defines constants
type DiagnosticSeverity uint
const (
/*SeverityInformation defined:
* Reports an information.
*/
SeverityInformation DiagnosticSeverity = 3
)
// DiagnosticTag defines constants
type DiagnosticTag uint
/*Diagnostic defined:
* Represents a diagnostic, such as a compiler error or warning. Diagnostic objects
* are only valid in the scope of a resource.
*/
type Diagnostic struct {
/*Range defined:
* The range at which the message applies
*/
Range Range `json:"range"`
/*Severity defined:
* The diagnostic's severity. Can be omitted. If omitted it is up to the
* client to interpret diagnostics as error, warning, info or hint.
*/
Severity DiagnosticSeverity `json:"severity,omitempty"` // always SeverityInformation for optimizer logging.
/*Code defined:
* The diagnostic's code, which usually appear in the user interface.
*/
Code string `json:"code,omitempty"` // LSP uses 'number | string' = gopls interface{}, but only string here, e.g. "boundsCheck", "nilcheck", etc.
/*Source defined:
* A human-readable string describing the source of this
* diagnostic, e.g. 'typescript' or 'super lint'. It usually
* appears in the user interface.
*/
Source string `json:"source,omitempty"` // "go compiler"
/*Message defined:
* The diagnostic's message. It usually appears in the user interface
*/
Message string `json:"message"` // sometimes used, provides additional information.
/*Tags defined:
* Additional metadata about the diagnostic.
*/
Tags []DiagnosticTag `json:"tags,omitempty"` // always empty for logging optimizations.
/*RelatedInformation defined:
* An array of related diagnostic information, e.g. when symbol-names within
* a scope collide all definitions can be marked via this property.
*/
RelatedInformation []DiagnosticRelatedInformation `json:"relatedInformation,omitempty"`
}
// A LoggedOpt is what the compiler produces and accumulates,
// to be converted to JSON for human or IDE consumption.
type LoggedOpt struct {
pos src.XPos // Source code position at which the event occurred. If it is inlined, outer and all inlined locations will appear in JSON.
lastPos src.XPos // Usually the same as pos; current exception is for reporting entire range of transformed loops
compilerPass string // Compiler pass. For human/adhoc consumption; does not appear in JSON (yet)
functionName string // Function name. For human/adhoc consumption; does not appear in JSON (yet)
what string // The (non) optimization; "nilcheck", "boundsCheck", "inline", "noInline"
target []interface{} // Optional target(s) or parameter(s) of "what" -- what was inlined, why it was not, size of copy, etc. 1st is most important/relevant.
}
type logFormat uint8
const (
None logFormat = iota
Json0 // version 0 for LSP 3.14, 3.15; future versions of LSP may change the format and the compiler may need to support both as clients are updated.
)
var Format = None
var dest string
// LogJsonOption parses and validates the version,directory value attached to the -json compiler flag.
func LogJsonOption(flagValue string) {
version, directory := parseLogFlag("json", flagValue)
if version != 0 {
log.Fatal("-json version must be 0")
}
dest = checkLogPath(directory)
Format = Json0
}
// parseLogFlag checks the flag passed to -json
// for version,destination format and returns the two parts.
func parseLogFlag(flag, value string) (version int, directory string) {
if Format != None {
log.Fatal("Cannot repeat -json flag")
}
commaAt := strings.Index(value, ",")
if commaAt <= 0 {
log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number", flag)
}
v, err := strconv.Atoi(value[:commaAt])
if err != nil {
log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number: err=%v", flag, err)
}
version = v
directory = value[commaAt+1:]
return
}
// isWindowsDriveURIPath returns true if the file URI is of the format used by
// Windows URIs. The url.Parse package does not specially handle Windows paths
// (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:").
// (copied from tools/internal/span/uri.go)
// this is less comprehensive that the processing in filepath.IsAbs on Windows.
func isWindowsDriveURIPath(uri string) bool {
if len(uri) < 4 {
return false
}
return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
}
func parseLogPath(destination string) (string, string) {
if filepath.IsAbs(destination) {
return filepath.Clean(destination), ""
}
if strings.HasPrefix(destination, "file://") { // IKWIAD, or Windows C:\foo\bar\baz
uri, err := url.Parse(destination)
if err != nil {
return "", fmt.Sprintf("optimizer logging destination looked like file:// URI but failed to parse: err=%v", err)
}
destination = uri.Host + uri.Path
if isWindowsDriveURIPath(destination) {
// strip leading / from /C:
// unlike tools/internal/span/uri.go, do not uppercase the drive letter -- let filepath.Clean do what it does.
destination = destination[1:]
}
return filepath.Clean(destination), ""
}
return "", fmt.Sprintf("optimizer logging destination %s was neither %s-prefixed directory nor file://-prefixed file URI", destination, string(filepath.Separator))
}
// checkLogPath does superficial early checking of the string specifying
// the directory to which optimizer logging is directed, and if
// it passes the test, stores the string in LO_dir.
func checkLogPath(destination string) string {
path, complaint := parseLogPath(destination)
if complaint != "" {
log.Fatal(complaint)
}
err := os.MkdirAll(path, 0755)
if err != nil {
log.Fatalf("optimizer logging destination '<version>,<directory>' but could not create <directory>: err=%v", err)
}
return path
}
var loggedOpts []*LoggedOpt
var mu = sync.Mutex{} // mu protects loggedOpts.
// NewLoggedOpt allocates a new LoggedOpt, to later be passed to either NewLoggedOpt or LogOpt as "args".
// Pos is the source position (including inlining), what is the message, pass is which pass created the message,
// funcName is the name of the function
// A typical use for this to accumulate an explanation for a missed optimization, for example, why did something escape?
func NewLoggedOpt(pos, lastPos src.XPos, what, pass, funcName string, args ...interface{}) *LoggedOpt {
pass = strings.Replace(pass, " ", "_", -1)
return &LoggedOpt{pos, lastPos, pass, funcName, what, args}
}
// LogOpt logs information about a (usually missed) optimization performed by the compiler.
// Pos is the source position (including inlining), what is the message, pass is which pass created the message,
// funcName is the name of the function.
func LogOpt(pos src.XPos, what, pass, funcName string, args ...interface{}) {
if Format == None {
return
}
lo := NewLoggedOpt(pos, pos, what, pass, funcName, args...)
mu.Lock()
defer mu.Unlock()
// Because of concurrent calls from back end, no telling what the order will be, but is stable-sorted by outer Pos before use.
loggedOpts = append(loggedOpts, lo)
}
// LogOptRange is the same as LogOpt, but includes the ability to express a range of positions,
// not just a point.
func LogOptRange(pos, lastPos src.XPos, what, pass, funcName string, args ...interface{}) {
if Format == None {
return
}
lo := NewLoggedOpt(pos, lastPos, what, pass, funcName, args...)
mu.Lock()
defer mu.Unlock()
// Because of concurrent calls from back end, no telling what the order will be, but is stable-sorted by outer Pos before use.
loggedOpts = append(loggedOpts, lo)
}
// Enabled returns whether optimization logging is enabled.
func Enabled() bool {
switch Format {
case None:
return false
case Json0:
return true
}
panic("Unexpected optimizer-logging level")
}
// byPos sorts diagnostics by source position.
type byPos struct {
ctxt *obj.Link
a []*LoggedOpt
}
func (x byPos) Len() int { return len(x.a) }
func (x byPos) Less(i, j int) bool {
return x.ctxt.OutermostPos(x.a[i].pos).Before(x.ctxt.OutermostPos(x.a[j].pos))
}
func (x byPos) Swap(i, j int) { x.a[i], x.a[j] = x.a[j], x.a[i] }
func writerForLSP(subdirpath, file string) io.WriteCloser {
basename := file
lastslash := strings.LastIndexAny(basename, "\\/")
if lastslash != -1 {
basename = basename[lastslash+1:]
}
lastdot := strings.LastIndex(basename, ".go")
if lastdot != -1 {
basename = basename[:lastdot]
}
basename = url.PathEscape(basename)
// Assume a directory, make a file
p := filepath.Join(subdirpath, basename+".json")
w, err := os.Create(p)
if err != nil {
log.Fatalf("Could not create file %s for logging optimizer actions, %v", p, err)
}
return w
}
func fixSlash(f string) string {
if os.PathSeparator == '/' {
return f
}
return strings.Replace(f, string(os.PathSeparator), "/", -1)
}
func uriIfy(f string) DocumentURI {
url := url.URL{
Scheme: "file",
Path: fixSlash(f),
}
return DocumentURI(url.String())
}
// Return filename, replacing a first occurrence of $GOROOT with the
// actual value of the GOROOT (because LSP does not speak "$GOROOT").
func uprootedPath(filename string) string {
if filename == "" {
return "__unnamed__"
}
if buildcfg.GOROOT == "" || !strings.HasPrefix(filename, "$GOROOT/") {
return filename
}
return buildcfg.GOROOT + filename[len("$GOROOT"):]
}
// FlushLoggedOpts flushes all the accumulated optimization log entries.
func FlushLoggedOpts(ctxt *obj.Link, slashPkgPath string) {
if Format == None {
return
}
sort.Stable(byPos{ctxt, loggedOpts}) // Stable is necessary to preserve the per-function order, which is repeatable.
switch Format {
case Json0: // LSP 3.15
var posTmp, lastTmp []src.Pos
var encoder *json.Encoder
var w io.WriteCloser
if slashPkgPath == "" {
slashPkgPath = "\000"
}
subdirpath := filepath.Join(dest, url.PathEscape(slashPkgPath))
err := os.MkdirAll(subdirpath, 0755)
if err != nil {
log.Fatalf("Could not create directory %s for logging optimizer actions, %v", subdirpath, err)
}
diagnostic := Diagnostic{Source: "go compiler", Severity: SeverityInformation}
// For LSP, make a subdirectory for the package, and for each file foo.go, create foo.json in that subdirectory.
currentFile := ""
for _, x := range loggedOpts {
posTmp, p0 := parsePos(ctxt, x.pos, posTmp)
lastTmp, l0 := parsePos(ctxt, x.lastPos, lastTmp) // These match posTmp/p0 except for most-inline, and that often also matches.
p0f := uprootedPath(p0.Filename())
if currentFile != p0f {
if w != nil {
w.Close()
}
currentFile = p0f
w = writerForLSP(subdirpath, currentFile)
encoder = json.NewEncoder(w)
encoder.Encode(VersionHeader{Version: 0, Package: slashPkgPath, Goos: buildcfg.GOOS, Goarch: buildcfg.GOARCH, GcVersion: buildcfg.Version, File: currentFile})
}
// The first "target" is the most important one.
var target string
if len(x.target) > 0 {
target = fmt.Sprint(x.target[0])
}
diagnostic.Code = x.what
diagnostic.Message = target
diagnostic.Range = newRange(p0, l0)
diagnostic.RelatedInformation = diagnostic.RelatedInformation[:0]
appendInlinedPos(posTmp, lastTmp, &diagnostic)
// Diagnostic explanation is stored in RelatedInformation after inlining info
if len(x.target) > 1 {
switch y := x.target[1].(type) {
case []*LoggedOpt:
for _, z := range y {
posTmp, p0 := parsePos(ctxt, z.pos, posTmp)
lastTmp, l0 := parsePos(ctxt, z.lastPos, lastTmp)
loc := newLocation(p0, l0)
msg := z.what
if len(z.target) > 0 {
msg = msg + ": " + fmt.Sprint(z.target[0])
}
diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: msg})
appendInlinedPos(posTmp, lastTmp, &diagnostic)
}
}
}
encoder.Encode(diagnostic)
}
if w != nil {
w.Close()
}
}
}
// newRange returns a single-position Range for the compiler source location p.
func newRange(p, last src.Pos) Range {
return Range{Start: Position{p.Line(), p.Col()},
End: Position{last.Line(), last.Col()}}
}
// newLocation returns the Location for the compiler source location p.
func newLocation(p, last src.Pos) Location {
loc := Location{URI: uriIfy(uprootedPath(p.Filename())), Range: newRange(p, last)}
return loc
}
// appendInlinedPos extracts inlining information from posTmp and append it to diagnostic.
func appendInlinedPos(posTmp, lastTmp []src.Pos, diagnostic *Diagnostic) {
for i := 1; i < len(posTmp); i++ {
loc := newLocation(posTmp[i], lastTmp[i])
diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: "inlineLoc"})
}
}
// parsePos expands a src.XPos into a slice of src.Pos, with the outermost first.
// It returns the slice, and the outermost.
func parsePos(ctxt *obj.Link, pos src.XPos, posTmp []src.Pos) ([]src.Pos, src.Pos) {
posTmp = posTmp[:0]
ctxt.AllPos(pos, func(p src.Pos) {
posTmp = append(posTmp, p)
})
return posTmp, posTmp[0]
}