1 /*
2  * Copyright (C) 2024 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 android.os;
17 
18 import android.annotation.FlaggedApi;
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.content.Context;
23 import android.os.profiling.Flags;
24 import android.util.Log;
25 
26 import com.android.internal.annotations.GuardedBy;
27 import com.android.internal.annotations.VisibleForTesting;
28 
29 import java.io.File;
30 import java.io.IOException;
31 import java.lang.annotation.Retention;
32 import java.lang.annotation.RetentionPolicy;
33 import java.nio.file.Files;
34 import java.nio.file.Path;
35 import java.util.ArrayList;
36 import java.util.UUID;
37 import java.util.concurrent.Executor;
38 import java.util.function.Consumer;
39 
40 /**
41  * API for apps to request and listen for app specific profiling.
42  */
43 @FlaggedApi(Flags.FLAG_TELEMETRY_APIS)
44 public final class ProfilingManager {
45     private static final String TAG = ProfilingManager.class.getSimpleName();
46     private static final boolean DEBUG = false;
47 
48     /** Profiling type for {@link #requestProfiling} to request a java heap dump. */
49     public static final int PROFILING_TYPE_JAVA_HEAP_DUMP = 1;
50 
51     /** Profiling type for {@link #requestProfiling} to request a heap profile. */
52     public static final int PROFILING_TYPE_HEAP_PROFILE = 2;
53 
54     /** Profiling type for {@link #requestProfiling} to request a stack sample. */
55     public static final int PROFILING_TYPE_STACK_SAMPLING = 3;
56 
57     /** Profiling type for {@link #requestProfiling} to request a system trace. */
58     public static final int PROFILING_TYPE_SYSTEM_TRACE = 4;
59 
60     /* Begin public API defined keys. */
61     /* End public API defined keys. */
62 
63     /* Begin not-public API defined keys/values. */
64     /**
65      * Can only be used with profiling type heap profile, stack sampling, or system trace.
66      * Value of type int.
67      * @hide
68      */
69     public static final String KEY_DURATION_MS = "KEY_DURATION_MS";
70 
71     /**
72      * Can only be used with profiling type heap profile. Value of type long.
73      * @hide
74      */
75     public static final String KEY_SAMPLING_INTERVAL_BYTES = "KEY_SAMPLING_INTERVAL_BYTES";
76 
77     /**
78      * Can only be used with profiling type heap profile. Value of type boolean.
79      * @hide
80      */
81     public static final String KEY_TRACK_JAVA_ALLOCATIONS = "KEY_TRACK_JAVA_ALLOCATIONS";
82 
83     /**
84      * Can only be used with profiling type stack sampling. Value of type int.
85      * @hide
86      */
87     public static final String KEY_FREQUENCY_HZ = "KEY_FREQUENCY_HZ";
88 
89     /**
90      * Can be used with all profiling types. Value of type int.
91      * @hide
92      */
93     public static final String KEY_SIZE_KB = "KEY_SIZE_KB";
94 
95     /**
96      * Can be used with profiling type system trace.
97      * Value of type int must be one of:
98      * {@link VALUE_BUFFER_FILL_POLICY_DISCARD}
99      * {@link VALUE_BUFFER_FILL_POLICY_RING_BUFFER}
100      * @hide
101      */
102     public static final String KEY_BUFFER_FILL_POLICY = "KEY_BUFFER_FILL_POLICY";
103 
104     /** @hide */
105     public static final int VALUE_BUFFER_FILL_POLICY_DISCARD = 1;
106 
107     /** @hide */
108     public static final int VALUE_BUFFER_FILL_POLICY_RING_BUFFER = 2;
109     /* End not-public API defined keys/values. */
110 
111     /**
112      * @hide *
113      */
114     @IntDef(
115         prefix = {"PROFILING_TYPE_"},
116         value = {
117             PROFILING_TYPE_JAVA_HEAP_DUMP,
118             PROFILING_TYPE_HEAP_PROFILE,
119             PROFILING_TYPE_STACK_SAMPLING,
120             PROFILING_TYPE_SYSTEM_TRACE,
121         })
122     @Retention(RetentionPolicy.SOURCE)
123     public @interface ProfilingType {}
124 
125     private final Object mLock = new Object();
126     private final Context mContext;
127 
128     /** @hide */
129     @VisibleForTesting
130     @GuardedBy("mLock")
131     public final ArrayList<ProfilingRequestCallbackWrapper> mCallbacks = new ArrayList<>();
132 
133     /** @hide */
134     @VisibleForTesting
135     @GuardedBy("mLock")
136     public IProfilingService mProfilingService;
137 
138     /**
139      * Constructor for ProfilingManager.
140      *
141      * @hide
142      */
ProfilingManager(Context context)143     public ProfilingManager(Context context) {
144         mContext = context;
145     }
146 
147     /**
148      * Request system profiling.
149      *
150      * <p class="note">
151      *   Note: use of this API directly is not recommended for most use cases.
152      *   Consider using the higher level wrappers provided by AndroidX that will construct the
153      *   request correctly, supporting available options with simplified request parameters
154      * </p>
155      *
156      * <p>
157      *   Both a listener and an executor must be set at the time of the request for the request to
158      *   be considered for fulfillment. Listener/executor pairs can be set in this method, with
159      *   {@link registerForAllProfilingResults}, or both. The listener and executor must be set
160      *   together, in the same call. If no listener and executor combination is set, the request
161      *   will be discarded and no callback will be received.
162      * </p>
163      *
164      * <p>
165      *   Requests will be rate limited and are not guaranteed to be filled.
166      * </p>
167      *
168      * <p>
169      *   There might be a delay before profiling begins.
170      *   For continuous profiling types (system tracing, stack sampling, and heap profiling),
171      *   we recommend starting the collection early and stopping it with {@link cancellationSignal}
172      *   immediately after the area of interest to ensure that the section you want profiled is
173      *   captured.
174      *   For heap dumps, we recommend testing locally to ensure that the heap dump is collected at
175      *   the proper time.
176      * </p>
177      *
178      * @param profilingType Type of profiling to collect.
179      * @param parameters Bundle of request related parameters. If the bundle contains any
180      *                  unrecognized parameters, the request will be fail with
181      *                  {@link #ProfilingResult#ERROR_FAILED_INVALID_REQUEST}. If the values for
182      *                  the parameters are out of supported range, the closest possible in range
183      *                  value will be chosen.
184      *                  Use of androidx wrappers is recommended over generating this directly.
185      * @param tag Caller defined data to help identify the output.
186      *                  The first 20 alphanumeric characters, plus dashes, will be lowercased
187      *                  and included in the output filename.
188      * @param cancellationSignal for caller requested cancellation.
189      *                  Results will be returned if available.
190      *                  If this is null, the requesting app will not be able to stop the collection.
191      *                  The collection will stop after timing out with either the provided
192      *                  configurations or with system defaults
193      * @param executor  The executor to call back with.
194      *                  Will only be used for the listener provided in this method.
195      *                  If this is null, and no global executor and listener combinations are
196      *                  registered at the time of the request, the request will be dropped.
197      * @param listener  Listener to be triggered with result. Any global listeners registered via
198      *                  {@link #registerForAllProfilingResults} will also be triggered. If this is
199      *                  null, and no global listener and executor combinations are registered at
200      *                  the time of the request, the request will be dropped.
201      */
requestProfiling( @rofilingType int profilingType, @Nullable Bundle parameters, @Nullable String tag, @Nullable CancellationSignal cancellationSignal, @Nullable Executor executor, @Nullable Consumer<ProfilingResult> listener)202     public void requestProfiling(
203             @ProfilingType int profilingType,
204             @Nullable Bundle parameters,
205             @Nullable String tag,
206             @Nullable CancellationSignal cancellationSignal,
207             @Nullable Executor executor,
208             @Nullable Consumer<ProfilingResult> listener) {
209         synchronized (mLock) {
210             try {
211                 final UUID key = UUID.randomUUID();
212 
213                 if (executor != null && listener != null) {
214                     // Listeners are provided, store them.
215                     mCallbacks.add(new ProfilingRequestCallbackWrapper(executor, listener, key));
216                 } else if (mCallbacks.isEmpty()) {
217                     // No listeners have been registered by any path, toss the request.
218                     throw new IllegalArgumentException(
219                             "No listeners have been registered. Request has been discarded.");
220                 }
221                 // If neither case above was hit, app wide listeners were provided. Continue.
222 
223                 final IProfilingService service = getOrCreateIProfilingServiceLocked(false);
224                 if (service == null) {
225                     executor.execute(() -> listener.accept(
226                             new ProfilingResult(ProfilingResult.ERROR_UNKNOWN, null, tag,
227                                 "ProfilingService is not available")));
228                     if (DEBUG) Log.d(TAG, "ProfilingService is not available");
229                     return;
230                 }
231 
232                 String packageName = mContext.getPackageName();
233                 if (packageName == null) {
234                     executor.execute(() -> listener.accept(
235                             new ProfilingResult(ProfilingResult.ERROR_UNKNOWN, null, tag,
236                                     "Failed to resolve package name")));
237                     if (DEBUG) Log.d(TAG, "Failed to resolve package name.");
238                     return;
239                 }
240 
241                 // For key, use most and least significant bits so we can create an identical UUID
242                 // after passing over binder.
243                 service.requestProfiling(profilingType, parameters,
244                         mContext.getFilesDir().getPath(), tag,
245                         key.getMostSignificantBits(), key.getLeastSignificantBits(),
246                         packageName);
247                 if (cancellationSignal != null) {
248                     cancellationSignal.setOnCancelListener(
249                             () -> {
250                                 synchronized (mLock) {
251                                     try {
252                                         service.requestCancel(key.getMostSignificantBits(),
253                                                 key.getLeastSignificantBits());
254                                     } catch (RemoteException e) {
255                                         // Ignore, request in flight already and we can't stop it.
256                                     }
257                                 }
258                             }
259                     );
260                 }
261             } catch (RemoteException e) {
262                 if (DEBUG) Log.d(TAG, "Binder exception processing request", e);
263                 executor.execute(() -> listener.accept(
264                         new ProfilingResult(ProfilingResult.ERROR_UNKNOWN, null, tag,
265                                 "Binder exception processing request")));
266                 throw new RuntimeException("Unable to request profiling.");
267             }
268         }
269     }
270 
271     /**
272      * Register a listener to be called for all profiling results for this uid. Listeners set here
273      * will be called in addition to any provided with the request.
274      *
275      * <p class="note"> Note: If a callback attempt fails (for example, because your app is killed
276      * while a trace is in progress) re-delivery may be attempted using a listener added via this
277      * method. </p>
278      *
279      * @param executor The executor to call back with.
280      * @param listener Listener to be triggered with result.
281      */
registerForAllProfilingResults( @onNull Executor executor, @NonNull Consumer<ProfilingResult> listener)282     public void registerForAllProfilingResults(
283             @NonNull Executor executor,
284             @NonNull Consumer<ProfilingResult> listener) {
285         synchronized (mLock) {
286             // Only notify {@link mProfilingService} of a general listener being added if it already
287             // exists as registering it also handles the notifying.
288             boolean shouldNotifyService = mProfilingService != null;
289 
290             if (getOrCreateIProfilingServiceLocked(true) == null) {
291                 // If the binder object was not successfully registered then this listener will
292                 // not ever be triggered.
293                 executor.execute(() -> listener.accept(new ProfilingResult(
294                         ProfilingResult.ERROR_UNKNOWN, null, null,
295                         "Binder exception processing request")));
296                 return;
297             }
298             mCallbacks.add(new ProfilingRequestCallbackWrapper(executor, listener, null));
299 
300             if (shouldNotifyService) {
301                 // Notify service that a general listener was added. General listeners are also used
302                 // for queued callbacks if any are waiting.
303                 try {
304                     mProfilingService.generalListenerAdded();
305                 } catch (RemoteException e) {
306                     // Do nothing. Binder callback is already registered, but service won't know
307                     // there is a general listener so queued callbacks won't occur.
308                     Log.d(TAG, "Exception notifying service of general callback,"
309                             + " queued callbacks will not occur.", e);
310                 }
311             }
312         }
313     }
314 
315     /**
316      * Unregister a listener that was to be called for all profiling results. If no listener is
317      * provided, all listeners for this process that were not submitted with a profiling request
318      * will be removed.
319      *
320      * @param listener Listener to unregister and no longer be triggered with the results.
321      *                 Null to remove all global listeners for this uid.
322      */
unregisterForAllProfilingResults( @ullable Consumer<ProfilingResult> listener)323     public void unregisterForAllProfilingResults(
324             @Nullable Consumer<ProfilingResult> listener) {
325         synchronized (mLock) {
326             if (mCallbacks.isEmpty()) {
327                 // No callbacks, nothing to remove.
328                 return;
329             }
330 
331             if (listener == null) {
332                 // Remove all global listeners.
333                 ArrayList<ProfilingRequestCallbackWrapper> listenersToRemove = new ArrayList<>();
334                 for (int i = 0; i < mCallbacks.size(); i++) {
335                     ProfilingRequestCallbackWrapper wrapper = mCallbacks.get(i);
336                     // Only remove global listeners which are not tied to a specific request. These
337                     // can be identified by checking that they do not have an associated key.
338                     if (wrapper.mKey == null) {
339                         listenersToRemove.add(wrapper);
340                     }
341                 }
342                 mCallbacks.removeAll(listenersToRemove);
343             } else {
344                 // Remove the provided listener only.
345                 for (int i = 0; i < mCallbacks.size(); i++) {
346                     ProfilingRequestCallbackWrapper wrapper = mCallbacks.get(i);
347                     if (listener.equals(wrapper.mListener)) {
348                         mCallbacks.remove(i);
349                         return;
350                     }
351                 }
352             }
353         }
354     }
355 
356 
357     /** @hide */
358     @VisibleForTesting
359     @GuardedBy("mLock")
getOrCreateIProfilingServiceLocked( boolean isGeneralListener)360     public @Nullable IProfilingService getOrCreateIProfilingServiceLocked(
361             boolean isGeneralListener) {
362         // We only register the callback with registerResultsCallback once per binder object, and we
363         // only create one binder object per ProfilingManager instance. If the object already exists
364         // then it was successfully created and registered previously so we can just return it.
365         if (mProfilingService != null) {
366             return mProfilingService;
367         }
368 
369         mProfilingService = IProfilingService.Stub.asInterface(
370                 ProfilingFrameworkInitializer.getProfilingServiceManager()
371                     .getProfilingServiceRegisterer().get());
372         if (mProfilingService == null) {
373             // Service is not accessible, all requests will fail.
374             return mProfilingService;
375         }
376         try {
377             mProfilingService.registerResultsCallback(isGeneralListener,
378                     new IProfilingResultCallback.Stub() {
379 
380                         /**
381                          * Called by {@link ProfilingService} when a result is ready,
382                          * both for success and failure.
383                          */
384                         @Override
385                         public void sendResult(@Nullable String resultFile, long keyMostSigBits,
386                                 long keyLeastSigBits, int status, @Nullable String tag,
387                                 @Nullable String error) {
388                             synchronized (mLock) {
389                                 if (mCallbacks.isEmpty()) {
390                                     // This shouldn't happen - no callbacks, nowhere to report this
391                                     // result.
392                                     if (DEBUG) Log.d(TAG, "No callbacks");
393                                     mProfilingService = null;
394                                     return;
395                                 }
396 
397                                 // This shouldn't be true, but if the file is null ensure the status
398                                 // represents a failure.
399                                 final boolean overrideStatusToError = resultFile == null
400                                         && status == ProfilingResult.ERROR_NONE;
401 
402                                 UUID key = new UUID(keyMostSigBits, keyLeastSigBits);
403                                 int removeListenerPos = -1;
404                                 for (int i = 0; i < mCallbacks.size(); i++) {
405                                     ProfilingRequestCallbackWrapper wrapper = mCallbacks.get(i);
406                                     if (key.equals(wrapper.mKey)) {
407                                         // At most 1 listener can have a key matching this result:
408                                         // the one registered with the request, remove that one
409                                         // only.
410                                         if (removeListenerPos == -1) {
411                                             removeListenerPos = i;
412                                         } else {
413                                             // This should never happen.
414                                             if (DEBUG) {
415                                                 Log.d(TAG,
416                                                         "More than 1 listener with the same key");
417                                             }
418                                         }
419                                     } else if (wrapper.mKey != null) {
420                                         // If the key is not null, and doesn't matched the result
421                                         // key, then this key belongs to another request and should
422                                         // not be triggered.
423                                         continue;
424                                     }
425 
426                                     // TODO: b/337017299 - check resultFile is valid before
427                                     // returning Now trigger the callback for any listener that
428                                     // doesn't belong to another request.
429                                     wrapper.mExecutor.execute(() -> wrapper.mListener.accept(
430                                             new ProfilingResult(overrideStatusToError
431                                                     ? ProfilingResult.ERROR_UNKNOWN : status,
432                                                     resultFile, tag, error)));
433                                 }
434 
435                                 // Remove the single listener that was tied to the request, if
436                                 // applicable.
437                                 if (removeListenerPos != -1) {
438                                     mCallbacks.remove(removeListenerPos);
439                                 }
440                             }
441                         }
442 
443                         /**
444                          * Called by {@link ProfilingService} when a trace is ready and needs to be
445                          * copied to callers internal storage.
446                          *
447                          * This method will open a new file and pass back the FileDescriptor for
448                          * ProfilingService to write to via a new binder call.
449                          *
450                          * Takes in key most/least significant bits which represent the key that
451                          * will be used to associate this back to a profiling session which will
452                          * write to the generated file.
453                          */
454                         @Override
455                         public void generateFile(String filePathAbsolute, String fileName,
456                                 long keyMostSigBits, long keyLeastSigBits) {
457                             synchronized (mLock) {
458                                 try {
459                                     // Ensure the profiling directory exists. Create it if it
460                                     // doesn't.
461                                     final File profilingDir = new File(filePathAbsolute);
462                                     if (!profilingDir.exists()) {
463                                         profilingDir.mkdir();
464                                     }
465 
466                                     // Create the profiling file for the output to be written to.
467                                     final File profilingFile = new File(
468                                             filePathAbsolute + fileName);
469                                     profilingFile.createNewFile();
470                                     if (!profilingFile.exists()) {
471                                         // Failed to create output file. Result may be lost.
472                                         if (DEBUG) Log.d(TAG, "Output file couldn't be created");
473                                         return;
474                                     }
475 
476                                     // Wrap the new output file in a {@link ParcelFileDescriptor} to
477                                     // send back to {@link ProfilingService} to write to.
478                                     ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
479                                             profilingFile,
480                                             ParcelFileDescriptor.MODE_READ_WRITE);
481                                     IProfilingService service =
482                                             getOrCreateIProfilingServiceLocked(false);
483 
484                                     if (service == null) {
485                                         // Unable to send file descriptor because we have nowhere to
486                                         // send it to. Result may be lost. Close descriptor and
487                                         // delete file.
488                                         if (DEBUG) Log.d(TAG, "Unable to send file descriptor");
489                                         tryToCleanupGeneratedFile(pfd, profilingFile);
490                                         return;
491                                     }
492 
493                                     try {
494                                         // Send the file descriptor to service to write to.
495                                         service.receiveFileDescriptor(pfd, keyMostSigBits,
496                                                 keyLeastSigBits);
497                                     } catch (RemoteException e) {
498                                         // If we failed to send it, try to clean it up as it won't
499                                         // be used.
500                                         if (DEBUG) {
501                                             Log.d(TAG, "Failed sending file descriptor to service",
502                                                     e);
503                                         }
504                                         tryToCleanupGeneratedFile(pfd, profilingFile);
505                                     }
506                                 } catch (Exception e) {
507                                     // Failure prepping output file. Result may be lost.
508                                     if (DEBUG) Log.d(TAG, "Exception preparing file", e);
509                                     return;
510                                 }
511                             }
512                         }
513 
514                         /**
515                          * Attempt to clean up the files created for service by closing the file
516                          * descriptor and deleting the file. This is intended for error cases where
517                          * the descriptor could not be sent. If it was successfully sent, service
518                          * will handle closing it and requesting a delete if necessary.
519                          */
520                         private void tryToCleanupGeneratedFile(ParcelFileDescriptor fileDescriptor,
521                                 File file) {
522                             if (fileDescriptor != null) {
523                                 try {
524                                     fileDescriptor.close();
525                                 } catch (IOException e) {
526                                     // Nothing else we can do, ignore.
527                                     if (DEBUG) Log.d(TAG, "Failed to cleanup file descriptor", e);
528                                 }
529                             }
530 
531                             if (file != null) {
532                                 try {
533                                     file.delete();
534                                 } catch (SecurityException e) {
535                                     // Nothing else we can do, ignore.
536                                     if (DEBUG) Log.d(TAG, "Failed to cleanup file", e);
537                                 }
538                             }
539                         }
540 
541                         /**
542                          * Delete a file. To be used only for files created by {@link generateFile}.
543                          */
544                         @Override
545                         public void deleteFile(String filePathAndName) {
546                             try {
547                                 Files.delete(Path.of(filePathAndName));
548                             } catch (Exception exception) {
549                                 if (DEBUG) Log.e(TAG, "Failed to delete file.", exception);
550                             }
551                         }
552                     });
553         } catch (RemoteException e) {
554             if (DEBUG) Log.d(TAG, "Exception registering service callback", e);
555             throw new RuntimeException("Unable to register profiling result callback."
556                     + " All Profiling requests will fail.");
557         }
558         return mProfilingService;
559     }
560 
561     private static final class ProfilingRequestCallbackWrapper {
562         /** executor provided with callback request */
563         final @NonNull Executor mExecutor;
564 
565         /** listener provided with callback request */
566         final @NonNull Consumer<ProfilingResult> mListener;
567 
568         /**
569          * Unique key generated with each profiling request {@link #requestProfiling}, but not with
570          * requests to register a listener only {@link #registerForAllProfilingResults}.
571          *
572          * Key is used to match the result with the listener added with the request so that it can
573          * removed after being triggered while the general registered callbacks remain active.
574          */
575         final @Nullable UUID mKey;
576 
ProfilingRequestCallbackWrapper(@onNull Executor executor, @NonNull Consumer<ProfilingResult> listener, @Nullable UUID key)577         ProfilingRequestCallbackWrapper(@NonNull Executor executor,
578                 @NonNull Consumer<ProfilingResult> listener,
579                 @Nullable UUID key) {
580             mExecutor = executor;
581             mListener = listener;
582             mKey = key;
583         }
584     }
585 }
586