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
5package main
6
7// This file contains functions and apis that support merging of
8// meta-data information.  It helps implement the "merge", "subtract",
9// and "intersect" subcommands.
10
11import (
12	"crypto/md5"
13	"fmt"
14	"internal/coverage"
15	"internal/coverage/calloc"
16	"internal/coverage/cmerge"
17	"internal/coverage/decodecounter"
18	"internal/coverage/decodemeta"
19	"internal/coverage/encodecounter"
20	"internal/coverage/encodemeta"
21	"internal/coverage/slicewriter"
22	"io"
23	"os"
24	"path/filepath"
25	"sort"
26	"time"
27	"unsafe"
28)
29
30// metaMerge provides state and methods to help manage the process
31// of selecting or merging meta data files. There are three cases
32// of interest here: the "-pcombine" flag provided by merge, the
33// "-pkg" option provided by all merge/subtract/intersect, and
34// a regular vanilla merge with no package selection
35//
36// In the -pcombine case, we're essentially glomming together all the
37// meta-data for all packages and all functions, meaning that
38// everything we see in a given package needs to be added into the
39// meta-data file builder; we emit a single meta-data file at the end
40// of the run.
41//
42// In the -pkg case, we will typically emit a single meta-data file
43// per input pod, where that new meta-data file contains entries for
44// just the selected packages.
45//
46// In the third case (vanilla merge with no combining or package
47// selection) we can carry over meta-data files without touching them
48// at all (only counter data files will be merged).
49type metaMerge struct {
50	calloc.BatchCounterAlloc
51	cmerge.Merger
52	// maps package import path to package state
53	pkm map[string]*pkstate
54	// list of packages
55	pkgs []*pkstate
56	// current package state
57	p *pkstate
58	// current pod state
59	pod *podstate
60	// counter data file osargs/goos/goarch state
61	astate *argstate
62}
63
64// pkstate
65type pkstate struct {
66	// index of package within meta-data file.
67	pkgIdx uint32
68	// this maps function index within the package to counter data payload
69	ctab map[uint32]decodecounter.FuncPayload
70	// pointer to meta-data blob for package
71	mdblob []byte
72	// filled in only for -pcombine merges
73	*pcombinestate
74}
75
76type podstate struct {
77	pmm      map[pkfunc]decodecounter.FuncPayload
78	mdf      string
79	mfr      *decodemeta.CoverageMetaFileReader
80	fileHash [16]byte
81}
82
83type pkfunc struct {
84	pk, fcn uint32
85}
86
87// pcombinestate
88type pcombinestate struct {
89	// Meta-data builder for the package.
90	cmdb *encodemeta.CoverageMetaDataBuilder
91	// Maps function meta-data hash to new function index in the
92	// new version of the package we're building.
93	ftab map[[16]byte]uint32
94}
95
96func newMetaMerge() *metaMerge {
97	return &metaMerge{
98		pkm:    make(map[string]*pkstate),
99		astate: &argstate{},
100	}
101}
102
103func (mm *metaMerge) visitMetaDataFile(mdf string, mfr *decodemeta.CoverageMetaFileReader) {
104	dbgtrace(2, "visitMetaDataFile(mdf=%s)", mdf)
105
106	// Record meta-data file name.
107	mm.pod.mdf = mdf
108	// Keep a pointer to the file-level reader.
109	mm.pod.mfr = mfr
110	// Record file hash.
111	mm.pod.fileHash = mfr.FileHash()
112	// Counter mode and granularity -- detect and record clashes here.
113	newgran := mfr.CounterGranularity()
114	newmode := mfr.CounterMode()
115	if err := mm.SetModeAndGranularity(mdf, newmode, newgran); err != nil {
116		fatal("%v", err)
117	}
118}
119
120func (mm *metaMerge) beginCounterDataFile(cdr *decodecounter.CounterDataReader) {
121	state := argvalues{
122		osargs: cdr.OsArgs(),
123		goos:   cdr.Goos(),
124		goarch: cdr.Goarch(),
125	}
126	mm.astate.Merge(state)
127}
128
129func copyMetaDataFile(inpath, outpath string) {
130	inf, err := os.Open(inpath)
131	if err != nil {
132		fatal("opening input meta-data file %s: %v", inpath, err)
133	}
134	defer inf.Close()
135
136	fi, err := inf.Stat()
137	if err != nil {
138		fatal("accessing input meta-data file %s: %v", inpath, err)
139	}
140
141	outf, err := os.OpenFile(outpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode())
142	if err != nil {
143		fatal("opening output meta-data file %s: %v", outpath, err)
144	}
145
146	_, err = io.Copy(outf, inf)
147	outf.Close()
148	if err != nil {
149		fatal("writing output meta-data file %s: %v", outpath, err)
150	}
151}
152
153func (mm *metaMerge) beginPod() {
154	mm.pod = &podstate{
155		pmm: make(map[pkfunc]decodecounter.FuncPayload),
156	}
157}
158
159// metaEndPod handles actions needed when we're done visiting all of
160// the things in a pod -- counter files and meta-data file. There are
161// three cases of interest here:
162//
163// Case 1: in an unconditional merge (we're not selecting a specific set of
164// packages using "-pkg", and the "-pcombine" option is not in use),
165// we can simply copy over the meta-data file from input to output.
166//
167// Case 2: if this is a select merge (-pkg is in effect), then at
168// this point we write out a new smaller meta-data file that includes
169// only the packages of interest. At this point we also emit a merged
170// counter data file as well.
171//
172// Case 3: if "-pcombine" is in effect, we don't write anything at
173// this point (all writes will happen at the end of the run).
174func (mm *metaMerge) endPod(pcombine bool) {
175	if pcombine {
176		// Just clear out the pod data, we'll do all the
177		// heavy lifting at the end.
178		mm.pod = nil
179		return
180	}
181
182	finalHash := mm.pod.fileHash
183	if matchpkg != nil {
184		// Emit modified meta-data file for this pod.
185		finalHash = mm.emitMeta(*outdirflag, pcombine)
186	} else {
187		// Copy meta-data file for this pod to the output directory.
188		inpath := mm.pod.mdf
189		mdfbase := filepath.Base(mm.pod.mdf)
190		outpath := filepath.Join(*outdirflag, mdfbase)
191		copyMetaDataFile(inpath, outpath)
192	}
193
194	// Emit accumulated counter data for this pod.
195	mm.emitCounters(*outdirflag, finalHash)
196
197	// Reset package state.
198	mm.pkm = make(map[string]*pkstate)
199	mm.pkgs = nil
200	mm.pod = nil
201
202	// Reset counter mode and granularity
203	mm.ResetModeAndGranularity()
204}
205
206// emitMeta encodes and writes out a new coverage meta-data file as
207// part of a merge operation, specifically a merge with the
208// "-pcombine" flag.
209func (mm *metaMerge) emitMeta(outdir string, pcombine bool) [16]byte {
210	fh := md5.New()
211	blobs := [][]byte{}
212	tlen := uint64(unsafe.Sizeof(coverage.MetaFileHeader{}))
213	for _, p := range mm.pkgs {
214		var blob []byte
215		if pcombine {
216			mdw := &slicewriter.WriteSeeker{}
217			p.cmdb.Emit(mdw)
218			blob = mdw.BytesWritten()
219		} else {
220			blob = p.mdblob
221		}
222		ph := md5.Sum(blob)
223		blobs = append(blobs, blob)
224		if _, err := fh.Write(ph[:]); err != nil {
225			panic(fmt.Sprintf("internal error: md5 sum failed: %v", err))
226		}
227		tlen += uint64(len(blob))
228	}
229	var finalHash [16]byte
230	fhh := fh.Sum(nil)
231	copy(finalHash[:], fhh)
232
233	// Open meta-file for writing.
234	fn := fmt.Sprintf("%s.%x", coverage.MetaFilePref, finalHash)
235	fpath := filepath.Join(outdir, fn)
236	mf, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
237	if err != nil {
238		fatal("unable to open output meta-data file %s: %v", fpath, err)
239	}
240
241	// Encode and write.
242	mfw := encodemeta.NewCoverageMetaFileWriter(fpath, mf)
243	err = mfw.Write(finalHash, blobs, mm.Mode(), mm.Granularity())
244	if err != nil {
245		fatal("error writing %s: %v\n", fpath, err)
246	}
247	return finalHash
248}
249
250func (mm *metaMerge) emitCounters(outdir string, metaHash [16]byte) {
251	// Open output file. The file naming scheme is intended to mimic
252	// that used when running a coverage-instrumented binary, for
253	// consistency (however the process ID is not meaningful here, so
254	// use a value of zero).
255	var dummyPID int
256	fn := fmt.Sprintf(coverage.CounterFileTempl, coverage.CounterFilePref, metaHash, dummyPID, time.Now().UnixNano())
257	fpath := filepath.Join(outdir, fn)
258	cf, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
259	if err != nil {
260		fatal("opening counter data file %s: %v", fpath, err)
261	}
262	defer func() {
263		if err := cf.Close(); err != nil {
264			fatal("error closing output meta-data file %s: %v", fpath, err)
265		}
266	}()
267
268	args := mm.astate.ArgsSummary()
269	cfw := encodecounter.NewCoverageDataWriter(cf, coverage.CtrULeb128)
270	if err := cfw.Write(metaHash, args, mm); err != nil {
271		fatal("counter file write failed: %v", err)
272	}
273	mm.astate = &argstate{}
274}
275
276// VisitFuncs is used while writing the counter data files; it
277// implements the 'VisitFuncs' method required by the interface
278// internal/coverage/encodecounter/CounterVisitor.
279func (mm *metaMerge) VisitFuncs(f encodecounter.CounterVisitorFn) error {
280	if *verbflag >= 4 {
281		fmt.Printf("counterVisitor invoked\n")
282	}
283	// For each package, for each function, construct counter
284	// array and then call "f" on it.
285	for pidx, p := range mm.pkgs {
286		fids := make([]int, 0, len(p.ctab))
287		for fid := range p.ctab {
288			fids = append(fids, int(fid))
289		}
290		sort.Ints(fids)
291		if *verbflag >= 4 {
292			fmt.Printf("fids for pk=%d: %+v\n", pidx, fids)
293		}
294		for _, fid := range fids {
295			fp := p.ctab[uint32(fid)]
296			if *verbflag >= 4 {
297				fmt.Printf("counter write for pk=%d fid=%d len(ctrs)=%d\n", pidx, fid, len(fp.Counters))
298			}
299			if err := f(uint32(pidx), uint32(fid), fp.Counters); err != nil {
300				return err
301			}
302		}
303	}
304	return nil
305}
306
307func (mm *metaMerge) visitPackage(pd *decodemeta.CoverageMetaDataDecoder, pkgIdx uint32, pcombine bool) {
308	p, ok := mm.pkm[pd.PackagePath()]
309	if !ok {
310		p = &pkstate{
311			pkgIdx: uint32(len(mm.pkgs)),
312		}
313		mm.pkgs = append(mm.pkgs, p)
314		mm.pkm[pd.PackagePath()] = p
315		if pcombine {
316			p.pcombinestate = new(pcombinestate)
317			cmdb, err := encodemeta.NewCoverageMetaDataBuilder(pd.PackagePath(), pd.PackageName(), pd.ModulePath())
318			if err != nil {
319				fatal("fatal error creating meta-data builder: %v", err)
320			}
321			dbgtrace(2, "install new pkm entry for package %s pk=%d", pd.PackagePath(), pkgIdx)
322			p.cmdb = cmdb
323			p.ftab = make(map[[16]byte]uint32)
324		} else {
325			var err error
326			p.mdblob, err = mm.pod.mfr.GetPackagePayload(pkgIdx, nil)
327			if err != nil {
328				fatal("error extracting package %d payload from %s: %v",
329					pkgIdx, mm.pod.mdf, err)
330			}
331		}
332		p.ctab = make(map[uint32]decodecounter.FuncPayload)
333	}
334	mm.p = p
335}
336
337func (mm *metaMerge) visitFuncCounterData(data decodecounter.FuncPayload) {
338	key := pkfunc{pk: data.PkgIdx, fcn: data.FuncIdx}
339	val := mm.pod.pmm[key]
340	// FIXME: in theory either A) len(val.Counters) is zero, or B)
341	// the two lengths are equal. Assert if not? Of course, we could
342	// see odd stuff if there is source file skew.
343	if *verbflag > 4 {
344		fmt.Printf("visit pk=%d fid=%d len(counters)=%d\n", data.PkgIdx, data.FuncIdx, len(data.Counters))
345	}
346	if len(val.Counters) < len(data.Counters) {
347		t := val.Counters
348		val.Counters = mm.AllocateCounters(len(data.Counters))
349		copy(val.Counters, t)
350	}
351	err, overflow := mm.MergeCounters(val.Counters, data.Counters)
352	if err != nil {
353		fatal("%v", err)
354	}
355	if overflow {
356		warn("uint32 overflow during counter merge")
357	}
358	mm.pod.pmm[key] = val
359}
360
361func (mm *metaMerge) visitFunc(pkgIdx uint32, fnIdx uint32, fd *coverage.FuncDesc, verb string, pcombine bool) {
362	if *verbflag >= 3 {
363		fmt.Printf("visit pk=%d fid=%d func %s\n", pkgIdx, fnIdx, fd.Funcname)
364	}
365
366	var counters []uint32
367	key := pkfunc{pk: pkgIdx, fcn: fnIdx}
368	v, haveCounters := mm.pod.pmm[key]
369	if haveCounters {
370		counters = v.Counters
371	}
372
373	if pcombine {
374		// If the merge is running in "combine programs" mode, then hash
375		// the function and look it up in the package ftab to see if we've
376		// encountered it before. If we haven't, then register it with the
377		// meta-data builder.
378		fnhash := encodemeta.HashFuncDesc(fd)
379		gfidx, ok := mm.p.ftab[fnhash]
380		if !ok {
381			// We haven't seen this function before, need to add it to
382			// the meta data.
383			gfidx = uint32(mm.p.cmdb.AddFunc(*fd))
384			mm.p.ftab[fnhash] = gfidx
385			if *verbflag >= 3 {
386				fmt.Printf("new meta entry for fn %s fid=%d\n", fd.Funcname, gfidx)
387			}
388		}
389		fnIdx = gfidx
390	}
391	if !haveCounters {
392		return
393	}
394
395	// Install counters in package ctab.
396	gfp, ok := mm.p.ctab[fnIdx]
397	if ok {
398		if verb == "subtract" || verb == "intersect" {
399			panic("should never see this for intersect/subtract")
400		}
401		if *verbflag >= 3 {
402			fmt.Printf("counter merge for %s fidx=%d\n", fd.Funcname, fnIdx)
403		}
404		// Merge.
405		err, overflow := mm.MergeCounters(gfp.Counters, counters)
406		if err != nil {
407			fatal("%v", err)
408		}
409		if overflow {
410			warn("uint32 overflow during counter merge")
411		}
412		mm.p.ctab[fnIdx] = gfp
413	} else {
414		if *verbflag >= 3 {
415			fmt.Printf("null merge for %s fidx %d\n", fd.Funcname, fnIdx)
416		}
417		gfp := v
418		gfp.PkgIdx = mm.p.pkgIdx
419		gfp.FuncIdx = fnIdx
420		mm.p.ctab[fnIdx] = gfp
421	}
422}
423