| // 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 main |
| |
| // This file contains functions and apis that support merging of |
| // meta-data information. It helps implement the "merge", "subtract", |
| // and "intersect" subcommands. |
| |
| import ( |
| "crypto/md5" |
| "fmt" |
| "internal/coverage" |
| "internal/coverage/calloc" |
| "internal/coverage/cmerge" |
| "internal/coverage/decodecounter" |
| "internal/coverage/decodemeta" |
| "internal/coverage/encodecounter" |
| "internal/coverage/encodemeta" |
| "internal/coverage/slicewriter" |
| "io" |
| "os" |
| "path/filepath" |
| "sort" |
| "time" |
| "unsafe" |
| ) |
| |
| // metaMerge provides state and methods to help manage the process |
| // of selecting or merging meta data files. There are three cases |
| // of interest here: the "-pcombine" flag provided by merge, the |
| // "-pkg" option provided by all merge/subtract/intersect, and |
| // a regular vanilla merge with no package selection |
| // |
| // In the -pcombine case, we're essentially glomming together all the |
| // meta-data for all packages and all functions, meaning that |
| // everything we see in a given package needs to be added into the |
| // meta-data file builder; we emit a single meta-data file at the end |
| // of the run. |
| // |
| // In the -pkg case, we will typically emit a single meta-data file |
| // per input pod, where that new meta-data file contains entries for |
| // just the selected packages. |
| // |
| // In the third case (vanilla merge with no combining or package |
| // selection) we can carry over meta-data files without touching them |
| // at all (only counter data files will be merged). |
| type metaMerge struct { |
| calloc.BatchCounterAlloc |
| cmerge.Merger |
| // maps package import path to package state |
| pkm map[string]*pkstate |
| // list of packages |
| pkgs []*pkstate |
| // current package state |
| p *pkstate |
| // current pod state |
| pod *podstate |
| // counter data file osargs/goos/goarch state |
| astate *argstate |
| } |
| |
| // pkstate |
| type pkstate struct { |
| // index of package within meta-data file. |
| pkgIdx uint32 |
| // this maps function index within the package to counter data payload |
| ctab map[uint32]decodecounter.FuncPayload |
| // pointer to meta-data blob for package |
| mdblob []byte |
| // filled in only for -pcombine merges |
| *pcombinestate |
| } |
| |
| type podstate struct { |
| pmm map[pkfunc]decodecounter.FuncPayload |
| mdf string |
| mfr *decodemeta.CoverageMetaFileReader |
| fileHash [16]byte |
| } |
| |
| type pkfunc struct { |
| pk, fcn uint32 |
| } |
| |
| // pcombinestate |
| type pcombinestate struct { |
| // Meta-data builder for the package. |
| cmdb *encodemeta.CoverageMetaDataBuilder |
| // Maps function meta-data hash to new function index in the |
| // new version of the package we're building. |
| ftab map[[16]byte]uint32 |
| } |
| |
| func newMetaMerge() *metaMerge { |
| return &metaMerge{ |
| pkm: make(map[string]*pkstate), |
| astate: &argstate{}, |
| } |
| } |
| |
| func (mm *metaMerge) visitMetaDataFile(mdf string, mfr *decodemeta.CoverageMetaFileReader) { |
| dbgtrace(2, "visitMetaDataFile(mdf=%s)", mdf) |
| |
| // Record meta-data file name. |
| mm.pod.mdf = mdf |
| // Keep a pointer to the file-level reader. |
| mm.pod.mfr = mfr |
| // Record file hash. |
| mm.pod.fileHash = mfr.FileHash() |
| // Counter mode and granularity -- detect and record clashes here. |
| newgran := mfr.CounterGranularity() |
| newmode := mfr.CounterMode() |
| if err := mm.SetModeAndGranularity(mdf, newmode, newgran); err != nil { |
| fatal("%v", err) |
| } |
| } |
| |
| func (mm *metaMerge) beginCounterDataFile(cdr *decodecounter.CounterDataReader) { |
| state := argvalues{ |
| osargs: cdr.OsArgs(), |
| goos: cdr.Goos(), |
| goarch: cdr.Goarch(), |
| } |
| mm.astate.Merge(state) |
| } |
| |
| func copyMetaDataFile(inpath, outpath string) { |
| inf, err := os.Open(inpath) |
| if err != nil { |
| fatal("opening input meta-data file %s: %v", inpath, err) |
| } |
| defer inf.Close() |
| |
| fi, err := inf.Stat() |
| if err != nil { |
| fatal("accessing input meta-data file %s: %v", inpath, err) |
| } |
| |
| outf, err := os.OpenFile(outpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fi.Mode()) |
| if err != nil { |
| fatal("opening output meta-data file %s: %v", outpath, err) |
| } |
| |
| _, err = io.Copy(outf, inf) |
| outf.Close() |
| if err != nil { |
| fatal("writing output meta-data file %s: %v", outpath, err) |
| } |
| } |
| |
| func (mm *metaMerge) beginPod() { |
| mm.pod = &podstate{ |
| pmm: make(map[pkfunc]decodecounter.FuncPayload), |
| } |
| } |
| |
| // metaEndPod handles actions needed when we're done visiting all of |
| // the things in a pod -- counter files and meta-data file. There are |
| // three cases of interest here: |
| // |
| // Case 1: in an unconditonal merge (we're not selecting a specific set of |
| // packages using "-pkg", and the "-pcombine" option is not in use), |
| // we can simply copy over the meta-data file from input to output. |
| // |
| // Case 2: if this is a select merge (-pkg is in effect), then at |
| // this point we write out a new smaller meta-data file that includes |
| // only the packages of interest). At this point we also emit a merged |
| // counter data file as well. |
| // |
| // Case 3: if "-pcombine" is in effect, we don't write anything at |
| // this point (all writes will happen at the end of the run). |
| func (mm *metaMerge) endPod(pcombine bool) { |
| if pcombine { |
| // Just clear out the pod data, we'll do all the |
| // heavy lifting at the end. |
| mm.pod = nil |
| return |
| } |
| |
| finalHash := mm.pod.fileHash |
| if matchpkg != nil { |
| // Emit modified meta-data file for this pod. |
| finalHash = mm.emitMeta(*outdirflag, pcombine) |
| } else { |
| // Copy meta-data file for this pod to the output directory. |
| inpath := mm.pod.mdf |
| mdfbase := filepath.Base(mm.pod.mdf) |
| outpath := filepath.Join(*outdirflag, mdfbase) |
| copyMetaDataFile(inpath, outpath) |
| } |
| |
| // Emit acccumulated counter data for this pod. |
| mm.emitCounters(*outdirflag, finalHash) |
| |
| // Reset package state. |
| mm.pkm = make(map[string]*pkstate) |
| mm.pkgs = nil |
| mm.pod = nil |
| |
| // Reset counter mode and granularity |
| mm.ResetModeAndGranularity() |
| } |
| |
| // emitMeta encodes and writes out a new coverage meta-data file as |
| // part of a merge operation, specifically a merge with the |
| // "-pcombine" flag. |
| func (mm *metaMerge) emitMeta(outdir string, pcombine bool) [16]byte { |
| fh := md5.New() |
| blobs := [][]byte{} |
| tlen := uint64(unsafe.Sizeof(coverage.MetaFileHeader{})) |
| for _, p := range mm.pkgs { |
| var blob []byte |
| if pcombine { |
| mdw := &slicewriter.WriteSeeker{} |
| p.cmdb.Emit(mdw) |
| blob = mdw.BytesWritten() |
| } else { |
| blob = p.mdblob |
| } |
| ph := md5.Sum(blob) |
| blobs = append(blobs, blob) |
| if _, err := fh.Write(ph[:]); err != nil { |
| panic(fmt.Sprintf("internal error: md5 sum failed: %v", err)) |
| } |
| tlen += uint64(len(blob)) |
| } |
| var finalHash [16]byte |
| fhh := fh.Sum(nil) |
| copy(finalHash[:], fhh) |
| |
| // Open meta-file for writing. |
| fn := fmt.Sprintf("%s.%x", coverage.MetaFilePref, finalHash) |
| fpath := filepath.Join(outdir, fn) |
| mf, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) |
| if err != nil { |
| fatal("unable to open output meta-data file %s: %v", fpath, err) |
| } |
| |
| // Encode and write. |
| mfw := encodemeta.NewCoverageMetaFileWriter(fpath, mf) |
| err = mfw.Write(finalHash, blobs, mm.Mode(), mm.Granularity()) |
| if err != nil { |
| fatal("error writing %s: %v\n", fpath, err) |
| } |
| return finalHash |
| } |
| |
| func (mm *metaMerge) emitCounters(outdir string, metaHash [16]byte) { |
| // Open output file. The file naming scheme is intended to mimic |
| // that used when running a coverage-instrumented binary, for |
| // consistency (however the process ID is not meaningful here, so |
| // use a value of zero). |
| var dummyPID int |
| fn := fmt.Sprintf(coverage.CounterFileTempl, coverage.CounterFilePref, metaHash, dummyPID, time.Now().UnixNano()) |
| fpath := filepath.Join(outdir, fn) |
| cf, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) |
| if err != nil { |
| fatal("opening counter data file %s: %v", fpath, err) |
| } |
| defer func() { |
| if err := cf.Close(); err != nil { |
| fatal("error closing output meta-data file %s: %v", fpath, err) |
| } |
| }() |
| |
| args := mm.astate.ArgsSummary() |
| cfw := encodecounter.NewCoverageDataWriter(cf, coverage.CtrULeb128) |
| if err := cfw.Write(metaHash, args, mm); err != nil { |
| fatal("counter file write failed: %v", err) |
| } |
| mm.astate = &argstate{} |
| } |
| |
| // NumFuncs is used while writing the counter data files; it |
| // implements the 'NumFuncs' method required by the interface |
| // internal/coverage/encodecounter/CounterVisitor. |
| func (mm *metaMerge) NumFuncs() (int, error) { |
| rval := 0 |
| for _, p := range mm.pkgs { |
| rval += len(p.ctab) |
| } |
| return rval, nil |
| } |
| |
| // VisitFuncs is used while writing the counter data files; it |
| // implements the 'VisitFuncs' method required by the interface |
| // internal/coverage/encodecounter/CounterVisitor. |
| func (mm *metaMerge) VisitFuncs(f encodecounter.CounterVisitorFn) error { |
| if *verbflag >= 4 { |
| fmt.Printf("counterVisitor invoked\n") |
| } |
| // For each package, for each function, construct counter |
| // array and then call "f" on it. |
| for pidx, p := range mm.pkgs { |
| fids := make([]int, 0, len(p.ctab)) |
| for fid := range p.ctab { |
| fids = append(fids, int(fid)) |
| } |
| sort.Ints(fids) |
| if *verbflag >= 4 { |
| fmt.Printf("fids for pk=%d: %+v\n", pidx, fids) |
| } |
| for _, fid := range fids { |
| fp := p.ctab[uint32(fid)] |
| if *verbflag >= 4 { |
| fmt.Printf("counter write for pk=%d fid=%d len(ctrs)=%d\n", pidx, fid, len(fp.Counters)) |
| } |
| if err := f(uint32(pidx), uint32(fid), fp.Counters); err != nil { |
| return err |
| } |
| } |
| } |
| return nil |
| } |
| |
| func (mm *metaMerge) visitPackage(pd *decodemeta.CoverageMetaDataDecoder, pkgIdx uint32, pcombine bool) { |
| p, ok := mm.pkm[pd.PackagePath()] |
| if !ok { |
| p = &pkstate{ |
| pkgIdx: uint32(len(mm.pkgs)), |
| } |
| mm.pkgs = append(mm.pkgs, p) |
| mm.pkm[pd.PackagePath()] = p |
| if pcombine { |
| p.pcombinestate = new(pcombinestate) |
| cmdb, err := encodemeta.NewCoverageMetaDataBuilder(pd.PackagePath(), pd.PackageName(), pd.ModulePath()) |
| if err != nil { |
| fatal("fatal error creating meta-data builder: %v", err) |
| } |
| dbgtrace(2, "install new pkm entry for package %s pk=%d", pd.PackagePath(), pkgIdx) |
| p.cmdb = cmdb |
| p.ftab = make(map[[16]byte]uint32) |
| } else { |
| var err error |
| p.mdblob, err = mm.pod.mfr.GetPackagePayload(pkgIdx, nil) |
| if err != nil { |
| fatal("error extracting package %d payload from %s: %v", |
| pkgIdx, mm.pod.mdf, err) |
| } |
| } |
| p.ctab = make(map[uint32]decodecounter.FuncPayload) |
| } |
| mm.p = p |
| } |
| |
| func (mm *metaMerge) visitFuncCounterData(data decodecounter.FuncPayload) { |
| key := pkfunc{pk: data.PkgIdx, fcn: data.FuncIdx} |
| val := mm.pod.pmm[key] |
| // FIXME: in theory either A) len(val.Counters) is zero, or B) |
| // the two lengths are equal. Assert if not? Of course, we could |
| // see odd stuff if there is source file skew. |
| if *verbflag > 4 { |
| fmt.Printf("visit pk=%d fid=%d len(counters)=%d\n", data.PkgIdx, data.FuncIdx, len(data.Counters)) |
| } |
| if len(val.Counters) < len(data.Counters) { |
| t := val.Counters |
| val.Counters = mm.AllocateCounters(len(data.Counters)) |
| copy(val.Counters, t) |
| } |
| err, overflow := mm.MergeCounters(val.Counters, data.Counters) |
| if err != nil { |
| fatal("%v", err) |
| } |
| if overflow { |
| warn("uint32 overflow during counter merge") |
| } |
| mm.pod.pmm[key] = val |
| } |
| |
| func (mm *metaMerge) visitFunc(pkgIdx uint32, fnIdx uint32, fd *coverage.FuncDesc, verb string, pcombine bool) { |
| if *verbflag >= 3 { |
| fmt.Printf("visit pk=%d fid=%d func %s\n", pkgIdx, fnIdx, fd.Funcname) |
| } |
| |
| var counters []uint32 |
| key := pkfunc{pk: pkgIdx, fcn: fnIdx} |
| v, haveCounters := mm.pod.pmm[key] |
| if haveCounters { |
| counters = v.Counters |
| } |
| |
| if pcombine { |
| // If the merge is running in "combine programs" mode, then hash |
| // the function and look it up in the package ftab to see if we've |
| // encountered it before. If we haven't, then register it with the |
| // meta-data builder. |
| fnhash := encodemeta.HashFuncDesc(fd) |
| gfidx, ok := mm.p.ftab[fnhash] |
| if !ok { |
| // We haven't seen this function before, need to add it to |
| // the meta data. |
| gfidx = uint32(mm.p.cmdb.AddFunc(*fd)) |
| mm.p.ftab[fnhash] = gfidx |
| if *verbflag >= 3 { |
| fmt.Printf("new meta entry for fn %s fid=%d\n", fd.Funcname, gfidx) |
| } |
| } |
| fnIdx = gfidx |
| } |
| if !haveCounters { |
| return |
| } |
| |
| // Install counters in package ctab. |
| gfp, ok := mm.p.ctab[fnIdx] |
| if ok { |
| if verb == "subtract" || verb == "intersect" { |
| panic("should never see this for intersect/subtract") |
| } |
| if *verbflag >= 3 { |
| fmt.Printf("counter merge for %s fidx=%d\n", fd.Funcname, fnIdx) |
| } |
| // Merge. |
| err, overflow := mm.MergeCounters(gfp.Counters, counters) |
| if err != nil { |
| fatal("%v", err) |
| } |
| if overflow { |
| warn("uint32 overflow during counter merge") |
| } |
| mm.p.ctab[fnIdx] = gfp |
| } else { |
| if *verbflag >= 3 { |
| fmt.Printf("null merge for %s fidx %d\n", fd.Funcname, fnIdx) |
| } |
| gfp := v |
| gfp.PkgIdx = mm.p.pkgIdx |
| gfp.FuncIdx = fnIdx |
| mm.p.ctab[fnIdx] = gfp |
| } |
| } |