cover.mx raw

   1  package runtime
   2  
   3  import "unsafe"
   4  
   5  // Per-block test coverage for -cover.
   6  //
   7  // The compiler emits a coverHit(siteID) call at the start of every basic block
   8  // and writes a side file mapping each id to "pkg.Func@file:line". An
   9  // instrumented build therefore reports exactly which blocks ran; coverBeforeExit
  10  // dumps the counters when the program ends - to the file named by
  11  // MOXIE_COVER_OUT when that is set and can be opened, stderr otherwise.
  12  //
  13  // The table and the dump hook are always linked (the hook is a nil check, the
  14  // table is allocated lazily on the first coverHit), so a program built without
  15  // -cover carries no cost and a coverage build needs no second runtime bake. The
  16  // -cover flag only decides whether the compiler emits hits and writes the site
  17  // file; the cover build tag is set too, for programs that want to gate their
  18  // own code on being instrumented.
  19  
  20  // maxCoverSites bounds the table. One site is numbered per basic block of the
  21  // whole link set, so this only has to exceed the block count of one build.
  22  const maxCoverSites = int32(1) << 18
  23  
  24  // The table is a slice, not an array: Moxie has no arrays. It is allocated from
  25  // the ROOT arena on the first coverHit call rather than in init(), so a build
  26  // that records nothing never pays for it, and the root arena outlives every
  27  // function frame (a frame arena is released at return).
  28  var coverCounters []uint64
  29  var numCoverSites int32
  30  
  31  func coverTables() {
  32  	if coverCounters != nil {
  33  		return
  34  	}
  35  	a := RootArena()
  36  	if a == nil {
  37  		return
  38  	}
  39  	coverCounters = rawSliceU64(uintptr(ArenaAlloc(a, uintptr(maxCoverSites)*8)), maxCoverSites)
  40  }
  41  
  42  //:noinline
  43  func coverHit(siteID int32) {
  44  	if siteID < 0 || siteID >= maxCoverSites {
  45  		return
  46  	}
  47  	coverTables()
  48  	if coverCounters == nil {
  49  		return
  50  	}
  51  	coverCounters[siteID]++
  52  	if siteID >= numCoverSites {
  53  		numCoverSites = siteID + 1
  54  	}
  55  }
  56  
  57  // PrintCoverCounters writes one line per site that ran: the site id and its
  58  // call count. The compiler's <output>.cover_sites.json maps the id to
  59  // "pkg.Func@file:line".
  60  func PrintCoverCounters() {
  61  	coverDump(2)
  62  }
  63  
  64  // coverPutStr appends s to buf at n and returns the new length.
  65  func coverPutStr(buf []byte, n int32, s string) int32 {
  66  	for i := int32(0); i < int32(len(s)); i++ {
  67  		buf[n] = s[i]
  68  		n++
  69  	}
  70  	return n
  71  }
  72  
  73  // coverPutU64 appends v in decimal to buf at n and returns the new length.
  74  func coverPutU64(buf []byte, n int32, v uint64) int32 {
  75  	if v == 0 {
  76  		buf[n] = '0'
  77  		return n + 1
  78  	}
  79  	d := int32(0)
  80  	for t := v; t > 0; t = t / 10 {
  81  		d++
  82  	}
  83  	end := n + d
  84  	for i := end - 1; i >= n; i-- {
  85  		buf[i] = byte(v%10) + '0'
  86  		v = v / 10
  87  	}
  88  	return end
  89  }
  90  
  91  // coverDump writes one "coversite <id> count=<n>\n" line per site that ran.
  92  // Each line is assembled in one buffer and written with a single write(2):
  93  // every domain of a spawn tree appends to the same file, and O_APPEND only
  94  // makes a write atomic as a whole. Emitting the line in pieces let domains
  95  // exiting at the same time interleave their writes inside one line, which the
  96  // reader then could not parse - the site vanished from the report.
  97  func coverDump(fd int32) {
  98  	buf := []byte{:64}
  99  	for i := int32(0); i < numCoverSites; i++ {
 100  		c := coverCounters[i]
 101  		if c == 0 {
 102  			continue
 103  		}
 104  		n := coverPutStr(buf, 0, "coversite ")
 105  		n = coverPutU64(buf, n, uint64(i))
 106  		n = coverPutStr(buf, n, " count=")
 107  		n = coverPutU64(buf, n, c)
 108  		buf[n] = '\n'
 109  		n++
 110  		MoxieWrite(fd, unsafe.Pointer(&buf[0]), n)
 111  	}
 112  }
 113  
 114  // coverBeforeExit dumps the counters when a domain ends. The runtime's exit
 115  // paths call it unconditionally: there is no cover_off twin to select, because
 116  // in a build the compiler did not instrument the counters are nil and the site
 117  // count is zero, which is the hook's entire cost there. Every domain appends to
 118  // the same file, because a fork's counters are a private copy of the table made
 119  // at fork: a spawn child has to write its own numbers rather than hand them
 120  // back, and appending is what makes the dumps of every domain and every process
 121  // of a run add up instead of overwrite.
 122  func coverBeforeExit() {
 123  	if coverCounters == nil || numCoverSites == 0 {
 124  		return
 125  	}
 126  	fd := coverOutFD()
 127  	coverDump(fd)
 128  	if fd != 2 {
 129  		moxieCoverClose(fd)
 130  	}
 131  }
 132  
 133  // coverOutFD (defined in cover_fd_native.mx, stubbed in cover_fd_stub.mx)
 134  // returns the descriptor to dump to: the file named by MOXIE_COVER_OUT when it
 135  // is set and can be opened, opened for appending, or stderr.
 136  
 137  //export moxie_cover_close
 138  func moxieCoverClose(fd int32) (r int32)
 139  
 140  // --- exported readers, used by a wasm host ---
 141  
 142  //export __moxie_cover_counters_ptr
 143  func coverCountersPtr() (p uintptr) {
 144  	if coverCounters == nil {
 145  		return 0
 146  	}
 147  	return uintptr(unsafe.Pointer(&coverCounters[0]))
 148  }
 149  
 150  //export __moxie_cover_n_sites
 151  func coverNSites() (n int32) {
 152  	return numCoverSites
 153  }
 154