blob: 449c9598426bb2ffb113f3a3c0f786aa69052594 [file] [log] [blame]
package internal
import (
"context"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/pinpoint/go/compare"
"go.skia.org/infra/pinpoint/go/midpoint"
"go.temporal.io/sdk/workflow"
)
const (
functional = "Functional"
performance = "Performance"
nudgeFactor = float64(1e-10)
)
type CommitPairValues struct {
Lower CommitValues
Higher CommitValues
}
type CommitValues struct {
Commit *midpoint.CombinedCommit
Values []float64
ErrorValues []float64
}
type CombinedResults struct {
Result *compare.CompareResults
OtherResult *compare.CompareResults // record the other comparison
ResultType string // either Functional or Performance
CommitPairValues CommitPairValues
}
// TODO(sunxiaodi@): Change GetAllDataForCompareLocalActivity to a regular function
func GetAllDataForCompareLocalActivity(ctx context.Context, lbr *BisectRun, hbr *BisectRun, chart string) (*CommitPairValues, error) {
return &CommitPairValues{
Lower: CommitValues{lbr.Build.Commit, lbr.AllValues(chart), lbr.AllErrorValues(chart)},
Higher: CommitValues{hbr.Build.Commit, hbr.AllValues(chart), hbr.AllErrorValues(chart)},
}, nil
}
// CompareActivity wraps compare.ComparePerformance and compare.CompareFunctional as activity
//
// commitA and commitB are passed in to make it easier to see on the Temporal
// UI what two commits are being tested. Errors are recorded in the activity but
// the ErrorVerdict is not passed back to the main workflow.
func CompareActivity(ctx context.Context, allValues CommitPairValues, magnitude, errRate float64, direction compare.ImprovementDir) (*CombinedResults, error) {
// TODO(sunxiaodi@): skip functional analysis if there are no errors
funcResult, err := compare.CompareFunctional(allValues.Lower.ErrorValues, allValues.Higher.ErrorValues, errRate)
if err != nil {
return &CombinedResults{Result: funcResult, ResultType: functional}, skerr.Wrap(err)
}
// always return different verdicts
if funcResult.Verdict == compare.Different {
return &CombinedResults{
Result: funcResult,
ResultType: functional,
}, nil
}
perfResult, err := compare.ComparePerformance(allValues.Lower.Values, allValues.Higher.Values, magnitude, direction)
if err != nil {
return &CombinedResults{
Result: funcResult,
OtherResult: perfResult,
ResultType: functional,
}, skerr.Wrap(err)
}
// decide which verdict to return
// occurs if all benchmark runs fail and we are relying on functional analysis results
if perfResult.Verdict == compare.NilVerdict {
return &CombinedResults{
Result: funcResult,
OtherResult: perfResult,
ResultType: functional,
}, nil
}
if funcResult.Verdict == compare.Unknown && perfResult.Verdict == compare.Same {
return &CombinedResults{
Result: funcResult,
OtherResult: perfResult,
ResultType: functional,
}, nil
}
return &CombinedResults{
Result: perfResult,
OtherResult: funcResult,
ResultType: performance,
CommitPairValues: allValues,
}, nil
}
func compareRuns(ctx workflow.Context, lRun, hRun *BisectRun, chart string, mag float64, dir compare.ImprovementDir) (*CombinedResults, error) {
var commitPairAllValues CommitPairValues
if err := workflow.ExecuteLocalActivity(ctx, GetAllDataForCompareLocalActivity, lRun, hRun, chart).Get(ctx, &commitPairAllValues); err != nil {
return nil, skerr.Wrap(err)
}
var result *CombinedResults
if err := workflow.ExecuteActivity(ctx, CompareActivity, commitPairAllValues, mag, compare.DefaultFunctionalErrRate, dir).Get(ctx, &result); err != nil {
return nil, skerr.Wrap(err)
}
return result, nil
}
// ComparePairwiseActivity wraps compare.ComparePairwise as a temporal activity
func ComparePairwiseActivity(ctx context.Context, valuesA, valuesB []float64, dir compare.ImprovementDir) (*compare.ComparePairwiseResult, error) {
valuesB = handlePairwiseEdgeCase(valuesA, valuesB)
return compare.ComparePairwise(valuesA, valuesB, dir)
}
// if every value in valuesA is identicial and every value in valuesB is identical,
// pairwise comparison will fail to return a confidence interval because the
// wilcoxon_signed_rank.go runs into an error:
// "cannot compute confidence interval when all observations are zero or tied"
// This edge case can happen for some very consistent, near-deterministic benchmark runs.
// However, this does not mean that the data collected is not useful. Only one data point
// needs to be nudged to return a confidence interval. Here we manipulate the data
// on a significant figure that should not have any adverse affect on the overall sample
func handlePairwiseEdgeCase(valuesA, valuesB []float64) []float64 {
allSameA := allSameValues(valuesA)
allSameB := allSameValues(valuesB)
if allSameA && allSameB {
sklog.Warningf("all values in A are identical and all values in B are identical. Nudging one element by %v in valuesB. ValuesA: %v; ValuesB: %v", valuesB[0]*nudgeFactor, valuesA, valuesB)
valuesB[0] += valuesB[0] * nudgeFactor
}
return valuesB
}
func allSameValues(values []float64) bool {
for i := 1; i < len(values); i++ {
if values[i] != values[0] {
return false
}
}
return true
}