blob: 4f6150bef7752c7abdd0ebacf2a1f95af6388dfb [file] [log] [blame]
package stepfit
import (
"math"
"github.com/aclements/go-moremath/stats"
"go.skia.org/infra/go/vec32"
"go.skia.org/infra/perf/go/types"
)
// StepFitStatus is the status of the StepFit.
type StepFitStatus string
const (
// The possible values for StepFit.Status are:
// LOW is a step down.
LOW StepFitStatus = "Low"
// HIGH is a step up.
HIGH StepFitStatus = "High"
// UNINTERESTING means no step occurred.
UNINTERESTING StepFitStatus = "Uninteresting"
// minTraceSize is the smallest trace length we can analyze.
minTraceSize = 3
)
// AllStepFitStatus is the list of all StepFitStatus values.
var AllStepFitStatus = []StepFitStatus{LOW, HIGH, UNINTERESTING}
// StepFit stores information on the best Step Function fit on a trace.
//
// Used in ClusterSummary.
type StepFit struct {
// LeastSquares is the Least Squares error for a step function curve fit to
// the trace. Will be set to InvalidLeastSquaresError if LSE isn't
// calculated for a given algorithm.
LeastSquares float32 `json:"least_squares"`
// TurningPoint is the index where the Step Function changes value.
TurningPoint int `json:"turning_point"`
// StepSize is the size of the step in the step function. Negative values
// indicate a step up, i.e. they look like a performance regression in the
// trace, as opposed to positive values which look like performance
// improvements.
StepSize float32 `json:"step_size"`
// The "Regression" value is calculated as Step Size / Least Squares Error.
//
// The better the fit the larger the number returned, because LSE
// gets smaller with a better fit. The higher the Step Size the
// larger the number returned.
Regression float32 `json:"regression"`
// Status of the cluster.
//
// Values can be "High", "Low", and "Uninteresting"
Status StepFitStatus `json:"status"`
}
// InvalidLeastSquaresError signals that the value of StepFit.LeastSquares is
// invalid, i.e. it is not calculated for the given algorithm.
const InvalidLeastSquaresError = -1
// NewStepFit creates an properly initialized StepFit struct.
func NewStepFit() *StepFit {
return &StepFit{
Status: UNINTERESTING,
}
}
// GetStepFitAtMid takes one []float32 trace and calculates and returns a
// *StepFit.
//
// stddevThreshold is the minimum standard deviation allowed when normalizing
// traces to a standard deviation of 1.
//
// interesting is the threshold for a particular step to be flagged as a
// regression.
//
// stepDetection is the algorithm to use to test for a regression.
//
// See StepFit for a description of the values being calculated.
func GetStepFitAtMid(trace []float32, stddevThreshold float32, interesting float32, stepDetection types.StepDetection) *StepFit {
ret := NewStepFit()
if len(trace) < minTraceSize {
return ret
}
// Only normalize the trace if doing ORIGINAL_STEP.
if stepDetection == types.OriginalStep {
trace = vec32.Dup(trace)
vec32.Norm(trace, stddevThreshold)
} else {
// For all non-ORIGINAL_STEP regression types we use a symmetric (2*N)
// trace, while in ORIGINAL_STEP uses the 2*N+1 length trace supplied.
trace = trace[0 : len(trace)-1]
}
var lse float32 = InvalidLeastSquaresError
var regression float32
stepSize := float32(-1.0)
i := len(trace) / 2
// Now do different work based on stepDetection
y0 := vec32.Mean(trace[:i])
y1 := vec32.Mean(trace[i:])
if stepDetection == types.OriginalStep {
// This is the original recipe step detection as described at
// https://bitworking.org/news/2014/11/detecting-benchmark-regressions
lse = float32(math.MaxFloat32)
if y0 != y1 {
d := vec32.SSE(trace[:i], y0) + vec32.SSE(trace[i:], y1)
if d < lse {
lse = d
stepSize = (y0 - y1)
}
}
// The next line of code should actually be math.Sqrt(lse/len(trace))
// instead it is math.Sqrt(lse)/len(trace), which does not give the stddev.
lse = float32(math.Sqrt(float64(lse))) / float32(len(trace))
if lse < stddevThreshold {
regression = stepSize / stddevThreshold
} else {
regression = stepSize / lse
}
} else if stepDetection == types.AbsoluteStep {
// A simple check if the step size is greater than some absolute value.
stepSize = (y0 - y1)
regression = stepSize
} else if stepDetection == types.Const {
// For this calculation we only look at the trace value at 'i', and only
// its absolute value.
absTraceValue := float32(math.Abs(float64(trace[i])))
stepSize = absTraceValue - interesting
regression = -1 * absTraceValue // * -1 So that regressions get flagged as HIGH.
} else if stepDetection == types.PercentStep {
// A simple check if the step size is greater than some percentage of
// the mean of the first half of the trace.
if len(trace) > 0 {
stepSize = (y0 - y1) / (y0) // The division can produce +/-Inf or NaN.
if math.IsInf(float64(stepSize), 0) {
stepSize = math.MaxFloat32
if y0 < y1 {
stepSize *= -1
}
}
if math.IsNaN(float64(stepSize)) {
stepSize = 0
}
regression = stepSize
} else {
stepSize = 0
regression = stepSize
}
} else if stepDetection == types.CohenStep {
// https://en.wikipedia.org/wiki/Effect_size#Cohen's_d
if len(trace) < 4 {
// The math for Cohen's d only makes sense for len(trace) >= 4.
stepSize = 0
regression = stepSize
} else {
s1 := vec32.StdDev(trace[:i], y0)
s2 := vec32.StdDev(trace[i:], y1)
s := (s1 + s2) / 2.0
if math.IsNaN(float64(s)) || s < stddevThreshold {
stepSize = (y0 - y1) / stddevThreshold
} else {
stepSize = (y0 - y1) / s
}
regression = stepSize
}
} else /* types.MannWhitneyU */ {
s1 := vec32.ToFloat64(trace[:i])
s2 := vec32.ToFloat64(trace[i:])
mwResults, err := stats.MannWhitneyUTest(s1, s2, stats.LocationDiffers)
if err != nil {
return ret
}
stepSize = (y0 - y1)
regression = float32(mwResults.P)
lse = float32(mwResults.U)
}
status := UNINTERESTING
if stepDetection == types.MannWhitneyU {
// There is a different interpretation of regression for MannWhitneyU,
// where regression = p. That is, when doing a hypothesis test we want
// to see if p < 0.05, for example. So that only tells us if a
// regression has occurred, i.e. we rejected the null hypothesis, so we
// need to use the sign of stepSize to determine the direction (status).
if regression <= interesting {
if stepSize < 0 {
status = HIGH
regression *= -1
} else {
status = LOW
}
}
} else {
if regression >= interesting {
status = LOW
} else if regression <= -interesting {
status = HIGH
}
}
ret.Status = status
ret.LeastSquares = lse
ret.StepSize = stepSize
ret.TurningPoint = i
ret.Regression = regression
return ret
}