Source file src/cmd/compile/internal/logopt/log_opts.go

     1  // Copyright 2019 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package logopt
     6  
     7  import (
     8  	"cmd/internal/obj"
     9  	"cmd/internal/src"
    10  	"encoding/json"
    11  	"fmt"
    12  	"internal/buildcfg"
    13  	"io"
    14  	"log"
    15  	"net/url"
    16  	"os"
    17  	"path/filepath"
    18  	"sort"
    19  	"strconv"
    20  	"strings"
    21  	"sync"
    22  	"unicode"
    23  )
    24  
    25  // This implements (non)optimization logging for -json option to the Go compiler
    26  // The option is -json 0,<destination>.
    27  //
    28  // 0 is the version number; to avoid the need for synchronized updates, if
    29  // new versions of the logging appear, the compiler will support both, for a while,
    30  // and clients will specify what they need.
    31  //
    32  // <destination> is a directory.
    33  // Directories are specified with a leading / or os.PathSeparator,
    34  // or more explicitly with file://directory.  The second form is intended to
    35  // deal with corner cases on Windows, and to allow specification of a relative
    36  // directory path (which is normally a bad idea, because the local directory
    37  // varies a lot in a build, especially with modules and/or vendoring, and may
    38  // not be writeable).
    39  //
    40  // For each package pkg compiled, a url.PathEscape(pkg)-named subdirectory
    41  // is created.  For each source file.go in that package that generates
    42  // diagnostics (no diagnostics means no file),
    43  // a url.PathEscape(file)+".json"-named file is created and contains the
    44  // logged diagnostics.
    45  //
    46  // For example, "cmd%2Finternal%2Fdwarf/%3Cautogenerated%3E.json"
    47  // for "cmd/internal/dwarf" and <autogenerated> (which is not really a file, but the compiler sees it)
    48  //
    49  // If the package string is empty, it is replaced internally with string(0) which encodes to %00.
    50  //
    51  // Each log file begins with a JSON record identifying version,
    52  // platform, and other context, followed by optimization-relevant
    53  // LSP Diagnostic records, one per line (LSP version 3.15, no difference from 3.14 on the subset used here
    54  // see https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/ )
    55  //
    56  // The fields of a Diagnostic are used in the following way:
    57  // Range: the outermost source position, for now begin and end are equal.
    58  // Severity: (always) SeverityInformation (3)
    59  // Source: (always) "go compiler"
    60  // Code: a string describing the missed optimization, e.g., "nilcheck", "cannotInline", "isInBounds", "escape"
    61  // Message: depending on code, additional information, e.g., the reason a function cannot be inlined.
    62  // RelatedInformation: if the missed optimization actually occurred at a function inlined at Range,
    63  //    then the sequence of inlined locations appears here, from (second) outermost to innermost,
    64  //    each with message="inlineLoc".
    65  //
    66  //    In the case of escape analysis explanations, after any outer inlining locations,
    67  //    the lines of the explanation appear, each potentially followed with its own inlining
    68  //    location if the escape flow occurred within an inlined function.
    69  //
    70  // For example <destination>/cmd%2Fcompile%2Finternal%2Fssa/prove.json
    71  // might begin with the following line (wrapped for legibility):
    72  //
    73  // {"version":0,"package":"cmd/compile/internal/ssa","goos":"darwin","goarch":"amd64",
    74  //  "gc_version":"devel +e1b9a57852 Fri Nov 1 15:07:00 2019 -0400",
    75  //  "file":"/Users/drchase/work/go/src/cmd/compile/internal/ssa/prove.go"}
    76  //
    77  // and later contain (also wrapped for legibility):
    78  //
    79  // {"range":{"start":{"line":191,"character":24},"end":{"line":191,"character":24}},
    80  //  "severity":3,"code":"nilcheck","source":"go compiler","message":"",
    81  //  "relatedInformation":[
    82  //    {"location":{"uri":"file:///Users/drchase/work/go/src/cmd/compile/internal/ssa/func.go",
    83  //                 "range":{"start":{"line":153,"character":16},"end":{"line":153,"character":16}}},
    84  //     "message":"inlineLoc"}]}
    85  //
    86  // That is, at prove.go (implicit from context, provided in both filename and header line),
    87  // line 191, column 24, a nilcheck occurred in the generated code.
    88  // The relatedInformation indicates that this code actually came from
    89  // an inlined call to func.go, line 153, character 16.
    90  //
    91  // prove.go:191:
    92  // 	ft.orderS = f.newPoset()
    93  // func.go:152 and 153:
    94  //  func (f *Func) newPoset() *poset {
    95  //	    if len(f.Cache.scrPoset) > 0 {
    96  //
    97  // In the case that the package is empty, the string(0) package name is also used in the header record, for example
    98  //
    99  //  go tool compile -json=0,file://logopt x.go       # no -p option to set the package
   100  //  head -1 logopt/%00/x.json
   101  //  {"version":0,"package":"\u0000","goos":"darwin","goarch":"amd64","gc_version":"devel +86487adf6a Thu Nov 7 19:34:56 2019 -0500","file":"x.go"}
   102  
   103  type VersionHeader struct {
   104  	Version   int    `json:"version"`
   105  	Package   string `json:"package"`
   106  	Goos      string `json:"goos"`
   107  	Goarch    string `json:"goarch"`
   108  	GcVersion string `json:"gc_version"`
   109  	File      string `json:"file,omitempty"` // LSP requires an enclosing resource, i.e., a file
   110  }
   111  
   112  // DocumentURI, Position, Range, Location, Diagnostic, DiagnosticRelatedInformation all reuse json definitions from gopls.
   113  // See https://github.com/golang/tools/blob/22afafe3322a860fcd3d88448768f9db36f8bc5f/internal/lsp/protocol/tsprotocol.go
   114  
   115  type DocumentURI string
   116  
   117  type Position struct {
   118  	Line      uint `json:"line"`      // gopls uses float64, but json output is the same for integers
   119  	Character uint `json:"character"` // gopls uses float64, but json output is the same for integers
   120  }
   121  
   122  // A Range in a text document expressed as (zero-based) start and end positions.
   123  // A range is comparable to a selection in an editor. Therefore the end position is exclusive.
   124  // If you want to specify a range that contains a line including the line ending character(s)
   125  // then use an end position denoting the start of the next line.
   126  type Range struct {
   127  	/*Start defined:
   128  	 * The range's start position
   129  	 */
   130  	Start Position `json:"start"`
   131  
   132  	/*End defined:
   133  	 * The range's end position
   134  	 */
   135  	End Position `json:"end"` // exclusive
   136  }
   137  
   138  // A Location represents a location inside a resource, such as a line inside a text file.
   139  type Location struct {
   140  	// URI is
   141  	URI DocumentURI `json:"uri"`
   142  
   143  	// Range is
   144  	Range Range `json:"range"`
   145  }
   146  
   147  /* DiagnosticRelatedInformation defined:
   148   * Represents a related message and source code location for a diagnostic. This should be
   149   * used to point to code locations that cause or related to a diagnostics, e.g when duplicating
   150   * a symbol in a scope.
   151   */
   152  type DiagnosticRelatedInformation struct {
   153  
   154  	/*Location defined:
   155  	 * The location of this related diagnostic information.
   156  	 */
   157  	Location Location `json:"location"`
   158  
   159  	/*Message defined:
   160  	 * The message of this related diagnostic information.
   161  	 */
   162  	Message string `json:"message"`
   163  }
   164  
   165  // DiagnosticSeverity defines constants
   166  type DiagnosticSeverity uint
   167  
   168  const (
   169  	/*SeverityInformation defined:
   170  	 * Reports an information.
   171  	 */
   172  	SeverityInformation DiagnosticSeverity = 3
   173  )
   174  
   175  // DiagnosticTag defines constants
   176  type DiagnosticTag uint
   177  
   178  /*Diagnostic defined:
   179   * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects
   180   * are only valid in the scope of a resource.
   181   */
   182  type Diagnostic struct {
   183  
   184  	/*Range defined:
   185  	 * The range at which the message applies
   186  	 */
   187  	Range Range `json:"range"`
   188  
   189  	/*Severity defined:
   190  	 * The diagnostic's severity. Can be omitted. If omitted it is up to the
   191  	 * client to interpret diagnostics as error, warning, info or hint.
   192  	 */
   193  	Severity DiagnosticSeverity `json:"severity,omitempty"` // always SeverityInformation for optimizer logging.
   194  
   195  	/*Code defined:
   196  	 * The diagnostic's code, which usually appear in the user interface.
   197  	 */
   198  	Code string `json:"code,omitempty"` // LSP uses 'number | string' = gopls interface{}, but only string here, e.g. "boundsCheck", "nilcheck", etc.
   199  
   200  	/*Source defined:
   201  	 * A human-readable string describing the source of this
   202  	 * diagnostic, e.g. 'typescript' or 'super lint'. It usually
   203  	 * appears in the user interface.
   204  	 */
   205  	Source string `json:"source,omitempty"` // "go compiler"
   206  
   207  	/*Message defined:
   208  	 * The diagnostic's message. It usually appears in the user interface
   209  	 */
   210  	Message string `json:"message"` // sometimes used, provides additional information.
   211  
   212  	/*Tags defined:
   213  	 * Additional metadata about the diagnostic.
   214  	 */
   215  	Tags []DiagnosticTag `json:"tags,omitempty"` // always empty for logging optimizations.
   216  
   217  	/*RelatedInformation defined:
   218  	 * An array of related diagnostic information, e.g. when symbol-names within
   219  	 * a scope collide all definitions can be marked via this property.
   220  	 */
   221  	RelatedInformation []DiagnosticRelatedInformation `json:"relatedInformation,omitempty"`
   222  }
   223  
   224  // A LoggedOpt is what the compiler produces and accumulates,
   225  // to be converted to JSON for human or IDE consumption.
   226  type LoggedOpt struct {
   227  	pos          src.XPos      // Source code position at which the event occurred. If it is inlined, outer and all inlined locations will appear in JSON.
   228  	compilerPass string        // Compiler pass.  For human/adhoc consumption; does not appear in JSON (yet)
   229  	functionName string        // Function name.  For human/adhoc consumption; does not appear in JSON (yet)
   230  	what         string        // The (non) optimization; "nilcheck", "boundsCheck", "inline", "noInline"
   231  	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.
   232  }
   233  
   234  type logFormat uint8
   235  
   236  const (
   237  	None  logFormat = iota
   238  	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.
   239  )
   240  
   241  var Format = None
   242  var dest string
   243  
   244  // LogJsonOption parses and validates the version,directory value attached to the -json compiler flag.
   245  func LogJsonOption(flagValue string) {
   246  	version, directory := parseLogFlag("json", flagValue)
   247  	if version != 0 {
   248  		log.Fatal("-json version must be 0")
   249  	}
   250  	dest = checkLogPath(directory)
   251  	Format = Json0
   252  }
   253  
   254  // parseLogFlag checks the flag passed to -json
   255  // for version,destination format and returns the two parts.
   256  func parseLogFlag(flag, value string) (version int, directory string) {
   257  	if Format != None {
   258  		log.Fatal("Cannot repeat -json flag")
   259  	}
   260  	commaAt := strings.Index(value, ",")
   261  	if commaAt <= 0 {
   262  		log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number", flag)
   263  	}
   264  	v, err := strconv.Atoi(value[:commaAt])
   265  	if err != nil {
   266  		log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number: err=%v", flag, err)
   267  	}
   268  	version = v
   269  	directory = value[commaAt+1:]
   270  	return
   271  }
   272  
   273  // isWindowsDriveURI returns true if the file URI is of the format used by
   274  // Windows URIs. The url.Parse package does not specially handle Windows paths
   275  // (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:").
   276  // (copied from tools/internal/span/uri.go)
   277  // this is less comprehensive that the processing in filepath.IsAbs on Windows.
   278  func isWindowsDriveURIPath(uri string) bool {
   279  	if len(uri) < 4 {
   280  		return false
   281  	}
   282  	return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
   283  }
   284  
   285  func parseLogPath(destination string) (string, string) {
   286  	if filepath.IsAbs(destination) {
   287  		return filepath.Clean(destination), ""
   288  	}
   289  	if strings.HasPrefix(destination, "file://") { // IKWIAD, or Windows C:\foo\bar\baz
   290  		uri, err := url.Parse(destination)
   291  		if err != nil {
   292  			return "", fmt.Sprintf("optimizer logging destination looked like file:// URI but failed to parse: err=%v", err)
   293  		}
   294  		destination = uri.Host + uri.Path
   295  		if isWindowsDriveURIPath(destination) {
   296  			// strip leading / from /C:
   297  			// unlike tools/internal/span/uri.go, do not uppercase the drive letter -- let filepath.Clean do what it does.
   298  			destination = destination[1:]
   299  		}
   300  		return filepath.Clean(destination), ""
   301  	}
   302  	return "", fmt.Sprintf("optimizer logging destination %s was neither %s-prefixed directory nor file://-prefixed file URI", destination, string(filepath.Separator))
   303  }
   304  
   305  // checkLogPath does superficial early checking of the string specifying
   306  // the directory to which optimizer logging is directed, and if
   307  // it passes the test, stores the string in LO_dir
   308  func checkLogPath(destination string) string {
   309  	path, complaint := parseLogPath(destination)
   310  	if complaint != "" {
   311  		log.Fatalf(complaint)
   312  	}
   313  	err := os.MkdirAll(path, 0755)
   314  	if err != nil {
   315  		log.Fatalf("optimizer logging destination '<version>,<directory>' but could not create <directory>: err=%v", err)
   316  	}
   317  	return path
   318  }
   319  
   320  var loggedOpts []*LoggedOpt
   321  var mu = sync.Mutex{} // mu protects loggedOpts.
   322  
   323  // NewLoggedOpt allocates a new LoggedOpt, to later be passed to either NewLoggedOpt or LogOpt as "args".
   324  // Pos is the source position (including inlining), what is the message, pass is which pass created the message,
   325  // funcName is the name of the function
   326  // A typical use for this to accumulate an explanation for a missed optimization, for example, why did something escape?
   327  func NewLoggedOpt(pos src.XPos, what, pass, funcName string, args ...interface{}) *LoggedOpt {
   328  	pass = strings.Replace(pass, " ", "_", -1)
   329  	return &LoggedOpt{pos, pass, funcName, what, args}
   330  }
   331  
   332  // Logopt logs information about a (usually missed) optimization performed by the compiler.
   333  // Pos is the source position (including inlining), what is the message, pass is which pass created the message,
   334  // funcName is the name of the function
   335  func LogOpt(pos src.XPos, what, pass, funcName string, args ...interface{}) {
   336  	if Format == None {
   337  		return
   338  	}
   339  	lo := NewLoggedOpt(pos, what, pass, funcName, args...)
   340  	mu.Lock()
   341  	defer mu.Unlock()
   342  	// Because of concurrent calls from back end, no telling what the order will be, but is stable-sorted by outer Pos before use.
   343  	loggedOpts = append(loggedOpts, lo)
   344  }
   345  
   346  // Enabled returns whether optimization logging is enabled.
   347  func Enabled() bool {
   348  	switch Format {
   349  	case None:
   350  		return false
   351  	case Json0:
   352  		return true
   353  	}
   354  	panic("Unexpected optimizer-logging level")
   355  }
   356  
   357  // byPos sorts diagnostics by source position.
   358  type byPos struct {
   359  	ctxt *obj.Link
   360  	a    []*LoggedOpt
   361  }
   362  
   363  func (x byPos) Len() int { return len(x.a) }
   364  func (x byPos) Less(i, j int) bool {
   365  	return x.ctxt.OutermostPos(x.a[i].pos).Before(x.ctxt.OutermostPos(x.a[j].pos))
   366  }
   367  func (x byPos) Swap(i, j int) { x.a[i], x.a[j] = x.a[j], x.a[i] }
   368  
   369  func writerForLSP(subdirpath, file string) io.WriteCloser {
   370  	basename := file
   371  	lastslash := strings.LastIndexAny(basename, "\\/")
   372  	if lastslash != -1 {
   373  		basename = basename[lastslash+1:]
   374  	}
   375  	lastdot := strings.LastIndex(basename, ".go")
   376  	if lastdot != -1 {
   377  		basename = basename[:lastdot]
   378  	}
   379  	basename = pathEscape(basename)
   380  
   381  	// Assume a directory, make a file
   382  	p := filepath.Join(subdirpath, basename+".json")
   383  	w, err := os.Create(p)
   384  	if err != nil {
   385  		log.Fatalf("Could not create file %s for logging optimizer actions, %v", p, err)
   386  	}
   387  	return w
   388  }
   389  
   390  func fixSlash(f string) string {
   391  	if os.PathSeparator == '/' {
   392  		return f
   393  	}
   394  	return strings.Replace(f, string(os.PathSeparator), "/", -1)
   395  }
   396  
   397  func uriIfy(f string) DocumentURI {
   398  	url := url.URL{
   399  		Scheme: "file",
   400  		Path:   fixSlash(f),
   401  	}
   402  	return DocumentURI(url.String())
   403  }
   404  
   405  // Return filename, replacing a first occurrence of $GOROOT with the
   406  // actual value of the GOROOT (because LSP does not speak "$GOROOT").
   407  func uprootedPath(filename string) string {
   408  	if !strings.HasPrefix(filename, "$GOROOT/") {
   409  		return filename
   410  	}
   411  	return buildcfg.GOROOT + filename[len("$GOROOT"):]
   412  }
   413  
   414  // FlushLoggedOpts flushes all the accumulated optimization log entries.
   415  func FlushLoggedOpts(ctxt *obj.Link, slashPkgPath string) {
   416  	if Format == None {
   417  		return
   418  	}
   419  
   420  	sort.Stable(byPos{ctxt, loggedOpts}) // Stable is necessary to preserve the per-function order, which is repeatable.
   421  	switch Format {
   422  
   423  	case Json0: // LSP 3.15
   424  		var posTmp []src.Pos
   425  		var encoder *json.Encoder
   426  		var w io.WriteCloser
   427  
   428  		if slashPkgPath == "" {
   429  			slashPkgPath = "\000"
   430  		}
   431  		subdirpath := filepath.Join(dest, pathEscape(slashPkgPath))
   432  		err := os.MkdirAll(subdirpath, 0755)
   433  		if err != nil {
   434  			log.Fatalf("Could not create directory %s for logging optimizer actions, %v", subdirpath, err)
   435  		}
   436  		diagnostic := Diagnostic{Source: "go compiler", Severity: SeverityInformation}
   437  
   438  		// For LSP, make a subdirectory for the package, and for each file foo.go, create foo.json in that subdirectory.
   439  		currentFile := ""
   440  		for _, x := range loggedOpts {
   441  			posTmp, p0 := x.parsePos(ctxt, posTmp)
   442  			p0f := uprootedPath(p0.Filename())
   443  
   444  			if currentFile != p0f {
   445  				if w != nil {
   446  					w.Close()
   447  				}
   448  				currentFile = p0f
   449  				w = writerForLSP(subdirpath, currentFile)
   450  				encoder = json.NewEncoder(w)
   451  				encoder.Encode(VersionHeader{Version: 0, Package: slashPkgPath, Goos: buildcfg.GOOS, Goarch: buildcfg.GOARCH, GcVersion: buildcfg.Version, File: currentFile})
   452  			}
   453  
   454  			// The first "target" is the most important one.
   455  			var target string
   456  			if len(x.target) > 0 {
   457  				target = fmt.Sprint(x.target[0])
   458  			}
   459  
   460  			diagnostic.Code = x.what
   461  			diagnostic.Message = target
   462  			diagnostic.Range = newPointRange(p0)
   463  			diagnostic.RelatedInformation = diagnostic.RelatedInformation[:0]
   464  
   465  			appendInlinedPos(posTmp, &diagnostic)
   466  
   467  			// Diagnostic explanation is stored in RelatedInformation after inlining info
   468  			if len(x.target) > 1 {
   469  				switch y := x.target[1].(type) {
   470  				case []*LoggedOpt:
   471  					for _, z := range y {
   472  						posTmp, p0 := z.parsePos(ctxt, posTmp)
   473  						loc := newLocation(p0)
   474  						msg := z.what
   475  						if len(z.target) > 0 {
   476  							msg = msg + ": " + fmt.Sprint(z.target[0])
   477  						}
   478  
   479  						diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: msg})
   480  						appendInlinedPos(posTmp, &diagnostic)
   481  					}
   482  				}
   483  			}
   484  
   485  			encoder.Encode(diagnostic)
   486  		}
   487  		if w != nil {
   488  			w.Close()
   489  		}
   490  	}
   491  }
   492  
   493  // newPointRange returns a single-position Range for the compiler source location p.
   494  func newPointRange(p src.Pos) Range {
   495  	return Range{Start: Position{p.Line(), p.Col()},
   496  		End: Position{p.Line(), p.Col()}}
   497  }
   498  
   499  // newLocation returns the Location for the compiler source location p
   500  func newLocation(p src.Pos) Location {
   501  	loc := Location{URI: uriIfy(uprootedPath(p.Filename())), Range: newPointRange(p)}
   502  	return loc
   503  }
   504  
   505  // appendInlinedPos extracts inlining information from posTmp and append it to diagnostic
   506  func appendInlinedPos(posTmp []src.Pos, diagnostic *Diagnostic) {
   507  	for i := 1; i < len(posTmp); i++ {
   508  		p := posTmp[i]
   509  		loc := newLocation(p)
   510  		diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: "inlineLoc"})
   511  	}
   512  }
   513  
   514  func (x *LoggedOpt) parsePos(ctxt *obj.Link, posTmp []src.Pos) ([]src.Pos, src.Pos) {
   515  	posTmp = ctxt.AllPos(x.pos, posTmp)
   516  	// Reverse posTmp to put outermost first.
   517  	l := len(posTmp)
   518  	for i := 0; i < l/2; i++ {
   519  		posTmp[i], posTmp[l-i-1] = posTmp[l-i-1], posTmp[i]
   520  	}
   521  	p0 := posTmp[0]
   522  	return posTmp, p0
   523  }
   524  

View as plain text