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