blob: 241d5a9ac81f5694ba17f8bac5d773848fb50a46 [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 arc
import (
"context"
"strings"
"time"
"chromiumos/tast/common/android/ui"
"chromiumos/tast/common/perf"
"chromiumos/tast/errors"
"chromiumos/tast/local/arc"
"chromiumos/tast/local/bundles/cros/arc/screen"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/chrome/display"
"chromiumos/tast/local/cpu"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: ScreenRotationPerf,
LacrosStatus: testing.LacrosVariantUnknown,
Desc: "Test ARC rotation performance",
Contacts: []string{
"khmel@chromium.org", // Maintainer.
"arc-framework+tast@google.com",
"ricardoq@chromium.org", // Tast port author.
},
Attr: []string{"group:crosbolt", "crosbolt_perbuild"},
SoftwareDeps: []string{"chrome"},
Fixture: "arcBooted",
// Sunflower.apk taken from: https://github.com/googlesamples/android-sunflower
// Commit hash: ce82cffeed8150cf97789065898f08f29a2a1c9b
Data: []string{"Sunflower.apk"},
Timeout: 8 * time.Minute,
Params: []testing.Param{{
ExtraSoftwareDeps: []string{"android_p"},
}, {
Name: "vm",
ExtraSoftwareDeps: []string{"android_vm"},
}},
})
}
func ScreenRotationPerf(ctx context.Context, s *testing.State) {
cr := s.FixtValue().(*arc.PreData).Chrome
a := s.FixtValue().(*arc.PreData).ARC
d := s.FixtValue().(*arc.PreData).UIDevice
tconn, err := cr.TestAPIConn(ctx)
if err != nil {
s.Fatal("Failed to get a Test API connection: ", err)
}
dispInfo, err := display.GetInternalInfo(ctx, tconn)
if err != nil {
s.Fatal("Failed to get internal display info: ", err)
}
const apkName = "Sunflower.apk"
s.Log("Installing ", apkName)
if err := a.Install(ctx, s.DataPath(apkName)); err != nil {
s.Fatal("Failed installing app: ", err)
}
const (
pkgName = "com.google.samples.apps.sunflower"
actName = ".GardenActivity"
)
act, err := arc.NewActivity(a, pkgName, actName)
if err != nil {
s.Fatal("Failed to create new activity: ", err)
}
defer act.Close()
s.Logf("Starting activity: %s/%s", pkgName, actName)
if err := act.StartWithDefaultOptions(ctx, tconn); err != nil {
s.Fatal("Failed start activity: ", err)
}
// Switch to the "Plant list" tab, which contains many widgets, they cover the entire screen,
// and they relayout when the screen rotates.
obj := d.Object(ui.ClassName("androidx.appcompat.app.ActionBar$Tab"), ui.Description("Plant list"))
if err := obj.WaitForExists(ctx, 10*time.Second); err != nil {
s.Fatal("Failed to find 'Plant list' widget: ", err)
}
if err := obj.Click(ctx); err != nil {
s.Fatal("Could not switch to 'Plant list' tab: ", err)
}
// Leave Chromebook in reasonable state.
rot0 := 0
p := display.DisplayProperties{Rotation: &rot0}
defer display.SetDisplayProperties(ctx, tconn, dispInfo.ID, p)
// And right before starting the perf test, wait for an idle CPU.
if err := cpu.WaitUntilIdle(ctx); err != nil {
s.Fatal("Failed to wait for idle CPU: ", err)
}
samples, err := grabPerfSamples(ctx, tconn, a, d, pkgName, dispInfo.ID)
if err != nil {
s.Fatal("Failed to grab performance samples: ", err)
}
values := perf.NewValues()
for pm, v := range samples {
values.Append(pm, v...)
accum := 0.0
for _, n := range v {
accum += n
}
s.Logf("Average: %q = %v", pm.Name, accum/float64(len(v)))
}
if err := values.Save(s.OutDir()); err != nil {
s.Fatal("Failed to save perf sample file: ", err)
}
}
// grabPerfSamples runs the performance test and returns the samples.
// The performance test consists of measuring how expensive, GFX-wise, is to rotate the device.
// The information is taken from "dumpsys gfxinfo".
func grabPerfSamples(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, pkgName, dispID string) (samples map[perf.Metric][]float64, err error) {
samples = make(map[perf.Metric][]float64)
const samplesPerRotation = 10
for i := 0; i < samplesPerRotation; i++ {
testing.ContextLog(ctx, "Iteration number: ", i)
for _, rot := range []display.RotationAngle{
display.Rotate90,
display.Rotate180,
display.Rotate270,
display.Rotate0,
} {
testing.ContextLog(ctx, "Rotating to: ", rot)
// Samples are grouped by vertical/horizontal rotation.
keySuffix := "-horizontal"
if rot == display.Rotate90 || rot == display.Rotate270 {
keySuffix = "-vertical"
}
// Before resetting the stats, it is important to wait until the activity
// is not generating new frame captures, otherwise it will generate "noise"
// in the stats.
if err := screen.WaitForStableFrames(ctx, a, pkgName); err != nil {
return nil, err
}
if err := gfxinfoResetStats(ctx, a, pkgName); err != nil {
return nil, err
}
if err := rotateDisplaySync(ctx, tconn, d, dispID, rot); err != nil {
return nil, err
}
stats, err := screen.GfxinfoDumpStats(ctx, a, pkgName)
if err != nil {
return nil, err
}
numFrames := stats[screen.KeyTotalFramesRendered]
if numFrames == 0 {
testing.ContextLog(ctx, "Ignoring stats since no frames were captured during the screen rotation")
continue
}
testing.ContextLogf(ctx, "Captured frames: %d", numFrames)
for key, value := range stats {
if err != nil {
return nil, err
}
if key == screen.KeyJankyFrames {
// Since "Janky frames" is meaningless by itself, we track the "Janky percentage" instead.
key = "Janky percentage"
value = 100 * value / numFrames
}
dir := perf.SmallerIsBetter
if strings.HasPrefix(key, screen.KeyTotalFramesRendered) {
dir = perf.BiggerIsBetter
}
unit := "count"
if strings.Contains(key, "percentage") {
unit = "percent"
} else if strings.Contains(key, "percentile") {
unit = "ms"
}
// Update 'key' at the latest to avoid interference with the comparisons.
// perf/perf.go defines the list of valid chars for the metric name.
key = key + keySuffix
key = strings.Replace(key, " ", "_", -1)
m := perf.Metric{
Name: key,
Unit: unit,
Direction: dir,
Multiple: true,
}
samples[m] = append(samples[m], float64(value))
}
}
}
return samples, nil
}
// gfxinfoResetStats resets the graphics stats associated with a package name.
func gfxinfoResetStats(ctx context.Context, a *arc.ARC, pkgName string) error {
return a.Command(ctx, "dumpsys", "gfxinfo", pkgName, "reset").Run()
}
// rotateDisplaySync rotates to display to a given angle. Waits until the rotation is complete in the Android side.
func rotateDisplaySync(ctx context.Context, tconn *chrome.TestConn, d *ui.Device, dispID string, rot display.RotationAngle) error {
// Android rotations as defined in Surface.java
// https://android.googlesource.com/platform/frameworks/base/+/refs/heads/android10-dev/core/java/android/view/Surface.java
rots := map[int]display.RotationAngle{
0: display.Rotate0, // ROTATION_0
1: display.Rotate90, // ROTATION_90
2: display.Rotate180, // ROTATION_180
3: display.Rotate270, // ROTATION_270
}
// To be sure that rotation has finished we do:
// - Start rotation from Ash.
// - Wait until Android reports that it has the desired rotation.
if err := display.SetDisplayRotationSync(ctx, tconn, dispID, rot); err != nil {
return errors.Wrap(err, "failed to wait for display rotation")
}
if err := testing.Poll(ctx, func(ctx context.Context) error {
info, err := d.GetInfo(ctx)
if err != nil {
return err
}
if val, ok := rots[info.DisplayRotation]; !ok {
return testing.PollBreak(errors.Errorf("unexpected rotation value: %v", info.DisplayRotation))
} else if val != rot {
return errors.Errorf("invalid rotation: want %q, got %q", rot, val)
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second}); err != nil {
return errors.Wrap(err, "failed to rotate device")
}
return nil
}