blob: 011738f27670441f3629efc3679bd6c090363f67 [file] [log] [blame]
/*
* 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<ProfilingRequestCallbackWrapper> mCallbacks = new ArrayList<>();
/** @hide */
@VisibleForTesting
@GuardedBy("mLock")
public IProfilingService mProfilingService;
/**
* Constructor for ProfilingManager.
*
* @hide
*/
public ProfilingManager(Context context) {
mContext = context;
}
/**
* Request system profiling.
*
* <p class="note">
* 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
* </p>
*
* <p>
* 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.
* </p>
*
* <p>
* Requests will be rate limited and are not guaranteed to be filled.
* </p>
*
* <p>
* 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.
* </p>
*
* @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<ProfilingResult> 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.
*
* <p class="note"> 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. </p>
*
* @param executor The executor to call back with.
* @param listener Listener to be triggered with result.
*/
public void registerForAllProfilingResults(
@NonNull Executor executor,
@NonNull Consumer<ProfilingResult> 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<ProfilingResult> listener) {
synchronized (mLock) {
if (mCallbacks.isEmpty()) {
// No callbacks, nothing to remove.
return;
}
if (listener == null) {
// Remove all global listeners.
ArrayList<ProfilingRequestCallbackWrapper> 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<ProfilingResult> 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<ProfilingResult> listener,
@Nullable UUID key) {
mExecutor = executor;
mListener = listener;
mKey = key;
}
}
}