blob: 3f1719feb0b51b7c4c99e19cfc5753c171b64f44 [file]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package perfutil provides utilities of storing performance data for UI tests.
package perfutil
import (
"context"
"sort"
"chromiumos/tast/common/perf"
"chromiumos/tast/errors"
"chromiumos/tast/testing"
)
// Values keeps the reporting values for multiple runs.
type Values struct {
metrics map[string]perf.Metric
values map[string][]float64
}
// NewValues creates a new Values instance.
func NewValues() *Values {
return &Values{
metrics: map[string]perf.Metric{},
values: map[string][]float64{},
}
}
// Append adds a new data points to values.
func (v *Values) Append(metric perf.Metric, value float64) {
name := metric.Name
if _, ok := v.metrics[name]; !ok {
v.metrics[name] = metric
}
v.values[name] = append(v.values[name], value)
}
func minMaxIndices(vs []float64) (minIndex, maxIndex int) {
max := vs[0]
min := vs[0]
for i, v := range vs {
if v > max {
max = v
maxIndex = i
}
if v < min {
min = v
minIndex = i
}
}
return minIndex, maxIndex
}
// Values creates a new perf.Values for its data points.
func (v *Values) Values(ctx context.Context) *perf.Values {
pv := perf.NewValues()
// Ensure that the iteration order is sorted by the name of the metrics, as
// this iteration also logs the name/results.
names := make([]string, 0, len(v.metrics))
for name := range v.metrics {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
metric := v.metrics[name]
vs := v.values[name]
if len(vs) == 0 {
continue
}
// Report the first value
firstMetric := metric
firstMetric.Variant = "first"
pv.Set(firstMetric, vs[0])
// Average metrics; just reporting individual values, as crosbolt makes
// the average calculation.
otherMetric := metric
otherMetric.Variant = "average"
otherMetric.Multiple = true
minIndex, maxIndex := minMaxIndices(vs)
var sum float64
var count int
for i, v := range vs {
if len(vs) < 3 || (i != maxIndex && i != minIndex) {
pv.Append(otherMetric, v)
sum += v
count++
}
}
testing.ContextLogf(ctx, "Average %s = %v", name, sum/float64(count))
}
return pv
}
// Verify verifies the stored values with the numbers in expects and returns a
// list of misses of the expectations. If no problem, it will return empty.
func (v *Values) Verify(ctx context.Context, expects map[string]float64) []error {
var errs []error
for name, metric := range v.metrics {
vs := v.values[name]
if len(vs) == 0 {
continue
}
exp, ok := expects[name]
if !ok {
testing.ContextLogf(ctx, "Skipping %s", name)
continue
}
var sum float64
var count int
if len(vs) < 3 {
count = len(vs)
for _, v := range vs {
sum += v
}
} else {
minIndex, maxIndex := minMaxIndices(vs)
for i, v := range vs {
if i != minIndex && i != maxIndex {
sum += v
count++
}
}
}
avg := sum / float64(count)
var isGood bool
var operator string
if metric.Direction == perf.BiggerIsBetter {
isGood = exp <= avg
operator = ">="
} else {
isGood = avg <= exp
operator = "<="
}
if !isGood {
errs = append(errs, errors.Errorf("%s: got %v want %s%v", name, avg, operator, exp))
}
}
return errs
}
// Save is a shortcut of Values().Save(outdir). Helpful when the test does not
// have to combine with other data points.
func (v *Values) Save(ctx context.Context, outdir string) error {
return v.Values(ctx).Save(outdir)
}