/* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.os; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.os.profiling.Flags; import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.io.File; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.UUID; import java.util.concurrent.Executor; import java.util.function.Consumer; /** * API for apps to request and listen for app specific profiling. */ @FlaggedApi(Flags.FLAG_TELEMETRY_APIS) public final class ProfilingManager { private static final String TAG = ProfilingManager.class.getSimpleName(); private static final boolean DEBUG = false; /** Profiling type for {@link #requestProfiling} to request a java heap dump. */ public static final int PROFILING_TYPE_JAVA_HEAP_DUMP = 1; /** Profiling type for {@link #requestProfiling} to request a heap profile. */ public static final int PROFILING_TYPE_HEAP_PROFILE = 2; /** Profiling type for {@link #requestProfiling} to request a stack sample. */ public static final int PROFILING_TYPE_STACK_SAMPLING = 3; /** Profiling type for {@link #requestProfiling} to request a system trace. */ public static final int PROFILING_TYPE_SYSTEM_TRACE = 4; /* Begin public API defined keys. */ /* End public API defined keys. */ /* Begin not-public API defined keys/values. */ /** * Can only be used with profiling type heap profile, stack sampling, or system trace. * Value of type int. * @hide */ public static final String KEY_DURATION_MS = "KEY_DURATION_MS"; /** * Can only be used with profiling type heap profile. Value of type long. * @hide */ public static final String KEY_SAMPLING_INTERVAL_BYTES = "KEY_SAMPLING_INTERVAL_BYTES"; /** * Can only be used with profiling type heap profile. Value of type boolean. * @hide */ public static final String KEY_TRACK_JAVA_ALLOCATIONS = "KEY_TRACK_JAVA_ALLOCATIONS"; /** * Can only be used with profiling type stack sampling. Value of type int. * @hide */ public static final String KEY_FREQUENCY_HZ = "KEY_FREQUENCY_HZ"; /** * Can be used with all profiling types. Value of type int. * @hide */ public static final String KEY_SIZE_KB = "KEY_SIZE_KB"; /** * Can be used with profiling type system trace. * Value of type int must be one of: * {@link VALUE_BUFFER_FILL_POLICY_DISCARD} * {@link VALUE_BUFFER_FILL_POLICY_RING_BUFFER} * @hide */ public static final String KEY_BUFFER_FILL_POLICY = "KEY_BUFFER_FILL_POLICY"; /** @hide */ public static final int VALUE_BUFFER_FILL_POLICY_DISCARD = 1; /** @hide */ public static final int VALUE_BUFFER_FILL_POLICY_RING_BUFFER = 2; /* End not-public API defined keys/values. */ /** * @hide * */ @IntDef( prefix = {"PROFILING_TYPE_"}, value = { PROFILING_TYPE_JAVA_HEAP_DUMP, PROFILING_TYPE_HEAP_PROFILE, PROFILING_TYPE_STACK_SAMPLING, PROFILING_TYPE_SYSTEM_TRACE, }) @Retention(RetentionPolicy.SOURCE) public @interface ProfilingType {} private final Object mLock = new Object(); private final Context mContext; /** @hide */ @VisibleForTesting @GuardedBy("mLock") public final ArrayList mCallbacks = new ArrayList<>(); /** @hide */ @VisibleForTesting @GuardedBy("mLock") public IProfilingService mProfilingService; /** * Constructor for ProfilingManager. * * @hide */ public ProfilingManager(Context context) { mContext = context; } /** * Request system profiling. * *

* Note: use of this API directly is not recommended for most use cases. * Consider using the higher level wrappers provided by AndroidX that will construct the * request correctly, supporting available options with simplified request parameters *

* *

* Both a listener and an executor must be set at the time of the request for the request to * be considered for fulfillment. Listener/executor pairs can be set in this method, with * {@link registerForAllProfilingResults}, or both. The listener and executor must be set * together, in the same call. If no listener and executor combination is set, the request * will be discarded and no callback will be received. *

* *

* Requests will be rate limited and are not guaranteed to be filled. *

* *

* There might be a delay before profiling begins. * For continuous profiling types (system tracing, stack sampling, and heap profiling), * we recommend starting the collection early and stopping it with {@link cancellationSignal} * immediately after the area of interest to ensure that the section you want profiled is * captured. * For heap dumps, we recommend testing locally to ensure that the heap dump is collected at * the proper time. *

* * @param profilingType Type of profiling to collect. * @param parameters Bundle of request related parameters. If the bundle contains any * unrecognized parameters, the request will be fail with * {@link #ProfilingResult#ERROR_FAILED_INVALID_REQUEST}. If the values for * the parameters are out of supported range, the closest possible in range * value will be chosen. * Use of androidx wrappers is recommended over generating this directly. * @param tag Caller defined data to help identify the output. * The first 20 alphanumeric characters, plus dashes, will be lowercased * and included in the output filename. * @param cancellationSignal for caller requested cancellation. * Results will be returned if available. * If this is null, the requesting app will not be able to stop the collection. * The collection will stop after timing out with either the provided * configurations or with system defaults * @param executor The executor to call back with. * Will only be used for the listener provided in this method. * If this is null, and no global executor and listener combinations are * registered at the time of the request, the request will be dropped. * @param listener Listener to be triggered with result. Any global listeners registered via * {@link #registerForAllProfilingResults} will also be triggered. If this is * null, and no global listener and executor combinations are registered at * the time of the request, the request will be dropped. */ public void requestProfiling( @ProfilingType int profilingType, @Nullable Bundle parameters, @Nullable String tag, @Nullable CancellationSignal cancellationSignal, @Nullable Executor executor, @Nullable Consumer listener) { synchronized (mLock) { try { final UUID key = UUID.randomUUID(); if (executor != null && listener != null) { // Listeners are provided, store them. mCallbacks.add(new ProfilingRequestCallbackWrapper(executor, listener, key)); } else if (mCallbacks.isEmpty()) { // No listeners have been registered by any path, toss the request. throw new IllegalArgumentException( "No listeners have been registered. Request has been discarded."); } // If neither case above was hit, app wide listeners were provided. Continue. final IProfilingService service = getOrCreateIProfilingServiceLocked(false); if (service == null) { executor.execute(() -> listener.accept( new ProfilingResult(ProfilingResult.ERROR_UNKNOWN, null, tag, "ProfilingService is not available"))); if (DEBUG) Log.d(TAG, "ProfilingService is not available"); return; } String packageName = mContext.getPackageName(); if (packageName == null) { executor.execute(() -> listener.accept( new ProfilingResult(ProfilingResult.ERROR_UNKNOWN, null, tag, "Failed to resolve package name"))); if (DEBUG) Log.d(TAG, "Failed to resolve package name."); return; } // For key, use most and least significant bits so we can create an identical UUID // after passing over binder. service.requestProfiling(profilingType, parameters, mContext.getFilesDir().getPath(), tag, key.getMostSignificantBits(), key.getLeastSignificantBits(), packageName); if (cancellationSignal != null) { cancellationSignal.setOnCancelListener( () -> { synchronized (mLock) { try { service.requestCancel(key.getMostSignificantBits(), key.getLeastSignificantBits()); } catch (RemoteException e) { // Ignore, request in flight already and we can't stop it. } } } ); } } catch (RemoteException e) { if (DEBUG) Log.d(TAG, "Binder exception processing request", e); executor.execute(() -> listener.accept( new ProfilingResult(ProfilingResult.ERROR_UNKNOWN, null, tag, "Binder exception processing request"))); throw new RuntimeException("Unable to request profiling."); } } } /** * Register a listener to be called for all profiling results for this uid. Listeners set here * will be called in addition to any provided with the request. * *

Note: If a callback attempt fails (for example, because your app is killed * while a trace is in progress) re-delivery may be attempted using a listener added via this * method.

* * @param executor The executor to call back with. * @param listener Listener to be triggered with result. */ public void registerForAllProfilingResults( @NonNull Executor executor, @NonNull Consumer listener) { synchronized (mLock) { // Only notify {@link mProfilingService} of a general listener being added if it already // exists as registering it also handles the notifying. boolean shouldNotifyService = mProfilingService != null; if (getOrCreateIProfilingServiceLocked(true) == null) { // If the binder object was not successfully registered then this listener will // not ever be triggered. executor.execute(() -> listener.accept(new ProfilingResult( ProfilingResult.ERROR_UNKNOWN, null, null, "Binder exception processing request"))); return; } mCallbacks.add(new ProfilingRequestCallbackWrapper(executor, listener, null)); if (shouldNotifyService) { // Notify service that a general listener was added. General listeners are also used // for queued callbacks if any are waiting. try { mProfilingService.generalListenerAdded(); } catch (RemoteException e) { // Do nothing. Binder callback is already registered, but service won't know // there is a general listener so queued callbacks won't occur. Log.d(TAG, "Exception notifying service of general callback," + " queued callbacks will not occur.", e); } } } } /** * Unregister a listener that was to be called for all profiling results. If no listener is * provided, all listeners for this process that were not submitted with a profiling request * will be removed. * * @param listener Listener to unregister and no longer be triggered with the results. * Null to remove all global listeners for this uid. */ public void unregisterForAllProfilingResults( @Nullable Consumer listener) { synchronized (mLock) { if (mCallbacks.isEmpty()) { // No callbacks, nothing to remove. return; } if (listener == null) { // Remove all global listeners. ArrayList listenersToRemove = new ArrayList<>(); for (int i = 0; i < mCallbacks.size(); i++) { ProfilingRequestCallbackWrapper wrapper = mCallbacks.get(i); // Only remove global listeners which are not tied to a specific request. These // can be identified by checking that they do not have an associated key. if (wrapper.mKey == null) { listenersToRemove.add(wrapper); } } mCallbacks.removeAll(listenersToRemove); } else { // Remove the provided listener only. for (int i = 0; i < mCallbacks.size(); i++) { ProfilingRequestCallbackWrapper wrapper = mCallbacks.get(i); if (listener.equals(wrapper.mListener)) { mCallbacks.remove(i); return; } } } } } /** @hide */ @VisibleForTesting @GuardedBy("mLock") public @Nullable IProfilingService getOrCreateIProfilingServiceLocked( boolean isGeneralListener) { // We only register the callback with registerResultsCallback once per binder object, and we // only create one binder object per ProfilingManager instance. If the object already exists // then it was successfully created and registered previously so we can just return it. if (mProfilingService != null) { return mProfilingService; } mProfilingService = IProfilingService.Stub.asInterface( ProfilingFrameworkInitializer.getProfilingServiceManager() .getProfilingServiceRegisterer().get()); if (mProfilingService == null) { // Service is not accessible, all requests will fail. return mProfilingService; } try { mProfilingService.registerResultsCallback(isGeneralListener, new IProfilingResultCallback.Stub() { /** * Called by {@link ProfilingService} when a result is ready, * both for success and failure. */ @Override public void sendResult(@Nullable String resultFile, long keyMostSigBits, long keyLeastSigBits, int status, @Nullable String tag, @Nullable String error) { synchronized (mLock) { if (mCallbacks.isEmpty()) { // This shouldn't happen - no callbacks, nowhere to report this // result. if (DEBUG) Log.d(TAG, "No callbacks"); mProfilingService = null; return; } // This shouldn't be true, but if the file is null ensure the status // represents a failure. final boolean overrideStatusToError = resultFile == null && status == ProfilingResult.ERROR_NONE; UUID key = new UUID(keyMostSigBits, keyLeastSigBits); int removeListenerPos = -1; for (int i = 0; i < mCallbacks.size(); i++) { ProfilingRequestCallbackWrapper wrapper = mCallbacks.get(i); if (key.equals(wrapper.mKey)) { // At most 1 listener can have a key matching this result: // the one registered with the request, remove that one // only. if (removeListenerPos == -1) { removeListenerPos = i; } else { // This should never happen. if (DEBUG) { Log.d(TAG, "More than 1 listener with the same key"); } } } else if (wrapper.mKey != null) { // If the key is not null, and doesn't matched the result // key, then this key belongs to another request and should // not be triggered. continue; } // TODO: b/337017299 - check resultFile is valid before // returning Now trigger the callback for any listener that // doesn't belong to another request. wrapper.mExecutor.execute(() -> wrapper.mListener.accept( new ProfilingResult(overrideStatusToError ? ProfilingResult.ERROR_UNKNOWN : status, resultFile, tag, error))); } // Remove the single listener that was tied to the request, if // applicable. if (removeListenerPos != -1) { mCallbacks.remove(removeListenerPos); } } } /** * Called by {@link ProfilingService} when a trace is ready and needs to be * copied to callers internal storage. * * This method will open a new file and pass back the FileDescriptor for * ProfilingService to write to via a new binder call. * * Takes in key most/least significant bits which represent the key that * will be used to associate this back to a profiling session which will * write to the generated file. */ @Override public void generateFile(String filePathAbsolute, String fileName, long keyMostSigBits, long keyLeastSigBits) { synchronized (mLock) { try { // Ensure the profiling directory exists. Create it if it // doesn't. final File profilingDir = new File(filePathAbsolute); if (!profilingDir.exists()) { profilingDir.mkdir(); } // Create the profiling file for the output to be written to. final File profilingFile = new File( filePathAbsolute + fileName); profilingFile.createNewFile(); if (!profilingFile.exists()) { // Failed to create output file. Result may be lost. if (DEBUG) Log.d(TAG, "Output file couldn't be created"); return; } // Wrap the new output file in a {@link ParcelFileDescriptor} to // send back to {@link ProfilingService} to write to. ParcelFileDescriptor pfd = ParcelFileDescriptor.open( profilingFile, ParcelFileDescriptor.MODE_READ_WRITE); IProfilingService service = getOrCreateIProfilingServiceLocked(false); if (service == null) { // Unable to send file descriptor because we have nowhere to // send it to. Result may be lost. Close descriptor and // delete file. if (DEBUG) Log.d(TAG, "Unable to send file descriptor"); tryToCleanupGeneratedFile(pfd, profilingFile); return; } try { // Send the file descriptor to service to write to. service.receiveFileDescriptor(pfd, keyMostSigBits, keyLeastSigBits); } catch (RemoteException e) { // If we failed to send it, try to clean it up as it won't // be used. if (DEBUG) { Log.d(TAG, "Failed sending file descriptor to service", e); } tryToCleanupGeneratedFile(pfd, profilingFile); } } catch (Exception e) { // Failure prepping output file. Result may be lost. if (DEBUG) Log.d(TAG, "Exception preparing file", e); return; } } } /** * Attempt to clean up the files created for service by closing the file * descriptor and deleting the file. This is intended for error cases where * the descriptor could not be sent. If it was successfully sent, service * will handle closing it and requesting a delete if necessary. */ private void tryToCleanupGeneratedFile(ParcelFileDescriptor fileDescriptor, File file) { if (fileDescriptor != null) { try { fileDescriptor.close(); } catch (IOException e) { // Nothing else we can do, ignore. if (DEBUG) Log.d(TAG, "Failed to cleanup file descriptor", e); } } if (file != null) { try { file.delete(); } catch (SecurityException e) { // Nothing else we can do, ignore. if (DEBUG) Log.d(TAG, "Failed to cleanup file", e); } } } /** * Delete a file. To be used only for files created by {@link generateFile}. */ @Override public void deleteFile(String filePathAndName) { try { Files.delete(Path.of(filePathAndName)); } catch (Exception exception) { if (DEBUG) Log.e(TAG, "Failed to delete file.", exception); } } }); } catch (RemoteException e) { if (DEBUG) Log.d(TAG, "Exception registering service callback", e); throw new RuntimeException("Unable to register profiling result callback." + " All Profiling requests will fail."); } return mProfilingService; } private static final class ProfilingRequestCallbackWrapper { /** executor provided with callback request */ final @NonNull Executor mExecutor; /** listener provided with callback request */ final @NonNull Consumer mListener; /** * Unique key generated with each profiling request {@link #requestProfiling}, but not with * requests to register a listener only {@link #registerForAllProfilingResults}. * * Key is used to match the result with the listener added with the request so that it can * removed after being triggered while the general registered callbacks remain active. */ final @Nullable UUID mKey; ProfilingRequestCallbackWrapper(@NonNull Executor executor, @NonNull Consumer listener, @Nullable UUID key) { mExecutor = executor; mListener = listener; mKey = key; } } }