1
0
mirror of https://github.com/ARM-software/workload-automation.git synced 2025-04-13 14:20:50 +01:00

Googlephotos productivity workload

A new workload to perform standard productivity tasks within
Googlephotos. Each user event/step is timed and reported back as a
metric. Dumpsys also captures SurfaceFlinger logs for each event for
post analysis.
This commit is contained in:
John Richardson 2016-05-03 11:49:05 +01:00
parent e1827ffde2
commit c2a68074be
6 changed files with 508 additions and 0 deletions

View File

@ -0,0 +1,99 @@
import os
import re
from wlauto import AndroidUiAutoBenchmark, Parameter
class Googlephotos(AndroidUiAutoBenchmark):
name = 'googlephotos'
package = 'com.google.android.apps.photos'
activity = 'com.google.android.apps.photos.home.HomeActivity'
description = """
A workload to perform standard productivity tasks with googlephotos.
The workload carries out various tasks, such as browsing images, performing zooms,
postprocessing and saving a selected image to file.
NOTE: This workload requires four jpeg files to be placed in the
dependencies directory to run.
Although this workload attempts to be network independent it requires a
network connection (ideally, wifi) to run. This is because the welcome
screen UI is dependent on an existing connection.
"""
parameters = [
Parameter('dumpsys_enabled', kind=bool, default=True,
description="""
If ``True``, dumpsys captures will be carried out during the
test run. The output is piped to log files which are then
pulled from the phone.
"""),
]
instrumentation_log = ''.join([name, '_instrumentation.log'])
file_prefix = 'wa_test_'
def __init__(self, device, **kwargs):
super(Googlephotos, self).__init__(device, **kwargs)
self.output_file = os.path.join(self.device.working_directory, self.instrumentation_log)
self.camera_dir = self.device.path.join(self.device.external_storage_directory,
'DCIM/Camera/')
def validate(self):
super(Googlephotos, self).validate()
self.uiauto_params['package'] = self.package
self.uiauto_params['output_dir'] = self.device.working_directory
self.uiauto_params['output_file'] = self.output_file
self.uiauto_params['dumpsys_enabled'] = self.dumpsys_enabled
def setup(self, context):
super(Googlephotos, self).setup(context)
for entry in os.listdir(self.dependencies_directory):
wa_file = ''.join([self.file_prefix, entry])
if entry.endswith(".jpg"):
self.device.push_file(os.path.join(self.dependencies_directory, entry),
os.path.join(self.camera_dir, wa_file),
timeout=300)
# Force a re-index of the mediaserver cache to pick up new files
self.device.execute('am broadcast -a android.intent.action.MEDIA_MOUNTED -d file:///sdcard')
def update_result(self, context):
super(Googlephotos, self).update_result(context)
if self.dumpsys_enabled:
self.device.pull_file(self.output_file, context.output_directory)
result_file = os.path.join(context.output_directory, self.instrumentation_log)
with open(result_file, 'r') as wfh:
pattern = r'(?P<key>\w+)\s+(?P<value1>\d+)\s+(?P<value2>\d+)\s+(?P<value3>\d+)'
regex = re.compile(pattern)
for line in wfh:
match = regex.search(line)
if match:
context.result.add_metric((match.group('key') + "_start"),
match.group('value1'))
context.result.add_metric((match.group('key') + "_finish"),
match.group('value2'))
context.result.add_metric((match.group('key') + "_duration"),
match.group('value3'))
def teardown(self, context):
super(Googlephotos, self).teardown(context)
for entry in self.device.listdir(self.device.working_directory):
if entry.startswith(self.name) and entry.endswith(".log"):
self.device.pull_file(os.path.join(self.device.working_directory, entry),
context.output_directory)
self.device.delete_file(os.path.join(self.device.working_directory, entry))
for entry in self.device.listdir(self.camera_dir):
if entry.startswith(self.file_prefix) and entry.endswith(".jpg"):
self.device.delete_file(os.path.join(self.camera_dir, entry))
# Force a re-index of the mediaserver cache to removed cached files
self.device.execute('am broadcast -a android.intent.action.MEDIA_MOUNTED -d file:///sdcard')

View File

@ -0,0 +1,12 @@
#!/bin/bash
class_dir=bin/classes/com/arm/wlauto/uiauto
base_class=`python -c "import os, wlauto; print os.path.join(os.path.dirname(wlauto.__file__), 'common', 'android', '*.class')"`
mkdir -p $class_dir
cp $base_class $class_dir
ant build
if [[ -f bin/com.arm.wlauto.uiauto.googlephotos.jar ]]; then
cp bin/com.arm.wlauto.uiauto.googlephotos.jar ..
fi

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="com.arm.wlauto.uiauto.googlephotos" default="help">
<!-- The local.properties file is created and updated by the 'android' tool.
It contains the path to the SDK. It should *NOT* be checked into
Version Control Systems. -->
<property file="local.properties" />
<!-- The ant.properties file can be created by you. It is only edited by the
'android' tool to add properties to it.
This is the place to change some Ant specific build properties.
Here are some properties you may want to change/update:
source.dir
The name of the source directory. Default is 'src'.
out.dir
The name of the output directory. Default is 'bin'.
For other overridable properties, look at the beginning of the rules
files in the SDK, at tools/ant/build.xml
Properties related to the SDK location or the project target should
be updated using the 'android' tool with the 'update' action.
This file is an integral part of the build system for your
application and should be checked into Version Control Systems.
-->
<property file="ant.properties" />
<!-- if sdk.dir was not set from one of the property file, then
get it from the ANDROID_HOME env var.
This must be done before we load project.properties since
the proguard config can use sdk.dir -->
<property environment="env" />
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
<isset property="env.ANDROID_HOME" />
</condition>
<!-- The project.properties file is created and updated by the 'android'
tool, as well as ADT.
This contains project specific properties such as project target, and library
dependencies. Lower level build properties are stored in ant.properties
(or in .classpath for Eclipse projects).
This file is an integral part of the build system for your
application and should be checked into Version Control Systems. -->
<loadproperties srcFile="project.properties" />
<!-- quick check on sdk.dir -->
<fail
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
unless="sdk.dir"
/>
<!--
Import per project custom build rules if present at the root of the project.
This is the place to put custom intermediary targets such as:
-pre-build
-pre-compile
-post-compile (This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override ${out.dex.input.absolute.dir})
-post-package
-post-build
-pre-clean
-->
<import file="custom_rules.xml" optional="true" />
<!-- Import the actual build file.
To customize existing targets, there are two options:
- Customize only one target:
- copy/paste the target into this file, *before* the
<import> task.
- customize it to your needs.
- Customize the whole content of build.xml
- copy/paste the content of the rules files (minus the top node)
into this file, replacing the <import> task.
- customize to your needs.
***********************
****** IMPORTANT ******
***********************
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
in order to avoid having your file be overridden by tools such as "android update project"
-->
<!-- version-tag: VERSION_TAG -->
<import file="${sdk.dir}/tools/ant/uibuild.xml" />
</project>

View File

@ -0,0 +1,14 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system edit
# "ant.properties", and override values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-23

View File

@ -0,0 +1,291 @@
package com.arm.wlauto.uiauto.googlephotos;
import android.os.Bundle;
import android.graphics.Point;
import android.graphics.Rect;
// Import the uiautomator libraries
import com.android.uiautomator.core.UiObject;
import com.android.uiautomator.core.UiObjectNotFoundException;
import com.android.uiautomator.core.UiSelector;
import com.arm.wlauto.uiauto.UxPerfUiAutomation;
import java.io.File;
import java.io.FileWriter;
import java.io.BufferedWriter;
import java.util.concurrent.TimeUnit;
import java.util.LinkedHashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
public class UiAutomation extends UxPerfUiAutomation {
public static String TAG = "uxperf_googlephotos";
public Bundle parameters;
private long viewTimeout = TimeUnit.SECONDS.toMillis(20);
private LinkedHashMap<String, Timer> timingResults = new LinkedHashMap<String, Timer>();
public void runUiAutomation() throws Exception {
Timer result = new Timer();
result.start();
parameters = getParams();
dismissWelcomeView();
gesturesTest();
editPhotoTest();
result.end();
timingResults.put("total", result);
writeResultsToFile(timingResults, parameters.getString("output_file"));
}
private void dismissWelcomeView() throws Exception {
// Click through the first two pages and make sure that we don't sign
// in to our google account. This ensures the same set of photographs
// are placed in the camera directory for each run.
sleep(3); // Pause while splash screen loads
UiObject getStarteddButton =
getUiObjectByResourceId("com.google.android.apps.photos:id/get_started",
"android.widget.Button");
getStarteddButton.clickAndWaitForNewWindow();
UiObject welcomeButton =
getUiObjectByResourceId("com.google.android.apps.photos:id/name",
"android.widget.TextView");
welcomeButton.clickAndWaitForNewWindow();
UiObject useWithoutAccount =
getUiObjectByText("Use without an account", "android.widget.TextView");
useWithoutAccount.clickAndWaitForNewWindow();
// Dismiss welcome views promoting app features
sleep(1);
uiDeviceSwipeLeft(10);
sleep(1);
uiDeviceSwipeLeft(10);
sleep(1);
uiDeviceSwipeLeft(10);
sleep(1);
UiObject nextButton =
getUiObjectByResourceId("com.google.android.apps.photos:id/next_button",
"android.widget.ImageView");
nextButton.clickAndWaitForNewWindow();
}
private void gesturesTest () throws Exception {
String testTag = "gestures";
// Perform a range of swipe tests while browsing photo gallery
LinkedHashMap<String, GestureTestParams> testParams = new LinkedHashMap<String, GestureTestParams>();
testParams.put("swipe_left", new GestureTestParams(GestureType.UIDEVICE_SWIPE, Direction.LEFT, 10));
testParams.put("pinch_out", new GestureTestParams(GestureType.PINCH, PinchType.OUT, 100, 50));
testParams.put("pinch_in", new GestureTestParams(GestureType.PINCH, PinchType.IN, 100, 50));
testParams.put("swipe_right", new GestureTestParams(GestureType.UIDEVICE_SWIPE, Direction.RIGHT, 10));
testParams.put("swipe_up", new GestureTestParams(GestureType.UIDEVICE_SWIPE, Direction.UP, 10));
Iterator<Entry<String, GestureTestParams>> it = testParams.entrySet().iterator();
// Select third photograph
selectPhoto(2);
while (it.hasNext()) {
Map.Entry<String, GestureTestParams> pair = it.next();
GestureType type = pair.getValue().gestureType;
Direction dir = pair.getValue().gestureDirection;
PinchType pinch = pair.getValue().pinchType;
int steps = pair.getValue().steps;
int percent = pair.getValue().percent;
String runName = String.format(testTag + "_" + pair.getKey());
String gfxInfologName = String.format(TAG + "_" + runName + "_gfxInfo.log");
String surfFlingerlogName = String.format(runName + "_surfFlinger.log");
String viewName = new String("com.google.android.apps.photos.home.HomeActivity");
UiObject view = new UiObject(new UiSelector().enabled(true));
if (!view.waitForExists(viewTimeout)) {
throw new UiObjectNotFoundException("Could not find \"photo view\".");
};
startDumpsysGfxInfo();
startDumpsysSurfaceFlinger(viewName);
Timer results = new Timer();
switch (type) {
case UIDEVICE_SWIPE:
results = uiDeviceSwipeTest(dir, steps);
break;
case UIOBJECT_SWIPE:
results = uiObjectSwipeTest(view, dir, steps);
break;
case PINCH:
results = uiObjectVertPinchTest(view, pinch, steps, percent);
break;
default:
break;
}
stopDumpsysSurfaceFlinger(viewName, surfFlingerlogName);
stopDumpsysGfxInfo(gfxInfologName);
timingResults.put(runName, results);
}
}
private void editPhotoTest() throws Exception {
String testTag = "edit_photo";
Timer result = new Timer();
result.start();
// Select first photograph
selectPhoto(0);
UiObject editView = getUiObjectByResourceId("com.google.android.apps.photos:id/edit",
"android.widget.ImageView");
editView.click();
UiObject editColor = getUiObjectByText("Colour", "android.widget.RadioButton");
editColor.click();
UiObject seekBar = getUiObjectByResourceId("com.google.android.apps.photos:id/cpe_strength_seek_bar",
"android.widget.SeekBar");
seekBar.swipeLeft(10);
UiObject accept = getUiObjectByDescription("Accept", "android.widget.ImageView");
accept.click();
UiObject save = getUiObjectByText("SAVE", "android.widget.TextView");
save.click();
// Return to application home screen
getUiDevice().pressBack();
result.end();
timingResults.put(testTag, result);
}
// Helper to click on an individual photographs based on index in Camera gallery.
private void selectPhoto(int index) throws Exception {
UiObject cameraHeading = new UiObject(new UiSelector().text("Camera"));
cameraHeading.clickAndWaitForNewWindow();
UiObject photo =
new UiObject(new UiSelector().resourceId("com.google.android.apps.photos:id/recycler_view")
.childSelector(new UiSelector()
.index(index)));
photo.click();
}
// Helper for testing zoom facility. NOTE: the built in UiObject methods
// pinchIn() and pinchOut() do not zoom appropriately for this application.
private Timer uiObjectVertPinchTest(
UiObject view, PinchType direction,
int steps, int percent) throws Exception {
Timer results = new Timer();
results.start();
final int FINGER_TOUCH_HALF_WIDTH = 20;
// make value between 1 and 100
percent = (percent < 0) ? 1 : (percent > 100) ? 100 : percent;
float percentage = percent / 100f;
Rect rect = view.getVisibleBounds();
if (rect.width() <= FINGER_TOUCH_HALF_WIDTH * 2)
throw new IllegalStateException("Object width is too small for operation");
// start from the same point at the center of the control
Point startPoint1 = new Point(rect.centerX(), rect.centerY() + FINGER_TOUCH_HALF_WIDTH);
Point startPoint2 = new Point(rect.centerX(), rect.centerY() - FINGER_TOUCH_HALF_WIDTH);
// End at the top-center and bottom-center of the control
Point endPoint1 = new Point(rect.centerX(), rect.centerY() + (int) ((rect.height() / 2) * percentage));
Point endPoint2 = new Point(rect.centerX(), rect.centerY() - (int) ((rect.height() / 2) * percentage));
if (direction.equals(PinchType.IN)) {
view.performTwoPointerGesture(endPoint1, endPoint2, startPoint1, startPoint2, steps);
} else if (direction.equals(PinchType.OUT)) {
view.performTwoPointerGesture(startPoint1, startPoint2, endPoint1, endPoint2, steps);
}
results.end();
return results;
}
private class GestureTestParams {
GestureType gestureType;
Direction gestureDirection;
PinchType pinchType;
private int percent;
private int steps;
GestureTestParams(GestureType gesture, Direction direction, int steps) {
this.gestureType = gesture;
this.gestureDirection = direction;
this.pinchType = PinchType.NULL;
this.steps = steps;
this.percent = 0;
}
GestureTestParams(GestureType gesture, PinchType pinchType, int steps, int percent) {
this.gestureType = gesture;
this.gestureDirection = Direction.NULL;
this.pinchType = pinchType;
this.steps = steps;
this.percent = percent;
}
}
private void writeResultsToFile(LinkedHashMap timingResults, String file) throws Exception {
// Write out the key/value pairs to the instrumentation log file
FileWriter fstream = new FileWriter(file);
BufferedWriter out = new BufferedWriter(fstream);
Iterator<Entry<String, Timer>> it = timingResults.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, Timer> pairs = it.next();
Timer results = pairs.getValue();
long start = results.getStart();
long finish = results.getFinish();
long duration = results.getDuration();
out.write(String.format(pairs .getKey() + " " + start + " " + finish + " " + duration + "\n"));
}
out.close();
}
private void startDumpsysSurfaceFlinger(String view) {
if (Boolean.parseBoolean(parameters.getString("dumpsys_enabled"))) {
initDumpsysSurfaceFlinger(parameters.getString("package"), view);
}
}
private void stopDumpsysSurfaceFlinger(String view, String filename) throws Exception {
if (Boolean.parseBoolean(parameters.getString("dumpsys_enabled"))) {
File out_file = new File(parameters.getString("output_dir"), filename);
exitDumpsysSurfaceFlinger(parameters.getString("package"), view, out_file);
}
}
private void startDumpsysGfxInfo() {
if (Boolean.parseBoolean(parameters.getString("dumpsys_enabled"))) {
initDumpsysGfxInfo(parameters.getString("package"));
}
}
private void stopDumpsysGfxInfo(String filename) throws Exception {
if (Boolean.parseBoolean(parameters.getString("dumpsys_enabled"))) {
File out_file = new File(parameters.getString("output_dir"), filename);
exitDumpsysGfxInfo(parameters.getString("package"), out_file);
}
}
}