blob: 91b8d675a40dfd79abab752b2e5224af1fb3ac50 [file] [log] [blame]
// summary summarizes the current state of triaging.
package summary
import (
"fmt"
"net/url"
"sort"
"sync"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/tiling"
"go.skia.org/infra/golden/go/blame"
"go.skia.org/infra/golden/go/diff"
"go.skia.org/infra/golden/go/digest_counter"
"go.skia.org/infra/golden/go/expstorage"
"go.skia.org/infra/golden/go/shared"
"go.skia.org/infra/golden/go/types"
)
type SummaryMap map[types.TestName]*Summary
// Summary contains rolled up metrics for one test.
// It is immutable and should be thread safe.
type Summary struct {
Name types.TestName `json:"name"`
Diameter int `json:"diameter"`
Pos int `json:"pos"`
Neg int `json:"neg"`
Untriaged int `json:"untriaged"`
UntHashes types.DigestSlice `json:"untHashes"`
Num int `json:"num"`
Corpus string `json:"corpus"`
Blame []blame.WeightedBlame `json:"blame"`
}
// TODO(jcgregorio) Make diameter faster, and also make the actual diameter
// metric better. Until then disable it.
const computeDiameter = false
// SummaryMapConfig is a helper struct for calculating SummaryMap.
type SummaryMapConfig struct {
ExpectationsStore expstorage.ExpectationsStore
DiffStore diff.DiffStore // only needed if computeDiameter = true
DigestCounter digest_counter.DigestCounter
Blamer blame.Blamer
}
// New creates a new instance of Summaries.
func NewSummaryMap(smc SummaryMapConfig, tile *tiling.Tile, testNames types.TestNameSet, query url.Values, head bool) (SummaryMap, error) {
return smc.calcSummaries(tile, testNames, query, head)
}
// Combine creates a new SummaryMap from this and the passed
// in map. The passed in map will "win" in the event there are tests
// in both.
func (s SummaryMap) Combine(other SummaryMap) SummaryMap {
copied := make(SummaryMap, len(s))
for k, v := range s {
copied[k] = v
}
for k, v := range other {
copied[k] = v
}
return copied
}
// tracePair is used to hold traces, along with their ids.
type tracePair struct {
id tiling.TraceId
tr tiling.Trace
}
// calcSummaries returns a Summary of the given tile. If testNames is not empty,
// then restrict the results to only tests with those names. If query is not empty,
// it will be used as an additional filter. Finally, if head is true, only consider
// the single most recent digest per trace.
func (s *SummaryMapConfig) calcSummaries(tile *tiling.Tile, testNames types.TestNameSet, query url.Values, head bool) (SummaryMap, error) {
defer shared.NewMetricsTimer("calc_summaries_total").Stop()
sklog.Infof("CalcSummaries: head %v", head)
ret := SummaryMap{}
t := shared.NewMetricsTimer("calc_summaries_expectations")
e, err := s.ExpectationsStore.Get()
t.Stop()
if err != nil {
return nil, fmt.Errorf("Couldn't get expectations: %s", err)
}
// Filter down to just the traces we are interested in, based on query.
filtered := map[types.TestName][]*tracePair{}
t = shared.NewMetricsTimer("calc_summaries_filter_traces")
for id, tr := range tile.Traces {
name := types.TestName(tr.Params()[types.PRIMARY_KEY_FIELD])
if len(testNames) > 0 && !testNames[name] {
continue
}
if tiling.Matches(tr, query) {
if slice, ok := filtered[name]; ok {
filtered[name] = append(slice, &tracePair{tr: tr, id: id})
} else {
filtered[name] = []*tracePair{{tr: tr, id: id}}
}
}
}
t.Stop()
digestsByTrace := s.DigestCounter.ByTrace()
// Now create summaries for each test using the filtered set of traces.
t = shared.NewMetricsTimer("calc_summaries_tally")
lastCommitIndex := tile.LastCommitIndex()
for name, traces := range filtered {
digestMap := types.DigestSet{}
// TODO(kjlubick): I don't think corpus is being calculated correctly.
// It saves whatever the last corpus seen was to the Summary field, but
// if a tile has a mix of corpora, it looks like it will just summarize
// everything?
corpus := ""
for _, trid := range traces {
corpus = trid.tr.Params()[types.CORPUS_FIELD]
if head {
// Find the last non-missing value in the trace.
for i := lastCommitIndex; i >= 0; i-- {
if trid.tr.IsMissing(i) {
continue
} else {
digestMap[trid.tr.(*types.GoldenTrace).Digests[i]] = true
break
}
}
} else {
// Use the digestsByTrace if available, otherwise just inspect the trace.
if t, ok := digestsByTrace[trid.id]; ok {
for k := range t {
digestMap[k] = true
}
} else {
for i := lastCommitIndex; i >= 0; i-- {
if !trid.tr.IsMissing(i) {
digestMap[trid.tr.(*types.GoldenTrace).Digests[i]] = true
}
}
}
}
}
ret[name] = s.makeSummary(name, e, corpus, digestMap.Keys())
}
t.Stop()
return ret, nil
}
// DigestInfo is test name and a digest found in that test. Returned from Search.
type DigestInfo struct {
Test types.TestName `json:"test"`
Digest types.Digest `json:"digest"`
}
// makeSummary returns a Summary for the given digests.
func (s *SummaryMapConfig) makeSummary(name types.TestName, exp types.Expectations, corpus string, digests types.DigestSlice) *Summary {
pos := 0
neg := 0
unt := 0
diamDigests := types.DigestSlice{}
untHashes := types.DigestSlice{}
if expectations, ok := exp[name]; ok {
for _, digest := range digests {
if dtype, ok := expectations[digest]; ok {
switch dtype {
case types.UNTRIAGED:
unt += 1
diamDigests = append(diamDigests, digest)
untHashes = append(untHashes, digest)
case types.NEGATIVE:
neg += 1
case types.POSITIVE:
pos += 1
diamDigests = append(diamDigests, digest)
}
} else {
unt += 1
diamDigests = append(diamDigests, digest)
untHashes = append(untHashes, digest)
}
}
} else {
unt += len(digests)
diamDigests = digests
untHashes = digests
}
sort.Sort(diamDigests)
sort.Sort(untHashes)
d := 0
if computeDiameter {
d = diameter(diamDigests, s.DiffStore)
}
return &Summary{
Name: name,
Diameter: d,
Pos: pos,
Neg: neg,
Untriaged: unt,
UntHashes: untHashes,
Num: pos + neg + unt,
Corpus: corpus,
Blame: s.Blamer.GetBlamesForTest(name),
}
}
func diameter(digests types.DigestSlice, diffStore diff.DiffStore) int {
// TODO Parallelize.
lock := sync.Mutex{}
max := 0
wg := sync.WaitGroup{}
for {
if len(digests) <= 2 {
break
}
wg.Add(1)
go func(d1 types.Digest, d2 types.DigestSlice) {
defer wg.Done()
dms, err := diffStore.Get(diff.PRIORITY_NOW, d1, d2)
if err != nil {
sklog.Errorf("Unable to get diff: %s", err)
return
}
localMax := 0
for _, dm := range dms {
diffMetrics := dm.(*diff.DiffMetrics)
if diffMetrics.NumDiffPixels > localMax {
localMax = diffMetrics.NumDiffPixels
}
}
lock.Lock()
defer lock.Unlock()
if localMax > max {
max = localMax
}
}(digests[0], digests[1:2])
digests = digests[1:]
}
wg.Wait()
return max
}