/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.performance.tests;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.testrunner.ITestRunListener.TestFailure;
import com.android.ddmlib.testrunner.TestIdentifier;
import com.android.tradefed.config.Option;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.SnapshotInputStreamSource;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.StreamUtil;
import junit.framework.Assert;
import junit.framework.TestCase;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Runs the app launch test.
* <p>
* Launches each app and records the amount of time it took to launch the app.
* </p>
*/
public class AppLaunchMetricsTest implements IDeviceTest, IRemoteTest {
private static final String APP_LAUNCH = "/data/framework/app_launch";
private static final String APP_LIST_FILE = "launch_list.txt";
private static final String APP_OUTPUT_FILE = "launch_perf_output.txt";
private static final String TEST_KEY = "ApplicationStartupTime";
private static final String LAUNCH_TIME_NAME = "app_launch_times";
private static final String BUGREPORT_NAME = "app_launch_bugreport";
/** The pattern to match the app-name argument */
private static final Pattern APP_NAME_PATTERN = Pattern.compile("(.+),(.+)");
/** The pattern of the output */
private static final Pattern APP_TIME_PATTERN = Pattern.compile("(.+)\\|(\\d+)");
private ITestDevice mTestDevice;
private String mAppListPath = null;
private String mAppOutputPath = null;
@Option(name = "app-name", description = "The name of the app in the launcher or an app, key "
+ "pair. E.G. \"Browser\" or \"Browser,android_browser\". May be repeated.")
private Collection<String> mAppNames = new ArrayList<String>();
/**
* Class that stores useful info about the app.
*/
static class AppInfo {
private String mName = null;
private String mOutputKey = null;
private String mPostKey = null;
private Integer mTime = null;
public AppInfo(String name) {
mName = name;
mPostKey = mOutputKey = makeOutputKey(name);
}
public AppInfo(String name, String key) {
mName = name;
mOutputKey = makeOutputKey(name);
mPostKey = key;
}
public String getAppListEntry() {
return String.format("%s,%s\n", mName, mPostKey);
}
public String getName() {
return mName;
}
public String getPostKey() {
return mPostKey;
}
public String getOutputKey() {
return mOutputKey;
}
public Integer getTime() {
return mTime;
}
public void setTime(Integer time) {
mTime = time;
}
private String makeOutputKey(String name) {
return name.toLowerCase().replaceAll(" ", "");
}
}
private Map<String, AppInfo> mAppInfos = new HashMap<String, AppInfo>();
/**
* {@inheritDoc}
*/
@Override
public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
Assert.assertNotNull(mTestDevice);
mAppListPath = new File(mTestDevice.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE),
APP_LIST_FILE).getAbsolutePath();
mAppOutputPath = new File(mTestDevice.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE),
APP_OUTPUT_FILE).getAbsolutePath();
setupAppInfos();
// Setup the device
mTestDevice.executeShellCommand(String.format("rm %s %s", mAppListPath, mAppOutputPath));
mTestDevice.pushString(generateAppList(), mAppListPath);
mTestDevice.executeShellCommand(String.format("chmod 750 %s", APP_LAUNCH));
// Sleep 30 seconds to let device settle.
RunUtil.getDefault().sleep(30 * 1000);
// Run the test
String output = mTestDevice.executeShellCommand(APP_LAUNCH);
CLog.d("App launch output: %s", output);
logOutputFile(listener);
}
/**
* Sets up the {@link AppInfo} map based on the app-name args.
* <p>
* Generates from {@link appNames}, a collection of Strings formated as either an app name or as
* an app name, key pair with a comma separator. The key for the map will be the lowercase name
* with spaces removed.
* </p>
*/
private void setupAppInfos() {
for (String app : mAppNames) {
Matcher m = APP_NAME_PATTERN.matcher(app);
AppInfo info;
if (m.matches()) {
info = new AppInfo(m.group(1), m.group(2));
} else {
info = new AppInfo(app);
}
mAppInfos.put(info.getOutputKey(), info);
}
}
/**
* Generate the app list as a String.
*
* @return the app list to push to the device.
*/
private String generateAppList() {
StringBuilder sb = new StringBuilder();
for (AppInfo info : mAppInfos.values()) {
sb.append(info.getAppListEntry());
}
return sb.toString();
}
/**
* Parses and logs the output file.
*
* @param listener the {@link ITestInvocationListener}
* @throws DeviceNotAvailableException If the device is not available.
*/
private void logOutputFile(ITestInvocationListener listener)
throws DeviceNotAvailableException {
File outputFile = null;
InputStreamSource outputSource = null;
try {
outputFile = mTestDevice.pullFile(mAppOutputPath);
if (outputFile != null) {
outputSource = new SnapshotInputStreamSource(new FileInputStream(outputFile));
listener.testLog(LAUNCH_TIME_NAME, LogDataType.TEXT, outputSource);
parseOutputFile(StreamUtil.getStringFromStream(new BufferedInputStream(
new FileInputStream(outputFile))));
}
} catch(IOException e) {
CLog.e("Got IOException: %s", e);
} finally {
if (outputFile != null) {
outputFile.delete();
}
if (outputSource != null) {
outputSource.cancel();
}
}
if (shouldTakeBugreport()) {
InputStreamSource bugreport = mTestDevice.getBugreport();
try {
listener.testLog(BUGREPORT_NAME, LogDataType.TEXT, bugreport);
} finally {
bugreport.cancel();
}
}
reportMetrics(listener);
}
/**
* Parses the output file and populate the {@link AppInfo} objects with the launch times.
*
* @param contents The file contents.
* @throws IOException If an IOException is caused.
*/
private void parseOutputFile(String contents) throws IOException {
for (String line : contents.split("\n")) {
Matcher m = APP_TIME_PATTERN.matcher(line);
if (m.matches()) {
AppInfo appInfo = mAppInfos.get(m.group(1).toLowerCase());
if (appInfo != null) {
appInfo.setTime(Integer.parseInt(m.group(2)));
}
}
}
}
/**
* Report the metrics and attach it to the listener.
* <p>
* If any of the app times are {@code null}, that app is assumed to not have launched and will
* be marked as failed.
* </p>
* @param listener the {@link ITestInvocationListener}
*/
private void reportMetrics(ITestInvocationListener listener) {
listener.testRunStarted(TEST_KEY, 0);
Map<String, String> metrics = new HashMap<String, String>();
for (AppInfo appInfo : mAppInfos.values()) {
TestIdentifier testId = new TestIdentifier(getClass().getCanonicalName(),
appInfo.getPostKey());
listener.testStarted(testId);
if (appInfo.getTime() != null) {
metrics.put(appInfo.getPostKey(), Integer.toString(appInfo.getTime()));
} else {
listener.testFailed(TestFailure.FAILURE, testId, "No app launch time");
}
Map<String, String> empty = Collections.emptyMap();
listener.testEnded(testId, empty);
}
CLog.d("About to report app launch metrics: %s", metrics);
listener.testRunEnded(0, metrics);
}
/**
* If a bugreport should be taken after the run.
*
* @return true if any of the apps have a {@code null} launch time.
*/
private boolean shouldTakeBugreport() {
for (AppInfo appInfo : mAppInfos.values()) {
if (appInfo.getTime() == null) {
return true;
}
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public void setDevice(ITestDevice device) {
mTestDevice = device;
}
/**
* {@inheritDoc}
*/
@Override
public ITestDevice getDevice() {
return mTestDevice;
}
public static class MetaTest extends TestCase {
AppLaunchMetricsTest mTestInstance = null;
@Override
public void setUp() throws Exception {
mTestInstance = new AppLaunchMetricsTest();
mTestInstance.mAppNames.add("App 1");
mTestInstance.mAppNames.add("App 2,key2");
}
public void testAppInfo() throws Exception {
AppInfo info = new AppInfo("app_name");
assertEquals("app_name", info.getName());
assertEquals("app_name", info.getOutputKey());
assertEquals("app_name", info.getPostKey());
info = new AppInfo("AppName");
assertEquals("AppName", info.getName());
assertEquals("appname", info.getOutputKey());
assertEquals("appname", info.getPostKey());
info = new AppInfo("App Name");
assertEquals("App Name", info.getName());
assertEquals("appname", info.getOutputKey());
assertEquals("appname", info.getPostKey());
info = new AppInfo("App & Name");
assertEquals("App & Name", info.getName());
assertEquals("app&name", info.getOutputKey());
assertEquals("app&name", info.getPostKey());
assertEquals("App & Name,app&name\n", info.getAppListEntry());
info = new AppInfo("App Name", "key");
assertEquals("App Name", info.getName());
assertEquals("appname", info.getOutputKey());
assertEquals("key", info.getPostKey());
assertNull(info.getTime());
info.setTime(0);
assertEquals(new Integer(0), info.getTime());
assertEquals("App Name,key\n", info.getAppListEntry());
}
public void testSetupAppInfos() throws Exception {
mTestInstance.setupAppInfos();
assertEquals(2, mTestInstance.mAppInfos.size());
assertNotNull(mTestInstance.mAppInfos.get("app1"));
assertEquals("App 1", mTestInstance.mAppInfos.get("app1").getName());
assertEquals("app1", mTestInstance.mAppInfos.get("app1").getOutputKey());
assertEquals("app1", mTestInstance.mAppInfos.get("app1").getPostKey());
assertNotNull(mTestInstance.mAppInfos.get("app2"));
assertEquals("App 2", mTestInstance.mAppInfos.get("app2").getName());
assertEquals("app2", mTestInstance.mAppInfos.get("app2").getOutputKey());
assertEquals("key2", mTestInstance.mAppInfos.get("app2").getPostKey());
}
public void testGenerateAppList() throws Exception {
mTestInstance.setupAppInfos();
assertEquals(2, mTestInstance.mAppInfos.size());
assertTrue(mTestInstance.generateAppList().contains("App 1,app1\n"));
assertTrue(mTestInstance.generateAppList().contains("App 2,key2\n"));
}
public void testParseOutputFile_success() throws Exception {
mTestInstance.setupAppInfos();
assertEquals(2, mTestInstance.mAppInfos.size());
mTestInstance.parseOutputFile("app1|1234\napp2|5678\n");
assertFalse(mTestInstance.shouldTakeBugreport());
assertEquals(new Integer(1234), mTestInstance.mAppInfos.get("app1").getTime());
assertEquals(new Integer(5678), mTestInstance.mAppInfos.get("app2").getTime());
}
public void testParseOutputFile_fail() throws Exception {
mTestInstance.setupAppInfos();
assertEquals(2, mTestInstance.mAppInfos.size());
mTestInstance.parseOutputFile("app1|1234\n");
assertTrue(mTestInstance.shouldTakeBugreport());
assertEquals(new Integer(1234), mTestInstance.mAppInfos.get("app1").getTime());
assertNull(mTestInstance.mAppInfos.get("app2").getTime());
}
}
}