1/*
2 * Copyright (C) 2012 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 */
16package com.android.tests.memoryusage;
17
18import android.app.ActivityManager;
19import android.app.ActivityManager.ProcessErrorStateInfo;
20import android.app.ActivityManager.RunningAppProcessInfo;
21import android.app.ActivityManagerNative;
22import android.app.IActivityManager;
23import android.app.UiAutomation;
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.PackageManager;
27import android.content.pm.PackageManager.NameNotFoundException;
28import android.content.pm.ResolveInfo;
29import android.os.Bundle;
30import android.os.Debug.MemoryInfo;
31import android.os.RemoteException;
32import android.os.UserHandle;
33import android.test.InstrumentationTestCase;
34import android.util.Log;
35
36import java.util.ArrayList;
37import java.util.HashMap;
38import java.util.HashSet;
39import java.util.List;
40import java.util.Map;
41import java.util.Set;
42
43/**
44 * This test is intended to measure the amount of memory applications use when
45 * they start. Names of the applications are passed in command line, and the
46 * test starts each application, waits until its memory usage is stabilized and
47 * reports the total PSS in kilobytes of each processes.
48 * The instrumentation expects the following key to be passed on the command line:
49 * apps - A list of applications to start and their corresponding result keys
50 * in the following format:
51 * -e apps <app name>^<result key>|<app name>^<result key>
52 */
53public class MemoryUsageTest extends InstrumentationTestCase {
54
55    private static final int SLEEP_TIME = 1000;
56    private static final int THRESHOLD = 1024;
57    private static final int MAX_ITERATIONS = 20;
58    private static final int MIN_ITERATIONS = 6;
59    private static final int JOIN_TIMEOUT = 10000;
60
61    private static final String TAG = "MemoryUsageInstrumentation";
62    private static final String KEY_APPS = "apps";
63    private static final String KEY_PROCS = "persistent";
64    private static final String LAUNCHER_KEY = "launcher";
65    private Map<String, Intent> mNameToIntent;
66    private Map<String, String> mNameToProcess;
67    private Map<String, String> mNameToResultKey;
68    private Set<String> mPersistentProcesses;
69    private IActivityManager mAm;
70
71    @Override
72    protected void setUp() throws Exception {
73        super.setUp();
74        getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_FREEZE_0);
75    }
76
77    @Override
78    protected void tearDown() throws Exception {
79        getInstrumentation().getUiAutomation().setRotation(UiAutomation.ROTATION_UNFREEZE);
80        super.tearDown();
81    }
82
83    public void testMemory() {
84        MemoryUsageInstrumentation instrumentation =
85                (MemoryUsageInstrumentation) getInstrumentation();
86        Bundle args = instrumentation.getBundle();
87        mAm = ActivityManagerNative.getDefault();
88
89        createMappings();
90        parseArgs(args);
91
92        Bundle results = new Bundle();
93        for (String app : mNameToResultKey.keySet()) {
94            if (!mPersistentProcesses.contains(app)) {
95                String processName;
96                try {
97                    processName = startApp(app);
98                    measureMemory(app, processName, results);
99                    closeApp();
100                } catch (NameNotFoundException e) {
101                    Log.i(TAG, "Application " + app + " not found");
102                }
103            } else {
104                measureMemory(app, app, results);
105            }
106        }
107        instrumentation.sendStatus(0, results);
108    }
109
110    private String getLauncherPackageName() {
111      Intent intent = new Intent(Intent.ACTION_MAIN);
112      intent.addCategory(Intent.CATEGORY_HOME);
113      ResolveInfo resolveInfo = getInstrumentation().getContext().
114          getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
115      return resolveInfo.activityInfo.packageName;
116    }
117
118    private Map<String, String> parseListToMap(String list) {
119        Map<String, String> map = new HashMap<String, String>();
120        String names[] = list.split("\\|");
121        for (String pair : names) {
122            String[] parts = pair.split("\\^");
123            if (parts.length != 2) {
124                Log.e(TAG, "The apps key is incorectly formatted");
125                fail();
126            }
127            map.put(parts[0], parts[1]);
128        }
129        return map;
130    }
131
132    private void parseArgs(Bundle args) {
133        mNameToResultKey = new HashMap<String, String>();
134        mPersistentProcesses = new HashSet<String>();
135        String appList = args.getString(KEY_APPS);
136        String procList = args.getString(KEY_PROCS);
137        String mLauncherPackageName = getLauncherPackageName();
138        mPersistentProcesses.add(mLauncherPackageName);
139        mNameToResultKey.put(mLauncherPackageName, LAUNCHER_KEY);
140        if (appList == null && procList == null)
141            return;
142        if (appList != null) {
143            mNameToResultKey.putAll(parseListToMap(appList));
144        }
145        if (procList != null) {
146            Map<String, String> procMap = parseListToMap(procList);
147            mPersistentProcesses.addAll(procMap.keySet());
148            mNameToResultKey.putAll(procMap);
149        }
150    }
151
152    private void createMappings() {
153        mNameToIntent = new HashMap<String, Intent>();
154        mNameToProcess = new HashMap<String, String>();
155
156        PackageManager pm = getInstrumentation().getContext()
157                .getPackageManager();
158        Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
159        intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER);
160        List<ResolveInfo> ris = pm.queryIntentActivities(intentToResolve, 0);
161        if (ris == null || ris.isEmpty()) {
162            Log.i(TAG, "Could not find any apps");
163        } else {
164            for (ResolveInfo ri : ris) {
165                Log.i(TAG, "Name: " + ri.loadLabel(pm).toString()
166                        + " package: " + ri.activityInfo.packageName
167                        + " name: " + ri.activityInfo.name);
168                Intent startIntent = new Intent(intentToResolve);
169                startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
170                        | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
171                startIntent.setClassName(ri.activityInfo.packageName,
172                        ri.activityInfo.name);
173                mNameToIntent.put(ri.loadLabel(pm).toString(), startIntent);
174                mNameToProcess.put(ri.loadLabel(pm).toString(),
175                        ri.activityInfo.processName);
176            }
177        }
178    }
179
180    private String startApp(String appName) throws NameNotFoundException {
181        Log.i(TAG, "Starting " + appName);
182
183        if (!mNameToProcess.containsKey(appName))
184            throw new NameNotFoundException("Could not find: " + appName);
185
186        String process = mNameToProcess.get(appName);
187        Intent startIntent = mNameToIntent.get(appName);
188
189        AppLaunchRunnable runnable = new AppLaunchRunnable(startIntent);
190        Thread t = new Thread(runnable);
191        t.start();
192        try {
193            t.join(JOIN_TIMEOUT);
194        } catch (InterruptedException e) {
195            // ignore
196        }
197
198        return process;
199    }
200
201    private void closeApp() {
202        Intent homeIntent = new Intent(Intent.ACTION_MAIN);
203        homeIntent.addCategory(Intent.CATEGORY_HOME);
204        homeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
205                | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
206        getInstrumentation().getContext().startActivity(homeIntent);
207        sleep(3000);
208    }
209
210    private void measureMemory(String appName, String processName,
211            Bundle results) {
212        List<Integer> pssData = new ArrayList<Integer>();
213        int pss = 0;
214        int iteration = 0;
215        while (iteration < MAX_ITERATIONS) {
216            sleep(SLEEP_TIME);
217            pss = getPss(processName);
218            Log.i(TAG, appName + "=" + pss);
219            if (pss < 0) {
220                reportError(appName, processName, results);
221                return;
222            }
223            pssData.add(pss);
224            if (iteration >= MIN_ITERATIONS && stabilized(pssData)) {
225                results.putInt(mNameToResultKey.get(appName), pss);
226                return;
227            }
228            iteration++;
229        }
230
231        Log.w(TAG, appName + " memory usage did not stabilize");
232        results.putInt(mNameToResultKey.get(appName), average(pssData));
233    }
234
235    private int average(List<Integer> pssData) {
236        int sum = 0;
237        for (int sample : pssData) {
238            sum += sample;
239        }
240
241        return sum / pssData.size();
242    }
243
244    private boolean stabilized(List<Integer> pssData) {
245        if (pssData.size() < 3)
246            return false;
247        int diff1 = Math.abs(pssData.get(pssData.size() - 1) - pssData.get(pssData.size() - 2));
248        int diff2 = Math.abs(pssData.get(pssData.size() - 2) - pssData.get(pssData.size() - 3));
249
250        Log.i(TAG, "diff1=" + diff1 + " diff2=" + diff2);
251
252        return (diff1 + diff2) < THRESHOLD;
253    }
254
255    private void sleep(int time) {
256        try {
257            Thread.sleep(time);
258        } catch (InterruptedException e) {
259            // ignore
260        }
261    }
262
263    private void reportError(String appName, String processName, Bundle results) {
264        ActivityManager am = (ActivityManager) getInstrumentation()
265                .getContext().getSystemService(Context.ACTIVITY_SERVICE);
266        List<ProcessErrorStateInfo> crashes = am.getProcessesInErrorState();
267        if (crashes != null) {
268            for (ProcessErrorStateInfo crash : crashes) {
269                if (!crash.processName.equals(processName))
270                    continue;
271
272                Log.w(TAG, appName + " crashed: " + crash.shortMsg);
273                results.putString(mNameToResultKey.get(appName), crash.shortMsg);
274                return;
275            }
276        }
277
278        results.putString(mNameToResultKey.get(appName),
279                "Crashed for unknown reason");
280        Log.w(TAG, appName
281                + " not found in process list, most likely it is crashed");
282    }
283
284    private int getPss(String processName) {
285        ActivityManager am = (ActivityManager) getInstrumentation()
286                .getContext().getSystemService(Context.ACTIVITY_SERVICE);
287        List<RunningAppProcessInfo> apps = am.getRunningAppProcesses();
288
289        for (RunningAppProcessInfo proc : apps) {
290            if (!proc.processName.equals(processName)) {
291                continue;
292            }
293
294            int[] pids = {
295                    proc.pid };
296
297            MemoryInfo meminfo = am.getProcessMemoryInfo(pids)[0];
298            return meminfo.getTotalPss();
299
300        }
301        return -1;
302    }
303
304    private class AppLaunchRunnable implements Runnable {
305        private Intent mLaunchIntent;
306
307        public AppLaunchRunnable(Intent intent) {
308            mLaunchIntent = intent;
309        }
310
311        public void run() {
312            try {
313                String mimeType = mLaunchIntent.getType();
314                if (mimeType == null && mLaunchIntent.getData() != null
315                        && "content".equals(mLaunchIntent.getData().getScheme())) {
316                    mimeType = mAm.getProviderMimeType(mLaunchIntent.getData(),
317                            UserHandle.USER_CURRENT);
318                }
319
320                mAm.startActivityAndWait(null, null, mLaunchIntent, mimeType,
321                        null, null, 0, mLaunchIntent.getFlags(), null, null,
322                        UserHandle.USER_CURRENT_OR_SELF);
323            } catch (RemoteException e) {
324                Log.w(TAG, "Error launching app", e);
325            }
326        }
327    }
328}
329