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 }