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