blob: f63552992e85ecc1ddff3d95a8984dbcd0baf6a1 [file] [log] [blame]
// Copyright 2019 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 encoding
import (
"context"
"fmt"
"io/ioutil"
"math"
"regexp"
"strconv"
"strings"
"chromiumos/tast/common/perf"
"chromiumos/tast/errors"
"chromiumos/tast/testing"
)
// This file contains helper functions for parsing metric values from log files generated by video_encode_accelerator_unittest.
var regExpFPS = regexp.MustCompile(`(?m)^Measured encoder FPS: ([+\-]?[0-9.]+)$`)
var regExpEncodeLatency50 = regexp.MustCompile(`(?m)^Encode latency for the 50th percentile: (\d+) us$`)
var regExpEncodeLatency75 = regexp.MustCompile(`(?m)^Encode latency for the 75th percentile: (\d+) us$`)
var regExpEncodeLatency95 = regexp.MustCompile(`(?m)^Encode latency for the 95th percentile: (\d+) us$`)
// ReportFPS reports FPS info from log file and sets as the perf metric.
func ReportFPS(ctx context.Context, p *perf.Values, name, logPath string) error {
b, err := ioutil.ReadFile(logPath)
if err != nil {
return errors.Wrapf(err, "failed to read file %s", logPath)
}
matches := regExpFPS.FindAllStringSubmatch(string(b), -1)
if len(matches) != 1 {
return errors.Errorf("found %d FPS matches in %q; want 1", len(matches), b)
}
fps, err := strconv.ParseFloat(matches[0][1], 64)
if err != nil {
return errors.Wrapf(err, "failed to parse FPS value %q", matches[0][1])
}
p.Set(perf.Metric{
Name: getMetricName(name, "fps"),
Unit: "fps",
Direction: perf.BiggerIsBetter,
}, fps)
testing.ContextLogf(ctx, "> FPS: %.2f", fps)
return nil
}
// ReportEncodeLatency reports encode latency from log file and sets as the perf metrics.
func ReportEncodeLatency(ctx context.Context, p *perf.Values, name, logPath string) error {
b, err := ioutil.ReadFile(logPath)
if err != nil {
return errors.Wrapf(err, "failed to read file %s", logPath)
}
// Iterate over different latency measurements, extracting and reporting each.
for _, st := range []struct {
key string // metric key
re *regexp.Regexp // matches latency stat
}{
{"encode_latency.50_percentile", regExpEncodeLatency50},
{"encode_latency.75_percentile", regExpEncodeLatency75},
{"encode_latency.95_percentile", regExpEncodeLatency95},
} {
match := st.re.FindStringSubmatch(string(b))
if match == nil {
return errors.Errorf("didn't find match for latency %q in %q", st.re, b)
}
val, err := strconv.Atoi(match[1])
if err != nil {
return errors.Wrapf(err, "failed converting %q latency %q", st.key, match[1])
}
p.Set(perf.Metric{
Name: getMetricName(name, st.key),
Unit: "us",
Direction: perf.SmallerIsBetter,
}, float64(val))
testing.ContextLogf(ctx, "> "+getMetricName(name, st.key)+": %vus", val)
}
return nil
}
// ReportCPUUsage reports CPU usage from log file and sets as the perf metric.
func ReportCPUUsage(ctx context.Context, p *perf.Values, name, logPath string) error {
b, err := ioutil.ReadFile(logPath)
if err != nil {
return errors.Wrapf(err, "failed to read file %s", logPath)
}
vstr := strings.TrimSuffix(string(b), "\n")
v, err := strconv.ParseFloat(vstr, 64)
if err != nil {
return errors.Wrapf(err, "failed to parse %q to float", vstr)
}
p.Set(perf.Metric{
Name: getMetricName(name, "cpu_usage"),
Unit: "percent",
Direction: perf.SmallerIsBetter,
}, v)
testing.ContextLogf(ctx, "> CPU usage: %.2f%%", v)
return nil
}
// ReportPowerConsumption reports power consumption from log file and sets as the perf metric.
func ReportPowerConsumption(ctx context.Context, p *perf.Values, name, logPath string) error {
b, err := ioutil.ReadFile(logPath)
if err != nil {
return errors.Wrapf(err, "failed to read file %s", logPath)
}
vstr := strings.TrimSuffix(string(b), "\n")
v, err := strconv.ParseFloat(vstr, 64)
if err != nil {
return errors.Wrapf(err, "failed to parse %q to float", vstr)
}
p.Set(perf.Metric{
Name: getMetricName(name, "power_consumption"),
Unit: "watt",
Direction: perf.SmallerIsBetter,
}, v)
testing.ContextLogf(ctx, "> Power consumption: %.2f", v)
return nil
}
// channelStats records min, max, sum of statistics (e.g. PSNR) for a channel.
type channelStats struct {
min, max, sum float64
num int
}
// report saves min, max, avg perf metrics to p.
func (cs *channelStats) report(p *perf.Values, prefix, unit string) {
setPerf := func(stat string, val float64) {
p.Set(perf.Metric{
Name: prefix + stat,
Unit: unit,
Direction: perf.BiggerIsBetter,
}, val)
}
setPerf("min", cs.min)
setPerf("max", cs.max)
if cs.num > 0 {
setPerf("avg", cs.sum/float64(cs.num))
}
}
// update updates statistics (e.g. PSNR) with the input value.
func (cs *channelStats) update(value float64) {
cs.min = math.Min(value, cs.min)
cs.max = math.Max(value, cs.max)
cs.sum += value
cs.num++
}
// newChannelStats returns a channelStats struct with default values.
func newChannelStats() channelStats { return channelStats{min: math.MaxFloat64} }
// frameStats records frame-wise statistics (e.g. PSNR) for YUV and combined channels.
type frameStats struct{ y, u, v, combined channelStats }
// report saves all stats to p.
// name is the initial arg passed to getMetricName.
// typ is the metric type, e.g. "ssim" or "psnr", and is as the unit of perf metric.
func (fs *frameStats) report(p *perf.Values, name, typ string) {
// For "y", "u", "v" channels, the metric key is formatted as "quality.<typ>.<channel>.<stat>", such as "quality.ssim.y.max".
// For "combined" channel, the metric key is formatted as "quality.<typ>.<stat>", such as "quality.psnr.avg".
prefix := getMetricName(name, fmt.Sprintf("quality.%s.", typ))
fs.y.report(p, prefix+"y.", typ)
fs.u.report(p, prefix+"u.", typ)
fs.v.report(p, prefix+"v.", typ)
fs.combined.report(p, prefix, typ)
}
// newFrameStats returns a frameStats struct with default values.
func newFrameStats() frameStats {
return frameStats{
y: newChannelStats(),
u: newChannelStats(),
v: newChannelStats(),
combined: newChannelStats(),
}
}
// ReportFrameStats reports quality from log file which assumes input is YUV420 (for MSE samples per channel), and sets as the perf metrics.
func ReportFrameStats(p *perf.Values, name, logPath string) error {
b, err := ioutil.ReadFile(logPath)
if err != nil {
return errors.Wrapf(err, "failed to read file %s", logPath)
}
ssim := newFrameStats()
psnr := newFrameStats()
for i, line := range strings.Split(string(b), "\n") {
if i == 0 || line == "" {
// Skip the first CSV header line or blank line.
continue
}
values := strings.Split(line, ",")
if len(values) != 9 {
return errors.Errorf("line %d does not contain 9 comma-separated values %q", i, line)
}
var index, width, height, ssimY, ssimU, ssimV, mseY, mseU, mseV float64
for j, dst := range []*float64{&index, &width, &height, &ssimY, &ssimU, &ssimV, &mseY, &mseU, &mseV} {
if *dst, err = strconv.ParseFloat(values[j], 64); err != nil {
return errors.Wrapf(err, "failed to parse %q in field %d", values[j], j)
}
}
ssim.y.update(ssimY)
ssim.u.update(ssimU)
ssim.v.update(ssimV)
// Weighting of YUV channels for SSIM taken from libvpx.
ssim.combined.update(0.8*ssimY + 0.1*(ssimU+ssimV))
// Samples per MSE score assumes YUV420 subsampling.
psnr.y.update(mseToPSNR(width*height*4/4, 255, mseY))
psnr.u.update(mseToPSNR(width*height*4/4, 255, mseU))
psnr.v.update(mseToPSNR(width*height*4/4, 255, mseV))
psnr.combined.update(mseToPSNR(width*height*6/4, 255, mseY+mseU+mseV))
}
if ssim.y.num == 0 {
return errors.New("frame statistics do not exist")
}
ssim.report(p, name, "ssim")
psnr.report(p, name, "psnr")
return nil
}
// mseToPSNR calculates PSNR from MSE for a frame.
func mseToPSNR(samples, peak, mse float64) float64 {
const maxPSNR = 100.0
// Prevent a divide-by-zero, MSE at 0 is perfect quality (no error).
if mse == 0 {
return maxPSNR
}
psnr := 10.0 * math.Log10(peak*peak*samples/mse)
return math.Min(psnr, maxPSNR)
}
// getMetricName wraps the stream name and key into the metric name.
// For example, name should contain both stream and codec name such like "tulip2-1280x720_h264", key is the metric name such like "fps".
func getMetricName(name, key string) string {
return fmt.Sprintf("%s.%s", name, key)
}