| // 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 ( | |
| "fmt" | |
| "hash/fnv" | |
| "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 unconditional 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 accumulated 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 := fnv.New128a() | |
| fhSum := fnv.New128a() | |
| 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 | |
| } | |
| fhSum.Reset() | |
| fhSum.Write(blob) | |
| ph := fhSum.Sum(nil) | |
| 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) | |
| } | |
| defer func() { | |
| if err := mf.Close(); err != nil { | |
| fatal("error closing 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{} | |
| } | |
| // 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 | |
| } | |
| } |