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