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