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