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.content.Context;
22import android.content.Intent;
23import android.content.pm.PackageInfo;
24import android.content.pm.PackageManager;
25import android.content.pm.PackageManager.NameNotFoundException;
26import android.content.pm.ResolveInfo;
27import android.os.Bundle;
28import android.os.Debug.MemoryInfo;
29import android.test.InstrumentationTestCase;
30import android.util.Log;
31
32import java.util.ArrayList;
33import java.util.HashMap;
34import java.util.List;
35import java.util.Map;
36
37/**
38 * This test is intended to measure the amount of memory applications use when
39 * they start. Names of the applications are passed in command line, and the
40 * test starts each application, waits until its memory usage is stabilized and
41 * reports the total PSS in kilobytes of each processes.
42 * The instrumentation expects the following key to be passed on the command line:
43 * apps - A list of applications to start and their corresponding result keys
44 * in the following format:
45 * -e apps <app name>^<result key>|<app name>^<result key>
46 */
47public class MemoryUsageTest extends InstrumentationTestCase {
48
49    private static final int SLEEP_TIME = 1000;
50    private static final int THRESHOLD = 1024;
51    private static final int MAX_ITERATIONS = 10;
52    private static final int MIN_ITERATIONS = 4;
53
54    private static final String TAG = "MemoryUsageInstrumentation";
55    private static final String KEY_APPS = "apps";
56
57    private Map<String, Intent> mNameToIntent;
58    private Map<String, String> mNameToProcess;
59    private Map<String, String> mNameToResultKey;
60
61    public void testMemory() {
62        MemoryUsageInstrumentation instrumentation =
63                    (MemoryUsageInstrumentation) getInstrumentation();
64        Bundle args = instrumentation.getBundle();
65
66        createMappings();
67        parseArgs(args);
68
69        Bundle results = new Bundle();
70        for (String app : mNameToResultKey.keySet()) {
71            String processName;
72            try {
73                processName = startApp(app);
74                measureMemory(app, processName, results);
75                closeApp();
76            } catch (NameNotFoundException e) {
77                Log.i(TAG, "Application " + app + " not found");
78            }
79
80        }
81        instrumentation.sendStatus(0, results);
82    }
83
84    private void parseArgs(Bundle args) {
85        mNameToResultKey = new HashMap<String, String>();
86        String appList = args.getString(KEY_APPS);
87
88        if (appList == null)
89            return;
90
91        String appNames[] = appList.split("\\|");
92        for (String pair : appNames) {
93            String[] parts = pair.split("\\^");
94            if (parts.length != 2) {
95                Log.e(TAG, "The apps key is incorectly formatted");
96                fail();
97            }
98
99            mNameToResultKey.put(parts[0], parts[1]);
100        }
101    }
102
103    private void createMappings() {
104        mNameToIntent = new HashMap<String, Intent>();
105        mNameToProcess = new HashMap<String, String>();
106
107        PackageManager pm = getInstrumentation().getContext()
108                .getPackageManager();
109        Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
110        intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER);
111        List<ResolveInfo> ris = pm.queryIntentActivities(intentToResolve, 0);
112        if (ris == null || ris.isEmpty()) {
113            Log.i(TAG, "Could not find any apps");
114        } else {
115            for (ResolveInfo ri : ris) {
116                Log.i(TAG, "Name: " + ri.loadLabel(pm).toString()
117                        + " package: " + ri.activityInfo.packageName
118                        + " name: " + ri.activityInfo.name);
119                Intent startIntent = new Intent(intentToResolve);
120                startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
121                        | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
122                startIntent.setClassName(ri.activityInfo.packageName,
123                        ri.activityInfo.name);
124                mNameToIntent.put(ri.loadLabel(pm).toString(), startIntent);
125                mNameToProcess.put(ri.loadLabel(pm).toString(),
126                        ri.activityInfo.processName);
127            }
128        }
129    }
130
131    private String startApp(String appName) throws NameNotFoundException {
132        Log.i(TAG, "Starting " + appName);
133
134        if (!mNameToProcess.containsKey(appName))
135            throw new NameNotFoundException("Could not find: " + appName);
136
137        String process = mNameToProcess.get(appName);
138        Intent startIntent = mNameToIntent.get(appName);
139        getInstrumentation().getContext().startActivity(startIntent);
140        return process;
141    }
142
143    private void closeApp() {
144        Intent homeIntent = new Intent(Intent.ACTION_MAIN);
145        homeIntent.addCategory(Intent.CATEGORY_HOME);
146        homeIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
147                | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
148        getInstrumentation().getContext().startActivity(homeIntent);
149        sleep(3000);
150    }
151
152    private void measureMemory(String appName, String processName,
153            Bundle results) {
154        List<Integer> pssData = new ArrayList<Integer>();
155        int pss = 0;
156        int iteration = 0;
157        while (iteration < MAX_ITERATIONS) {
158            sleep(SLEEP_TIME);
159            pss = getPss(processName);
160            Log.i(TAG, appName + "=" + pss);
161            if (pss < 0) {
162                reportError(appName, processName, results);
163                return;
164            }
165            pssData.add(pss);
166            if (iteration >= MIN_ITERATIONS && stabilized(pssData)) {
167                results.putInt(mNameToResultKey.get(appName), pss);
168                return;
169            }
170            iteration++;
171        }
172
173        Log.w(TAG, appName + " memory usage did not stabilize");
174        results.putInt(mNameToResultKey.get(appName), average(pssData));
175    }
176
177    private int average(List<Integer> pssData) {
178        int sum = 0;
179        for (int sample : pssData) {
180            sum += sample;
181        }
182
183        return sum / pssData.size();
184    }
185
186    private boolean stabilized(List<Integer> pssData) {
187        if (pssData.size() < 3)
188            return false;
189        int diff1 = Math.abs(pssData.get(pssData.size() - 1) - pssData.get(pssData.size() - 2));
190        int diff2 = Math.abs(pssData.get(pssData.size() - 2) - pssData.get(pssData.size() - 3));
191
192        Log.i(TAG, "diff1=" + diff1 + " diff2=" + diff2);
193
194        return (diff1 + diff2) < THRESHOLD;
195    }
196
197    private void sleep(int time) {
198        try {
199            Thread.sleep(time);
200        } catch (InterruptedException e) {
201            // ignore
202        }
203    }
204
205    private void reportError(String appName, String processName, Bundle results) {
206        ActivityManager am = (ActivityManager) getInstrumentation()
207                .getContext().getSystemService(Context.ACTIVITY_SERVICE);
208        List<ProcessErrorStateInfo> crashes = am.getProcessesInErrorState();
209        if (crashes != null) {
210            for (ProcessErrorStateInfo crash : crashes) {
211                if (!crash.processName.equals(processName))
212                    continue;
213
214                Log.w(TAG, appName + " crashed: " + crash.shortMsg);
215                results.putString(mNameToResultKey.get(appName), crash.shortMsg);
216                return;
217            }
218        }
219
220        results.putString(mNameToResultKey.get(appName),
221                "Crashed for unknown reason");
222        Log.w(TAG, appName
223                + " not found in process list, most likely it is crashed");
224    }
225
226    private int getPss(String processName) {
227        ActivityManager am = (ActivityManager) getInstrumentation()
228                .getContext().getSystemService(Context.ACTIVITY_SERVICE);
229        List<RunningAppProcessInfo> apps = am.getRunningAppProcesses();
230
231        for (RunningAppProcessInfo proc : apps) {
232            if (!proc.processName.equals(processName)) {
233                continue;
234            }
235
236            int[] pids = {
237                proc.pid };
238
239            MemoryInfo meminfo = am.getProcessMemoryInfo(pids)[0];
240            return meminfo.getTotalPss();
241
242        }
243        return -1;
244    }
245}
246