blob: e37b005ef45339b24e0f59019f3307fb1e950cde [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.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.Binder;
import android.os.CancellationSignal;
import android.os.IProfilingService;
import android.os.ProfilingRequest;
import android.os.profiling.Flags;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.Exception;
import java.lang.IllegalArgumentException;
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 class ProfilingManager {
private static final String TAG = ProfilingManager.class.getSimpleName();
private static final boolean DEBUG = false;
private static final Object sLock = new Object();
private final Context mContext;
@GuardedBy("sLock")
private final ArrayList<ProfilingRequestCallbackWrapper> mCallbacks = new ArrayList<>();
@GuardedBy("sLock")
private IProfilingService mProfilingService;
/**
* Constructor for ProfilingManager.
*
* @hide
*/
public ProfilingManager(Context context) {
mContext = context;
}
/**
* Request profiling via perfetto.
*
* <p class="note"> Note: use of this API directly is not recommended for most use cases.
* Please use the higher level wrappers provided by androidx that will construct the request
* correctly based on available options and simplified user provided request parameters.</p>
*
* <p class="note"> Note: requests are not guaranteed to be filled.</p>
*
* <p class="note"> Note: a listener must be set for the request to be considered for
* fulfillment. Listeners can be set in this method, with {@see #registerForProfilingResult},
* or both. If no listener is set the request will be discarded.</p>
*
* @param profilingRequest byte array representation of ProfilingRequest proto containing all
* necessary information about the collection being requested.
* @param tag Caller defined data to help identify the output.
* @param cancellationSignal for caller requested cancellation.
* @param executor The executor to call back with.
* @param listener Listener to be triggered with result.
*/
public void requestProfiling(
@NonNull byte[] profilingRequest,
@Nullable String tag,
@Nullable CancellationSignal cancellationSignal,
@Nullable Executor executor,
@Nullable Consumer<ProfilingResult> listener) {
synchronized (sLock) {
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 = getIProfilingServiceLocked();
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;
}
// For key, use most and least signifcant bits so we can create an identical UUID
// after passing over binder.
service.requestProfiling(profilingRequest, tag, key.getMostSignificantBits(),
key.getLeastSignificantBits());
if (cancellationSignal != null) {
cancellationSignal.setOnCancelListener(
() -> {
synchronized (sLock) {
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.
*
* @param executor The executor to call back with.
* @param listener Listener to be triggered with result.
*/
public void registerForProfilingResults(
@NonNull Executor executor,
@NonNull Consumer<ProfilingResult> listener) {
synchronized (sLock) {
mCallbacks.add(new ProfilingRequestCallbackWrapper(executor, listener, null));
}
}
/**
* Unregister a listener to be called for all profiling results. If no listener is provided,
* all listeners for this process that were not submitted with a currently running trace will
* be removed.
*
* @param listener Listener to unregister and no longer be triggered with the result.
*/
public void unregisterForProfilingResults(
@Nullable Consumer<ProfilingResult> listener) {
synchronized (sLock) {
if (mCallbacks.isEmpty()) {
// No callbacks, nothing to remove.
return;
}
for (int i = 0; i < mCallbacks.size(); i++) {
ProfilingRequestCallbackWrapper wrapper = mCallbacks.get(i);
if (listener.equals(wrapper.mListener)) {
mCallbacks.remove(i);
return;
}
}
}
}
@GuardedBy("sLock")
private @Nullable IProfilingService getIProfilingServiceLocked() {
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(new IProfilingResultCallback.Stub() {
@Override
public void sendResult(long keyMostSigBits, long keyLeastSigBits, int status,
String filePath, String tag, String error) {
synchronized (sLock) {
if (mCallbacks.isEmpty()) {
// This shouldn't happen - no callbacks, nowhere to report this result.
if (DEBUG) Log.d(TAG, "No callbacks");
return;
}
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;
}
wrapper.mExecutor.execute(() -> wrapper.mListener.accept(
new ProfilingResult(status, filePath, tag, error)));
}
if (removeListenerPos != -1) {
mCallbacks.remove(removeListenerPos);
}
}
}
});
} 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 {@see #requestProfiling}, but not with
* requests to register a listener only {@see #registerForProfilingResult}.
*
* 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;
}
}
}