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 lastPos src.XPos // Usually the same as pos; current exception is for reporting entire range of transformed loops 229 compilerPass string // Compiler pass. For human/adhoc consumption; does not appear in JSON (yet) 230 functionName string // Function name. For human/adhoc consumption; does not appear in JSON (yet) 231 what string // The (non) optimization; "nilcheck", "boundsCheck", "inline", "noInline" 232 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. 233 } 234 235 type logFormat uint8 236 237 const ( 238 None logFormat = iota 239 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. 240 ) 241 242 var Format = None 243 var dest string 244 245 // LogJsonOption parses and validates the version,directory value attached to the -json compiler flag. 246 func LogJsonOption(flagValue string) { 247 version, directory := parseLogFlag("json", flagValue) 248 if version != 0 { 249 log.Fatal("-json version must be 0") 250 } 251 dest = checkLogPath(directory) 252 Format = Json0 253 } 254 255 // parseLogFlag checks the flag passed to -json 256 // for version,destination format and returns the two parts. 257 func parseLogFlag(flag, value string) (version int, directory string) { 258 if Format != None { 259 log.Fatal("Cannot repeat -json flag") 260 } 261 commaAt := strings.Index(value, ",") 262 if commaAt <= 0 { 263 log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number", flag) 264 } 265 v, err := strconv.Atoi(value[:commaAt]) 266 if err != nil { 267 log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number: err=%v", flag, err) 268 } 269 version = v 270 directory = value[commaAt+1:] 271 return 272 } 273 274 // isWindowsDriveURIPath returns true if the file URI is of the format used by 275 // Windows URIs. The url.Parse package does not specially handle Windows paths 276 // (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:"). 277 // (copied from tools/internal/span/uri.go) 278 // this is less comprehensive that the processing in filepath.IsAbs on Windows. 279 func isWindowsDriveURIPath(uri string) bool { 280 if len(uri) < 4 { 281 return false 282 } 283 return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':' 284 } 285 286 func parseLogPath(destination string) (string, string) { 287 if filepath.IsAbs(destination) { 288 return filepath.Clean(destination), "" 289 } 290 if strings.HasPrefix(destination, "file://") { // IKWIAD, or Windows C:\foo\bar\baz 291 uri, err := url.Parse(destination) 292 if err != nil { 293 return "", fmt.Sprintf("optimizer logging destination looked like file:// URI but failed to parse: err=%v", err) 294 } 295 destination = uri.Host + uri.Path 296 if isWindowsDriveURIPath(destination) { 297 // strip leading / from /C: 298 // unlike tools/internal/span/uri.go, do not uppercase the drive letter -- let filepath.Clean do what it does. 299 destination = destination[1:] 300 } 301 return filepath.Clean(destination), "" 302 } 303 return "", fmt.Sprintf("optimizer logging destination %s was neither %s-prefixed directory nor file://-prefixed file URI", destination, string(filepath.Separator)) 304 } 305 306 // checkLogPath does superficial early checking of the string specifying 307 // the directory to which optimizer logging is directed, and if 308 // it passes the test, stores the string in LO_dir. 309 func checkLogPath(destination string) string { 310 path, complaint := parseLogPath(destination) 311 if complaint != "" { 312 log.Fatalf(complaint) 313 } 314 err := os.MkdirAll(path, 0755) 315 if err != nil { 316 log.Fatalf("optimizer logging destination '<version>,<directory>' but could not create <directory>: err=%v", err) 317 } 318 return path 319 } 320 321 var loggedOpts []*LoggedOpt 322 var mu = sync.Mutex{} // mu protects loggedOpts. 323 324 // NewLoggedOpt allocates a new LoggedOpt, to later be passed to either NewLoggedOpt or LogOpt as "args". 325 // Pos is the source position (including inlining), what is the message, pass is which pass created the message, 326 // funcName is the name of the function 327 // A typical use for this to accumulate an explanation for a missed optimization, for example, why did something escape? 328 func NewLoggedOpt(pos, lastPos src.XPos, what, pass, funcName string, args ...interface{}) *LoggedOpt { 329 pass = strings.Replace(pass, " ", "_", -1) 330 return &LoggedOpt{pos, lastPos, pass, funcName, what, args} 331 } 332 333 // LogOpt logs information about a (usually missed) optimization performed by the compiler. 334 // Pos is the source position (including inlining), what is the message, pass is which pass created the message, 335 // funcName is the name of the function. 336 func LogOpt(pos src.XPos, what, pass, funcName string, args ...interface{}) { 337 if Format == None { 338 return 339 } 340 lo := NewLoggedOpt(pos, pos, what, pass, funcName, args...) 341 mu.Lock() 342 defer mu.Unlock() 343 // Because of concurrent calls from back end, no telling what the order will be, but is stable-sorted by outer Pos before use. 344 loggedOpts = append(loggedOpts, lo) 345 } 346 347 // LogOptRange is the same as LogOpt, but includes the ability to express a range of positions, 348 // not just a point. 349 func LogOptRange(pos, lastPos src.XPos, what, pass, funcName string, args ...interface{}) { 350 if Format == None { 351 return 352 } 353 lo := NewLoggedOpt(pos, lastPos, what, pass, funcName, args...) 354 mu.Lock() 355 defer mu.Unlock() 356 // Because of concurrent calls from back end, no telling what the order will be, but is stable-sorted by outer Pos before use. 357 loggedOpts = append(loggedOpts, lo) 358 } 359 360 // Enabled returns whether optimization logging is enabled. 361 func Enabled() bool { 362 switch Format { 363 case None: 364 return false 365 case Json0: 366 return true 367 } 368 panic("Unexpected optimizer-logging level") 369 } 370 371 // byPos sorts diagnostics by source position. 372 type byPos struct { 373 ctxt *obj.Link 374 a []*LoggedOpt 375 } 376 377 func (x byPos) Len() int { return len(x.a) } 378 func (x byPos) Less(i, j int) bool { 379 return x.ctxt.OutermostPos(x.a[i].pos).Before(x.ctxt.OutermostPos(x.a[j].pos)) 380 } 381 func (x byPos) Swap(i, j int) { x.a[i], x.a[j] = x.a[j], x.a[i] } 382 383 func writerForLSP(subdirpath, file string) io.WriteCloser { 384 basename := file 385 lastslash := strings.LastIndexAny(basename, "\\/") 386 if lastslash != -1 { 387 basename = basename[lastslash+1:] 388 } 389 lastdot := strings.LastIndex(basename, ".go") 390 if lastdot != -1 { 391 basename = basename[:lastdot] 392 } 393 basename = url.PathEscape(basename) 394 395 // Assume a directory, make a file 396 p := filepath.Join(subdirpath, basename+".json") 397 w, err := os.Create(p) 398 if err != nil { 399 log.Fatalf("Could not create file %s for logging optimizer actions, %v", p, err) 400 } 401 return w 402 } 403 404 func fixSlash(f string) string { 405 if os.PathSeparator == '/' { 406 return f 407 } 408 return strings.Replace(f, string(os.PathSeparator), "/", -1) 409 } 410 411 func uriIfy(f string) DocumentURI { 412 url := url.URL{ 413 Scheme: "file", 414 Path: fixSlash(f), 415 } 416 return DocumentURI(url.String()) 417 } 418 419 // Return filename, replacing a first occurrence of $GOROOT with the 420 // actual value of the GOROOT (because LSP does not speak "$GOROOT"). 421 func uprootedPath(filename string) string { 422 if filename == "" { 423 return "__unnamed__" 424 } 425 if buildcfg.GOROOT == "" || !strings.HasPrefix(filename, "$GOROOT/") { 426 return filename 427 } 428 return buildcfg.GOROOT + filename[len("$GOROOT"):] 429 } 430 431 // FlushLoggedOpts flushes all the accumulated optimization log entries. 432 func FlushLoggedOpts(ctxt *obj.Link, slashPkgPath string) { 433 if Format == None { 434 return 435 } 436 437 sort.Stable(byPos{ctxt, loggedOpts}) // Stable is necessary to preserve the per-function order, which is repeatable. 438 switch Format { 439 440 case Json0: // LSP 3.15 441 var posTmp, lastTmp []src.Pos 442 var encoder *json.Encoder 443 var w io.WriteCloser 444 445 if slashPkgPath == "" { 446 slashPkgPath = "\000" 447 } 448 subdirpath := filepath.Join(dest, url.PathEscape(slashPkgPath)) 449 err := os.MkdirAll(subdirpath, 0755) 450 if err != nil { 451 log.Fatalf("Could not create directory %s for logging optimizer actions, %v", subdirpath, err) 452 } 453 diagnostic := Diagnostic{Source: "go compiler", Severity: SeverityInformation} 454 455 // For LSP, make a subdirectory for the package, and for each file foo.go, create foo.json in that subdirectory. 456 currentFile := "" 457 for _, x := range loggedOpts { 458 posTmp, p0 := parsePos(ctxt, x.pos, posTmp) 459 lastTmp, l0 := parsePos(ctxt, x.lastPos, lastTmp) // These match posTmp/p0 except for most-inline, and that often also matches. 460 p0f := uprootedPath(p0.Filename()) 461 462 if currentFile != p0f { 463 if w != nil { 464 w.Close() 465 } 466 currentFile = p0f 467 w = writerForLSP(subdirpath, currentFile) 468 encoder = json.NewEncoder(w) 469 encoder.Encode(VersionHeader{Version: 0, Package: slashPkgPath, Goos: buildcfg.GOOS, Goarch: buildcfg.GOARCH, GcVersion: buildcfg.Version, File: currentFile}) 470 } 471 472 // The first "target" is the most important one. 473 var target string 474 if len(x.target) > 0 { 475 target = fmt.Sprint(x.target[0]) 476 } 477 478 diagnostic.Code = x.what 479 diagnostic.Message = target 480 diagnostic.Range = newRange(p0, l0) 481 diagnostic.RelatedInformation = diagnostic.RelatedInformation[:0] 482 483 appendInlinedPos(posTmp, lastTmp, &diagnostic) 484 485 // Diagnostic explanation is stored in RelatedInformation after inlining info 486 if len(x.target) > 1 { 487 switch y := x.target[1].(type) { 488 case []*LoggedOpt: 489 for _, z := range y { 490 posTmp, p0 := parsePos(ctxt, z.pos, posTmp) 491 lastTmp, l0 := parsePos(ctxt, z.lastPos, lastTmp) 492 loc := newLocation(p0, l0) 493 msg := z.what 494 if len(z.target) > 0 { 495 msg = msg + ": " + fmt.Sprint(z.target[0]) 496 } 497 498 diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: msg}) 499 appendInlinedPos(posTmp, lastTmp, &diagnostic) 500 } 501 } 502 } 503 504 encoder.Encode(diagnostic) 505 } 506 if w != nil { 507 w.Close() 508 } 509 } 510 } 511 512 // newRange returns a single-position Range for the compiler source location p. 513 func newRange(p, last src.Pos) Range { 514 return Range{Start: Position{p.Line(), p.Col()}, 515 End: Position{last.Line(), last.Col()}} 516 } 517 518 // newLocation returns the Location for the compiler source location p. 519 func newLocation(p, last src.Pos) Location { 520 loc := Location{URI: uriIfy(uprootedPath(p.Filename())), Range: newRange(p, last)} 521 return loc 522 } 523 524 // appendInlinedPos extracts inlining information from posTmp and append it to diagnostic. 525 func appendInlinedPos(posTmp, lastTmp []src.Pos, diagnostic *Diagnostic) { 526 for i := 1; i < len(posTmp); i++ { 527 loc := newLocation(posTmp[i], lastTmp[i]) 528 diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: "inlineLoc"}) 529 } 530 } 531 532 // parsePos expands a src.XPos into a slice of src.Pos, with the outermost first. 533 // It returns the slice, and the outermost. 534 func parsePos(ctxt *obj.Link, pos src.XPos, posTmp []src.Pos) ([]src.Pos, src.Pos) { 535 posTmp = posTmp[:0] 536 ctxt.AllPos(pos, func(p src.Pos) { 537 posTmp = append(posTmp, p) 538 }) 539 return posTmp, posTmp[0] 540 } 541