1 /* 2 * Copyright (C) 2014 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 android.support.test.aupt; 18 19 import java.io.BufferedReader; 20 import java.io.BufferedWriter; 21 import java.io.ByteArrayOutputStream; 22 import java.io.File; 23 import java.io.FileInputStream; 24 import java.io.FileNotFoundException; 25 import java.io.FileOutputStream; 26 import java.io.FileWriter; 27 import java.io.IOException; 28 import java.io.InputStreamReader; 29 import java.io.PrintWriter; 30 import java.util.ArrayList; 31 import java.util.Collection; 32 import java.util.HashMap; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.regex.Matcher; 36 import java.util.regex.Pattern; 37 38 import android.accounts.Account; 39 import android.accounts.AccountManager; 40 import android.content.Context; 41 import android.content.Intent; 42 import android.content.pm.PackageInfo; 43 import android.content.pm.PackageManager; 44 import android.content.pm.PackageManager.NameNotFoundException; 45 import android.os.Bundle; 46 import android.os.Environment; 47 import android.os.SystemClock; 48 import android.support.test.uiautomator.By; 49 import android.support.test.uiautomator.UiDevice; 50 import android.support.test.uiautomator.UiWatcher; 51 import android.test.InstrumentationTestCase; 52 import android.test.InstrumentationTestRunner; 53 import android.util.Log; 54 55 import junit.framework.Assert; 56 57 /** 58 * Base class for AuptTests. 59 */ 60 public class AuptTestCase extends InstrumentationTestCase { 61 private static final String SDCARD = 62 Environment.getExternalStorageDirectory().getAbsolutePath(); 63 private static final String RECORD_MEMINFO_PARAM = "record_meminfo"; 64 private static final long DEFAULT_SHORT_SLEEP = 5 * 1000; 65 private static final long DEFAULT_LONG_SLEEP = 30 * 1000; 66 protected static final int STEPS_BACK = 10; 67 protected static final int RECOVERY_SLEEP = 2000; 68 private static final String TAG = AuptTestCase.class.getSimpleName(); 69 70 private boolean mRecordMeminfo = false; 71 private UiWatchers mWatchers; 72 private IProcessStatusTracker mProcessStatusTracker; 73 private DataCollector mDataCollector; 74 75 // We want to periodically collect dumpheap output if the process grows to large to proactivelly 76 // help with catching memory leaks, but don't want to do it too often so it does not disturb the 77 // test. We are going to limit the total number of dumpheap commands per proccess to 5 by 78 // default, rate limit it to one per hour be default, and only do it for monitored processes 79 // which grow larger than a certain size (200MB by default). 80 private boolean mDumpheapEnabled = false; 81 private long mDumpheapThreshold; 82 private long mDumpheapInterval ; 83 private long mMaxDumpheaps; 84 private Map<String, Long> mLastDumpheap = new HashMap<String, Long>(); 85 private Map<String, Long> mDumpheapCount = new HashMap<String, Long>(); 86 87 private UiDevice mDevice; 88 89 public static class MemHealthRecord { 90 public enum Context { FOREGROUND, BACKGROUND }; 91 92 private final long mTimeMs; 93 private final long mDalvikHeap; 94 private final long mNativeHeap; 95 private final long mPss; 96 private final Context mContext; 97 MemHealthRecord(long timeMs, long dalvikHeap, long nativeHeap, long pss, Context context)98 public MemHealthRecord(long timeMs, long dalvikHeap, long nativeHeap, long pss, 99 Context context) { 100 mTimeMs = timeMs; 101 mDalvikHeap = dalvikHeap; 102 mNativeHeap = nativeHeap; 103 mPss = pss; 104 mContext = context; 105 } 106 getForegroundDalvikHeap(Collection<MemHealthRecord> samples)107 public static List<Long> getForegroundDalvikHeap(Collection<MemHealthRecord> samples) { 108 List<Long> ret = new ArrayList<>(samples.size()); 109 for (MemHealthRecord sample : samples) { 110 if (Context.FOREGROUND.equals(sample.mContext)) { 111 ret.add(sample.mDalvikHeap); 112 } 113 } 114 return ret; 115 } 116 getBackgroundDalvikHeap(Collection<MemHealthRecord> samples)117 public static List<Long> getBackgroundDalvikHeap(Collection<MemHealthRecord> samples) { 118 List<Long> ret = new ArrayList<>(samples.size()); 119 for (MemHealthRecord sample : samples) { 120 if (Context.BACKGROUND.equals(sample.mContext)) { 121 ret.add(sample.mDalvikHeap); 122 } 123 } 124 return ret; 125 } 126 getForegroundNativeHeap(Collection<MemHealthRecord> samples)127 public static List<Long> getForegroundNativeHeap(Collection<MemHealthRecord> samples) { 128 List<Long> ret = new ArrayList<>(samples.size()); 129 for (MemHealthRecord sample : samples) { 130 if (Context.FOREGROUND.equals(sample.mContext)) { 131 ret.add(sample.mNativeHeap); 132 } 133 } 134 return ret; 135 } 136 getBackgroundNativeHeap(Collection<MemHealthRecord> samples)137 public static List<Long> getBackgroundNativeHeap(Collection<MemHealthRecord> samples) { 138 List<Long> ret = new ArrayList<>(samples.size()); 139 for (MemHealthRecord sample : samples) { 140 if (Context.BACKGROUND.equals(sample.mContext)) { 141 ret.add(sample.mNativeHeap); 142 } 143 } 144 return ret; 145 } 146 getForegroundPss(Collection<MemHealthRecord> samples)147 public static List<Long> getForegroundPss(Collection<MemHealthRecord> samples) { 148 List<Long> ret = new ArrayList<>(samples.size()); 149 for (MemHealthRecord sample : samples) { 150 if (Context.FOREGROUND.equals(sample.mContext)) { 151 ret.add(sample.mPss); 152 } 153 } 154 return ret; 155 } 156 getBackgroundPss(Collection<MemHealthRecord> samples)157 public static List<Long> getBackgroundPss(Collection<MemHealthRecord> samples) { 158 List<Long> ret = new ArrayList<>(samples.size()); 159 for (MemHealthRecord sample : samples) { 160 if (Context.BACKGROUND.equals(sample.mContext)) { 161 ret.add(sample.mPss); 162 } 163 } 164 return ret; 165 } 166 getMax(Collection<Long> samples)167 private static Long getMax(Collection<Long> samples) { 168 Long max = null; 169 for (Long sample : samples) { 170 if (max == null || sample > max) { 171 max = sample; 172 } 173 } 174 return max; 175 } 176 getAverage(Collection<Long> samples)177 private static Long getAverage(Collection<Long> samples) { 178 if (samples.size() == 0) { 179 return null; 180 } 181 182 double sum = 0; 183 for (Long sample : samples) { 184 sum += sample; 185 } 186 return (long) (sum / samples.size()); 187 } 188 } 189 190 private Map<String, List<MemHealthRecord>> mMemHealthRecords; 191 private String[] mProcsToTrack; 192 private String mResultsDirectory; 193 setMemHealthRecords(Map<String, List<MemHealthRecord>> records)194 public void setMemHealthRecords(Map<String, List<MemHealthRecord>> records) { 195 mMemHealthRecords = records; 196 } 197 198 @Override setUp()199 protected void setUp() throws Exception { 200 super.setUp(); 201 202 mDevice = UiDevice.getInstance(getInstrumentation()); 203 mWatchers = new UiWatchers(); 204 mWatchers.registerAnrAndCrashWatchers(getInstrumentation()); 205 mDevice.registerWatcher("LockScreenWatcher", new LockScreenWatcher()); 206 mRecordMeminfo = "true".equals(getParams().getString(RECORD_MEMINFO_PARAM, "false")); 207 208 mDevice.setOrientationNatural(); 209 210 mResultsDirectory = SDCARD + "/" + getParams().getString( 211 "outputLocation", "aupt_results"); 212 213 String processes = getParams().getString("trackMemory", null); 214 if (processes != null) { 215 mProcsToTrack = processes.split(","); 216 } else { 217 readProcessesFromFile(); 218 } 219 220 mDumpheapEnabled = "true".equals(getParams().getString("enableDumpheap")); 221 if (mDumpheapEnabled) { 222 mDumpheapThreshold = getLongParam("dumpheapThreshold", 200 * 1024 * 1024); // 200MB 223 mDumpheapInterval = getLongParam("dumpheapInterval", 60 * 60 * 1000); // one hour 224 mMaxDumpheaps = getLongParam("maxDumpheaps", 5); 225 } 226 } 227 readProcessesFromFile()228 private void readProcessesFromFile() { 229 File trackFile = new File(SDCARD + "/track_memory.txt"); 230 if (trackFile.exists()) { 231 BufferedReader in = null; 232 try { 233 in = new BufferedReader(new InputStreamReader(new FileInputStream(trackFile))); 234 String processes = in.readLine(); 235 in.close(); 236 237 if (!"".equals(processes)) { 238 mProcsToTrack = processes.split(","); 239 } 240 } catch (FileNotFoundException e) { 241 Log.e(TAG, "Error opening track file", e); 242 } catch (IOException e) { 243 Log.e(TAG, "Error opening track file", e); 244 } 245 } 246 } 247 248 /** 249 * {@inheritDoc} 250 */ 251 @Override tearDown()252 protected void tearDown() throws Exception { 253 mDevice.removeWatcher("LockScreenWatcher"); 254 mDevice.unfreezeRotation(); 255 256 saveMemoryStats(); 257 258 super.tearDown(); 259 } 260 261 private class LockScreenWatcher implements UiWatcher { 262 263 @Override checkForCondition()264 public boolean checkForCondition() { 265 if (mDevice.hasObject(By.desc("Slide area."))) { 266 mDevice.pressMenu(); 267 return true; 268 } 269 return false; 270 } 271 } 272 273 /** 274 * Looks up a parameter or returns a default value if parameter is not 275 * present. 276 * @param key 277 * @param defaultValue 278 * @return passed in parameter or default value if parameter is not found. 279 */ getLongParam(String key, long defaultValue)280 public long getLongParam(String key, long defaultValue) throws NumberFormatException { 281 if (getParams().containsKey(key)) { 282 return Long.parseLong(getParams().getString(key)); 283 } else { 284 return defaultValue; 285 } 286 } 287 288 /** 289 * Returns the timeout for short sleep. Can be set with shortSleep command 290 * line option. Default is 5 seconds. 291 * @return time in milliseconds 292 */ getShortSleep()293 public long getShortSleep() { 294 return getLongParam("shortSleep", DEFAULT_SHORT_SLEEP); 295 } 296 297 /** 298 * Returns the timeout for long sleep. Can be set with longSleep command 299 * line option. Default is 30 seconds 300 * @return time in milliseconds. 301 */ getLongSleep()302 public long getLongSleep() { 303 return getLongParam("longSleep", DEFAULT_LONG_SLEEP); 304 } 305 306 /** 307 * Press back button repeatedly in order to attempt to bring the app back to home screen. 308 * This is intended so that an app can recover if the previous session left an app in a weird 309 * state. 310 */ navigateToHome()311 public void navigateToHome() { 312 int iterations = 0; 313 String launcherPkg = mDevice.getLauncherPackageName(); 314 while (!launcherPkg.equals(mDevice.getCurrentPackageName()) 315 && iterations < STEPS_BACK) { 316 mDevice.pressBack(); 317 SystemClock.sleep(RECOVERY_SLEEP); 318 iterations++; 319 } 320 } 321 322 /** 323 * Writes out condensed memory data about the running processes. 324 * @param notes about when the dump was taken. 325 */ dumpMemInfo(String notes)326 public void dumpMemInfo(String notes) { 327 if (mRecordMeminfo) { 328 mDevice.waitForIdle(); 329 mDataCollector.dumpMeminfo(notes); 330 } 331 if (mProcsToTrack != null) { 332 recordMemoryUsage(); 333 } 334 } 335 saveMemoryStats()336 private void saveMemoryStats() { 337 if (mProcsToTrack == null) { 338 return; 339 } 340 try { 341 PrintWriter out = new PrintWriter(new BufferedWriter( 342 new FileWriter(mResultsDirectory + "/memory-health.txt"))); 343 out.println("Foreground"); 344 for (Map.Entry<String, List<MemHealthRecord>> record : mMemHealthRecords.entrySet()) { 345 List<Long> nativeHeap = MemHealthRecord.getForegroundNativeHeap(record.getValue()); 346 List<Long> dalvikHeap = MemHealthRecord.getForegroundDalvikHeap(record.getValue()); 347 List<Long> pss = MemHealthRecord.getForegroundPss(record.getValue()); 348 349 // nativeHeap, dalvikHeap, and pss all have the same size, just use one 350 if (nativeHeap.size() == 0) { 351 continue; 352 } 353 354 out.println(record.getKey()); 355 out.printf("Average Native Heap: %d\n", MemHealthRecord.getAverage(nativeHeap)); 356 out.printf("Average Dalvik Heap: %d\n", MemHealthRecord.getAverage(dalvikHeap)); 357 out.printf("Average PSS: %d\n", MemHealthRecord.getAverage(pss)); 358 out.printf("Peak Native Heap: %d\n", MemHealthRecord.getMax(nativeHeap)); 359 out.printf("Peak Dalvik Heap: %d\n", MemHealthRecord.getMax(dalvikHeap)); 360 out.printf("Peak PSS: %d\n", MemHealthRecord.getMax(pss)); 361 out.printf("Count %d\n", nativeHeap.size()); 362 } 363 out.println("Background"); 364 for (Map.Entry<String, List<MemHealthRecord>> record : mMemHealthRecords.entrySet()) { 365 List<Long> nativeHeap = MemHealthRecord.getBackgroundNativeHeap(record.getValue()); 366 List<Long> dalvikHeap = MemHealthRecord.getBackgroundDalvikHeap(record.getValue()); 367 List<Long> pss = MemHealthRecord.getBackgroundPss(record.getValue()); 368 369 // nativeHeap, dalvikHeap, and pss all have the same size, just use one 370 if (nativeHeap.size() == 0) { 371 continue; 372 } 373 374 out.println(record.getKey()); 375 out.printf("Average Native Heap: %d\n", MemHealthRecord.getAverage(nativeHeap)); 376 out.printf("Average Dalvik Heap: %d\n", MemHealthRecord.getAverage(dalvikHeap)); 377 out.printf("Average PSS: %d\n", MemHealthRecord.getAverage(pss)); 378 out.printf("Peak Native Heap: %d\n", MemHealthRecord.getMax(nativeHeap)); 379 out.printf("Peak Dalvik Heap: %d\n", MemHealthRecord.getMax(dalvikHeap)); 380 out.printf("Peak PSS: %d\n", MemHealthRecord.getMax(pss)); 381 out.printf("Count %d\n", nativeHeap.size()); 382 } 383 out.close(); 384 } catch (IOException e) { 385 Log.e(TAG, "Error while saving memory stats", e); 386 } 387 388 // Temporary hack to write full logs 389 try { 390 PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter( 391 mResultsDirectory + "/memory-health-details.txt"))); 392 for (Map.Entry<String, List<MemHealthRecord>> record : mMemHealthRecords.entrySet()) { 393 out.println(record.getKey()); 394 out.printf("time,native_heap,dalvik_heap,pss,context\n"); 395 for (MemHealthRecord sample : record.getValue()) { 396 out.printf("%d,%d,%d,%s\n", sample.mTimeMs, sample.mNativeHeap, 397 sample.mDalvikHeap, sample.mContext.toString().toLowerCase()); 398 } 399 } 400 out.close(); 401 } catch (IOException e) { 402 Log.e(TAG, "Error while saving memory stat details", e); 403 } 404 } 405 recordMemoryUsage()406 private void recordMemoryUsage() { 407 if (mProcsToTrack == null) { 408 return; 409 } 410 long timeMs = System.currentTimeMillis(); 411 List<String> foregroundProcs = getForegroundProc(); 412 for (String proc : mProcsToTrack) { 413 recordMemoryUsage(proc, timeMs, foregroundProcs); 414 } 415 } 416 recordMemoryUsage(String proc, long timeMs, List<String> foregroundProcs)417 private void recordMemoryUsage(String proc, long timeMs, List<String> foregroundProcs) { 418 try { 419 String meminfo = getMeminfoOutput(proc); 420 int nativeHeap = parseMeminfoLine(meminfo, "Native Heap\\s+\\d+\\s+(\\d+)"); 421 int dalvikHeap = parseMeminfoLine(meminfo, "Dalvik Heap\\s+\\d+\\s+(\\d+)"); 422 int pss = parseMeminfoLine(meminfo, "TOTAL\\s+(\\d+)"); 423 424 if (nativeHeap < 0 || dalvikHeap < 0 || pss < 0) { 425 return; 426 } 427 MemHealthRecord.Context context = foregroundProcs.contains(proc) ? 428 MemHealthRecord.Context.FOREGROUND : MemHealthRecord.Context.BACKGROUND; 429 if (!mMemHealthRecords.containsKey(proc)) { 430 mMemHealthRecords.put(proc, new ArrayList<MemHealthRecord>()); 431 } 432 mMemHealthRecords.get(proc).add( 433 new MemHealthRecord(timeMs, dalvikHeap, nativeHeap, pss, context)); 434 recordDumpheap(proc, pss); 435 } catch (IOException e) { 436 Log.e(TAG, "exception while memory stats", e); 437 } 438 } 439 parseMeminfoLine(String meminfo, String pattern)440 private int parseMeminfoLine(String meminfo, String pattern) 441 { 442 Pattern p = Pattern.compile(pattern); 443 Matcher m = p.matcher(meminfo); 444 if (m.find()) { 445 return Integer.parseInt(m.group(1)); 446 } else { 447 return -1; 448 } 449 } 450 getMeminfoOutput(String processName)451 private String getMeminfoOutput(String processName) throws IOException { 452 return getProcessOutput("dumpsys meminfo " + processName); 453 } 454 recordDumpheap(String proc, long pss)455 private void recordDumpheap(String proc, long pss) throws IOException { 456 if (!mDumpheapEnabled) { 457 return; 458 } 459 Long count = mDumpheapCount.get(proc); 460 if (count == null) { 461 count = 0L; 462 } 463 Long lastDumpheap = mLastDumpheap.get(proc); 464 if (lastDumpheap == null) { 465 lastDumpheap = 0L; 466 } 467 long currentTime = SystemClock.uptimeMillis(); 468 if (pss > mDumpheapThreshold && count < mMaxDumpheaps && 469 currentTime - lastDumpheap > mDumpheapInterval) { 470 recordDumpheap(proc); 471 mDumpheapCount.put(proc, count + 1); 472 mLastDumpheap.put(proc, currentTime); 473 } 474 } 475 recordDumpheap(String proc)476 private void recordDumpheap(String proc) throws IOException { 477 // Turns out getting dumpheap output is non-trivial. The command runs as shell user, and 478 // only has access to /data/local/tmp directory to write files to. The test does not have 479 // access to the output file by default because of the permissions dumpheap sets. So we need 480 // to run dumpheap, change permissions on the output file and copy it to where test harness 481 // can pick it up. 482 Long count = mDumpheapCount.get(proc); 483 if (count == null) { 484 count = 0L; 485 } 486 String filename = String.format("dumpheap-%s-%d", proc, count); 487 String tempFilename = "/data/local/tmp/" + filename; 488 String finalFilename = mResultsDirectory +"/" + filename; 489 String command = String.format("am dumpheap %s %s", proc, tempFilename); 490 getProcessOutput(command); 491 SystemClock.sleep(3000); 492 getProcessOutput(String.format("cp %s %s", tempFilename, finalFilename)); 493 } 494 getProcessOutput(String command)495 public String getProcessOutput(String command) throws IOException { 496 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 497 mDataCollector.saveProcessOutput(command, baos); 498 baos.close(); 499 return baos.toString(); 500 } 501 getForegroundProc()502 private List<String> getForegroundProc() { 503 List<String> foregroundProcs = new ArrayList<String>(); 504 try { 505 String compactMeminfo = getProcessOutput("dumpsys meminfo -c"); 506 for (String line : compactMeminfo.split("\\r?\\n")) { 507 if (line.contains("proc,fore")) { 508 String proc = line.split(",")[2]; 509 foregroundProcs.add(proc); 510 } 511 } 512 } catch (IOException e) { 513 Log.e(TAG, "Error while getting foreground process", e); 514 } finally { 515 return foregroundProcs; 516 } 517 } 518 setProcessStatusTracker(IProcessStatusTracker processStatusTracker)519 public void setProcessStatusTracker(IProcessStatusTracker processStatusTracker) { 520 mProcessStatusTracker = processStatusTracker; 521 } 522 getProcessStatusTracker()523 public IProcessStatusTracker getProcessStatusTracker() { 524 return mProcessStatusTracker; 525 } 526 launchIntent(Intent intent)527 public void launchIntent(Intent intent) { 528 getInstrumentation().getContext().startActivity(intent); 529 } 530 getParams()531 protected Bundle getParams() { 532 return ((InstrumentationTestRunner)getInstrumentation()).getArguments(); 533 } 534 getUiDevice()535 protected UiDevice getUiDevice() { 536 return mDevice; 537 } 538 setDataCollector(DataCollector collector)539 public void setDataCollector(DataCollector collector) { 540 mDataCollector = collector; 541 } 542 getPackageVersion(String packageName)543 public String getPackageVersion(String packageName) throws NameNotFoundException { 544 if (null == packageName || packageName.isEmpty()) { 545 throw new RuntimeException("Package name can't be null or empty"); 546 } 547 PackageManager pm = getInstrumentation().getContext().getPackageManager(); 548 PackageInfo pInfo = pm.getPackageInfo(packageName, 0); 549 String version = pInfo.versionName; 550 if (null == version || version.isEmpty()) { 551 throw new RuntimeException( 552 String.format("Version isn't found for package = %s", packageName)); 553 } 554 555 return version; 556 } 557 558 /** 559 * Get registered accounts 560 * Ensures there is at least one account registered 561 * returns the google account name 562 */ getRegisteredEmailAccount()563 public String getRegisteredEmailAccount() { 564 Account[] accounts = AccountManager.get(getInstrumentation().getContext()).getAccounts(); 565 Assert.assertTrue("Device doesn't have any account registered", accounts.length >= 1); 566 for(int i =0; i < accounts.length; ++i) { 567 if(accounts[i].type.equals("com.google")) { 568 return accounts[i].name; 569 } 570 } 571 572 throw new RuntimeException("The device is not registered with a google account"); 573 } 574 } 575