Snap for 12597713 from cc8ea7a889f4d9ab095a97f00782ee0c1d45dd35 to mainline-adservices-release
Change-Id: I5b0e6255c14dfe06793b7fadc9e11aebd84fc766
diff --git a/aidl/android/os/IProfilingService.aidl b/aidl/android/os/IProfilingService.aidl
index cebebae..c1b66c4 100644
--- a/aidl/android/os/IProfilingService.aidl
+++ b/aidl/android/os/IProfilingService.aidl
@@ -18,6 +18,7 @@
import android.os.Bundle;
import android.os.IProfilingResultCallback;
+import android.os.ProfilingTriggerValueParcel;
/**
* {@hide}
@@ -34,4 +35,12 @@
oneway void receiveFileDescriptor(in ParcelFileDescriptor fileDescriptor, long keyMostSigBits, long keyLeastSigBits);
+ oneway void addProfilingTriggers(in List<ProfilingTriggerValueParcel> triggers, String packageName);
+
+ oneway void removeProfilingTriggers(in int[] triggers, String packageName);
+
+ oneway void clearProfilingTriggers(String packageName);
+
+ oneway void processTrigger(int uid, String packageName, int triggerType);
+
}
diff --git a/aidl/android/os/ProfilingTriggerValueParcel.aidl b/aidl/android/os/ProfilingTriggerValueParcel.aidl
new file mode 100644
index 0000000..c9ebf2c
--- /dev/null
+++ b/aidl/android/os/ProfilingTriggerValueParcel.aidl
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+/**
+ * {@hide}
+ */
+parcelable ProfilingTriggerValueParcel {
+ int triggerType;
+ int rateLimitingPeriodHours;
+}
\ No newline at end of file
diff --git a/framework/api/current.txt b/framework/api/current.txt
index fca446a..ae73bf5 100644
--- a/framework/api/current.txt
+++ b/framework/api/current.txt
@@ -2,7 +2,10 @@
package android.os {
@FlaggedApi("android.os.profiling.telemetry_apis") public final class ProfilingManager {
+ method @FlaggedApi("android.os.profiling.system_triggered_profiling_new") public void addProfilingTriggers(@NonNull java.util.List<android.os.ProfilingTrigger>);
+ method @FlaggedApi("android.os.profiling.system_triggered_profiling_new") public void clearProfilingTriggers();
method public void registerForAllProfilingResults(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.os.ProfilingResult>);
+ method @FlaggedApi("android.os.profiling.system_triggered_profiling_new") public void removeProfilingTriggers(@NonNull java.util.List<java.lang.Integer>);
method public void requestProfiling(int, @Nullable android.os.Bundle, @Nullable String, @Nullable android.os.CancellationSignal, @Nullable java.util.concurrent.Executor, @Nullable java.util.function.Consumer<android.os.ProfilingResult>);
method public void unregisterForAllProfilingResults(@Nullable java.util.function.Consumer<android.os.ProfilingResult>);
field public static final int PROFILING_TYPE_HEAP_PROFILE = 2; // 0x2
@@ -17,6 +20,7 @@
method @Nullable public String getErrorMessage();
method @Nullable public String getResultFilePath();
method @Nullable public String getTag();
+ method @FlaggedApi("android.os.profiling.system_triggered_profiling_new") public int getTriggerType();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.os.ProfilingResult> CREATOR;
field public static final int ERROR_FAILED_EXECUTING = 4; // 0x4
@@ -30,5 +34,19 @@
field public static final int ERROR_UNKNOWN = 8; // 0x8
}
+ @FlaggedApi("android.os.profiling.system_triggered_profiling_new") public final class ProfilingTrigger {
+ method public int getRateLimitingPeriodHours();
+ method public int getTriggerType();
+ field public static final int TRIGGER_TYPE_ANR = 2; // 0x2
+ field public static final int TRIGGER_TYPE_APP_COLD_START_ACTIVITY = 1; // 0x1
+ field public static final int TRIGGER_TYPE_NONE = 0; // 0x0
+ }
+
+ @FlaggedApi("android.os.profiling.system_triggered_profiling_new") public static final class ProfilingTrigger.Builder {
+ ctor public ProfilingTrigger.Builder(int);
+ method @NonNull public android.os.ProfilingTrigger build();
+ method @NonNull public android.os.ProfilingTrigger.Builder setRateLimitingPeriodHours(int);
+ }
+
}
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 2f6a35a..2aff331 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -6,5 +6,10 @@
method public static void setProfilingServiceManager(@NonNull android.os.ProfilingServiceManager);
}
+ @FlaggedApi("android.os.profiling.system_triggered_profiling_new") public class ProfilingServiceHelper {
+ method @NonNull public static android.os.ProfilingServiceHelper getInstance();
+ method public void onProfilingTriggerOccurred(int, @NonNull String, int);
+ }
+
}
diff --git a/framework/java/android/os/ProfilingManager.java b/framework/java/android/os/ProfilingManager.java
index 7a3f6e8..417c44e 100644
--- a/framework/java/android/os/ProfilingManager.java
+++ b/framework/java/android/os/ProfilingManager.java
@@ -33,14 +33,17 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.List;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
/**
* <p>
- * This class allows the caller to request profiling and listen for results. Profiling types
- * supported are: system traces, java heap dumps, heap profiles, and stack traces.
+ * This class allows the caller to:
+ * - Request profiling and listen for results. Profiling types supported are: system traces,
+ * java heap dumps, heap profiles, and stack traces.
+ * - Register triggers for the system to capture profiling on the apps behalf.
* </p>
*
* <p>
@@ -59,16 +62,27 @@
* - A request-specific listener included with the request. This will trigger only with a result
* from the request it was provided with.
* - A global listener provided by {@link #registerForAllProfilingResults}. This will be triggered
- * for all results belonging to your app.
+ * for all results belonging to your app. This listener is the only way to receive results from
+ * system triggered profiling instances set up with {@link #addProfilingTriggers}.
* </p>
*
* <p>
* Requests are rate limited and not guaranteed to be filled. Rate limiting can be disabled for
- * local testing using the shell command
+ * local testing of {@link #requestProfiling} using the shell command
* {@code device_config put profiling_testing rate_limiter.disabled true}
* </p>
*
* <p>
+ * In order to test profiling triggers, enable testing mode for your app with the shell command
+ * {@code device_config put profiling_testing system_triggered_profiling.testing_package_name
+ * com.your.app} which will:
+ * - Ensure that a background trace is running.
+ * - Allow all triggers for the provided package name to pass the system level rate limiter.
+ * This mode will continue until manually stopped with the shell command
+ * {@code device_config delete profiling_testing system_triggered_profiling.testing_package_name}
+ * </p>
+ *
+ * <p>
* Results are redacted and contain specific information about the requesting process only.
* </p>
*/
@@ -256,7 +270,9 @@
if (service == null) {
executor.execute(() -> listener.accept(
new ProfilingResult(ProfilingResult.ERROR_UNKNOWN, null, tag,
- "ProfilingService is not available")));
+ "ProfilingService is not available",
+ Flags.systemTriggeredProfilingNew()
+ ? ProfilingTrigger.TRIGGER_TYPE_NONE : 0)));
if (DEBUG) Log.d(TAG, "ProfilingService is not available");
return;
}
@@ -265,7 +281,9 @@
if (packageName == null) {
executor.execute(() -> listener.accept(
new ProfilingResult(ProfilingResult.ERROR_UNKNOWN, null, tag,
- "Failed to resolve package name")));
+ "Failed to resolve package name",
+ Flags.systemTriggeredProfilingNew()
+ ? ProfilingTrigger.TRIGGER_TYPE_NONE : 0)));
if (DEBUG) Log.d(TAG, "Failed to resolve package name.");
return;
}
@@ -293,7 +311,9 @@
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")));
+ "Binder exception processing request",
+ Flags.systemTriggeredProfilingNew()
+ ? ProfilingTrigger.TRIGGER_TYPE_NONE : 0)));
throw new RuntimeException("Unable to request profiling.");
}
}
@@ -323,7 +343,9 @@
// not ever be triggered.
executor.execute(() -> listener.accept(new ProfilingResult(
ProfilingResult.ERROR_UNKNOWN, null, null,
- "Binder exception processing request")));
+ "Binder exception processing request",
+ Flags.systemTriggeredProfilingNew()
+ ? ProfilingTrigger.TRIGGER_TYPE_NONE : 0)));
return;
}
mCallbacks.add(new ProfilingRequestCallbackWrapper(executor, listener, null));
@@ -384,6 +406,146 @@
}
}
+ /**
+ * Register the provided list of triggers for this process.
+ *
+ * Profiling triggers are system triggered events that an app can register interest in receiving
+ * profiling of. There is no guarantee that these triggers will be filled. Results, if
+ * available, will be delivered only to a global listener added using
+ * {@link #registerForAllProfilingResults}.
+ *
+ * Only one of each trigger type can be added at a time.
+ * - If the provided list contains a trigger type that is already registered then the new one
+ * will replace the existing one.
+ * - If the provided list contains more than one trigger object for a trigger type then only one
+ * will be kept.
+ */
+ @FlaggedApi(Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
+ public void addProfilingTriggers(@NonNull List<ProfilingTrigger> triggers) {
+ synchronized (mLock) {
+ if (triggers.isEmpty()) {
+ // No triggers are being added, nothing to do.
+ if (DEBUG) Log.d(TAG, "Trying to add an empty list of triggers.");
+ return;
+ }
+
+ final IProfilingService service = getOrCreateIProfilingServiceLocked(false);
+ if (service == null) {
+ // If we can't access service then we can't do anything. Return.
+ if (DEBUG) Log.d(TAG, "ProfilingService is not available, triggers will be lost.");
+ return;
+ }
+
+ String packageName = mContext.getPackageName();
+ if (packageName == null) {
+ if (DEBUG) Log.d(TAG, "Failed to resolve package name.");
+ return;
+ }
+
+ try {
+ service.addProfilingTriggers(toValueParcelList(triggers), packageName);
+ } catch (RemoteException e) {
+ if (DEBUG) Log.d(TAG, "Binder exception processing request", e);
+ throw new RuntimeException("Unable to add profiling triggers.");
+ }
+ }
+ }
+
+ @FlaggedApi(Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
+ private List<ProfilingTriggerValueParcel> toValueParcelList(
+ List<ProfilingTrigger> triggerList) {
+ List<ProfilingTriggerValueParcel> triggerValueParcelList =
+ new ArrayList<ProfilingTriggerValueParcel>();
+
+ for (int i = 0; i < triggerList.size(); i++) {
+ triggerValueParcelList.add(triggerList.get(i).toValueParcel());
+ }
+
+ return triggerValueParcelList;
+ }
+
+ /** Remove the provided list of triggers for this process. */
+ @FlaggedApi(Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
+ public void removeProfilingTriggers(@NonNull List<Integer> triggers) {
+ synchronized (mLock) {
+ if (triggers.isEmpty()) {
+ // No triggers are being removed, nothing to do.
+ if (DEBUG) Log.d(TAG, "Trying to remove an empty list of triggers.");
+ return;
+ }
+
+ final IProfilingService service = getOrCreateIProfilingServiceLocked(false);
+ if (service == null) {
+ // If we can't access service then we can't do anything. Return.
+ if (DEBUG) {
+ Log.d(TAG, "ProfilingService is not available, triggers will not be removed.");
+ }
+ return;
+ }
+
+ String packageName = mContext.getPackageName();
+ if (packageName == null) {
+ if (DEBUG) Log.d(TAG, "Failed to resolve package name.");
+ return;
+ }
+
+ // First filter for valid triggers only.
+ ArrayList<Integer> validTriggers = new ArrayList<Integer>();
+ for (int i = 0; i < triggers.size(); i++) {
+ int trigger = triggers.get(i).intValue();
+ if (ProfilingTrigger.isValidRequestTriggerType(trigger)) {
+ validTriggers.add(trigger);
+ }
+ }
+
+ if (validTriggers.isEmpty()) {
+ // No valid triggers are being removed, nothing to do.
+ if (DEBUG) Log.d(TAG, "Trying to remove a list of invalid triggers only.");
+ return;
+ }
+
+ // Move to array for binder.
+ int[] arr = new int[validTriggers.size()];
+ for (int i = 0; i < validTriggers.size(); i++) {
+ arr[i] = validTriggers.get(i).intValue();
+ }
+
+ try {
+ service.removeProfilingTriggers(arr, packageName);
+ } catch (RemoteException e) {
+ if (DEBUG) Log.d(TAG, "Binder exception processing request", e);
+ throw new RuntimeException("Unable to remove profiling triggers.");
+ }
+ }
+ }
+
+ /** Remove all triggers for this process. */
+ @FlaggedApi(Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
+ public void clearProfilingTriggers() {
+ synchronized (mLock) {
+ final IProfilingService service = getOrCreateIProfilingServiceLocked(false);
+ if (service == null) {
+ // If we can't access service then we can't do anything. Return.
+ if (DEBUG) {
+ Log.d(TAG, "ProfilingService is not available, triggers will not be removed.");
+ }
+ return;
+ }
+
+ String packageName = mContext.getPackageName();
+ if (packageName == null) {
+ if (DEBUG) Log.d(TAG, "Failed to resolve package name.");
+ return;
+ }
+
+ try {
+ service.clearProfilingTriggers(packageName);
+ } catch (RemoteException e) {
+ if (DEBUG) Log.d(TAG, "Binder exception processing request", e);
+ throw new RuntimeException("Unable to clear profiling triggers.");
+ }
+ }
+ }
/** @hide */
@VisibleForTesting
@@ -460,7 +622,10 @@
wrapper.mExecutor.execute(() -> wrapper.mListener.accept(
new ProfilingResult(overrideStatusToError
? ProfilingResult.ERROR_UNKNOWN : status,
- getAppFileDir() + resultFile, tag, error)));
+ getAppFileDir() + resultFile, tag, error,
+ Flags.systemTriggeredProfilingNew()
+ ? ProfilingTrigger.TRIGGER_TYPE_NONE
+ : 0)));
}
// Remove the single listener that was tied to the request, if
diff --git a/framework/java/android/os/ProfilingResult.java b/framework/java/android/os/ProfilingResult.java
index 61357e1..0ed84ea 100644
--- a/framework/java/android/os/ProfilingResult.java
+++ b/framework/java/android/os/ProfilingResult.java
@@ -43,6 +43,9 @@
/** @see #getErrorMessage */
@Nullable final String mErrorMessage;
+ /** @see #getTriggerType */
+ final int mTriggerType;
+
/** The request was executed and succeeded. */
public static final int ERROR_NONE = 0;
@@ -85,11 +88,12 @@
@interface ErrorCode {}
ProfilingResult(@ErrorCode int errorCode, String resultFilePath, String tag,
- String errorMessage) {
+ String errorMessage, int triggerType) {
mErrorCode = errorCode;
mResultFilePath = resultFilePath;
mTag = tag;
mErrorMessage = errorMessage;
+ mTriggerType = triggerType;
}
private ProfilingResult(@NonNull Parcel in) {
@@ -97,6 +101,7 @@
mResultFilePath = in.readString();
mTag = in.readString();
mErrorMessage = in.readString();
+ mTriggerType = in.readInt();
}
@Override
@@ -105,6 +110,7 @@
dest.writeString(mResultFilePath);
dest.writeString(mTag);
dest.writeString(mErrorMessage);
+ dest.writeInt(mTriggerType);
}
@Override
@@ -154,4 +160,13 @@
public @Nullable String getErrorMessage() {
return mErrorMessage;
}
+
+ /**
+ * Trigger type that started this profiling, or {@link ProfilingTrigger#TRIGGER_TYPE_NONE} for
+ * profiling not started by a trigger.
+ */
+ @FlaggedApi(Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
+ public int getTriggerType() {
+ return mTriggerType;
+ }
}
diff --git a/framework/java/android/os/ProfilingServiceHelper.java b/framework/java/android/os/ProfilingServiceHelper.java
new file mode 100644
index 0000000..9c2c588
--- /dev/null
+++ b/framework/java/android/os/ProfilingServiceHelper.java
@@ -0,0 +1,93 @@
+/*
+ * 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.annotation.SystemApi;
+import android.annotation.SystemApi.Client;
+import android.os.profiling.Flags;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+/**
+ * Class for system to interact with {@link ProfilingService} to notify of trigger occurrences.
+ *
+ * @hide
+ */
+@FlaggedApi(Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
+@SystemApi(client = Client.MODULE_LIBRARIES)
+public class ProfilingServiceHelper {
+ private static final String TAG = ProfilingServiceHelper.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ private static final Object sLock = new Object();
+
+ @Nullable
+ @GuardedBy("sLock")
+ private static ProfilingServiceHelper sInstance;
+
+ private final Object mLock = new Object();
+
+ @NonNull
+ @GuardedBy("mLock")
+ private final IProfilingService mProfilingService;
+
+ private ProfilingServiceHelper(@NonNull IProfilingService service) {
+ mProfilingService = service;
+ }
+
+ /**
+ * Returns an instance of {@link ProfilingServiceHelper}.
+ *
+ * @throws IllegalStateException if called before ProfilingService is set up.
+ */
+ @NonNull
+ public static ProfilingServiceHelper getInstance() {
+ synchronized (sLock) {
+ if (sInstance != null) {
+ return sInstance;
+ }
+
+ IProfilingService service = Flags.telemetryApis() ? IProfilingService.Stub.asInterface(
+ ProfilingFrameworkInitializer.getProfilingServiceManager()
+ .getProfilingServiceRegisterer().get()) : null;
+
+ if (service == null) {
+ throw new IllegalStateException("ProfilingService not yet set up.");
+ }
+
+ sInstance = new ProfilingServiceHelper(service);
+
+ return sInstance;
+ }
+ }
+
+ /** Send a trigger to {@link ProfilingService}. */
+ public void onProfilingTriggerOccurred(int uid, @NonNull String packageName, int triggerType) {
+ synchronized (mLock) {
+ try {
+ mProfilingService.processTrigger(uid, packageName, triggerType);
+ } catch (RemoteException e) {
+ // Exception sending trigger to service. Nothing to do here, trigger will be lost.
+ if (DEBUG) Log.e(TAG, "Exception sending trigger", e);
+ }
+ }
+ }
+}
diff --git a/framework/java/android/os/ProfilingTrigger.java b/framework/java/android/os/ProfilingTrigger.java
new file mode 100644
index 0000000..7cfc113
--- /dev/null
+++ b/framework/java/android/os/ProfilingTrigger.java
@@ -0,0 +1,167 @@
+/*
+ * 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.os.profiling.Flags;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Encapsulates a single profiling trigger.
+ */
+@FlaggedApi(Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
+public final class ProfilingTrigger {
+
+ /** No trigger. Used in {@link ProfilingResult} for non trigger caused results. */
+ public static final int TRIGGER_TYPE_NONE = 0;
+
+ /**
+ * Trigger occurs after a cold start has been completed for an activity component start. See
+ * {@link android.app.ApplicationStartInfo#getStartComponent} for more detail on start
+ * components.
+ */
+ public static final int TRIGGER_TYPE_APP_COLD_START_ACTIVITY = 1;
+
+ /** Trigger occurs after the app was killed due to an ANR */
+ public static final int TRIGGER_TYPE_ANR = 2;
+
+ /** @hide */
+ @IntDef(value = {
+ TRIGGER_TYPE_NONE,
+ TRIGGER_TYPE_APP_COLD_START_ACTIVITY,
+ TRIGGER_TYPE_ANR,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface TriggerType {}
+
+ /** @see #getTriggerType */
+ private final @TriggerType int mTriggerType;
+
+ /** @see #getRateLimitingPeriodHours */
+ private final int mRateLimitingPeriodHours;
+
+ private ProfilingTrigger(@TriggerType int triggerType, int rateLimitingPeriodHours) {
+ mTriggerType = triggerType;
+ mRateLimitingPeriodHours = rateLimitingPeriodHours;
+ }
+
+ /**
+ * Builder class to create a {@link ProfilingTrigger} object.
+ */
+ @FlaggedApi(Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
+ public static final class Builder {
+ // Trigger type has to be set, so make it an object and set to null.
+ private int mBuilderTriggerType;
+
+ // Rate limiter period default is 0 which will make it do nothing.
+ private int mBuilderRateLimitingPeriodHours = 0;
+
+ /**
+ * Create a new builder instance to create a {@link ProfilingTrigger} object.
+ *
+ * Requires a trigger type. An app can only have one registered trigger per trigger type.
+ * Adding a new trigger with the same type will override the previously set one.
+ *
+ * @throws IllegalArgumentException if the trigger type is not valid.
+ */
+ public Builder(@TriggerType int triggerType) {
+ if (!isValidRequestTriggerType(triggerType)) {
+ throw new IllegalArgumentException("Invalid trigger type.");
+ }
+
+ mBuilderTriggerType = triggerType;
+ }
+
+ /** Build the {@link ProfilingTrigger} object. */
+ @NonNull
+ public ProfilingTrigger build() {
+ return new ProfilingTrigger(mBuilderTriggerType,
+ mBuilderRateLimitingPeriodHours);
+ }
+
+ /**
+ * Set a rate limiting period in hours.
+ *
+ * The period is the minimum time the system should wait before providing another
+ * profiling result for the same trigger; actual time between events may be longer.
+ *
+ * If the rate limiting period is not provided or set to 0, no app-provided rate limiting
+ * will be used.
+ *
+ * This rate limiting is in addition to any system level rate limiting that may be applied.
+ *
+ * @throws IllegalArgumentException if the value is less than 0.
+ */
+ @NonNull
+ public Builder setRateLimitingPeriodHours(int rateLimitingPeriodHours) {
+ if (rateLimitingPeriodHours < 0) {
+ throw new IllegalArgumentException("Hours can't be negative. Try again.");
+ }
+
+ mBuilderRateLimitingPeriodHours = rateLimitingPeriodHours;
+ return this;
+ }
+ }
+
+ /** The trigger type indicates which event should trigger the requested profiling. */
+ public @TriggerType int getTriggerType() {
+ return mTriggerType;
+ }
+
+ /**
+ * The requester set rate limiting period in hours.
+ *
+ * The period is the minimum time the system should wait before providing another
+ * profiling result for the same trigger; actual time between events may be longer.
+ *
+ * If the rate limiting period is set to 0, no app-provided rate limiting will be used.
+ *
+ * This rate limiting is in addition to any system level rate limiting that may be applied.
+ */
+ public int getRateLimitingPeriodHours() {
+ return mRateLimitingPeriodHours;
+ }
+
+ /**
+ * Convert to value parcel. Used for binder.
+ *
+ * @hide
+ */
+ public ProfilingTriggerValueParcel toValueParcel() {
+ ProfilingTriggerValueParcel valueParcel = new ProfilingTriggerValueParcel();
+
+ valueParcel.triggerType = mTriggerType;
+ valueParcel.rateLimitingPeriodHours = mRateLimitingPeriodHours;
+
+ return valueParcel;
+ }
+
+ /**
+ * Check whether the trigger type is valid for request use. Note that this means that a value of
+ * {@link TRIGGER_TYPE_NONE} will return false.
+ *
+ * @hide
+ */
+ public static boolean isValidRequestTriggerType(int triggerType) {
+ return triggerType == TRIGGER_TYPE_APP_COLD_START_ACTIVITY
+ || triggerType == TRIGGER_TYPE_ANR;
+ }
+
+}
diff --git a/service/java/com/android/os/profiling/ProfilingService.java b/service/java/com/android/os/profiling/ProfilingService.java
index 33e602e..5b50ef8 100644
--- a/service/java/com/android/os/profiling/ProfilingService.java
+++ b/service/java/com/android/os/profiling/ProfilingService.java
@@ -34,6 +34,7 @@
import android.os.ParcelFileDescriptor;
import android.os.ProfilingManager;
import android.os.ProfilingResult;
+import android.os.ProfilingTriggerValueParcel;
import android.os.ProfilingTriggersWrapper;
import android.os.QueuedResultsWrapper;
import android.os.RemoteException;
@@ -1042,6 +1043,47 @@
}
/**
+ * Add the provided list of validated triggers with the provided package name and the callers
+ * uid being applied to all.
+ */
+ public void addProfilingTriggers(List<ProfilingTriggerValueParcel> triggers,
+ String packageName) {
+ int uid = Binder.getCallingUid();
+ for (int i = 0; i < triggers.size(); i++) {
+ ProfilingTriggerValueParcel trigger = triggers.get(i);
+ addTrigger(uid, packageName, trigger.triggerType, trigger.rateLimitingPeriodHours);
+ }
+ }
+
+ /**
+ * Remove the provided list of validated trigger codes from a process with the provided package
+ * name and the uid of the caller.
+ */
+ public void removeProfilingTriggers(int[] triggerTypesToRemove, String packageName) {
+ SparseArray<ProfilingTrigger> triggers =
+ mAppTriggers.get(packageName, Binder.getCallingUid());
+
+ for (int i = 0; i < triggerTypesToRemove.length; i++) {
+ int index = triggers.indexOfKey(triggerTypesToRemove[i]);
+ if (index >= 0) {
+ triggers.removeAt(index);
+ }
+ }
+
+ if (triggers.size() == 0) {
+ // Nothing left, remove.
+ mAppTriggers.remove(packageName, Binder.getCallingUid());
+ }
+ }
+
+ /**
+ * Remove all triggers from a process with the provided packagename and the uid of the caller.
+ */
+ public void clearProfilingTriggers(String packageName) {
+ mAppTriggers.remove(packageName, Binder.getCallingUid());
+ }
+
+ /**
* Method called by manager, after creating a file from within application context, to send a
* file descriptor for service to write the result of the profiling session to.
*
diff --git a/tests/cts/src/android/profiling/cts/ProfilingFrameworkTests.java b/tests/cts/src/android/profiling/cts/ProfilingFrameworkTests.java
index acf60b4..21316d1 100644
--- a/tests/cts/src/android/profiling/cts/ProfilingFrameworkTests.java
+++ b/tests/cts/src/android/profiling/cts/ProfilingFrameworkTests.java
@@ -30,10 +30,13 @@
import android.app.Instrumentation;
import android.content.Context;
+import android.os.Binder;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ProfilingManager;
import android.os.ProfilingResult;
+import android.os.ProfilingServiceHelper;
+import android.os.ProfilingTrigger;
import android.os.profiling.DeviceConfigHelper;
import android.os.profiling.Flags;
import android.os.profiling.ProfilingService;
@@ -64,6 +67,7 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
@@ -92,8 +96,13 @@
private static final int RATE_LIMITER_WAIT_TIME_INCREMENTS_COUNT = 12;
// Wait 2 seconds for profiling to get started before attempting to cancel it.
+ // TODO: b/376440094 - change to query perfetto and confirm profiling is running.
private static final int WAIT_TIME_FOR_PROFILING_START_MS = 2 * 1000;
+ // Wait 10 seconds for profiling to potentially clone, process, and return result to confirm it
+ // did not occur.
+ private static final int WAIT_TIME_FOR_TRIGGERED_PROFILING_NO_RESULT = 10 * 1000;
+
// Keep in sync with {@link ProfilingService} because we can't access it.
private static final String OUTPUT_FILE_JAVA_HEAP_DUMP_SUFFIX = ".perfetto-java-heap-dump";
private static final String OUTPUT_FILE_HEAP_PROFILE_SUFFIX = ".perfetto-heap-profile";
@@ -105,6 +114,11 @@
private static final String COMMAND_OVERRIDE_DEVICE_CONFIG_INT = "device_config put %s %s %d";
private static final String COMMAND_OVERRIDE_DEVICE_CONFIG_BOOL = "device_config put %s %s %b";
+ private static final String COMMAND_OVERRIDE_DEVICE_CONFIG_STRING =
+ "device_config put %s %s %s";
+ private static final String COMMAND_DELETE_DEVICE_CONFIG_STRING = "device_config delete %s %s";
+
+ private static final String REAL_PACKAGE_NAME = "com.android.profiling.tests";
private static final int ONE_SECOND_MS = 1 * 1000;
private static final int FIVE_SECONDS_MS = 5 * 1000;
@@ -143,8 +157,10 @@
@SuppressWarnings("GuardedBy") // Suppress warning for mProfilingManager.mProfilingService lock.
@After
- public void cleanup() {
+ public void cleanup() throws Exception {
mProfilingManager.mProfilingService = null;
+ executeShellCmd(COMMAND_DELETE_DEVICE_CONFIG_STRING, DeviceConfigHelper.NAMESPACE_TESTING,
+ DeviceConfigHelper.SYSTEM_TRIGGERED_TEST_PACKAGE_NAME);
}
/** Check and see if we can get a reference to the ProfilingManager service. */
@@ -862,6 +878,98 @@
verify(mProfilingManager.mProfilingService, times(0)).generalListenerAdded();
}
+ /**
+ * Test adding a profiling trigger and receiving a result works correctly.
+ *
+ * This is done by: adding the trigger through the public api, force starting a system triggered
+ * trace, sending a fake trigger as if from the system, and then confirming the result is
+ * received.
+ */
+ @SuppressWarnings("GuardedBy") // Suppress warning for mProfilingManager lock.
+ @Test
+ @RequiresFlagsEnabled(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
+ public void testSystemTriggeredProfiling() throws Exception {
+ if (mProfilingManager == null) throw new TestException("mProfilingManager can not be null");
+
+ // First add a trigger
+ ProfilingTrigger trigger = new ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_ANR)
+ .setRateLimitingPeriodHours(1)
+ .build();
+ mProfilingManager.addProfilingTriggers(List.of(trigger));
+
+ // And add a global listener
+ AppCallback callbackGeneral = new AppCallback();
+ mProfilingManager.registerForAllProfilingResults(
+ new ProfilingTestUtils.ImmediateExecutor(), callbackGeneral);
+
+ // Then start the system triggered trace for testing.
+ executeShellCmd(COMMAND_OVERRIDE_DEVICE_CONFIG_STRING,
+ DeviceConfigHelper.NAMESPACE_TESTING,
+ DeviceConfigHelper.SYSTEM_TRIGGERED_TEST_PACKAGE_NAME,
+ REAL_PACKAGE_NAME);
+
+ // Wait a bit so the trace can get started and actually collect something.
+ sleep(WAIT_TIME_FOR_PROFILING_START_MS);
+
+ // Now fake a system trigger.
+ ProfilingServiceHelper.getInstance().onProfilingTriggerOccurred(Binder.getCallingUid(),
+ REAL_PACKAGE_NAME,
+ ProfilingTrigger.TRIGGER_TYPE_ANR);
+
+ // Wait for the trace to process.
+ waitForCallback(callbackGeneral);
+
+ // Finally, confirm that a result was received.
+ confirmCollectionSuccess(callbackGeneral.mResult, OUTPUT_FILE_TRACE_SUFFIX);
+ }
+
+ /**
+ * Test removing profiling trigger.
+ *
+ * There is no way to check the data structure from this context and that specifically is tested
+ * in {@link ProfilingServiceTests}, so this test just ensures that a result is not received.
+ */
+ @SuppressWarnings("GuardedBy") // Suppress warning for mProfilingManager lock.
+ @Test
+ @RequiresFlagsEnabled(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
+ public void testSystemTriggeredProfilingRemove() throws Exception {
+ if (mProfilingManager == null) throw new TestException("mProfilingManager can not be null");
+
+ // First add a trigger
+ ProfilingTrigger trigger = new ProfilingTrigger.Builder(ProfilingTrigger.TRIGGER_TYPE_ANR)
+ .setRateLimitingPeriodHours(1)
+ .build();
+ mProfilingManager.addProfilingTriggers(List.of(trigger));
+
+ // And add a global listener
+ AppCallback callbackGeneral = new AppCallback();
+ mProfilingManager.registerForAllProfilingResults(
+ new ProfilingTestUtils.ImmediateExecutor(), callbackGeneral);
+
+ // Then start the system triggered trace for testing.
+ executeShellCmd(COMMAND_OVERRIDE_DEVICE_CONFIG_STRING,
+ DeviceConfigHelper.NAMESPACE_TESTING,
+ DeviceConfigHelper.SYSTEM_TRIGGERED_TEST_PACKAGE_NAME,
+ REAL_PACKAGE_NAME);
+
+ // Wait a bit so the trace can get started and actually collect something.
+ sleep(WAIT_TIME_FOR_PROFILING_START_MS);
+
+ // Remove the trigger.
+ mProfilingManager.removeProfilingTriggers(List.of(ProfilingTrigger.TRIGGER_TYPE_ANR));
+
+ // Now fake a system trigger.
+ ProfilingServiceHelper.getInstance().onProfilingTriggerOccurred(Binder.getCallingUid(),
+ REAL_PACKAGE_NAME,
+ ProfilingTrigger.TRIGGER_TYPE_ANR);
+
+ // We can't wait for nothing to happen, so wait 10 seconds which should be long enough.
+ sleep(WAIT_TIME_FOR_TRIGGERED_PROFILING_NO_RESULT);
+
+ // Finally, confirm that no callback was received.
+ assertNull(callbackGeneral.mResult);
+ }
+
/** Disable the rate limiter and wait long enough for the update to be picked up. */
private void disableRateLimiter() {
SystemUtil.runShellCommand(