1 /*
2  * Copyright (C) 2022 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.car.telemetry.publisher;
18 
19 import android.annotation.NonNull;
20 import android.app.ActivityManager;
21 import android.car.builtin.os.TraceHelper;
22 import android.car.builtin.util.Slogf;
23 import android.car.builtin.util.TimingsTraceLog;
24 import android.car.telemetry.TelemetryProto;
25 import android.car.telemetry.TelemetryProto.Publisher.PublisherCase;
26 import android.content.Context;
27 import android.os.Debug;
28 import android.os.Handler;
29 import android.os.PersistableBundle;
30 import android.provider.Settings;
31 
32 import com.android.car.CarLog;
33 import com.android.car.telemetry.CarTelemetryService;
34 import com.android.car.telemetry.ResultStore;
35 import com.android.car.telemetry.databroker.DataSubscriber;
36 import com.android.car.telemetry.sessioncontroller.SessionAnnotation;
37 import com.android.car.telemetry.sessioncontroller.SessionController;
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.internal.util.Preconditions;
40 
41 import java.io.IOException;
42 import java.nio.file.Files;
43 import java.nio.file.Path;
44 import java.nio.file.Paths;
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.HashSet;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.Set;
51 
52 /**
53  * Publisher implementation for {@link TelemetryProto.MemoryPublisher}.
54  *
55  * <p>It pulls data from /proc/meminfo periodically and sends the file as String to the Lua script.
56  *
57  * <p>This publisher only allows for one Subscriber at a time. It will pull data until the
58  * MetricsConfig is removed or until the maximum snapshot is reached.
59  *
60  * <p>Failure to read from /proc/meminfo will cause a {@link TelemetryProto.TelemetryError} to be
61  * returned to the client.
62  *
63  * <p>The published data format is:
64  * <code>
65  * - mem.timestamp_millis :              long
66  * - mem.meminfo :                       string (device meminfo)
67  * - package_name:uid:process_name : PersistableBundle (parsed from Debug.MemInfo, listed below)
68  *   - mem.summary.total-pss : int
69  *   - mem.summary.java-heap : int
70  *   - mem.summary.native-heap : int
71  *   - mem.summary.stack : int
72  *   - mem.summary.system : int
73  *   - mem.summary.code : int
74  *   - mem.summary.graphics : int
75  *   - mem.summary.private-other : int
76  *   - mem.summary.total-swap : int
77  *   - mem.total_shared_clean : int
78  *   - mem.total_shared_dirty : int
79  *   - mem.total_private_clean : int
80  *   - mem.total_private_dirty : int
81  *   - mem.total_swappable_pss : int
82  * </code>
83  */
84 public class MemoryPublisher extends AbstractPublisher {
85 
86     private static final int MILLIS_IN_SECOND = 1000;
87     private static final String ACTIVITY_MANAGER_CONSTANTS = "activity_manager_constants";
88     @VisibleForTesting
89     static final int THROTTLE_MILLIS = 60 * MILLIS_IN_SECOND;
90     @VisibleForTesting
91     static final String BUNDLE_KEY_NUM_SNAPSHOTS_UNTIL_FINISH = "num_snapshots_left";
92     @VisibleForTesting
93     static final String BUNDLE_KEY_COLLECT_INDEFINITELY = "collect_indefinitely";
94 
95     private final ActivityManager mActivityManager;
96     private final Context mContext;
97     private final Handler mTelemetryHandler;
98     private final Path mMeminfoPath;
99     private final ResultStore mResultStore;
100     private final Runnable mReadMeminfoRunnable = this::readMemInfo;
101     private final TimingsTraceLog mTraceLog;
102 
103     private MemorySubscriberWrapper mSubscriber;
104     private PersistableBundle mPublisherState;
105     private SessionAnnotation mSessionAnnotation;
106 
MemoryPublisher( @onNull Context context, @NonNull PublisherListener listener, @NonNull Handler telemetryHandler, @NonNull ResultStore resultStore, @NonNull SessionController sessionController)107     MemoryPublisher(
108             @NonNull Context context,
109             @NonNull PublisherListener listener,
110             @NonNull Handler telemetryHandler,
111             @NonNull ResultStore resultStore,
112             @NonNull SessionController sessionController) {
113         this(context, listener, telemetryHandler, resultStore, sessionController,
114                 Paths.get("/proc/meminfo"));
115     }
116 
117     @VisibleForTesting
MemoryPublisher( @onNull Context context, @NonNull PublisherListener listener, @NonNull Handler telemetryHandler, @NonNull ResultStore resultStore, @NonNull SessionController sessionController, @NonNull Path meminfoPath)118     MemoryPublisher(
119             @NonNull Context context,
120             @NonNull PublisherListener listener,
121             @NonNull Handler telemetryHandler,
122             @NonNull ResultStore resultStore,
123             @NonNull SessionController sessionController,
124             @NonNull Path meminfoPath) {
125         super(listener);
126         mContext = context;
127         mTelemetryHandler = telemetryHandler;
128         mResultStore = resultStore;
129         mMeminfoPath = meminfoPath;
130         mActivityManager = mContext.getSystemService(ActivityManager.class);
131         mPublisherState = mResultStore.getPublisherData(
132                 MemoryPublisher.class.getSimpleName(), false);
133         mTraceLog = new TimingsTraceLog(
134                 CarLog.TAG_TELEMETRY, TraceHelper.TRACE_TAG_CAR_SERVICE);
135         // Subscribes the publisher to driving session updates by SessionController.
136         sessionController.registerCallback(this::handleSessionStateChange);
137     }
138 
139     @Override
handleSessionStateChange(@onNull SessionAnnotation annotation)140     protected void handleSessionStateChange(@NonNull SessionAnnotation annotation) {
141         mSessionAnnotation = annotation;
142     }
143 
144     @Override
addDataSubscriber(@onNull DataSubscriber subscriber)145     public void addDataSubscriber(@NonNull DataSubscriber subscriber) {
146         if (mSubscriber != null) {
147             throw new IllegalStateException("Only one subscriber is allowed for MemoryPublisher.");
148         }
149         Preconditions.checkArgument(
150                 subscriber.getPublisherParam().getPublisherCase() == PublisherCase.MEMORY,
151                 "Only subscribers for memory statistics are supported by this class.");
152         // the minimum allowed read_rate is 1, i.e. one snapshot per second
153         if (subscriber.getPublisherParam().getMemory().getReadIntervalSec() <= 0) {
154             throw new IllegalArgumentException("MemoryPublisher read_rate must be at least 1");
155         }
156         if (subscriber.getPublisherParam().getMemory().getMaxPendingTasks() <= 0) {
157             throw new IllegalArgumentException("max_pending_tasks in MemoryPublisher must be set"
158                     + " as a throttling threshold");
159         }
160         // if the subscriber is new, i.e. it is added from CarTelemetryManager#addMetricsConifg(),
161         // then the protobuf max_snapshots field is the number of snapshots left
162         int numSnapshotsLeft = subscriber.getPublisherParam().getMemory().getMaxSnapshots();
163         // if client does not specify max_snapshots, the publisher will publish until the
164         // MetricsConfig's lifecycle ends via Lua callback or when the MetricsConfig is removed
165         boolean collectIndefinitely = numSnapshotsLeft <= 0;
166         // if the subscriber is not new, i.e. it is loaded from disk, then the number of snapshots
167         // left is whatever value from last time, which is stored in the publisher state
168         if (mPublisherState != null) {
169             numSnapshotsLeft = mPublisherState.getInt(
170                     BUNDLE_KEY_NUM_SNAPSHOTS_UNTIL_FINISH, numSnapshotsLeft);
171             collectIndefinitely = mPublisherState.getBoolean(
172                     BUNDLE_KEY_COLLECT_INDEFINITELY, numSnapshotsLeft <= 0);
173         }
174         mSubscriber = new MemorySubscriberWrapper(
175                 subscriber, numSnapshotsLeft, collectIndefinitely);
176         readMemInfo();
177     }
178 
179     @Override
removeDataSubscriber(@onNull DataSubscriber subscriber)180     public void removeDataSubscriber(@NonNull DataSubscriber subscriber) {
181         if (mSubscriber == null || !mSubscriber.mDataSubscriber.equals(subscriber)) {
182             return;
183         }
184         resetPublisher();
185     }
186 
187     @Override
removeAllDataSubscribers()188     public void removeAllDataSubscribers() {
189         resetPublisher();
190     }
191 
192     @Override
hasDataSubscriber(@onNull DataSubscriber subscriber)193     public boolean hasDataSubscriber(@NonNull DataSubscriber subscriber) {
194         return mSubscriber != null && mSubscriber.mDataSubscriber.equals(subscriber);
195     }
196 
resetPublisher()197     private void resetPublisher() {
198         mTelemetryHandler.removeCallbacks(mReadMeminfoRunnable);
199         mSubscriber = null;
200         mPublisherState = null;
201         mResultStore.removePublisherData(MemoryPublisher.class.getSimpleName());
202     }
203 
readMemInfo()204     private void readMemInfo() {
205         if (mSubscriber == null) {
206             return;
207         }
208         if (mSubscriber.isDone()) {
209             // terminate the MetricsConfig
210             onConfigFinished(mSubscriber.mMetricsConfig);
211             resetPublisher();
212             return;
213         }
214         mTraceLog.traceBegin("Reading meminfo and publishing");
215         // Read timestamp and meminfo and create published data
216         PersistableBundle data = new PersistableBundle();
217         data.putLong(Constants.MEMORY_BUNDLE_KEY_TIMESTAMP, System.currentTimeMillis());
218         String meminfo;
219         try {
220             meminfo = new String(Files.readAllBytes(mMeminfoPath));
221         } catch (IOException e) {
222             // Return failure to client as error
223             onPublisherFailure(Arrays.asList(mSubscriber.mMetricsConfig), e);
224             resetPublisher();
225             mTraceLog.traceEnd();
226             return;
227         }
228         data.putString(Constants.MEMORY_BUNDLE_KEY_MEMINFO, meminfo);
229 
230         if (!mSubscriber.mPackageNames.isEmpty()) {
231             readProcessMeminfo(data);
232         }
233 
234         // add sessions info to published data if available
235         if (mSessionAnnotation != null) {
236             mSessionAnnotation.addAnnotationsToBundle(data);
237         }
238         // publish data, enqueue data for script execution
239         int numPendingTasks = mSubscriber.push(data);
240 
241         // update publisher state
242         if (mPublisherState == null) {
243             mPublisherState = new PersistableBundle();
244         }
245         mPublisherState.putBoolean(
246                 BUNDLE_KEY_COLLECT_INDEFINITELY,
247                 mSubscriber.mCollectIndefinitely);
248         mPublisherState.putInt(
249                 BUNDLE_KEY_NUM_SNAPSHOTS_UNTIL_FINISH,
250                 mSubscriber.mNumSnapshotsLeft);
251         mResultStore.putPublisherData(MemoryPublisher.class.getSimpleName(), mPublisherState);
252 
253         // if there are too many pending tasks from this publisher, throttle this publisher
254         // by reducing the publishing frequency
255         int delayMillis = numPendingTasks < mSubscriber.mMaxPendingTasks
256                 ? mSubscriber.mPublisherProto.getReadIntervalSec() * MILLIS_IN_SECOND
257                 : THROTTLE_MILLIS;
258         // schedule the next Runnable to read meminfo
259         mTelemetryHandler.postDelayed(mReadMeminfoRunnable, delayMillis);
260         mTraceLog.traceEnd();
261     }
262 
263     /**
264      * Helper method to get process meminfo statistics from the declared packages in
265      * {@code mSubscriber}. Each process meminfo is a PersistableBundle and it will be put into the
266      * parameter {@code data};
267      */
268     private void readProcessMeminfo(PersistableBundle data) {
269         mTraceLog.traceBegin("reading process meminfo");
270         // update the throttle time for ActivityManager#getProcessMemoryInfo API call
271         String restore = Settings.Global.getString(
272                 mContext.getContentResolver(), ACTIVITY_MANAGER_CONSTANTS);
273         Settings.Global.putString(
274                 mContext.getContentResolver(),
275                 ACTIVITY_MANAGER_CONSTANTS,
276                 "memory_info_throttle_time=1");
277         // find PIDs from package names because the API accept an array of PIDs
278         List<ActivityManager.RunningAppProcessInfo> runningAppProcesses =
279                 mActivityManager.getRunningAppProcesses();
280         // the 2 lists run in parallel, where pidList.get(i) is the pid of the pkgList.get(i)
281         List<Integer> pidList = new ArrayList<>();
282         List<String> bundleKeys = new ArrayList<>();
283         // find PIDs from package names specified by the MetricsConfig
284         for (ActivityManager.RunningAppProcessInfo process : runningAppProcesses) {
285             // Default apps use package name as process name, some apps will have the format
286             // package_name:process_name, so we extract package name and process name with split
287             String[] split = process.processName.split(":");
288             if (mSubscriber.mPackageNames.contains(split[0])) {
289                 if (CarTelemetryService.DEBUG) {
290                     Slogf.d(CarLog.TAG_TELEMETRY,
291                             "MemoryPublisher found matching process " + process.processName);
292                 }
293                 pidList.add(process.pid);
294                 bundleKeys.add(split[0] + ":" + process.uid + ":" + split[split.length - 1]);
295             }
296         }
297 
298         // return early if no matching process found
299         if (pidList.size() == 0) {
300             return;
301         }
302 
303         // convert ArrayList<Integer> into int[]
304         int[] pids = new int[pidList.size()];
305         for (int i = 0; i < pids.length; i++) {
306             pids[i] = pidList.get(i);
307         }
308         // get process meminfo and parse it into a PersistableBundle
309         Debug.MemoryInfo[] mis = mActivityManager.getProcessMemoryInfo(pids);
310         for (int i = 0; i < mis.length; i++) {
311             Debug.MemoryInfo mi = mis[i];
312             PersistableBundle processMeminfoBundle = new PersistableBundle();
313             processMeminfoBundle.putInt(
314                     Constants.MEMORY_BUNDLE_KEY_TOTAL_SWAPPABLE_PSS,
315                     Integer.valueOf(mi.getTotalSwappablePss()));
316             processMeminfoBundle.putInt(
317                     Constants.MEMORY_BUNDLE_KEY_TOTAL_PRIVATE_DIRTY,
318                     Integer.valueOf(mi.getTotalPrivateDirty()));
319             processMeminfoBundle.putInt(
320                     Constants.MEMORY_BUNDLE_KEY_TOTAL_SHARED_DIRTY,
321                     Integer.valueOf(mi.getTotalSharedDirty()));
322             processMeminfoBundle.putInt(
323                     Constants.MEMORY_BUNDLE_KEY_TOTAL_PRIVATE_CLEAN,
324                     Integer.valueOf(mi.getTotalPrivateClean()));
325             processMeminfoBundle.putInt(
326                     Constants.MEMORY_BUNDLE_KEY_TOTAL_SHARED_CLEAN,
327                     Integer.valueOf(mi.getTotalSharedClean()));
328             Map<String, String> map = mi.getMemoryStats();
329             for (Map.Entry<String, String> entry : map.entrySet()) {
330                 processMeminfoBundle.putInt(Constants.MEMORY_BUNDLE_KEY_PREFIX + entry.getKey(),
331                         Integer.valueOf(entry.getValue()));
332             }
333             data.putPersistableBundle(bundleKeys.get(i), processMeminfoBundle);
334         }
335         // restore the settings constant to the original state
336         Settings.Global.putString(mContext.getContentResolver(),
337                 ACTIVITY_MANAGER_CONSTANTS, restore);
338         mTraceLog.traceEnd();
339     }
340 
341     private static final class MemorySubscriberWrapper {
342         /**
343          * Whether to keep collecting the meminfo snapshots until end of MetricsConfig lifecycle or
344          * MetricsConfig removed.
345          * This flag should be set to true when the max_snapshots field is unspecified in
346          * {@link TelemetryProto.Publisher}.
347          */
348         private final boolean mCollectIndefinitely;
349         /**
350          * Maximum number of memory-related pending tasks before throttling this publisher
351          */
352         private final int mMaxPendingTasks;
353         private final DataSubscriber mDataSubscriber;
354         private final TelemetryProto.MetricsConfig mMetricsConfig;
355         private final TelemetryProto.MemoryPublisher mPublisherProto;
356         private final Set<String> mPackageNames = new HashSet<>();
357 
358         /**
359          * Number of snapshots until the publisher stops collecting data.
360          */
361         private int mNumSnapshotsLeft;
362 
363         private MemorySubscriberWrapper(
364                 DataSubscriber dataSubscriber, int numSnapshotsLeft, boolean collectIndefinitely) {
365             mDataSubscriber = dataSubscriber;
366             mNumSnapshotsLeft = numSnapshotsLeft;
367             mCollectIndefinitely = collectIndefinitely;
368             mMetricsConfig = dataSubscriber.getMetricsConfig();
369             mPublisherProto = dataSubscriber.getPublisherParam().getMemory();
370             mMaxPendingTasks = mPublisherProto.getMaxPendingTasks();
371             mPackageNames.addAll(mPublisherProto.getPackageNamesList());
372         }
373 
374         /** Publishes data and returns the number of pending tasks by this publisher. */
375         private int push(PersistableBundle data) {
376             if (mNumSnapshotsLeft > 0) {
377                 mNumSnapshotsLeft--;
378             }
379             boolean isLargeData =
380                     data.toString().length() >= DataSubscriber.SCRIPT_INPUT_SIZE_THRESHOLD_BYTES
381                             ? true : false;
382             return mDataSubscriber.push(data, isLargeData);
383         }
384 
385         private boolean isDone() {
386             if (mCollectIndefinitely) {
387                 return false;
388             }
389             return mNumSnapshotsLeft == 0;
390         }
391     }
392 }
393