blob: 3b999a1e128a05ceee402df5df90d2eac2c18aa4 [file] [log] [blame]
/*
* 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.framework.tests;
import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner;
import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
import com.android.framework.tests.BandwidthStats.CompareResult;
import com.android.framework.tests.BandwidthStats.ComparisonRecord;
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.CollectingTestListener;
import com.android.tradefed.result.FileInputStreamSource;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.result.InputStreamSource;
import com.android.tradefed.result.LogDataType;
import com.android.tradefed.result.TestResult;
import com.android.tradefed.testtype.IDeviceTest;
import com.android.tradefed.testtype.IRemoteTest;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IRunUtil.IRunnableResult;
import com.android.tradefed.util.MultiMap;
import com.android.tradefed.util.RunUtil;
import com.android.tradefed.util.StreamUtil;
import com.android.tradefed.util.net.HttpHelper;
import com.android.tradefed.util.net.IHttpHelper;
import com.android.tradefed.util.net.IHttpHelper.DataSizeException;
import com.android.tradefed.util.proto.TfMetricProtoUtil;
import org.junit.Assert;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* Test that instruments a bandwidth test, gathers bandwidth metrics, and posts
* the results to the Release Dashboard.
*/
public class BandwidthMicroBenchMarkTest implements IDeviceTest, IRemoteTest {
ITestDevice mTestDevice = null;
@Option(name = "test-package-name", description = "Android test package name.")
private String mTestPackageName;
@Option(name = "test-class-name", description = "Test class name.")
private String mTestClassName;
@Option(name = "test-method-name", description = "Test method name.")
private String mTestMethodName;
@Option(name = "test-label",
description = "Test label to identify the test run.")
private String mTestLabel;
@Option(name = "bandwidth-test-server",
description = "Test label to use when posting to dashboard.",
importance=Option.Importance.IF_UNSET)
private String mTestServer;
@Option(name = "ssid",
description = "The ssid to use for the wifi connection.")
private String mSsid;
@Option(name = "initial-server-poll-interval-ms",
description = "The initial poll interval in msecs for querying the test server.")
private int mInitialPollIntervalMs = 1 * 1000;
@Option(name = "server-total-timeout-ms",
description = "The total timeout in msecs for querying the test server.")
private int mTotalTimeoutMs = 40 * 60 * 1000;
@Option(name = "server-query-op-timeout-ms",
description = "The timeout in msecs for a single operation to query the test server.")
private int mQueryOpTimeoutMs = 2 * 60 * 1000;
@Option(name="difference-threshold",
description="The maximum allowed difference between network stats in percent")
private int mDifferenceThreshold = 5;
@Option(name="server-difference-threshold",
description="The maximum difference between the stats reported by the " +
"server and the device in percent")
private int mServerDifferenceThreshold = 6;
@Option(name = "compact-ru-key",
description = "Name of the reporting unit for pass/fail results")
private String mCompactRuKey;
@Option(name = "iface", description="Network interface on the device to use for stats",
importance = Option.Importance.ALWAYS)
private String mIface;
private static final String TEST_RUNNER = "com.android.bandwidthtest.BandwidthTestRunner";
private static final String TEST_SERVER_QUERY = "query";
private static final String DEVICE_ID_LABEL = "device_id";
private static final String TIMESTAMP_LABEL = "timestamp";
@Override
public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
Assert.assertNotNull(mTestDevice);
Assert.assertNotNull("Need a test server, specify it using --bandwidth-test-server",
mTestServer);
// Run test
IRemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(mTestPackageName,
TEST_RUNNER, mTestDevice.getIDevice());
runner.setMethodName(mTestClassName, mTestMethodName);
if (mSsid != null) {
runner.addInstrumentationArg("ssid", mSsid);
}
runner.addInstrumentationArg("server", mTestServer);
CollectingTestListener collectingListener = new CollectingTestListener();
Assert.assertTrue(
mTestDevice.runInstrumentationTests(runner, collectingListener, listener));
// Collect bandwidth metrics from the instrumentation test out.
Map<String, String> bandwidthTestMetrics = new HashMap<String, String>();
Collection<TestResult> testResults =
collectingListener.getCurrentRunResults().getTestResults().values();
if (testResults != null && testResults.iterator().hasNext()) {
Map<String, String> testMetrics = testResults.iterator().next().getMetrics();
if (testMetrics != null) {
bandwidthTestMetrics.putAll(testMetrics);
}
}
// Fetch the data from the test server.
String deviceId = bandwidthTestMetrics.get(DEVICE_ID_LABEL);
String timestamp = bandwidthTestMetrics.get(TIMESTAMP_LABEL);
Assert.assertNotNull("Failed to fetch deviceId from server", deviceId);
Assert.assertNotNull("Failed to fetch timestamp from server", timestamp);
Map<String, String> serverData = fetchDataFromTestServer(deviceId, timestamp);
// Calculate additional network sanity stats - pre-framework logic network stats
BandwidthUtils bw = new BandwidthUtils(mTestDevice, mIface);
reportPassFail(listener, mCompactRuKey, bw, serverData, bandwidthTestMetrics);
saveFile("/proc/net/dev", "proc_net_dev", listener);
saveFile("/proc/net/xt_qtaguid/stats", "qtaguid_stats", listener);
}
private void saveFile(String remoteFilename, String spongeName,
ITestInvocationListener listener) throws DeviceNotAvailableException {
File f = mTestDevice.pullFile(remoteFilename);
if (f == null) {
CLog.w("Failed to pull %s", remoteFilename);
return;
}
saveFile(spongeName, listener, f);
}
private void saveFile(String spongeName, ITestInvocationListener listener, File file) {
try (InputStreamSource stream = new FileInputStreamSource(file)) {
listener.testLog(spongeName, LogDataType.TEXT, stream);
}
}
/**
* Fetch the bandwidth test data recorded on the test server.
*
* @param deviceId
* @param timestamp
* @return a map of the data that was recorded by the test server.
*/
private Map<String, String> fetchDataFromTestServer(String deviceId, String timestamp) {
IHttpHelper httphelper = new HttpHelper();
MultiMap<String,String> params = new MultiMap<String,String> ();
params.put("device_id", deviceId);
params.put("timestamp", timestamp);
String queryUrl = mTestServer;
if (!queryUrl.endsWith("/")) {
queryUrl += "/";
}
queryUrl += TEST_SERVER_QUERY;
QueryRunnable runnable = new QueryRunnable(httphelper, queryUrl, params);
if (RunUtil.getDefault().runEscalatingTimedRetry(mQueryOpTimeoutMs, mInitialPollIntervalMs,
mQueryOpTimeoutMs, mTotalTimeoutMs, runnable)) {
return runnable.getServerResponse();
} else {
CLog.w("Failed to query test server", runnable.getException());
}
return null;
}
private static class QueryRunnable implements IRunnableResult {
private final IHttpHelper mHttpHelper;
private final String mBaseUrl;
private final MultiMap<String,String> mParams;
private Map<String, String> mServerResponse = null;
private Exception mException = null;
public QueryRunnable(IHttpHelper helper, String testServerUrl,
MultiMap<String,String> params) {
mHttpHelper = helper;
mBaseUrl = testServerUrl;
mParams = params;
}
/**
* Perform a single bandwidth test server query, storing the response or
* the associated exception in case of error.
*/
@Override
public boolean run() {
try {
String serverResponse = mHttpHelper.doGet(mHttpHelper.buildUrl(mBaseUrl, mParams));
mServerResponse = parseServerResponse(serverResponse);
return true;
} catch (IOException e) {
CLog.i("IOException %s when contacting test server", e.getMessage());
mException = e;
} catch (DataSizeException e) {
CLog.i("Unexpected oversized response when contacting test server");
mException = e;
}
return false;
}
/**
* Returns exception.
*
* @return the last {@link Exception} that occurred when performing
* run().
*/
public Exception getException() {
return mException;
}
/**
* Returns the server response.
*
* @return a map of the server response.
*/
public Map<String, String> getServerResponse() {
return mServerResponse;
}
/**
* {@inheritDoc}
*/
@Override
public void cancel() {
// ignore
}
}
/**
* Helper to parse test server's response into a map
* <p>
* Exposed for unit testing.
*
* @param serverResponse {@link String} for the test server http request
* @return a map representation of the server response
*/
public static Map<String, String> parseServerResponse(String serverResponse) {
// No such test run was recorded.
if (serverResponse == null || serverResponse.trim().length() == 0) {
return null;
}
final String[] responseLines = serverResponse.split("\n");
Map<String, String> results = new HashMap<String, String>();
for (String responseLine : responseLines) {
final String[] responsePairs = responseLine.split(" ");
for (String responsePair : responsePairs) {
final String[] pair = responsePair.split(":", 2);
if (pair.length >= 2) {
results.put(pair[0], pair[1]);
} else {
CLog.w("Invalid server response: %s", responsePair);
}
}
}
return results;
}
/**
* Fetch the last stats from event log and calculate the differences.
*
* @throws DeviceNotAvailableException
*/
private boolean evaluateEventLog(ITestInvocationListener listener) throws DeviceNotAvailableException {
// issue a force update of stats
String res = mTestDevice.executeShellCommand("dumpsys netstats poll");
if (!res.contains("Forced poll")) {
CLog.w("Failed to force a poll on the device.");
}
// fetch events log
String log = mTestDevice.executeShellCommand("logcat -d -b events");
if (log != null) {
return evaluateStats("netstats_mobile_sample", log, listener);
}
return false;
}
/**
* Parse a log output for a given key and calculate the network stats.
*
* @param key {@link String} to search for in the log
* @param log obtained from adb logcat -b events
* @param listener the {@link ITestInvocationListener} where to report results.
*/
private boolean evaluateStats(String key, String log, ITestInvocationListener listener) {
File filteredEventLog = null;
BufferedWriter out = null;
boolean passed = true;
try {
filteredEventLog = File.createTempFile(String.format("%s_event_log", key), ".txt");
out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filteredEventLog)));
String[] parts = log.split("\n");
for (int i = parts.length - 1; i > 0; i--) {
String str = parts[i];
if (str.contains(key)) {
out.write(str);
passed = passed && evaluateEventLogLine(str);
}
}
out.flush();
saveFile(key + "_event_log", listener, filteredEventLog);
return passed;
} catch (FileNotFoundException e) {
CLog.w("Could not create file to save event log: %s", e.getMessage());
return false;
} catch (IOException e) {
CLog.w("Could not save event log file: %s", e.getMessage());
} finally {
StreamUtil.close(out);
FileUtil.deleteFile(filteredEventLog);
}
return false;
}
private boolean evaluateEventLogLine(String line) {
int start = line.lastIndexOf("[");
int end = line.lastIndexOf("]");
String subStr = line.substring(start + 1, end);
String[] statsStrArray = subStr.split(",");
if (statsStrArray.length != 13) {
CLog.e("Failed to parse for \"%s\" in log.", line);
return false;
}
long xtRb = Long.parseLong(statsStrArray[4].trim());
long xtTb = Long.parseLong(statsStrArray[5].trim());
long xtRp = Long.parseLong(statsStrArray[6].trim());
long xtTp = Long.parseLong(statsStrArray[7].trim());
long uidRb = Long.parseLong(statsStrArray[8].trim());
long uidTb = Long.parseLong(statsStrArray[9].trim());
long uidRp = Long.parseLong(statsStrArray[10].trim());
long uidTp = Long.parseLong(statsStrArray[11].trim());
BandwidthStats xtStats = new BandwidthStats(xtRb, xtRp, xtTb, xtTp);
BandwidthStats uidStats = new BandwidthStats(uidRb, uidRp, uidTb, uidTp);
boolean result = true;
CompareResult compareResult = xtStats.compareAll(uidStats, mDifferenceThreshold);
result &= compareResult.getResult();
if (!compareResult.getResult()) {
CLog.i("Failure comparing netstats_mobile_sample xt and uid");
printFailures(compareResult);
}
if (!result) {
CLog.i("Failed line: %s", line);
}
return result;
}
/**
* Compare the data reported by instrumentation to uid breakdown reported by the kernel,
* the sum of uid breakdown and the total reported by the kernel and the data reported by
* instrumentation to the data reported by the server.
* @param listener result reporter
* @param compactRuKey key to use when posting to rdb.
* @param utils data parsed from the kernel.
* @param instrumentationData data reported by the test.
* @param serverData data reported by the server.
* @throws DeviceNotAvailableException
*/
private void reportPassFail(ITestInvocationListener listener, String compactRuKey,
BandwidthUtils utils, Map<String, String> serverData,
Map<String, String> instrumentationData) throws DeviceNotAvailableException {
if (compactRuKey == null) return;
int passCount = 0;
int failCount = 0;
// Calculate the difference between what framework reports and what the kernel reports
boolean download = Boolean.parseBoolean(serverData.get("download"));
long frameworkUidBytes = 0;
if (download) {
frameworkUidBytes = Long.parseLong(instrumentationData.get("PROF_rx"));
} else {
frameworkUidBytes = Long.parseLong(instrumentationData.get("PROF_tx"));
}
// Compare data reported by the server and the instrumentation
long serverBytes = Long.parseLong(serverData.get("size"));
float diff = Math.abs(BandwidthStats.computePercentDifference(
serverBytes, frameworkUidBytes));
if (diff < mServerDifferenceThreshold) {
passCount += 1;
} else {
CLog.i("Comparing between server and instrumentation failed expected %d got %d",
serverBytes, frameworkUidBytes);
failCount += 1;
}
if (evaluateEventLog(listener)) {
passCount += 1;
} else {
failCount += 1;
}
Map<String, String> postMetrics = new HashMap<String, String>();
postMetrics.put("Pass", String.valueOf(passCount));
postMetrics.put("Fail", String.valueOf(failCount));
listener.testRunStarted(compactRuKey, 0);
listener.testRunEnded(0, TfMetricProtoUtil.upgradeConvert(postMetrics));
}
private void printFailures(CompareResult result) {
for (ComparisonRecord failure : result.getFailures()) {
CLog.i(failure.toString());
}
}
/**
* {@inheritDoc}
*/
@Override
public void setDevice(ITestDevice device) {
mTestDevice = device;
}
/**
* {@inheritDoc}
*/
@Override
public ITestDevice getDevice() {
return mTestDevice;
}
}