blob: 4d3fcdc6fd46af098a472dd28b471d6eebbe055f [file] [log] [blame]
package fuzzpool
// The main feature of the fuzzpool package is FuzzPool, a simple in-memory
// cache of FuzzReports that supports simple querying.
import (
"bytes"
"encoding/gob"
"fmt"
"sort"
"sync"
"go.skia.org/infra/fuzzer/go/common"
"go.skia.org/infra/fuzzer/go/data"
"go.skia.org/infra/go/sklog"
)
// FuzzPool has a staging copy of the FuzzReports and a Current copy.
// All queries return values from Current and all writes are to staging. This
// allows us to fill the staging FuzzPool incrementally and then flip it over
// to Current when everything is loaded w/o being in a partially available state.
type FuzzPool struct {
staging data.SortedFuzzReports
// Current is exported so it can be stored to fuzzcache
Current data.SortedFuzzReports
mutex sync.Mutex
}
// New returns an empty *FuzzPool.
func New() *FuzzPool {
return &FuzzPool{
staging: data.SortedFuzzReports{},
Current: data.SortedFuzzReports{},
}
}
// NewForTests is a convenience function for creating a pre-loaded *FuzzPool.
func NewForTests(r []data.FuzzReport) *FuzzPool {
return &FuzzPool{
staging: data.SortedFuzzReports{},
Current: r,
}
}
// AddFuzzReport adds the given fuzz report to the pool. There is no deduplication, as that
// is assumed to be done before uploading to GCS, which is before reports end up in the pool.
func (p *FuzzPool) AddFuzzReport(r data.FuzzReport) {
p.mutex.Lock()
defer p.mutex.Unlock()
r.FileName, r.FunctionName, r.LineNumber = extractStacktraceInfo(r.Stacktraces)
p.staging = p.staging.Append(r)
}
// ClearStaging empties the staging portion of the pool.
func (p *FuzzPool) ClearStaging() {
p.mutex.Lock()
defer p.mutex.Unlock()
p.staging = data.SortedFuzzReports{}
}
// CurrentFromStaging copies the staging portion of the pool to current.
func (p *FuzzPool) CurrentFromStaging() {
p.mutex.Lock()
defer p.mutex.Unlock()
p.Current = cloneReports(p.staging)
}
// StagingFromCurrent copies the current portion of the pool to staging.
func (p *FuzzPool) StagingFromCurrent() {
p.mutex.Lock()
defer p.mutex.Unlock()
p.staging = cloneReports(p.Current)
}
// Reports returns all FuzzReports
func (p *FuzzPool) Reports() []data.FuzzReport {
p.mutex.Lock()
defer p.mutex.Unlock()
return p.Current
}
// FindFuzzDetailForFuzz returns a FuzzReport of the fuzz with the specified name
// or an error if it could not be found.
func (p *FuzzPool) FindFuzzDetailForFuzz(name string) (data.FuzzReport, error) {
p.mutex.Lock()
defer p.mutex.Unlock()
i := sort.Search(len(p.Current), func(j int) bool {
return p.Current[j].FuzzName >= name
})
if i >= len(p.Current) || p.Current[i].FuzzName != name {
return data.FuzzReport{}, fmt.Errorf("Fuzz with name %s not found", name)
}
return p.Current[i], nil
}
// FindFuzzDetails returns a slice of FuzzReports that match the specified parameters. "" means
// don't care, except for line, in which common.UNKNOWN_LINE means don't care. It returns
// an error if there are no matches.
func (p *FuzzPool) FindFuzzDetails(category, architecture, greyOrBad, file, function string, line int) ([]data.FuzzReport, error) {
p.mutex.Lock()
defer p.mutex.Unlock()
retVal := []data.FuzzReport{}
for _, report := range p.Current {
if category != "" && category != report.FuzzCategory {
continue
}
if architecture != "" && architecture != report.FuzzArchitecture {
continue
}
if file != "" && file != report.FileName {
continue
}
if function != "" && function != report.FunctionName {
continue
}
if greyOrBad != "" && ((greyOrBad == "grey") != (report.IsGrey)) {
continue
}
if line != common.UNKNOWN_LINE && line != report.LineNumber {
continue
}
retVal = append(retVal, report)
}
if len(retVal) == 0 {
return nil, fmt.Errorf("No fuzzes matched the input critera: C:%s A: %s, badorGrey: %s, file: %s, function: %s, line: %d", category, architecture, greyOrBad, file, function, line)
}
return retVal, nil
}
// cloneReport makes a copy of the input using the gob library.
func cloneReports(r data.SortedFuzzReports) data.SortedFuzzReports {
var temp bytes.Buffer
enc := gob.NewEncoder(&temp)
dec := gob.NewDecoder(&temp)
if err := enc.Encode(r); err != nil {
// This should never happen, but log it if it does
sklog.Errorf("Error while cloning report: %v", err)
}
var clone data.SortedFuzzReports
if err := dec.Decode(&clone); err != nil {
// This should never happen, but log it if it does
sklog.Errorf("Error while cloning report: %v", err)
}
return clone
}
// extractStacktraceInfo returns the file name, function name and line number that
// a report with the given debug and release stacktrace should be sorted by.
// this tries to read the release stacktrace first, falling back to the debug stacktrace,
// failling back to Unknown.
func extractStacktraceInfo(traces map[string]data.StackTrace) (reportFileName, reportFunctionName string, reportLineNumber int) {
reportFileName = common.UNKNOWN_FILE
reportFunctionName = common.UNKNOWN_FUNCTION
reportLineNumber = common.UNKNOWN_LINE
for _, c := range common.STACKTRACE_ORDER {
st := traces[c]
if !st.IsEmpty() {
frame := st.Frames[0]
return frame.PackageName + frame.FileName, frame.FunctionName, frame.LineNumber
}
}
return
}