github.com/twelsh-aw/go/src@v0.0.0-20230516233729-a56fe86a7c81/runtime/coverage/emit.go (about) 1 // Copyright 2022 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 coverage 6 7 import ( 8 "crypto/md5" 9 "fmt" 10 "internal/coverage" 11 "internal/coverage/encodecounter" 12 "internal/coverage/encodemeta" 13 "internal/coverage/rtcov" 14 "io" 15 "os" 16 "path/filepath" 17 "reflect" 18 "runtime" 19 "strconv" 20 "sync/atomic" 21 "time" 22 "unsafe" 23 ) 24 25 // This file contains functions that support the writing of data files 26 // emitted at the end of code coverage testing runs, from instrumented 27 // executables. 28 29 // getCovMetaList returns a list of meta-data blobs registered 30 // for the currently executing instrumented program. It is defined in the 31 // runtime. 32 func getCovMetaList() []rtcov.CovMetaBlob 33 34 // getCovCounterList returns a list of counter-data blobs registered 35 // for the currently executing instrumented program. It is defined in the 36 // runtime. 37 func getCovCounterList() []rtcov.CovCounterBlob 38 39 // getCovPkgMap returns a map storing the remapped package IDs for 40 // hard-coded runtime packages (see internal/coverage/pkgid.go for 41 // more on why hard-coded package IDs are needed). This function 42 // is defined in the runtime. 43 func getCovPkgMap() map[int]int 44 45 // emitState holds useful state information during the emit process. 46 // 47 // When an instrumented program finishes execution and starts the 48 // process of writing out coverage data, it's possible that an 49 // existing meta-data file already exists in the output directory. In 50 // this case openOutputFiles() below will leave the 'mf' field below 51 // as nil. If a new meta-data file is needed, field 'mfname' will be 52 // the final desired path of the meta file, 'mftmp' will be a 53 // temporary file, and 'mf' will be an open os.File pointer for 54 // 'mftmp'. The meta-data file payload will be written to 'mf', the 55 // temp file will be then closed and renamed (from 'mftmp' to 56 // 'mfname'), so as to insure that the meta-data file is created 57 // atomically; we want this so that things work smoothly in cases 58 // where there are several instances of a given instrumented program 59 // all terminating at the same time and trying to create meta-data 60 // files simultaneously. 61 // 62 // For counter data files there is less chance of a collision, hence 63 // the openOutputFiles() stores the counter data file in 'cfname' and 64 // then places the *io.File into 'cf'. 65 type emitState struct { 66 mfname string // path of final meta-data output file 67 mftmp string // path to meta-data temp file (if needed) 68 mf *os.File // open os.File for meta-data temp file 69 cfname string // path of final counter data file 70 cftmp string // path to counter data temp file 71 cf *os.File // open os.File for counter data file 72 outdir string // output directory 73 74 // List of meta-data symbols obtained from the runtime 75 metalist []rtcov.CovMetaBlob 76 77 // List of counter-data symbols obtained from the runtime 78 counterlist []rtcov.CovCounterBlob 79 80 // Table to use for remapping hard-coded pkg ids. 81 pkgmap map[int]int 82 83 // emit debug trace output 84 debug bool 85 } 86 87 var ( 88 // finalHash is computed at init time from the list of meta-data 89 // symbols registered during init. It is used both for writing the 90 // meta-data file and counter-data files. 91 finalHash [16]byte 92 // Set to true when we've computed finalHash + finalMetaLen. 93 finalHashComputed bool 94 // Total meta-data length. 95 finalMetaLen uint64 96 // Records whether we've already attempted to write meta-data. 97 metaDataEmitAttempted bool 98 // Counter mode for this instrumented program run. 99 cmode coverage.CounterMode 100 // Counter granularity for this instrumented program run. 101 cgran coverage.CounterGranularity 102 // Cached value of GOCOVERDIR environment variable. 103 goCoverDir string 104 // Copy of os.Args made at init time, converted into map format. 105 capturedOsArgs map[string]string 106 // Flag used in tests to signal that coverage data already written. 107 covProfileAlreadyEmitted bool 108 ) 109 110 // fileType is used to select between counter-data files and 111 // meta-data files. 112 type fileType int 113 114 const ( 115 noFile = 1 << iota 116 metaDataFile 117 counterDataFile 118 ) 119 120 // emitMetaData emits the meta-data output file for this coverage run. 121 // This entry point is intended to be invoked by the compiler from 122 // an instrumented program's main package init func. 123 func emitMetaData() { 124 if covProfileAlreadyEmitted { 125 return 126 } 127 ml, err := prepareForMetaEmit() 128 if err != nil { 129 fmt.Fprintf(os.Stderr, "error: coverage meta-data prep failed: %v\n", err) 130 if os.Getenv("GOCOVERDEBUG") != "" { 131 panic("meta-data write failure") 132 } 133 } 134 if len(ml) == 0 { 135 fmt.Fprintf(os.Stderr, "program not built with -cover\n") 136 return 137 } 138 139 goCoverDir = os.Getenv("GOCOVERDIR") 140 if goCoverDir == "" { 141 fmt.Fprintf(os.Stderr, "warning: GOCOVERDIR not set, no coverage data emitted\n") 142 return 143 } 144 145 if err := emitMetaDataToDirectory(goCoverDir, ml); err != nil { 146 fmt.Fprintf(os.Stderr, "error: coverage meta-data emit failed: %v\n", err) 147 if os.Getenv("GOCOVERDEBUG") != "" { 148 panic("meta-data write failure") 149 } 150 } 151 } 152 153 func modeClash(m coverage.CounterMode) bool { 154 if m == coverage.CtrModeRegOnly || m == coverage.CtrModeTestMain { 155 return false 156 } 157 if cmode == coverage.CtrModeInvalid { 158 cmode = m 159 return false 160 } 161 return cmode != m 162 } 163 164 func granClash(g coverage.CounterGranularity) bool { 165 if cgran == coverage.CtrGranularityInvalid { 166 cgran = g 167 return false 168 } 169 return cgran != g 170 } 171 172 // prepareForMetaEmit performs preparatory steps needed prior to 173 // emitting a meta-data file, notably computing a final hash of 174 // all meta-data blobs and capturing os args. 175 func prepareForMetaEmit() ([]rtcov.CovMetaBlob, error) { 176 // Ask the runtime for the list of coverage meta-data symbols. 177 ml := getCovMetaList() 178 179 // In the normal case (go build -o prog.exe ... ; ./prog.exe) 180 // len(ml) will always be non-zero, but we check here since at 181 // some point this function will be reachable via user-callable 182 // APIs (for example, to write out coverage data from a server 183 // program that doesn't ever call os.Exit). 184 if len(ml) == 0 { 185 return nil, nil 186 } 187 188 s := &emitState{ 189 metalist: ml, 190 debug: os.Getenv("GOCOVERDEBUG") != "", 191 } 192 193 // Capture os.Args() now so as to avoid issues if args 194 // are rewritten during program execution. 195 capturedOsArgs = captureOsArgs() 196 197 if s.debug { 198 fmt.Fprintf(os.Stderr, "=+= GOCOVERDIR is %s\n", os.Getenv("GOCOVERDIR")) 199 fmt.Fprintf(os.Stderr, "=+= contents of covmetalist:\n") 200 for k, b := range ml { 201 fmt.Fprintf(os.Stderr, "=+= slot: %d path: %s ", k, b.PkgPath) 202 if b.PkgID != -1 { 203 fmt.Fprintf(os.Stderr, " hcid: %d", b.PkgID) 204 } 205 fmt.Fprintf(os.Stderr, "\n") 206 } 207 pm := getCovPkgMap() 208 fmt.Fprintf(os.Stderr, "=+= remap table:\n") 209 for from, to := range pm { 210 fmt.Fprintf(os.Stderr, "=+= from %d to %d\n", 211 uint32(from), uint32(to)) 212 } 213 } 214 215 h := md5.New() 216 tlen := uint64(unsafe.Sizeof(coverage.MetaFileHeader{})) 217 for _, entry := range ml { 218 if _, err := h.Write(entry.Hash[:]); err != nil { 219 return nil, err 220 } 221 tlen += uint64(entry.Len) 222 ecm := coverage.CounterMode(entry.CounterMode) 223 if modeClash(ecm) { 224 return nil, fmt.Errorf("coverage counter mode clash: package %s uses mode=%d, but package %s uses mode=%s\n", ml[0].PkgPath, cmode, entry.PkgPath, ecm) 225 } 226 ecg := coverage.CounterGranularity(entry.CounterGranularity) 227 if granClash(ecg) { 228 return nil, fmt.Errorf("coverage counter granularity clash: package %s uses gran=%d, but package %s uses gran=%s\n", ml[0].PkgPath, cgran, entry.PkgPath, ecg) 229 } 230 } 231 232 // Hash mode and granularity as well. 233 h.Write([]byte(cmode.String())) 234 h.Write([]byte(cgran.String())) 235 236 // Compute final digest. 237 fh := h.Sum(nil) 238 copy(finalHash[:], fh) 239 finalHashComputed = true 240 finalMetaLen = tlen 241 242 return ml, nil 243 } 244 245 // emitMetaDataToDirectory emits the meta-data output file to the specified 246 // directory, returning an error if something went wrong. 247 func emitMetaDataToDirectory(outdir string, ml []rtcov.CovMetaBlob) error { 248 ml, err := prepareForMetaEmit() 249 if err != nil { 250 return err 251 } 252 if len(ml) == 0 { 253 return nil 254 } 255 256 metaDataEmitAttempted = true 257 258 s := &emitState{ 259 metalist: ml, 260 debug: os.Getenv("GOCOVERDEBUG") != "", 261 outdir: outdir, 262 } 263 264 // Open output files. 265 if err := s.openOutputFiles(finalHash, finalMetaLen, metaDataFile); err != nil { 266 return err 267 } 268 269 // Emit meta-data file only if needed (may already be present). 270 if s.needMetaDataFile() { 271 if err := s.emitMetaDataFile(finalHash, finalMetaLen); err != nil { 272 return err 273 } 274 } 275 return nil 276 } 277 278 // emitCounterData emits the counter data output file for this coverage run. 279 // This entry point is intended to be invoked by the runtime when an 280 // instrumented program is terminating or calling os.Exit(). 281 func emitCounterData() { 282 if goCoverDir == "" || !finalHashComputed || covProfileAlreadyEmitted { 283 return 284 } 285 if err := emitCounterDataToDirectory(goCoverDir); err != nil { 286 fmt.Fprintf(os.Stderr, "error: coverage counter data emit failed: %v\n", err) 287 if os.Getenv("GOCOVERDEBUG") != "" { 288 panic("counter-data write failure") 289 } 290 } 291 } 292 293 // emitCounterDataToDirectory emits the counter-data output file for this coverage run. 294 func emitCounterDataToDirectory(outdir string) error { 295 // Ask the runtime for the list of coverage counter symbols. 296 cl := getCovCounterList() 297 if len(cl) == 0 { 298 // no work to do here. 299 return nil 300 } 301 302 if !finalHashComputed { 303 return fmt.Errorf("error: meta-data not available (binary not built with -cover?)") 304 } 305 306 // Ask the runtime for the list of coverage counter symbols. 307 pm := getCovPkgMap() 308 s := &emitState{ 309 counterlist: cl, 310 pkgmap: pm, 311 outdir: outdir, 312 debug: os.Getenv("GOCOVERDEBUG") != "", 313 } 314 315 // Open output file. 316 if err := s.openOutputFiles(finalHash, finalMetaLen, counterDataFile); err != nil { 317 return err 318 } 319 if s.cf == nil { 320 return fmt.Errorf("counter data output file open failed (no additional info") 321 } 322 323 // Emit counter data file. 324 if err := s.emitCounterDataFile(finalHash, s.cf); err != nil { 325 return err 326 } 327 if err := s.cf.Close(); err != nil { 328 return fmt.Errorf("closing counter data file: %v", err) 329 } 330 331 // Counter file has now been closed. Rename the temp to the 332 // final desired path. 333 if err := os.Rename(s.cftmp, s.cfname); err != nil { 334 return fmt.Errorf("writing %s: rename from %s failed: %v\n", s.cfname, s.cftmp, err) 335 } 336 337 return nil 338 } 339 340 // emitCounterDataToWriter emits counter data for this coverage run to an io.Writer. 341 func (s *emitState) emitCounterDataToWriter(w io.Writer) error { 342 if err := s.emitCounterDataFile(finalHash, w); err != nil { 343 return err 344 } 345 return nil 346 } 347 348 // openMetaFile determines whether we need to emit a meta-data output 349 // file, or whether we can reuse the existing file in the coverage out 350 // dir. It updates mfname/mftmp/mf fields in 's', returning an error 351 // if something went wrong. See the comment on the emitState type 352 // definition above for more on how file opening is managed. 353 func (s *emitState) openMetaFile(metaHash [16]byte, metaLen uint64) error { 354 355 // Open meta-outfile for reading to see if it exists. 356 fn := fmt.Sprintf("%s.%x", coverage.MetaFilePref, metaHash) 357 s.mfname = filepath.Join(s.outdir, fn) 358 fi, err := os.Stat(s.mfname) 359 if err != nil || fi.Size() != int64(metaLen) { 360 // We need a new meta-file. 361 tname := "tmp." + fn + strconv.FormatInt(time.Now().UnixNano(), 10) 362 s.mftmp = filepath.Join(s.outdir, tname) 363 s.mf, err = os.Create(s.mftmp) 364 if err != nil { 365 return fmt.Errorf("creating meta-data file %s: %v", s.mftmp, err) 366 } 367 } 368 return nil 369 } 370 371 // openCounterFile opens an output file for the counter data portion 372 // of a test coverage run. If updates the 'cfname' and 'cf' fields in 373 // 's', returning an error if something went wrong. 374 func (s *emitState) openCounterFile(metaHash [16]byte) error { 375 processID := os.Getpid() 376 fn := fmt.Sprintf(coverage.CounterFileTempl, coverage.CounterFilePref, metaHash, processID, time.Now().UnixNano()) 377 s.cfname = filepath.Join(s.outdir, fn) 378 s.cftmp = filepath.Join(s.outdir, "tmp."+fn) 379 var err error 380 s.cf, err = os.Create(s.cftmp) 381 if err != nil { 382 return fmt.Errorf("creating counter data file %s: %v", s.cftmp, err) 383 } 384 return nil 385 } 386 387 // openOutputFiles opens output files in preparation for emitting 388 // coverage data. In the case of the meta-data file, openOutputFiles 389 // may determine that we can reuse an existing meta-data file in the 390 // outdir, in which case it will leave the 'mf' field in the state 391 // struct as nil. If a new meta-file is needed, the field 'mfname' 392 // will be the final desired path of the meta file, 'mftmp' will be a 393 // temporary file, and 'mf' will be an open os.File pointer for 394 // 'mftmp'. The idea is that the client/caller will write content into 395 // 'mf', close it, and then rename 'mftmp' to 'mfname'. This function 396 // also opens the counter data output file, setting 'cf' and 'cfname' 397 // in the state struct. 398 func (s *emitState) openOutputFiles(metaHash [16]byte, metaLen uint64, which fileType) error { 399 fi, err := os.Stat(s.outdir) 400 if err != nil { 401 return fmt.Errorf("output directory %q inaccessible (err: %v); no coverage data written", s.outdir, err) 402 } 403 if !fi.IsDir() { 404 return fmt.Errorf("output directory %q not a directory; no coverage data written", s.outdir) 405 } 406 407 if (which & metaDataFile) != 0 { 408 if err := s.openMetaFile(metaHash, metaLen); err != nil { 409 return err 410 } 411 } 412 if (which & counterDataFile) != 0 { 413 if err := s.openCounterFile(metaHash); err != nil { 414 return err 415 } 416 } 417 return nil 418 } 419 420 // emitMetaDataFile emits coverage meta-data to a previously opened 421 // temporary file (s.mftmp), then renames the generated file to the 422 // final path (s.mfname). 423 func (s *emitState) emitMetaDataFile(finalHash [16]byte, tlen uint64) error { 424 if err := writeMetaData(s.mf, s.metalist, cmode, cgran, finalHash); err != nil { 425 return fmt.Errorf("writing %s: %v\n", s.mftmp, err) 426 } 427 if err := s.mf.Close(); err != nil { 428 return fmt.Errorf("closing meta data temp file: %v", err) 429 } 430 431 // Temp file has now been flushed and closed. Rename the temp to the 432 // final desired path. 433 if err := os.Rename(s.mftmp, s.mfname); err != nil { 434 return fmt.Errorf("writing %s: rename from %s failed: %v\n", s.mfname, s.mftmp, err) 435 } 436 437 return nil 438 } 439 440 // needMetaDataFile returns TRUE if we need to emit a meta-data file 441 // for this program run. It should be used only after 442 // openOutputFiles() has been invoked. 443 func (s *emitState) needMetaDataFile() bool { 444 return s.mf != nil 445 } 446 447 func writeMetaData(w io.Writer, metalist []rtcov.CovMetaBlob, cmode coverage.CounterMode, gran coverage.CounterGranularity, finalHash [16]byte) error { 448 mfw := encodemeta.NewCoverageMetaFileWriter("<io.Writer>", w) 449 450 // Note: "sd" is re-initialized on each iteration of the loop 451 // below, and would normally be declared inside the loop, but 452 // placed here escape analysis since we capture it in bufHdr. 453 var sd []byte 454 bufHdr := (*reflect.SliceHeader)(unsafe.Pointer(&sd)) 455 456 var blobs [][]byte 457 for _, e := range metalist { 458 bufHdr.Data = uintptr(unsafe.Pointer(e.P)) 459 bufHdr.Len = int(e.Len) 460 bufHdr.Cap = int(e.Len) 461 blobs = append(blobs, sd) 462 } 463 return mfw.Write(finalHash, blobs, cmode, gran) 464 } 465 466 func (s *emitState) VisitFuncs(f encodecounter.CounterVisitorFn) error { 467 var sd []atomic.Uint32 468 var tcounters []uint32 469 bufHdr := (*reflect.SliceHeader)(unsafe.Pointer(&sd)) 470 471 rdCounters := func(actrs []atomic.Uint32, ctrs []uint32) []uint32 { 472 ctrs = ctrs[:0] 473 for i := range actrs { 474 ctrs = append(ctrs, actrs[i].Load()) 475 } 476 return ctrs 477 } 478 479 dpkg := uint32(0) 480 for _, c := range s.counterlist { 481 bufHdr.Data = uintptr(unsafe.Pointer(c.Counters)) 482 bufHdr.Len = int(c.Len) 483 bufHdr.Cap = int(c.Len) 484 for i := 0; i < len(sd); i++ { 485 // Skip ahead until the next non-zero value. 486 sdi := sd[i].Load() 487 if sdi == 0 { 488 continue 489 } 490 491 // We found a function that was executed. 492 nCtrs := sd[i+coverage.NumCtrsOffset].Load() 493 pkgId := sd[i+coverage.PkgIdOffset].Load() 494 funcId := sd[i+coverage.FuncIdOffset].Load() 495 cst := i + coverage.FirstCtrOffset 496 counters := sd[cst : cst+int(nCtrs)] 497 498 // Check to make sure that we have at least one live 499 // counter. See the implementation note in ClearCoverageCounters 500 // for a description of why this is needed. 501 isLive := false 502 for i := 0; i < len(counters); i++ { 503 if counters[i].Load() != 0 { 504 isLive = true 505 break 506 } 507 } 508 if !isLive { 509 // Skip this function. 510 i += coverage.FirstCtrOffset + int(nCtrs) - 1 511 continue 512 } 513 514 if s.debug { 515 if pkgId != dpkg { 516 dpkg = pkgId 517 fmt.Fprintf(os.Stderr, "\n=+= %d: pk=%d visit live fcn", 518 i, pkgId) 519 } 520 fmt.Fprintf(os.Stderr, " {i=%d F%d NC%d}", i, funcId, nCtrs) 521 } 522 523 // Vet and/or fix up package ID. A package ID of zero 524 // indicates that there is some new package X that is a 525 // runtime dependency, and this package has code that 526 // executes before its corresponding init package runs. 527 // This is a fatal error that we should only see during 528 // Go development (e.g. tip). 529 ipk := int32(pkgId) 530 if ipk == 0 { 531 fmt.Fprintf(os.Stderr, "\n") 532 reportErrorInHardcodedList(int32(i), ipk, funcId, nCtrs) 533 } else if ipk < 0 { 534 if newId, ok := s.pkgmap[int(ipk)]; ok { 535 pkgId = uint32(newId) 536 } else { 537 fmt.Fprintf(os.Stderr, "\n") 538 reportErrorInHardcodedList(int32(i), ipk, funcId, nCtrs) 539 } 540 } else { 541 // The package ID value stored in the counter array 542 // has 1 added to it (so as to preclude the 543 // possibility of a zero value ; see 544 // runtime.addCovMeta), so subtract off 1 here to form 545 // the real package ID. 546 pkgId-- 547 } 548 549 tcounters = rdCounters(counters, tcounters) 550 if err := f(pkgId, funcId, tcounters); err != nil { 551 return err 552 } 553 554 // Skip over this function. 555 i += coverage.FirstCtrOffset + int(nCtrs) - 1 556 } 557 if s.debug { 558 fmt.Fprintf(os.Stderr, "\n") 559 } 560 } 561 return nil 562 } 563 564 // captureOsArgs converts os.Args() into the format we use to store 565 // this info in the counter data file (counter data file "args" 566 // section is a generic key-value collection). See the 'args' section 567 // in internal/coverage/defs.go for more info. The args map 568 // is also used to capture GOOS + GOARCH values as well. 569 func captureOsArgs() map[string]string { 570 m := make(map[string]string) 571 m["argc"] = strconv.Itoa(len(os.Args)) 572 for k, a := range os.Args { 573 m[fmt.Sprintf("argv%d", k)] = a 574 } 575 m["GOOS"] = runtime.GOOS 576 m["GOARCH"] = runtime.GOARCH 577 return m 578 } 579 580 // emitCounterDataFile emits the counter data portion of a 581 // coverage output file (to the file 's.cf'). 582 func (s *emitState) emitCounterDataFile(finalHash [16]byte, w io.Writer) error { 583 cfw := encodecounter.NewCoverageDataWriter(w, coverage.CtrULeb128) 584 if err := cfw.Write(finalHash, capturedOsArgs, s); err != nil { 585 return err 586 } 587 return nil 588 } 589 590 // markProfileEmitted signals the runtime/coverage machinery that 591 // coverate data output files have already been written out, and there 592 // is no need to take any additional action at exit time. This 593 // function is called (via linknamed reference) from the 594 // coverage-related boilerplate code in _testmain.go emitted for go 595 // unit tests. 596 func markProfileEmitted(val bool) { 597 covProfileAlreadyEmitted = val 598 } 599 600 func reportErrorInHardcodedList(slot, pkgID int32, fnID, nCtrs uint32) { 601 metaList := getCovMetaList() 602 pkgMap := getCovPkgMap() 603 604 println("internal error in coverage meta-data tracking:") 605 println("encountered bad pkgID:", pkgID, " at slot:", slot, 606 " fnID:", fnID, " numCtrs:", nCtrs) 607 println("list of hard-coded runtime package IDs needs revising.") 608 println("[see the comment on the 'rtPkgs' var in ") 609 println(" <goroot>/src/internal/coverage/pkid.go]") 610 println("registered list:") 611 for k, b := range metaList { 612 print("slot: ", k, " path='", b.PkgPath, "' ") 613 if b.PkgID != -1 { 614 print(" hard-coded id: ", b.PkgID) 615 } 616 println("") 617 } 618 println("remap table:") 619 for from, to := range pkgMap { 620 println("from ", from, " to ", to) 621 } 622 }