1 /*
2  * Copyright (C) 2016 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.performance.tests;
18 
19 import com.android.tradefed.config.Option;
20 import com.android.tradefed.device.DeviceNotAvailableException;
21 import com.android.tradefed.device.ITestDevice;
22 import com.android.tradefed.log.LogUtil.CLog;
23 import com.android.tradefed.result.ByteArrayInputStreamSource;
24 import com.android.tradefed.result.ITestInvocationListener;
25 import com.android.tradefed.result.LogDataType;
26 import com.android.tradefed.testtype.IDeviceTest;
27 import com.android.tradefed.testtype.IRemoteTest;
28 import com.android.tradefed.util.ProcessInfo;
29 import com.android.tradefed.util.RunUtil;
30 import com.android.tradefed.util.StreamUtil;
31 import com.android.tradefed.util.proto.TfMetricProtoUtil;
32 
33 import org.junit.Assert;
34 
35 import java.util.HashMap;
36 import java.util.Map;
37 import java.util.regex.Matcher;
38 import java.util.regex.Pattern;
39 
40 /**
41  * Test to gather post launch memory details after launching app
42  * that include app memory usage and system memory usage
43  */
44 public class HermeticMemoryTest implements IDeviceTest, IRemoteTest {
45 
46     private static final String AM_START = "am start -n %s";
47     private static final String PROC_MEMINFO = "cat /proc/meminfo";
48     private static final String MEM_AVAILABLE = "cat /proc/meminfo| grep MemAvailable:";
49     private static final String CACHED_PROCESSES = "dumpsys meminfo|awk '/Total PSS by category:"
50             + "/{found=0} {if(found) print} /: Cached/{found=1}'|tr -d ' '";
51     private static final Pattern PID_PATTERN = Pattern.compile("^.*pid(?<processid>[0-9]*).*$");
52     private static final String DUMPSYS_PROCESS = "dumpsys meminfo %s |grep 'TOTAL'";
53     private static final String DUMPSYS_MEMINFO = "dumpsys meminfo -a ";
54     private static final String MAPS_INFO = "cat /proc/%d/maps";
55     private static final String SMAPS_INFO = "cat /proc/%d/smaps";
56     private static final String STATUS_INFO = "cat /proc/%d/status";
57     private static final String NATIVE_HEAP = "Native";
58     private static final String DALVIK_HEAP = "Dalvik";
59     private static final String HEAP = "Heap";
60     private static final String MEMTOTAL = "MemTotal";
61     private static final String MEMFREE = "MemFree";
62     private static final String CACHED = "Cached";
63     private static final int NO_PROCESS_ID = -1;
64     private static final String DROP_CACHE = "echo 3 > /proc/sys/vm/drop_caches";
65     private static final String SEPARATOR ="\\s+";
66     private static final String LINE_SEPARATOR = "\\n";
67 
68 
69     @Option(name = "post-app-launch-delay",
70             description = "The delay, between the app launch and the meminfo dump",
71             isTimeVal = true)
72     private long mPostAppLaunchDelay = 60;
73 
74     @Option(name = "component-name",
75             description = "package/activity name to launch the activity")
76     private String mComponentName = new String();
77 
78     @Option(name = "total-memory-kb",
79             description = "Built in total memory of the device")
80     private long mTotalMemory = 0;
81 
82     @Option(name = "reporting-key", description = "Reporting key is the unique identifier"
83             + "used to report data in the dashboard.")
84     private String mRuKey = "";
85 
86     private ITestDevice mTestDevice = null;
87     private ITestInvocationListener mlistener = null;
88     private Map<String, String> mMetrics = new HashMap<String, String> ();
89 
90     @Override
run(ITestInvocationListener listener)91     public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
92         mlistener = listener;
93 
94         calculateFreeMem();
95 
96         String preMemInfo = mTestDevice.executeShellCommand(PROC_MEMINFO);
97 
98         if (!preMemInfo.isEmpty()) {
99 
100             uploadLogFile(preMemInfo, "BeforeLaunchProcMemInfo");
101         } else {
102             CLog.e("Not able to collect the /proc/meminfo before launching app");
103         }
104 
105         Assert.assertTrue("Device built in memory in kb is mandatory.Use --total-memory-kb value"
106                 + "command line parameter",
107                 mTotalMemory != 0);
108         RunUtil.getDefault().sleep(5000);
109         mTestDevice.executeShellCommand(DROP_CACHE);
110         RunUtil.getDefault().sleep(5000);
111         Assert.assertTrue("Not a valid component name to start the activity",
112                 (mComponentName.split("/").length == 2));
113         mTestDevice.executeShellCommand(String.format(AM_START, mComponentName));
114 
115         RunUtil.getDefault().sleep(mPostAppLaunchDelay);
116         String postMemInfo = mTestDevice.executeShellCommand(PROC_MEMINFO);
117         int processId = getProcessId();
118         String dumpsysMemInfo = mTestDevice.executeShellCommand(
119                 String.format("%s %d", DUMPSYS_MEMINFO, processId));
120         String mapsInfo = mTestDevice.executeShellCommand(
121                 String.format(MAPS_INFO, processId));
122         String sMapsInfo = mTestDevice.executeShellCommand(
123                 String.format(SMAPS_INFO, processId));
124         String statusInfo = mTestDevice.executeShellCommand(
125                 String.format(STATUS_INFO, processId));
126 
127         if (!postMemInfo.isEmpty()) {
128             uploadLogFile(postMemInfo, "AfterLaunchProcMemInfo");
129             parseProcInfo(postMemInfo);
130         } else {
131             CLog.e("Not able to collect the proc/meminfo after launching app");
132         }
133 
134         if (NO_PROCESS_ID == processId) {
135             CLog.e("Process Id not found for the activity launched");
136         } else {
137             if (!dumpsysMemInfo.isEmpty()) {
138                 uploadLogFile(dumpsysMemInfo, "DumpsysMemInfo");
139                 parseDumpsysInfo(dumpsysMemInfo);
140             } else {
141                 CLog.e("Not able to collect the Dumpsys meminfo after launching app");
142             }
143             if (!mapsInfo.isEmpty()) {
144                 uploadLogFile(mapsInfo, "mapsInfo");
145             } else {
146                 CLog.e("Not able to collect maps info after launching app");
147             }
148             if (!sMapsInfo.isEmpty()) {
149                 uploadLogFile(sMapsInfo, "smapsInfo");
150             } else {
151                 CLog.e("Not able to collect smaps info after launching app");
152             }
153             if (!statusInfo.isEmpty()) {
154                 uploadLogFile(statusInfo, "statusInfo");
155             } else {
156                 CLog.e("Not able to collect status info after launching app");
157             }
158         }
159 
160         reportMetrics(listener, mRuKey, mMetrics);
161 
162     }
163 
164     /**
165      * Method to get the process id of the target package/activity name
166      *
167      * @return processId of the activity launched
168      * @throws DeviceNotAvailableException
169      */
getProcessId()170     private int getProcessId() throws DeviceNotAvailableException {
171         String pkgActivitySplit[] = mComponentName.split("/");
172         if (pkgActivitySplit[0] != null) {
173             ProcessInfo processData = mTestDevice.getProcessByName(pkgActivitySplit[0]);
174             if (null != processData) {
175                 return processData.getPid();
176             }
177         }
178         return NO_PROCESS_ID;
179     }
180 
181     /**
182      * Method to write the data to test logs.
183      * @param data
184      * @param fileName
185      */
uploadLogFile(String data, String fileName)186     private void uploadLogFile(String data, String fileName) {
187         ByteArrayInputStreamSource inputStreamSrc = null;
188         try {
189             inputStreamSrc = new ByteArrayInputStreamSource(data.getBytes());
190             mlistener.testLog(fileName, LogDataType.TEXT, inputStreamSrc);
191         } finally {
192             StreamUtil.cancel(inputStreamSrc);
193         }
194     }
195 
196     /**
197      * Method to parse dalvik and heap info for launched app
198      */
parseDumpsysInfo(String dumpInfo)199     private void parseDumpsysInfo(String dumpInfo) {
200         String line[] = dumpInfo.split(LINE_SEPARATOR);
201         for (int lineCount = 0; lineCount < line.length; lineCount++) {
202             String dataSplit[] = line[lineCount].trim().split(SEPARATOR);
203             if ((dataSplit[0].equalsIgnoreCase(NATIVE_HEAP) && dataSplit[1]
204                     .equalsIgnoreCase(HEAP)) ||
205                     (dataSplit[0].equalsIgnoreCase(DALVIK_HEAP) && dataSplit[1]
206                             .equalsIgnoreCase(HEAP)) ||
207                     dataSplit[0].equalsIgnoreCase("Total")) {
208                 if (dataSplit.length > 10) {
209                     if (dataSplit[0].contains(NATIVE_HEAP)
210                             || dataSplit[0].contains(DALVIK_HEAP)) {
211                         mMetrics.put(dataSplit[0] + ":PSS_TOTAL", dataSplit[2]);
212                         mMetrics.put(dataSplit[0] + ":SHARED_DIRTY", dataSplit[4]);
213                         mMetrics.put(dataSplit[0] + ":PRIVATE_DIRTY", dataSplit[5]);
214                         mMetrics.put(dataSplit[0] + ":HEAP_TOTAL", dataSplit[9]);
215                         mMetrics.put(dataSplit[0] + ":HEAP_ALLOC", dataSplit[10]);
216                     } else {
217                         mMetrics.put(dataSplit[0] + ":PSS", dataSplit[1]);
218                     }
219                 }
220             }
221         }
222     }
223 
224     /**
225      * Method to parse the system memory details
226      */
parseProcInfo(String memInfo)227     private void parseProcInfo(String memInfo) {
228         String lineSplit[] = memInfo.split(LINE_SEPARATOR);
229         long memTotal = 0;
230         long memFree = 0;
231         long cached = 0;
232         for (int lineCount = 0; lineCount < lineSplit.length; lineCount++) {
233             String line = lineSplit[lineCount].replace(":", "").trim();
234             String dataSplit[] = line.split(SEPARATOR);
235             if (dataSplit[0].equalsIgnoreCase(MEMTOTAL) ||
236                     dataSplit[0].equalsIgnoreCase(MEMFREE) ||
237                     dataSplit[0].equalsIgnoreCase(CACHED)) {
238                 if (dataSplit[0].equalsIgnoreCase(MEMTOTAL)) {
239                     memTotal = Long.parseLong(dataSplit[1]);
240                 }
241                 if (dataSplit[0].equalsIgnoreCase(MEMFREE)) {
242                     memFree = Long.parseLong(dataSplit[1]);
243                 }
244                 if (dataSplit[0].equalsIgnoreCase(CACHED)) {
245                     cached = Long.parseLong(dataSplit[1]);
246                 }
247                 mMetrics.put("System_" + dataSplit[0], dataSplit[1]);
248             }
249         }
250         mMetrics.put("System_Kernal_Firmware", String.valueOf((mTotalMemory - memTotal)));
251         mMetrics.put("System_Framework_Apps", String.valueOf((memTotal - (memFree + cached))));
252     }
253 
254     /**
255      * Method to parse the free memory based on total memory available from proc/meminfo and
256      * private dirty and private clean information of the cached processess
257      * from dumpsys meminfo.
258      */
calculateFreeMem()259     private void calculateFreeMem() throws DeviceNotAvailableException {
260         String memAvailable[] = mTestDevice.executeShellCommand(MEM_AVAILABLE).split(SEPARATOR);
261         int cacheProcDirty = Integer.parseInt(memAvailable[1]);
262 
263         String cachedProcesses = mTestDevice.executeShellCommand(CACHED_PROCESSES);
264         String processes[] = cachedProcesses.split(LINE_SEPARATOR);
265         for (String process : processes) {
266             Matcher match = null;
267             if (((match = matches(PID_PATTERN, process))) != null) {
268                 String processId = match.group("processid");
269                 String processInfoStr = mTestDevice.executeShellCommand(String.format(
270                         DUMPSYS_PROCESS, processId));
271                 String processInfo[] = null;
272                 if (processInfoStr != null && !processInfoStr.isEmpty()) {
273                     processInfo = processInfoStr.split(LINE_SEPARATOR);
274                 }
275                 if (null != processInfo && processInfo.length > 0) {
276                     String procDetails[] = processInfo[0].trim().split(SEPARATOR);
277                     cacheProcDirty = cacheProcDirty + Integer.parseInt(procDetails[2].trim())
278                             + Integer.parseInt(procDetails[3]);
279                 }
280             }
281         }
282         mMetrics.put("MemAvailable_CacheProcDirty", String.valueOf(cacheProcDirty));
283     }
284 
285     /**
286      * Report run metrics by creating an empty test run to stick them in
287      *
288      * @param listener the {@link ITestInvocationListener} of test results
289      * @param runName the test name
290      * @param metrics the {@link Map} that contains metrics for the given test
291      */
reportMetrics(ITestInvocationListener listener, String runName, Map<String, String> metrics)292     void reportMetrics(ITestInvocationListener listener, String runName,
293             Map<String, String> metrics) {
294         // Create an empty testRun to report the parsed runMetrics
295         CLog.d("About to report metrics: %s", metrics);
296         listener.testRunStarted(runName, 0);
297         listener.testRunEnded(0, TfMetricProtoUtil.upgradeConvert(metrics));
298     }
299 
300     /**
301      * Checks whether {@code line} matches the given {@link Pattern}.
302      *
303      * @return The resulting {@link Matcher} obtained by matching the {@code line} against
304      *         {@code pattern}, or null if the {@code line} does not match.
305      */
matches(Pattern pattern, String line)306     private static Matcher matches(Pattern pattern, String line) {
307         Matcher ret = pattern.matcher(line);
308         return ret.matches() ? ret : null;
309     }
310 
311     @Override
setDevice(ITestDevice device)312     public void setDevice(ITestDevice device) {
313         mTestDevice = device;
314     }
315 
316     @Override
getDevice()317     public ITestDevice getDevice() {
318         return mTestDevice;
319     }
320 }
321