diff options
Diffstat (limited to 'src/cmd/compile/internal/logopt/log_opts.go')
-rw-r--r-- | src/cmd/compile/internal/logopt/log_opts.go | 523 |
1 files changed, 523 insertions, 0 deletions
diff --git a/src/cmd/compile/internal/logopt/log_opts.go b/src/cmd/compile/internal/logopt/log_opts.go new file mode 100644 index 0000000..97ebf56 --- /dev/null +++ b/src/cmd/compile/internal/logopt/log_opts.go @@ -0,0 +1,523 @@ +// 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. + 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 +} + +// isWindowsDriveURI 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.Fatalf(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 src.XPos, what, pass, funcName string, args ...interface{}) *LoggedOpt { + pass = strings.Replace(pass, " ", "_", -1) + return &LoggedOpt{pos, 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, 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 = 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 !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 []src.Pos + var encoder *json.Encoder + var w io.WriteCloser + + if slashPkgPath == "" { + slashPkgPath = "\000" + } + subdirpath := filepath.Join(dest, 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 := x.parsePos(ctxt, posTmp) + 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 = newPointRange(p0) + diagnostic.RelatedInformation = diagnostic.RelatedInformation[:0] + + appendInlinedPos(posTmp, &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 := z.parsePos(ctxt, posTmp) + loc := newLocation(p0) + 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, &diagnostic) + } + } + } + + encoder.Encode(diagnostic) + } + if w != nil { + w.Close() + } + } +} + +// newPointRange returns a single-position Range for the compiler source location p. +func newPointRange(p src.Pos) Range { + return Range{Start: Position{p.Line(), p.Col()}, + End: Position{p.Line(), p.Col()}} +} + +// newLocation returns the Location for the compiler source location p +func newLocation(p src.Pos) Location { + loc := Location{URI: uriIfy(uprootedPath(p.Filename())), Range: newPointRange(p)} + return loc +} + +// appendInlinedPos extracts inlining information from posTmp and append it to diagnostic +func appendInlinedPos(posTmp []src.Pos, diagnostic *Diagnostic) { + for i := 1; i < len(posTmp); i++ { + p := posTmp[i] + loc := newLocation(p) + diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: "inlineLoc"}) + } +} + +func (x *LoggedOpt) parsePos(ctxt *obj.Link, posTmp []src.Pos) ([]src.Pos, src.Pos) { + posTmp = ctxt.AllPos(x.pos, posTmp) + // Reverse posTmp to put outermost first. + l := len(posTmp) + for i := 0; i < l/2; i++ { + posTmp[i], posTmp[l-i-1] = posTmp[l-i-1], posTmp[i] + } + p0 := posTmp[0] + return posTmp, p0 +} |