blob: aadfb429b2c72eda07caeda597cba29763df510f [file] [log] [blame]
// 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 power
import (
"context"
"io/ioutil"
"path"
"regexp"
"time"
"chromiumos/tast/common/perf"
"chromiumos/tast/errors"
"chromiumos/tast/testing"
)
const c0State = "C0"
// computeCpuidleStateFiles returns a mapping from cpuidle states to files
// containing the corresponding residency information.
func computeCpuidleStateFiles(ctx context.Context) (map[string][]string, int, error) {
ret := make(map[string][]string)
numCpus := 0
const cpusDir = "/sys/devices/system/cpu/"
cpuInfos, err := ioutil.ReadDir(cpusDir)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to find cpus")
}
for _, cpuInfo := range cpuInfos {
// Match files with name cpu0, cpu1, ....
if match, err := regexp.MatchString(`^cpu\d+$`, cpuInfo.Name()); err != nil {
return nil, 0, errors.Wrap(err, "error trying to match cpu name")
} else if !match {
continue
}
numCpus++
cpuDir := path.Join(cpusDir, cpuInfo.Name(), "cpuidle")
cpuidles, err := ioutil.ReadDir(cpuDir)
if err != nil {
testing.ContextLogf(ctx, "System does not expose %v, skipping CPU", cpuDir)
continue
}
for _, cpuidle := range cpuidles {
// Match files with name state0, state1, ....
if match, err := regexp.MatchString(`^state\d+$`, cpuidle.Name()); err != nil {
return nil, 0, errors.Wrap(err, "error trying to match idle state name")
} else if !match {
continue
}
name, err := readFirstLine(path.Join(cpuDir, cpuidle.Name(), "name"))
if err != nil {
return nil, 0, errors.Wrap(err, "failed to read cpuidle name")
}
latency, err := readFirstLine(path.Join(cpuDir, cpuidle.Name(), "latency"))
if err != nil {
return nil, 0, errors.Wrap(err, "failed to read cpuidle latency")
}
if latency == "0" && name == "POLL" {
// C0 state. Kernel stats aren't right, so calculate by
// subtracting all other states from total time (using epoch
// timer since we calculate differences in the end anyway).
// NOTE: Only x86 lists C0 under cpuidle, ARM does not.
continue
}
ret[name] = append(ret[name], path.Join(cpuDir, cpuidle.Name(), "time"))
}
}
return ret, numCpus, nil
}
// CpuidleStateMetrics records the C-states of the DUT.
//
// NOTE: The cpuidle timings are measured according to the kernel. They resemble
// hardware cstates, but they might not have a direct correspondence. Furthermore,
// they generally may be greater than the time the CPU actually spends in the
// corresponding cstate, as the hardware may enter shallower states than requested.
type CpuidleStateMetrics struct {
cpuidleFiles map[string][]string
numCpus int
lastTime time.Time
lastStats map[string]int64
metrics map[string]perf.Metric
}
// Assert that CpuidleStateMetrics can be used in perf.Timeline.
var _ perf.TimelineDatasource = &CpuidleStateMetrics{}
// NewCpuidleStateMetrics creates a timeline metric to collect C-state numbers.
func NewCpuidleStateMetrics() *CpuidleStateMetrics {
return &CpuidleStateMetrics{nil, 0, time.Time{}, nil, make(map[string]perf.Metric)}
}
// Setup determines what C-states are supported and which CPUs should be queried.
func (cs *CpuidleStateMetrics) Setup(ctx context.Context, prefix string) error {
cpuidleFiles, numCpus, err := computeCpuidleStateFiles(ctx)
if err != nil {
return errors.Wrap(err, "error finding cpuidles")
}
cs.cpuidleFiles = cpuidleFiles
cs.numCpus = numCpus
return nil
}
// readCpuidleStateTimes reads the cpuidle timings.
func readCpuidleStateTimes(cpuidleFiles map[string][]string) (map[string]int64, time.Time, error) {
ret := make(map[string]int64)
for cpuidle, files := range cpuidleFiles {
ret[cpuidle] = 0
for _, file := range files {
t, err := readInt64(file)
if err != nil {
return nil, time.Time{}, errors.Wrap(err, "failed to read cpuidle timing")
}
ret[cpuidle] += t
}
}
return ret, time.Now(), nil
}
// Start collects initial cpuidle numbers which we can use to
// compute the residency between now and the first Snapshot.
func (cs *CpuidleStateMetrics) Start(ctx context.Context) error {
if cs.numCpus == 0 {
return nil
}
stats, statTime, err := readCpuidleStateTimes(cs.cpuidleFiles)
if err != nil {
return errors.Wrap(err, "failed to collect initial metrics")
}
for name := range stats {
cs.metrics[name] = perf.Metric{Name: "cpu-" + name, Unit: "percent",
Direction: perf.SmallerIsBetter, Multiple: true}
}
cs.metrics[c0State] = perf.Metric{Name: "cpu-" + c0State, Unit: "percent",
Direction: perf.SmallerIsBetter, Multiple: true}
cs.lastStats = stats
cs.lastTime = statTime
return nil
}
// Snapshot computes the cpuidle residency between this and
// the previous snapshot, and reports them as metrics.
func (cs *CpuidleStateMetrics) Snapshot(ctx context.Context, values *perf.Values) error {
if cs.numCpus == 0 {
return nil
}
stats, statTime, err := readCpuidleStateTimes(cs.cpuidleFiles)
if err != nil {
return errors.Wrap(err, "failed to collect metrics")
}
diffs := make(map[string]int64)
for name, stat := range stats {
diffs[name] = stat - cs.lastStats[name]
}
total := statTime.Sub(cs.lastTime).Microseconds() * int64(cs.numCpus)
c0Residency := total
for name, diff := range diffs {
values.Append(cs.metrics[name], float64(diff)/float64(total))
c0Residency -= diff
}
values.Append(cs.metrics[c0State], float64(c0Residency)/float64(total))
cs.lastStats = stats
cs.lastTime = statTime
return nil
}
// Stop does nothing.
func (cs *CpuidleStateMetrics) Stop(ctx context.Context, values *perf.Values) error {
return nil
}