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