github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/compile/logopt/log_opts.go (about)

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