Home | History | Annotate | Download | only in tests
      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
     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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    312     public void setDevice(ITestDevice device) {
    313         mTestDevice = device;
    314     }
    315 
    316     @Override
    317     public ITestDevice getDevice() {
    318         return mTestDevice;
    319     }
    320 }
    321