1 /*
2  * Copyright (C) 2018 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 package com.android.tests.nativeprocesses;
17 
18 import com.android.tradefed.device.DeviceNotAvailableException;
19 import com.android.tradefed.device.ITestDevice;
20 import com.android.tradefed.log.LogUtil.CLog;
21 import com.android.tradefed.result.ByteArrayInputStreamSource;
22 import com.android.tradefed.result.LogDataType;
23 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
24 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestLogData;
25 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner.TestMetrics;
26 import com.android.tradefed.testtype.IDeviceTest;
27 
28 import org.junit.Rule;
29 import org.junit.Test;
30 import org.junit.runner.RunWith;
31 
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.HashMap;
35 import java.util.InputMismatchException;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Optional;
39 import java.util.Scanner;
40 
41 /**
42  * Test to gather native processes count and memory usage.
43  *
44  * Native processes are parsed from dumpsys meminfo --oom -c
45  *
46  * <pre>
47  * time,35857009,35857009
48  * oom,native,331721,N/A
49  * proc,native,init,1,2715,N/A,e
50  * proc,native,init,445,1492,N/A,e
51  * ...
52  * </pre>
53  *
54  * For each native process we also look at its showmap output, and gather the PSS, VSS, and RSS.
55  * The showmap output is also saved to test logs.
56  *
57  * The metrics reported are:
58  *
59  * <pre>
60  *   - number of native processes
61  *   - total memory use of native processes
62  *   - memory usage of each native process (one measurement for each process)
63  * </pre>
64  */
65 @RunWith(DeviceJUnit4ClassRunner.class)
66 public class NativeProcessesMemoryTest implements IDeviceTest {
67 
68     @Rule public TestMetrics metrics = new TestMetrics();
69     @Rule public TestLogData logs = new TestLogData();
70 
71     // the dumpsys process comes and go as we run this test, changing pids, so ignore it
72     private static final List<String> PROCESSES_TO_IGNORE = Arrays.asList("dumpsys");
73 
74     // -c gives us a compact output which is comma separated
75     private static final String DUMPSYS_MEMINFO_OOM_CMD = "dumpsys meminfo --oom -c";
76 
77     private static final String SEPARATOR = ",";
78     private static final String LINE_SEPARATOR = "\\n";
79 
80     // key used to report the number of native processes
81     private static final String NUM_NATIVE_PROCESSES_KEY = "Num_native_processes";
82 
83     // identity for summing over MemoryMetric
84     private final MemoryMetric mZero = new MemoryMetric(0, 0, 0);
85 
86     private ITestDevice mTestDevice;
87 
88     @Test
run()89     public void run() throws DeviceNotAvailableException {
90         // showmap requires root, we enable it here for the rest of the test
91         getDevice().enableAdbRoot();
92         // process name -> list of pids with that name
93         Map<String, List<String>> nativeProcesses = collectNativeProcesses();
94         sampleAndLogAllProcesses(nativeProcesses);
95 
96         // want to also record the number of native processes
97         metrics.addTestMetric(
98                 NUM_NATIVE_PROCESSES_KEY, Integer.toString(nativeProcesses.size()));
99     }
100 
101     /** Samples memory of all processes and logs the memory use. */
sampleAndLogAllProcesses(Map<String, List<String>> nativeProcesses)102     private void sampleAndLogAllProcesses(Map<String, List<String>> nativeProcesses)
103             throws DeviceNotAvailableException {
104         for (Map.Entry<String, List<String>> entry : nativeProcesses.entrySet()) {
105             String processName = entry.getKey();
106             if (PROCESSES_TO_IGNORE.contains(processName)) {
107                 continue;
108             }
109 
110             // for all pids associated with this process name, record their memory usage
111             List<MemoryMetric> allMemsForProcess = new ArrayList<>();
112             for (String pid : entry.getValue()) {
113                 Optional<MemoryMetric> mem = snapMemoryUsage(processName, pid);
114                 if (mem.isPresent()) {
115                     allMemsForProcess.add(mem.get());
116                 }
117             }
118 
119             // if for some reason a process does not have any MemoryMetric, don't log it
120             if (allMemsForProcess.isEmpty()) {
121                 continue;
122             }
123 
124             // combine all the memory metrics of process with the same name
125             MemoryMetric combined = allMemsForProcess.stream().reduce(mZero, MemoryMetric::sum);
126             logMemoryMetric(processName, combined);
127         }
128     }
129 
130     @Override
setDevice(ITestDevice device)131     public void setDevice(ITestDevice device) {
132         mTestDevice = device;
133     }
134 
135     @Override
getDevice()136     public ITestDevice getDevice() {
137         return mTestDevice;
138     }
139 
140     /**
141      * Query system for a list of native process.
142      *
143      * @return a map from process name to a list of pids that share the same name
144      */
collectNativeProcesses()145     private Map<String, List<String>> collectNativeProcesses() throws DeviceNotAvailableException {
146         HashMap<String, List<String>> nativeProcesses = new HashMap<>();
147         String memInfo = getDevice().executeShellCommand(DUMPSYS_MEMINFO_OOM_CMD);
148 
149         for (String line : memInfo.split(LINE_SEPARATOR)) {
150             String[] splits = line.split(SEPARATOR);
151             // ignore lines that don't list a native process
152             if (splits.length < 4 || !splits[0].equals("proc") || !splits[1].equals("native")) {
153                 continue;
154             }
155 
156             String processName = splits[2];
157             String pid = splits[3];
158             if (nativeProcesses.containsKey(processName)) {
159                 nativeProcesses.get(processName).add(pid);
160             } else {
161                 nativeProcesses.put(processName, new ArrayList<>(Arrays.asList(pid)));
162             }
163         }
164         return nativeProcesses;
165     }
166 
167     /** Logs an entire showmap output to the test logs. */
logShowmap(String label, String showmap)168     private void logShowmap(String label, String showmap) {
169         try (ByteArrayInputStreamSource source =
170                 new ByteArrayInputStreamSource(showmap.getBytes())) {
171             logs.addTestLog(label + "_showmap", LogDataType.TEXT, source);
172         }
173     }
174 
175     /**
176      * Extract VSS, PSS, and RSS from showmap of a process.
177      * The showmap output is also added to test logs.
178      */
snapMemoryUsage(String processName, String pid)179     private Optional<MemoryMetric> snapMemoryUsage(String processName, String pid)
180             throws DeviceNotAvailableException {
181         // TODO(zhin): copied from com.android.tests.sysmem.host.Metrics#sample(), extract?
182         String showmap = getDevice().executeShellCommand("showmap " + pid);
183         logShowmap(processName + "_" + pid, showmap);
184 
185         // CHECKSTYLE:OFF Generated code
186         // The last lines of the showmap output looks like:
187         // ------- -------- -------- -------- -------- -------- -------- -------- -------- ---- ------------------------------
188         // virtual                     shared   shared  private  private
189         //    size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS   # object
190         // -------- -------- -------- -------- -------- -------- -------- -------- -------- ---- ------------------------------
191         //   12848     4240     1543     2852       64       36     1288        0        0  171 TOTAL
192         // CHECKSTYLE:ON Generated code
193         try {
194             int pos = showmap.lastIndexOf("----");
195             Scanner sc = new Scanner(showmap.substring(pos));
196             sc.next();
197             long vss = sc.nextLong();
198             long rss = sc.nextLong();
199             long pss = sc.nextLong();
200             return Optional.of(new MemoryMetric(pss, rss, vss));
201         } catch (InputMismatchException e) {
202             // this might occur if we have transient processes, it was collected earlier,
203             // but by the time we look at showmap the process is gone
204             CLog.e("Unable to parse MemoryMetric from showmap of pid: " + pid + " processName: "
205                     + processName);
206             CLog.e(showmap);
207             return Optional.empty();
208         }
209     }
210 
211     /** Logs a MemoryMetric of a process. */
logMemoryMetric(String processName, MemoryMetric memoryMetric)212     private void logMemoryMetric(String processName, MemoryMetric memoryMetric) {
213         metrics.addTestMetric(processName + "_pss", Long.toString(memoryMetric.pss));
214         metrics.addTestMetric(processName + "_rss", Long.toString(memoryMetric.rss));
215         metrics.addTestMetric(processName + "_vss", Long.toString(memoryMetric.vss));
216     }
217 
218     /** Container of memory numbers we want to log. */
219     private final class MemoryMetric {
220         final long pss;
221         final long rss;
222         final long vss;
223 
MemoryMetric(long pss, long rss, long vss)224         MemoryMetric(long pss, long rss, long vss) {
225             this.pss = pss;
226             this.rss = rss;
227             this.vss = vss;
228         }
229 
sum(MemoryMetric other)230         MemoryMetric sum(MemoryMetric other) {
231             return new MemoryMetric(
232                     pss + other.pss, rss + other.rss, vss + other.vss);
233         }
234     }
235 }
236