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 }