1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.framework.tests; 18 19 import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner; 20 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner; 21 import com.android.framework.tests.BandwidthStats.CompareResult; 22 import com.android.framework.tests.BandwidthStats.ComparisonRecord; 23 import com.android.tradefed.config.Option; 24 import com.android.tradefed.device.DeviceNotAvailableException; 25 import com.android.tradefed.device.ITestDevice; 26 import com.android.tradefed.log.LogUtil.CLog; 27 import com.android.tradefed.result.CollectingTestListener; 28 import com.android.tradefed.result.FileInputStreamSource; 29 import com.android.tradefed.result.ITestInvocationListener; 30 import com.android.tradefed.result.InputStreamSource; 31 import com.android.tradefed.result.LogDataType; 32 import com.android.tradefed.result.TestResult; 33 import com.android.tradefed.testtype.IDeviceTest; 34 import com.android.tradefed.testtype.IRemoteTest; 35 import com.android.tradefed.util.FileUtil; 36 import com.android.tradefed.util.IRunUtil.IRunnableResult; 37 import com.android.tradefed.util.MultiMap; 38 import com.android.tradefed.util.RunUtil; 39 import com.android.tradefed.util.StreamUtil; 40 import com.android.tradefed.util.net.HttpHelper; 41 import com.android.tradefed.util.net.IHttpHelper; 42 import com.android.tradefed.util.net.IHttpHelper.DataSizeException; 43 import com.android.tradefed.util.proto.TfMetricProtoUtil; 44 45 import org.junit.Assert; 46 47 import java.io.BufferedWriter; 48 import java.io.File; 49 import java.io.FileNotFoundException; 50 import java.io.FileOutputStream; 51 import java.io.IOException; 52 import java.io.OutputStreamWriter; 53 import java.util.Collection; 54 import java.util.HashMap; 55 import java.util.Map; 56 57 /** 58 * Test that instruments a bandwidth test, gathers bandwidth metrics, and posts 59 * the results to the Release Dashboard. 60 */ 61 public class BandwidthMicroBenchMarkTest implements IDeviceTest, IRemoteTest { 62 63 ITestDevice mTestDevice = null; 64 65 @Option(name = "test-package-name", description = "Android test package name.") 66 private String mTestPackageName; 67 68 @Option(name = "test-class-name", description = "Test class name.") 69 private String mTestClassName; 70 71 @Option(name = "test-method-name", description = "Test method name.") 72 private String mTestMethodName; 73 74 @Option(name = "test-label", 75 description = "Test label to identify the test run.") 76 private String mTestLabel; 77 78 @Option(name = "bandwidth-test-server", 79 description = "Test label to use when posting to dashboard.", 80 importance=Option.Importance.IF_UNSET) 81 private String mTestServer; 82 83 @Option(name = "ssid", 84 description = "The ssid to use for the wifi connection.") 85 private String mSsid; 86 87 @Option(name = "initial-server-poll-interval-ms", 88 description = "The initial poll interval in msecs for querying the test server.") 89 private int mInitialPollIntervalMs = 1 * 1000; 90 91 @Option(name = "server-total-timeout-ms", 92 description = "The total timeout in msecs for querying the test server.") 93 private int mTotalTimeoutMs = 40 * 60 * 1000; 94 95 @Option(name = "server-query-op-timeout-ms", 96 description = "The timeout in msecs for a single operation to query the test server.") 97 private int mQueryOpTimeoutMs = 2 * 60 * 1000; 98 99 @Option(name="difference-threshold", 100 description="The maximum allowed difference between network stats in percent") 101 private int mDifferenceThreshold = 5; 102 103 @Option(name="server-difference-threshold", 104 description="The maximum difference between the stats reported by the " + 105 "server and the device in percent") 106 private int mServerDifferenceThreshold = 6; 107 108 @Option(name = "compact-ru-key", 109 description = "Name of the reporting unit for pass/fail results") 110 private String mCompactRuKey; 111 112 @Option(name = "iface", description="Network interface on the device to use for stats", 113 importance = Option.Importance.ALWAYS) 114 private String mIface; 115 116 private static final String TEST_RUNNER = "com.android.bandwidthtest.BandwidthTestRunner"; 117 private static final String TEST_SERVER_QUERY = "query"; 118 private static final String DEVICE_ID_LABEL = "device_id"; 119 private static final String TIMESTAMP_LABEL = "timestamp"; 120 121 122 @Override 123 public void run(ITestInvocationListener listener) throws DeviceNotAvailableException { 124 Assert.assertNotNull(mTestDevice); 125 126 Assert.assertNotNull("Need a test server, specify it using --bandwidth-test-server", 127 mTestServer); 128 129 // Run test 130 IRemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(mTestPackageName, 131 TEST_RUNNER, mTestDevice.getIDevice()); 132 runner.setMethodName(mTestClassName, mTestMethodName); 133 if (mSsid != null) { 134 runner.addInstrumentationArg("ssid", mSsid); 135 } 136 runner.addInstrumentationArg("server", mTestServer); 137 138 CollectingTestListener collectingListener = new CollectingTestListener(); 139 Assert.assertTrue( 140 mTestDevice.runInstrumentationTests(runner, collectingListener, listener)); 141 142 // Collect bandwidth metrics from the instrumentation test out. 143 Map<String, String> bandwidthTestMetrics = new HashMap<String, String>(); 144 Collection<TestResult> testResults = 145 collectingListener.getCurrentRunResults().getTestResults().values(); 146 if (testResults != null && testResults.iterator().hasNext()) { 147 Map<String, String> testMetrics = testResults.iterator().next().getMetrics(); 148 if (testMetrics != null) { 149 bandwidthTestMetrics.putAll(testMetrics); 150 } 151 } 152 153 // Fetch the data from the test server. 154 String deviceId = bandwidthTestMetrics.get(DEVICE_ID_LABEL); 155 String timestamp = bandwidthTestMetrics.get(TIMESTAMP_LABEL); 156 Assert.assertNotNull("Failed to fetch deviceId from server", deviceId); 157 Assert.assertNotNull("Failed to fetch timestamp from server", timestamp); 158 Map<String, String> serverData = fetchDataFromTestServer(deviceId, timestamp); 159 160 // Calculate additional network sanity stats - pre-framework logic network stats 161 BandwidthUtils bw = new BandwidthUtils(mTestDevice, mIface); 162 reportPassFail(listener, mCompactRuKey, bw, serverData, bandwidthTestMetrics); 163 164 saveFile("/proc/net/dev", "proc_net_dev", listener); 165 saveFile("/proc/net/xt_qtaguid/stats", "qtaguid_stats", listener); 166 167 } 168 169 private void saveFile(String remoteFilename, String spongeName, 170 ITestInvocationListener listener) throws DeviceNotAvailableException { 171 File f = mTestDevice.pullFile(remoteFilename); 172 if (f == null) { 173 CLog.w("Failed to pull %s", remoteFilename); 174 return; 175 } 176 177 saveFile(spongeName, listener, f); 178 } 179 180 private void saveFile(String spongeName, ITestInvocationListener listener, File file) { 181 try (InputStreamSource stream = new FileInputStreamSource(file)) { 182 listener.testLog(spongeName, LogDataType.TEXT, stream); 183 } 184 } 185 186 /** 187 * Fetch the bandwidth test data recorded on the test server. 188 * 189 * @param deviceId 190 * @param timestamp 191 * @return a map of the data that was recorded by the test server. 192 */ 193 private Map<String, String> fetchDataFromTestServer(String deviceId, String timestamp) { 194 IHttpHelper httphelper = new HttpHelper(); 195 MultiMap<String,String> params = new MultiMap<String,String> (); 196 params.put("device_id", deviceId); 197 params.put("timestamp", timestamp); 198 String queryUrl = mTestServer; 199 if (!queryUrl.endsWith("/")) { 200 queryUrl += "/"; 201 } 202 queryUrl += TEST_SERVER_QUERY; 203 QueryRunnable runnable = new QueryRunnable(httphelper, queryUrl, params); 204 if (RunUtil.getDefault().runEscalatingTimedRetry(mQueryOpTimeoutMs, mInitialPollIntervalMs, 205 mQueryOpTimeoutMs, mTotalTimeoutMs, runnable)) { 206 return runnable.getServerResponse(); 207 } else { 208 CLog.w("Failed to query test server", runnable.getException()); 209 } 210 return null; 211 } 212 213 private static class QueryRunnable implements IRunnableResult { 214 private final IHttpHelper mHttpHelper; 215 private final String mBaseUrl; 216 private final MultiMap<String,String> mParams; 217 private Map<String, String> mServerResponse = null; 218 private Exception mException = null; 219 220 public QueryRunnable(IHttpHelper helper, String testServerUrl, 221 MultiMap<String,String> params) { 222 mHttpHelper = helper; 223 mBaseUrl = testServerUrl; 224 mParams = params; 225 } 226 227 /** 228 * Perform a single bandwidth test server query, storing the response or 229 * the associated exception in case of error. 230 */ 231 @Override 232 public boolean run() { 233 try { 234 String serverResponse = mHttpHelper.doGet(mHttpHelper.buildUrl(mBaseUrl, mParams)); 235 mServerResponse = parseServerResponse(serverResponse); 236 return true; 237 } catch (IOException e) { 238 CLog.i("IOException %s when contacting test server", e.getMessage()); 239 mException = e; 240 } catch (DataSizeException e) { 241 CLog.i("Unexpected oversized response when contacting test server"); 242 mException = e; 243 } 244 return false; 245 } 246 247 /** 248 * Returns exception. 249 * 250 * @return the last {@link Exception} that occurred when performing 251 * run(). 252 */ 253 public Exception getException() { 254 return mException; 255 } 256 257 /** 258 * Returns the server response. 259 * 260 * @return a map of the server response. 261 */ 262 public Map<String, String> getServerResponse() { 263 return mServerResponse; 264 } 265 266 /** 267 * {@inheritDoc} 268 */ 269 @Override 270 public void cancel() { 271 // ignore 272 } 273 } 274 275 /** 276 * Helper to parse test server's response into a map 277 * <p> 278 * Exposed for unit testing. 279 * 280 * @param serverResponse {@link String} for the test server http request 281 * @return a map representation of the server response 282 */ 283 public static Map<String, String> parseServerResponse(String serverResponse) { 284 // No such test run was recorded. 285 if (serverResponse == null || serverResponse.trim().length() == 0) { 286 return null; 287 } 288 final String[] responseLines = serverResponse.split("\n"); 289 Map<String, String> results = new HashMap<String, String>(); 290 for (String responseLine : responseLines) { 291 final String[] responsePairs = responseLine.split(" "); 292 for (String responsePair : responsePairs) { 293 final String[] pair = responsePair.split(":", 2); 294 if (pair.length >= 2) { 295 results.put(pair[0], pair[1]); 296 } else { 297 CLog.w("Invalid server response: %s", responsePair); 298 } 299 } 300 } 301 return results; 302 } 303 304 /** 305 * Fetch the last stats from event log and calculate the differences. 306 * 307 * @throws DeviceNotAvailableException 308 */ 309 private boolean evaluateEventLog(ITestInvocationListener listener) throws DeviceNotAvailableException { 310 // issue a force update of stats 311 String res = mTestDevice.executeShellCommand("dumpsys netstats poll"); 312 if (!res.contains("Forced poll")) { 313 CLog.w("Failed to force a poll on the device."); 314 } 315 // fetch events log 316 String log = mTestDevice.executeShellCommand("logcat -d -b events"); 317 if (log != null) { 318 return evaluateStats("netstats_mobile_sample", log, listener); 319 } 320 return false; 321 } 322 323 /** 324 * Parse a log output for a given key and calculate the network stats. 325 * 326 * @param key {@link String} to search for in the log 327 * @param log obtained from adb logcat -b events 328 * @param listener the {@link ITestInvocationListener} where to report results. 329 */ 330 private boolean evaluateStats(String key, String log, ITestInvocationListener listener) { 331 File filteredEventLog = null; 332 BufferedWriter out = null; 333 boolean passed = true; 334 335 try { 336 filteredEventLog = File.createTempFile(String.format("%s_event_log", key), ".txt"); 337 out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(filteredEventLog))); 338 String[] parts = log.split("\n"); 339 for (int i = parts.length - 1; i > 0; i--) { 340 String str = parts[i]; 341 if (str.contains(key)) { 342 out.write(str); 343 passed = passed && evaluateEventLogLine(str); 344 } 345 } 346 out.flush(); 347 saveFile(key + "_event_log", listener, filteredEventLog); 348 return passed; 349 } catch (FileNotFoundException e) { 350 CLog.w("Could not create file to save event log: %s", e.getMessage()); 351 return false; 352 } catch (IOException e) { 353 CLog.w("Could not save event log file: %s", e.getMessage()); 354 } finally { 355 StreamUtil.close(out); 356 FileUtil.deleteFile(filteredEventLog); 357 } 358 return false; 359 } 360 361 private boolean evaluateEventLogLine(String line) { 362 int start = line.lastIndexOf("["); 363 int end = line.lastIndexOf("]"); 364 String subStr = line.substring(start + 1, end); 365 String[] statsStrArray = subStr.split(","); 366 if (statsStrArray.length != 13) { 367 CLog.e("Failed to parse for \"%s\" in log.", line); 368 return false; 369 } 370 long xtRb = Long.parseLong(statsStrArray[4].trim()); 371 long xtTb = Long.parseLong(statsStrArray[5].trim()); 372 long xtRp = Long.parseLong(statsStrArray[6].trim()); 373 long xtTp = Long.parseLong(statsStrArray[7].trim()); 374 long uidRb = Long.parseLong(statsStrArray[8].trim()); 375 long uidTb = Long.parseLong(statsStrArray[9].trim()); 376 long uidRp = Long.parseLong(statsStrArray[10].trim()); 377 long uidTp = Long.parseLong(statsStrArray[11].trim()); 378 379 BandwidthStats xtStats = new BandwidthStats(xtRb, xtRp, xtTb, xtTp); 380 BandwidthStats uidStats = new BandwidthStats(uidRb, uidRp, uidTb, uidTp); 381 boolean result = true; 382 CompareResult compareResult = xtStats.compareAll(uidStats, mDifferenceThreshold); 383 result &= compareResult.getResult(); 384 if (!compareResult.getResult()) { 385 CLog.i("Failure comparing netstats_mobile_sample xt and uid"); 386 printFailures(compareResult); 387 } 388 if (!result) { 389 CLog.i("Failed line: %s", line); 390 } 391 return result; 392 } 393 394 /** 395 * Compare the data reported by instrumentation to uid breakdown reported by the kernel, 396 * the sum of uid breakdown and the total reported by the kernel and the data reported by 397 * instrumentation to the data reported by the server. 398 * @param listener result reporter 399 * @param compactRuKey key to use when posting to rdb. 400 * @param utils data parsed from the kernel. 401 * @param instrumentationData data reported by the test. 402 * @param serverData data reported by the server. 403 * @throws DeviceNotAvailableException 404 */ 405 private void reportPassFail(ITestInvocationListener listener, String compactRuKey, 406 BandwidthUtils utils, Map<String, String> serverData, 407 Map<String, String> instrumentationData) throws DeviceNotAvailableException { 408 if (compactRuKey == null) return; 409 410 int passCount = 0; 411 int failCount = 0; 412 413 // Calculate the difference between what framework reports and what the kernel reports 414 boolean download = Boolean.parseBoolean(serverData.get("download")); 415 long frameworkUidBytes = 0; 416 if (download) { 417 frameworkUidBytes = Long.parseLong(instrumentationData.get("PROF_rx")); 418 } else { 419 frameworkUidBytes = Long.parseLong(instrumentationData.get("PROF_tx")); 420 } 421 422 // Compare data reported by the server and the instrumentation 423 long serverBytes = Long.parseLong(serverData.get("size")); 424 float diff = Math.abs(BandwidthStats.computePercentDifference( 425 serverBytes, frameworkUidBytes)); 426 if (diff < mServerDifferenceThreshold) { 427 passCount += 1; 428 } else { 429 CLog.i("Comparing between server and instrumentation failed expected %d got %d", 430 serverBytes, frameworkUidBytes); 431 failCount += 1; 432 } 433 434 if (evaluateEventLog(listener)) { 435 passCount += 1; 436 } else { 437 failCount += 1; 438 } 439 440 Map<String, String> postMetrics = new HashMap<String, String>(); 441 postMetrics.put("Pass", String.valueOf(passCount)); 442 postMetrics.put("Fail", String.valueOf(failCount)); 443 444 listener.testRunStarted(compactRuKey, 0); 445 listener.testRunEnded(0, TfMetricProtoUtil.upgradeConvert(postMetrics)); 446 } 447 448 private void printFailures(CompareResult result) { 449 for (ComparisonRecord failure : result.getFailures()) { 450 CLog.i(failure.toString()); 451 } 452 } 453 454 /** 455 * {@inheritDoc} 456 */ 457 @Override 458 public void setDevice(ITestDevice device) { 459 mTestDevice = device; 460 } 461 462 /** 463 * {@inheritDoc} 464 */ 465 @Override 466 public ITestDevice getDevice() { 467 return mTestDevice; 468 } 469 } 470