summaryrefslogtreecommitdiffstats
path: root/src/runtime/coverage/emit.go
blob: bb0c6fb6a2fba7c580cf0a671a19a79ff07e3cb6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package coverage

import (
	"crypto/md5"
	"fmt"
	"internal/coverage"
	"internal/coverage/encodecounter"
	"internal/coverage/encodemeta"
	"internal/coverage/rtcov"
	"io"
	"os"
	"path/filepath"
	"reflect"
	"runtime"
	"strconv"
	"sync/atomic"
	"time"
	"unsafe"
)

// This file contains functions that support the writing of data files
// emitted at the end of code coverage testing runs, from instrumented
// executables.

// getCovMetaList returns a list of meta-data blobs registered
// for the currently executing instrumented program. It is defined in the
// runtime.
func getCovMetaList() []rtcov.CovMetaBlob

// getCovCounterList returns a list of counter-data blobs registered
// for the currently executing instrumented program. It is defined in the
// runtime.
func getCovCounterList() []rtcov.CovCounterBlob

// getCovPkgMap returns a map storing the remapped package IDs for
// hard-coded runtime packages (see internal/coverage/pkgid.go for
// more on why hard-coded package IDs are needed). This function
// is defined in the runtime.
func getCovPkgMap() map[int]int

// emitState holds useful state information during the emit process.
//
// When an instrumented program finishes execution and starts the
// process of writing out coverage data, it's possible that an
// existing meta-data file already exists in the output directory. In
// this case openOutputFiles() below will leave the 'mf' field below
// as nil. If a new meta-data file is needed, field 'mfname' will be
// the final desired path of the meta file, 'mftmp' will be a
// temporary file, and 'mf' will be an open os.File pointer for
// 'mftmp'. The meta-data file payload will be written to 'mf', the
// temp file will be then closed and renamed (from 'mftmp' to
// 'mfname'), so as to insure that the meta-data file is created
// atomically; we want this so that things work smoothly in cases
// where there are several instances of a given instrumented program
// all terminating at the same time and trying to create meta-data
// files simultaneously.
//
// For counter data files there is less chance of a collision, hence
// the openOutputFiles() stores the counter data file in 'cfname' and
// then places the *io.File into 'cf'.
type emitState struct {
	mfname string   // path of final meta-data output file
	mftmp  string   // path to meta-data temp file (if needed)
	mf     *os.File // open os.File for meta-data temp file
	cfname string   // path of final counter data file
	cftmp  string   // path to counter data temp file
	cf     *os.File // open os.File for counter data file
	outdir string   // output directory

	// List of meta-data symbols obtained from the runtime
	metalist []rtcov.CovMetaBlob

	// List of counter-data symbols obtained from the runtime
	counterlist []rtcov.CovCounterBlob

	// Table to use for remapping hard-coded pkg ids.
	pkgmap map[int]int

	// emit debug trace output
	debug bool
}

var (
	// finalHash is computed at init time from the list of meta-data
	// symbols registered during init. It is used both for writing the
	// meta-data file and counter-data files.
	finalHash [16]byte
	// Set to true when we've computed finalHash + finalMetaLen.
	finalHashComputed bool
	// Total meta-data length.
	finalMetaLen uint64
	// Records whether we've already attempted to write meta-data.
	metaDataEmitAttempted bool
	// Counter mode for this instrumented program run.
	cmode coverage.CounterMode
	// Counter granularity for this instrumented program run.
	cgran coverage.CounterGranularity
	// Cached value of GOCOVERDIR environment variable.
	goCoverDir string
	// Copy of os.Args made at init time, converted into map format.
	capturedOsArgs map[string]string
	// Flag used in tests to signal that coverage data already written.
	covProfileAlreadyEmitted bool
)

// fileType is used to select between counter-data files and
// meta-data files.
type fileType int

const (
	noFile = 1 << iota
	metaDataFile
	counterDataFile
)

// emitMetaData emits the meta-data output file for this coverage run.
// This entry point is intended to be invoked by the compiler from
// an instrumented program's main package init func.
func emitMetaData() {
	if covProfileAlreadyEmitted {
		return
	}
	ml, err := prepareForMetaEmit()
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: coverage meta-data prep failed: %v\n", err)
		if os.Getenv("GOCOVERDEBUG") != "" {
			panic("meta-data write failure")
		}
	}
	if len(ml) == 0 {
		fmt.Fprintf(os.Stderr, "program not built with -cover\n")
		return
	}

	goCoverDir = os.Getenv("GOCOVERDIR")
	if goCoverDir == "" {
		fmt.Fprintf(os.Stderr, "warning: GOCOVERDIR not set, no coverage data emitted\n")
		return
	}

	if err := emitMetaDataToDirectory(goCoverDir, ml); err != nil {
		fmt.Fprintf(os.Stderr, "error: coverage meta-data emit failed: %v\n", err)
		if os.Getenv("GOCOVERDEBUG") != "" {
			panic("meta-data write failure")
		}
	}
}

func modeClash(m coverage.CounterMode) bool {
	if m == coverage.CtrModeRegOnly || m == coverage.CtrModeTestMain {
		return false
	}
	if cmode == coverage.CtrModeInvalid {
		cmode = m
		return false
	}
	return cmode != m
}

func granClash(g coverage.CounterGranularity) bool {
	if cgran == coverage.CtrGranularityInvalid {
		cgran = g
		return false
	}
	return cgran != g
}

// prepareForMetaEmit performs preparatory steps needed prior to
// emitting a meta-data file, notably computing a final hash of
// all meta-data blobs and capturing os args.
func prepareForMetaEmit() ([]rtcov.CovMetaBlob, error) {
	// Ask the runtime for the list of coverage meta-data symbols.
	ml := getCovMetaList()

	// In the normal case (go build -o prog.exe ... ; ./prog.exe)
	// len(ml) will always be non-zero, but we check here since at
	// some point this function will be reachable via user-callable
	// APIs (for example, to write out coverage data from a server
	// program that doesn't ever call os.Exit).
	if len(ml) == 0 {
		return nil, nil
	}

	s := &emitState{
		metalist: ml,
		debug:    os.Getenv("GOCOVERDEBUG") != "",
	}

	// Capture os.Args() now so as to avoid issues if args
	// are rewritten during program execution.
	capturedOsArgs = captureOsArgs()

	if s.debug {
		fmt.Fprintf(os.Stderr, "=+= GOCOVERDIR is %s\n", os.Getenv("GOCOVERDIR"))
		fmt.Fprintf(os.Stderr, "=+= contents of covmetalist:\n")
		for k, b := range ml {
			fmt.Fprintf(os.Stderr, "=+= slot: %d path: %s ", k, b.PkgPath)
			if b.PkgID != -1 {
				fmt.Fprintf(os.Stderr, " hcid: %d", b.PkgID)
			}
			fmt.Fprintf(os.Stderr, "\n")
		}
		pm := getCovPkgMap()
		fmt.Fprintf(os.Stderr, "=+= remap table:\n")
		for from, to := range pm {
			fmt.Fprintf(os.Stderr, "=+= from %d to %d\n",
				uint32(from), uint32(to))
		}
	}

	h := md5.New()
	tlen := uint64(unsafe.Sizeof(coverage.MetaFileHeader{}))
	for _, entry := range ml {
		if _, err := h.Write(entry.Hash[:]); err != nil {
			return nil, err
		}
		tlen += uint64(entry.Len)
		ecm := coverage.CounterMode(entry.CounterMode)
		if modeClash(ecm) {
			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)
		}
		ecg := coverage.CounterGranularity(entry.CounterGranularity)
		if granClash(ecg) {
			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)
		}
	}

	// Hash mode and granularity as well.
	h.Write([]byte(cmode.String()))
	h.Write([]byte(cgran.String()))

	// Compute final digest.
	fh := h.Sum(nil)
	copy(finalHash[:], fh)
	finalHashComputed = true
	finalMetaLen = tlen

	return ml, nil
}

// emitMetaDataToDirectory emits the meta-data output file to the specified
// directory, returning an error if something went wrong.
func emitMetaDataToDirectory(outdir string, ml []rtcov.CovMetaBlob) error {
	ml, err := prepareForMetaEmit()
	if err != nil {
		return err
	}
	if len(ml) == 0 {
		return nil
	}

	metaDataEmitAttempted = true

	s := &emitState{
		metalist: ml,
		debug:    os.Getenv("GOCOVERDEBUG") != "",
		outdir:   outdir,
	}

	// Open output files.
	if err := s.openOutputFiles(finalHash, finalMetaLen, metaDataFile); err != nil {
		return err
	}

	// Emit meta-data file only if needed (may already be present).
	if s.needMetaDataFile() {
		if err := s.emitMetaDataFile(finalHash, finalMetaLen); err != nil {
			return err
		}
	}
	return nil
}

// emitCounterData emits the counter data output file for this coverage run.
// This entry point is intended to be invoked by the runtime when an
// instrumented program is terminating or calling os.Exit().
func emitCounterData() {
	if goCoverDir == "" || !finalHashComputed || covProfileAlreadyEmitted {
		return
	}
	if err := emitCounterDataToDirectory(goCoverDir); err != nil {
		fmt.Fprintf(os.Stderr, "error: coverage counter data emit failed: %v\n", err)
		if os.Getenv("GOCOVERDEBUG") != "" {
			panic("counter-data write failure")
		}
	}
}

// emitCounterDataToDirectory emits the counter-data output file for this coverage run.
func emitCounterDataToDirectory(outdir string) error {
	// Ask the runtime for the list of coverage counter symbols.
	cl := getCovCounterList()
	if len(cl) == 0 {
		// no work to do here.
		return nil
	}

	if !finalHashComputed {
		return fmt.Errorf("error: meta-data not available (binary not built with -cover?)")
	}

	// Ask the runtime for the list of coverage counter symbols.
	pm := getCovPkgMap()
	s := &emitState{
		counterlist: cl,
		pkgmap:      pm,
		outdir:      outdir,
		debug:       os.Getenv("GOCOVERDEBUG") != "",
	}

	// Open output file.
	if err := s.openOutputFiles(finalHash, finalMetaLen, counterDataFile); err != nil {
		return err
	}
	if s.cf == nil {
		return fmt.Errorf("counter data output file open failed (no additional info")
	}

	// Emit counter data file.
	if err := s.emitCounterDataFile(finalHash, s.cf); err != nil {
		return err
	}
	if err := s.cf.Close(); err != nil {
		return fmt.Errorf("closing counter data file: %v", err)
	}

	// Counter file has now been closed. Rename the temp to the
	// final desired path.
	if err := os.Rename(s.cftmp, s.cfname); err != nil {
		return fmt.Errorf("writing %s: rename from %s failed: %v\n", s.cfname, s.cftmp, err)
	}

	return nil
}

// emitCounterDataToWriter emits counter data for this coverage run to an io.Writer.
func (s *emitState) emitCounterDataToWriter(w io.Writer) error {
	if err := s.emitCounterDataFile(finalHash, w); err != nil {
		return err
	}
	return nil
}

// openMetaFile determines whether we need to emit a meta-data output
// file, or whether we can reuse the existing file in the coverage out
// dir. It updates mfname/mftmp/mf fields in 's', returning an error
// if something went wrong. See the comment on the emitState type
// definition above for more on how file opening is managed.
func (s *emitState) openMetaFile(metaHash [16]byte, metaLen uint64) error {

	// Open meta-outfile for reading to see if it exists.
	fn := fmt.Sprintf("%s.%x", coverage.MetaFilePref, metaHash)
	s.mfname = filepath.Join(s.outdir, fn)
	fi, err := os.Stat(s.mfname)
	if err != nil || fi.Size() != int64(metaLen) {
		// We need a new meta-file.
		tname := "tmp." + fn + strconv.FormatInt(time.Now().UnixNano(), 10)
		s.mftmp = filepath.Join(s.outdir, tname)
		s.mf, err = os.Create(s.mftmp)
		if err != nil {
			return fmt.Errorf("creating meta-data file %s: %v", s.mftmp, err)
		}
	}
	return nil
}

// openCounterFile opens an output file for the counter data portion
// of a test coverage run. If updates the 'cfname' and 'cf' fields in
// 's', returning an error if something went wrong.
func (s *emitState) openCounterFile(metaHash [16]byte) error {
	processID := os.Getpid()
	fn := fmt.Sprintf(coverage.CounterFileTempl, coverage.CounterFilePref, metaHash, processID, time.Now().UnixNano())
	s.cfname = filepath.Join(s.outdir, fn)
	s.cftmp = filepath.Join(s.outdir, "tmp."+fn)
	var err error
	s.cf, err = os.Create(s.cftmp)
	if err != nil {
		return fmt.Errorf("creating counter data file %s: %v", s.cftmp, err)
	}
	return nil
}

// openOutputFiles opens output files in preparation for emitting
// coverage data. In the case of the meta-data file, openOutputFiles
// may determine that we can reuse an existing meta-data file in the
// outdir, in which case it will leave the 'mf' field in the state
// struct as nil. If a new meta-file is needed, the field 'mfname'
// will be the final desired path of the meta file, 'mftmp' will be a
// temporary file, and 'mf' will be an open os.File pointer for
// 'mftmp'. The idea is that the client/caller will write content into
// 'mf', close it, and then rename 'mftmp' to 'mfname'. This function
// also opens the counter data output file, setting 'cf' and 'cfname'
// in the state struct.
func (s *emitState) openOutputFiles(metaHash [16]byte, metaLen uint64, which fileType) error {
	fi, err := os.Stat(s.outdir)
	if err != nil {
		return fmt.Errorf("output directory %q inaccessible (err: %v); no coverage data written", s.outdir, err)
	}
	if !fi.IsDir() {
		return fmt.Errorf("output directory %q not a directory; no coverage data written", s.outdir)
	}

	if (which & metaDataFile) != 0 {
		if err := s.openMetaFile(metaHash, metaLen); err != nil {
			return err
		}
	}
	if (which & counterDataFile) != 0 {
		if err := s.openCounterFile(metaHash); err != nil {
			return err
		}
	}
	return nil
}

// emitMetaDataFile emits coverage meta-data to a previously opened
// temporary file (s.mftmp), then renames the generated file to the
// final path (s.mfname).
func (s *emitState) emitMetaDataFile(finalHash [16]byte, tlen uint64) error {
	if err := writeMetaData(s.mf, s.metalist, cmode, cgran, finalHash); err != nil {
		return fmt.Errorf("writing %s: %v\n", s.mftmp, err)
	}
	if err := s.mf.Close(); err != nil {
		return fmt.Errorf("closing meta data temp file: %v", err)
	}

	// Temp file has now been flushed and closed. Rename the temp to the
	// final desired path.
	if err := os.Rename(s.mftmp, s.mfname); err != nil {
		return fmt.Errorf("writing %s: rename from %s failed: %v\n", s.mfname, s.mftmp, err)
	}

	return nil
}

// needMetaDataFile returns TRUE if we need to emit a meta-data file
// for this program run. It should be used only after
// openOutputFiles() has been invoked.
func (s *emitState) needMetaDataFile() bool {
	return s.mf != nil
}

func writeMetaData(w io.Writer, metalist []rtcov.CovMetaBlob, cmode coverage.CounterMode, gran coverage.CounterGranularity, finalHash [16]byte) error {
	mfw := encodemeta.NewCoverageMetaFileWriter("<io.Writer>", w)

	// Note: "sd" is re-initialized on each iteration of the loop
	// below, and would normally be declared inside the loop, but
	// placed here escape analysis since we capture it in bufHdr.
	var sd []byte
	bufHdr := (*reflect.SliceHeader)(unsafe.Pointer(&sd))

	var blobs [][]byte
	for _, e := range metalist {
		bufHdr.Data = uintptr(unsafe.Pointer(e.P))
		bufHdr.Len = int(e.Len)
		bufHdr.Cap = int(e.Len)
		blobs = append(blobs, sd)
	}
	return mfw.Write(finalHash, blobs, cmode, gran)
}

func (s *emitState) VisitFuncs(f encodecounter.CounterVisitorFn) error {
	var sd []atomic.Uint32
	var tcounters []uint32
	bufHdr := (*reflect.SliceHeader)(unsafe.Pointer(&sd))

	rdCounters := func(actrs []atomic.Uint32, ctrs []uint32) []uint32 {
		ctrs = ctrs[:0]
		for i := range actrs {
			ctrs = append(ctrs, actrs[i].Load())
		}
		return ctrs
	}

	dpkg := uint32(0)
	for _, c := range s.counterlist {
		bufHdr.Data = uintptr(unsafe.Pointer(c.Counters))
		bufHdr.Len = int(c.Len)
		bufHdr.Cap = int(c.Len)
		for i := 0; i < len(sd); i++ {
			// Skip ahead until the next non-zero value.
			sdi := sd[i].Load()
			if sdi == 0 {
				continue
			}

			// We found a function that was executed.
			nCtrs := sd[i+coverage.NumCtrsOffset].Load()
			pkgId := sd[i+coverage.PkgIdOffset].Load()
			funcId := sd[i+coverage.FuncIdOffset].Load()
			cst := i + coverage.FirstCtrOffset
			counters := sd[cst : cst+int(nCtrs)]

			// Check to make sure that we have at least one live
			// counter. See the implementation note in ClearCoverageCounters
			// for a description of why this is needed.
			isLive := false
			for i := 0; i < len(counters); i++ {
				if counters[i].Load() != 0 {
					isLive = true
					break
				}
			}
			if !isLive {
				// Skip this function.
				i += coverage.FirstCtrOffset + int(nCtrs) - 1
				continue
			}

			if s.debug {
				if pkgId != dpkg {
					dpkg = pkgId
					fmt.Fprintf(os.Stderr, "\n=+= %d: pk=%d visit live fcn",
						i, pkgId)
				}
				fmt.Fprintf(os.Stderr, " {i=%d F%d NC%d}", i, funcId, nCtrs)
			}

			// Vet and/or fix up package ID. A package ID of zero
			// indicates that there is some new package X that is a
			// runtime dependency, and this package has code that
			// executes before its corresponding init package runs.
			// This is a fatal error that we should only see during
			// Go development (e.g. tip).
			ipk := int32(pkgId)
			if ipk == 0 {
				fmt.Fprintf(os.Stderr, "\n")
				reportErrorInHardcodedList(int32(i), ipk, funcId, nCtrs)
			} else if ipk < 0 {
				if newId, ok := s.pkgmap[int(ipk)]; ok {
					pkgId = uint32(newId)
				} else {
					fmt.Fprintf(os.Stderr, "\n")
					reportErrorInHardcodedList(int32(i), ipk, funcId, nCtrs)
				}
			} else {
				// The package ID value stored in the counter array
				// has 1 added to it (so as to preclude the
				// possibility of a zero value ; see
				// runtime.addCovMeta), so subtract off 1 here to form
				// the real package ID.
				pkgId--
			}

			tcounters = rdCounters(counters, tcounters)
			if err := f(pkgId, funcId, tcounters); err != nil {
				return err
			}

			// Skip over this function.
			i += coverage.FirstCtrOffset + int(nCtrs) - 1
		}
		if s.debug {
			fmt.Fprintf(os.Stderr, "\n")
		}
	}
	return nil
}

// captureOsArgs converts os.Args() into the format we use to store
// this info in the counter data file (counter data file "args"
// section is a generic key-value collection). See the 'args' section
// in internal/coverage/defs.go for more info. The args map
// is also used to capture GOOS + GOARCH values as well.
func captureOsArgs() map[string]string {
	m := make(map[string]string)
	m["argc"] = strconv.Itoa(len(os.Args))
	for k, a := range os.Args {
		m[fmt.Sprintf("argv%d", k)] = a
	}
	m["GOOS"] = runtime.GOOS
	m["GOARCH"] = runtime.GOARCH
	return m
}

// emitCounterDataFile emits the counter data portion of a
// coverage output file (to the file 's.cf').
func (s *emitState) emitCounterDataFile(finalHash [16]byte, w io.Writer) error {
	cfw := encodecounter.NewCoverageDataWriter(w, coverage.CtrULeb128)
	if err := cfw.Write(finalHash, capturedOsArgs, s); err != nil {
		return err
	}
	return nil
}

// markProfileEmitted signals the runtime/coverage machinery that
// coverate data output files have already been written out, and there
// is no need to take any additional action at exit time. This
// function is called (via linknamed reference) from the
// coverage-related boilerplate code in _testmain.go emitted for go
// unit tests.
func markProfileEmitted(val bool) {
	covProfileAlreadyEmitted = val
}

func reportErrorInHardcodedList(slot, pkgID int32, fnID, nCtrs uint32) {
	metaList := getCovMetaList()
	pkgMap := getCovPkgMap()

	println("internal error in coverage meta-data tracking:")
	println("encountered bad pkgID:", pkgID, " at slot:", slot,
		" fnID:", fnID, " numCtrs:", nCtrs)
	println("list of hard-coded runtime package IDs needs revising.")
	println("[see the comment on the 'rtPkgs' var in ")
	println(" <goroot>/src/internal/coverage/pkid.go]")
	println("registered list:")
	for k, b := range metaList {
		print("slot: ", k, " path='", b.PkgPath, "' ")
		if b.PkgID != -1 {
			print(" hard-coded id: ", b.PkgID)
		}
		println("")
	}
	println("remap table:")
	for from, to := range pkgMap {
		println("from ", from, " to ", to)
	}
}