Snap for 12582784 from a5aa2cde40a6c66c9751517361c3a966c9cf06fa to mainline-adservices-release
Change-Id: I3ba080c26186a1616937a1bc7793aba1e062e426
diff --git a/framework/java/android/os/flags.aconfig b/framework/java/android/os/flags.aconfig
index 7ccdaa7..1b21e88 100644
--- a/framework/java/android/os/flags.aconfig
+++ b/framework/java/android/os/flags.aconfig
@@ -27,12 +27,13 @@
is_exported: true
description: "Enables profiling queue persist and restore."
bug: "342435438"
- }
+}
flag {
- name: "system_triggered_profiling"
+ name: "system_triggered_profiling_new"
namespace: "system_performance"
is_exported: true
description: "Enables system triggered profiling apis and functionality."
+ is_fixed_read_only: true
bug: "373461116"
}
diff --git a/service/java/com/android/os/profiling/Configs.java b/service/java/com/android/os/profiling/Configs.java
index 7b8be88..27225ca 100644
--- a/service/java/com/android/os/profiling/Configs.java
+++ b/service/java/com/android/os/profiling/Configs.java
@@ -41,6 +41,8 @@
private static final int FOUR_MB = 4096;
+ private static final int ONE_DAY_MS = 24 * 60 * 60 * 1000;
+
private static boolean sSystemTriggeredSystemTraceConfigsInitialized = false;
private static boolean sSystemTraceConfigsInitialized = false;
private static boolean sHeapProfileConfigsInitialized = false;
@@ -771,9 +773,13 @@
return builder.build().toByteArray();
}
- /** Generate config for system triggered background system trace. */
+ /**
+ * Generate config for system triggered background system trace.
+ *
+ * @param extraLong should only be set to true for testing.
+ */
public static byte[] generateSystemTriggeredTraceConfig(String uniqueSessionName,
- String[] packageNames) {
+ String[] packageNames, boolean extraLong) {
// Make sure we have our config values set. This is the only config specific method which is
// called directly and therefore needs to verify the config value initialization directly.
initializeSystemTriggeredSystemTraceConfigsIfNecessary();
@@ -785,7 +791,9 @@
packageNames,
sSystemTriggeredSystemTraceDiscardBufferSizeKb,
sSystemTriggeredSystemTraceRingBufferSizeKb,
- sSystemTriggeredSystemTraceDurationMs,
+ extraLong
+ ? ONE_DAY_MS
+ : sSystemTriggeredSystemTraceDurationMs,
TraceConfig.BufferConfig.FillPolicy.RING_BUFFER);
builder.setUniqueSessionName(uniqueSessionName);
diff --git a/service/java/com/android/os/profiling/DeviceConfigHelper.java b/service/java/com/android/os/profiling/DeviceConfigHelper.java
index 72d1621..f29de40 100644
--- a/service/java/com/android/os/profiling/DeviceConfigHelper.java
+++ b/service/java/com/android/os/profiling/DeviceConfigHelper.java
@@ -33,6 +33,8 @@
public static final String RATE_LIMITER_DISABLE_PROPERTY = "rate_limiter.disabled";
public static final String DISABLE_DELETE_UNREDACTED_TRACE =
"delete_unredacted_trace.disabled";
+ public static final String SYSTEM_TRIGGERED_TEST_PACKAGE_NAME =
+ "system_triggered_profiling.testing_package_name";
// End section: Testing specific constants
@@ -130,8 +132,6 @@
"clear_temporary_directory_frequency_ms";
public static final String CLEAR_TEMPORARY_DIRECTORY_BOOT_DELAY_MS =
"clear_temporary_directory_boot_delay_ms";
- public static final String PERSIST_QUEUE_TO_DISK_FREQUENCY_MS =
- "persist_queue_to_disk_frequency_ms";
// Post Processing Configs
public static final String PROFILING_RECHECK_DELAY_MS = "profiling_recheck_delay_ms";
diff --git a/service/java/com/android/os/profiling/ProfilingService.java b/service/java/com/android/os/profiling/ProfilingService.java
index 2342b5a..33e602e 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.ProfilingTriggersWrapper;
import android.os.QueuedResultsWrapper;
import android.os.RemoteException;
import android.provider.DeviceConfig;
@@ -62,6 +63,7 @@
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
public class ProfilingService extends IProfilingService.Stub {
private static final String TAG = ProfilingService.class.getSimpleName();
@@ -80,9 +82,10 @@
private static final String OUTPUT_FILE_TRIGGER = "trigger";
private static final String OUTPUT_FILE_IN_PROGRESS = "in-progress";
- private static final String QUEUED_RESULTS_SYSTEM_DIR = "system";
- private static final String QUEUED_RESULTS_STORE_DIR = "profiling_queued_results_store";
+ private static final String PERSIST_SYSTEM_DIR = "system";
+ private static final String PERSIST_STORE_DIR = "profiling_service_data";
private static final String QUEUED_RESULTS_INFO_FILE = "profiling_queued_results_info";
+ private static final String APP_TRIGGERS_INFO_FILE = "profiling_app_triggers_info";
// Used for unique session name only, not filename.
private static final String SYSTEM_TRIGGERED_SESSION_NAME_PREFIX = "system_triggered_session_";
@@ -109,7 +112,7 @@
@VisibleForTesting
public static final int QUEUED_RESULT_MAX_RETAINED_DURATION_MS = 7 * 24 * 60 * 60 * 1000;
- private static final int PERSIST_QUEUE_TO_DISK_FREQUENCY_MS = 30 * 60 * 1000;
+ private static final int PERSIST_TO_DISK_DEFAULT_FREQUENCY_MS = 30 * 60 * 1000;
private final Context mContext;
private final Object mLock = new Object();
@@ -156,36 +159,54 @@
// Map of uid + package name to a sparse array of trigger objects.
@VisibleForTesting
public ProcessMap<SparseArray<ProfilingTrigger>> mAppTriggers = new ProcessMap<>();
+ @VisibleForTesting
+ public boolean mAppTriggersLoaded = false;
// uid indexed storage of completed tracing sessions that have not yet successfully handled the
// result.
@VisibleForTesting
public SparseArray<List<TracingSession>> mQueuedTracingResults = new SparseArray<>();
- private boolean mPersistQueueScheduled = false;
+ private boolean mPersistScheduled = false;
// Frequency of 0 would result in immediate persist.
@GuardedBy("mLock")
- private AtomicInteger mPersistQueueFrequencyMs;
+ private AtomicInteger mPersistFrequencyMs;
@GuardedBy("mLock")
- private long mLastPersistedQueueTimestampMs = 0L;
- private Runnable mPersistQueueRunnable = null;
+ private long mLastPersistedTimestampMs = 0L;
+ private Runnable mPersistRunnable = null;
- /**
- * The path to the directory which includes the queued results data file as specified in
- * {@link #mPersistQueueFile}.
- */
+ /** The path to the directory which includes all persisted results from this class. */
@VisibleForTesting
- public File mPersistQueueStoreDir;
+ public File mPersistStoreDir = null;
/** The queued results data file, persisted in the storage. */
@VisibleForTesting
- public File mPersistQueueFile;
+ public File mPersistQueueFile = null;
+
+ /** The app triggers results data file, persisted in the storage. */
+ @VisibleForTesting
+ public File mPersistAppTriggersFile = null;
/** To be disabled for testing only. */
@GuardedBy("mLock")
private boolean mKeepUnredactedTrace = false;
/**
+ * Package name of app being tested, or null if no app is being tested. To be used both for
+ * automated testing and developer manual testing.
+ *
+ * Setting this package name will:
+ * - Ensure a system triggered trace is always running.
+ * - Allow all triggers for the specified package name to be executed.
+ *
+ * This is not intended to be set directly. Instead, set this package name by using
+ * device_config commands described at {@link ProfilingManager}.
+ *
+ * There is no time limit on how long this can be left enabled for.
+ */
+ private String mTestPackageName = null;
+
+ /**
* State the {@link TracingSession} is in.
*
* State represents the most recently confirmed completed step in the process. Steps represent
@@ -285,9 +306,9 @@
mKeepUnredactedTrace = DeviceConfigHelper.getTestBoolean(
DeviceConfigHelper.DISABLE_DELETE_UNREDACTED_TRACE, false);
- mPersistQueueFrequencyMs = new AtomicInteger(DeviceConfigHelper.getInt(
- DeviceConfigHelper.PERSIST_QUEUE_TO_DISK_FREQUENCY_MS,
- PERSIST_QUEUE_TO_DISK_FREQUENCY_MS));
+ mPersistFrequencyMs = new AtomicInteger(DeviceConfigHelper.getInt(
+ DeviceConfigHelper.PERSIST_TO_DISK_FREQUENCY_MS,
+ PERSIST_TO_DISK_DEFAULT_FREQUENCY_MS));
}
// Now subscribe to updates on test config.
DeviceConfig.addOnPropertiesChangedListener(DeviceConfigHelper.NAMESPACE_TESTING,
@@ -298,6 +319,10 @@
mKeepUnredactedTrace = properties.getBoolean(
DeviceConfigHelper.DISABLE_DELETE_UNREDACTED_TRACE, false);
getRateLimiter().maybeUpdateRateLimiterDisabled(properties);
+
+ String newTestPackageName = properties.getString(
+ DeviceConfigHelper.SYSTEM_TRIGGERED_TEST_PACKAGE_NAME, null);
+ handleTestPackageChangeLocked(newTestPackageName);
}
}
});
@@ -340,9 +365,9 @@
DeviceConfigHelper.REDACTION_MAX_RUNTIME_ALLOTTED_MS,
mRedactionMaxRuntimeAllottedMs);
- mPersistQueueFrequencyMs.set(properties.getInt(
- DeviceConfigHelper.PERSIST_QUEUE_TO_DISK_FREQUENCY_MS,
- mPersistQueueFrequencyMs.get()));
+ mPersistFrequencyMs.set(properties.getInt(
+ DeviceConfigHelper.PERSIST_TO_DISK_FREQUENCY_MS,
+ mPersistFrequencyMs.get()));
}
}
});
@@ -356,9 +381,9 @@
}
}, mClearTemporaryDirectoryBootDelayMs);
- // Load the queue right away so we can start delivering results as apps register global
- // listeners.
+ // Load the queue and triggers right away.
loadQueueFromPersistedData();
+ loadAppTriggersFromPersistedData();
}
/**
@@ -446,17 +471,122 @@
}
}
+ /**
+ * Load persisted app triggers from disk.
+ *
+ * If any issue is encountered during loading, mark as completed and delete the file. Persisted
+ * app triggers will be lost.
+ */
+ @VisibleForTesting
+ public void loadAppTriggersFromPersistedData() {
+ // Setup persist files
+ try {
+ if (!setupPersistAppTriggerFiles()) {
+ // If setting up the directory and file was unsuccessful then just return without
+ // marking loaded so it can be tried again.
+ if (DEBUG) Log.d(TAG, "Failed to setup app trigger persistence directory/files.");
+ return;
+ }
+ } catch (SecurityException e) {
+ // Can't access files.
+ Log.w(TAG, "Failed to setup app trigger persistence directory/files.", e);
+ return;
+ }
+
+ // Check if file exists
+ try {
+ if (!mPersistAppTriggersFile.exists()) {
+ // No file, nothing to load. This is an expected state for before the feature has
+ // ever been used or if the triggers were empty.
+ if (DEBUG) {
+ Log.d(TAG, "App trigger persistence file does not exist, skipping load from "
+ + "disk.");
+ }
+ mAppTriggersLoaded = true;
+ return;
+ }
+ } catch (SecurityException e) {
+ // Can't access file.
+ if (DEBUG) Log.e(TAG, "Exception accessing app triggers persistence file", e);
+ return;
+ }
+
+ // Read the file
+ AtomicFile persistFile = new AtomicFile(mPersistAppTriggersFile);
+ byte[] bytes;
+ try {
+ bytes = persistFile.readFully();
+ } catch (IOException e) {
+ Log.w(TAG, "Exception reading app triggers persistence file", e);
+ // Failed to read the file. No reason to believe we'll have better luck next time,
+ // delete the file and return. Persisted triggers will be lost until the app re-adds
+ // them.
+ deletePersistAppTriggersFile();
+ mAppTriggersLoaded = true;
+ return;
+ }
+ if (bytes.length == 0) {
+ if (DEBUG) Log.d(TAG, "App triggers persistence file empty, skipping load from disk.");
+ // Empty app triggers persist file. Delete the file, mark loaded, and return.
+ deletePersistAppTriggersFile();
+ mAppTriggersLoaded = true;
+ return;
+ }
+
+ // Parse file bytes to proto
+ ProfilingTriggersWrapper wrapper;
+ try {
+ wrapper = ProfilingTriggersWrapper.parseFrom(bytes);
+ } catch (Exception e) {
+ Log.w(TAG, "Error parsing proto from persisted bytes", e);
+ // Failed to parse the file contents. No reason to believe we'll have better luck next
+ // time, delete the file, mark loaded, and return. Persisted app triggers will be lost
+ // until re-added by the app.
+ deletePersistAppTriggersFile();
+ mAppTriggersLoaded = true;
+ return;
+ }
+
+ // Populate in memory app triggers store
+ for (int i = 0; i < wrapper.getTriggersCount(); i++) {
+ ProfilingTriggersWrapper.ProfilingTrigger triggerProto = wrapper.getTriggers(i);
+ addTrigger(new ProfilingTrigger(triggerProto), false);
+ }
+
+ mAppTriggersLoaded = true;
+ }
+
/** Setup the directory and file for persisting queue. */
@VisibleForTesting
public boolean setupPersistQueueFiles() {
- File dataDir = Environment.getDataDirectory();
- File systemDir = new File(dataDir, QUEUED_RESULTS_SYSTEM_DIR);
- mPersistQueueStoreDir = new File(systemDir, QUEUED_RESULTS_STORE_DIR);
- if (createDir(mPersistQueueStoreDir)) {
- mPersistQueueFile = new File(mPersistQueueStoreDir, QUEUED_RESULTS_INFO_FILE);
- return true;
+ if (mPersistStoreDir == null) {
+ if (!setupPersistDir()) {
+ return false;
+ }
}
- return false;
+ mPersistQueueFile = new File(mPersistStoreDir, QUEUED_RESULTS_INFO_FILE);
+ return true;
+ }
+
+ /** Setup the directory and file for persisting app triggers. */
+ @VisibleForTesting
+ public boolean setupPersistAppTriggerFiles() {
+ if (mPersistStoreDir == null) {
+ if (!setupPersistDir()) {
+ return false;
+ }
+ }
+ mPersistAppTriggersFile = new File(mPersistStoreDir, APP_TRIGGERS_INFO_FILE);
+ return true;
+ }
+
+ /** Setup the directory and file for persisting. */
+ @VisibleForTesting
+ public boolean setupPersistDir() {
+ File dataDir = Environment.getDataDirectory();
+ File systemDir = new File(dataDir, PERSIST_SYSTEM_DIR);
+ mPersistStoreDir = new File(systemDir, PERSIST_STORE_DIR);
+ return createDir(mPersistStoreDir);
}
/** Delete the persist queue file. */
@@ -471,6 +601,18 @@
}
}
+ /** Delete the persist app triggers file. */
+ @VisibleForTesting
+ public void deletePersistAppTriggersFile() {
+ try {
+ mPersistAppTriggersFile.delete();
+ if (DEBUG) Log.d(TAG, "Deleted app triggers persist file.");
+ } catch (SecurityException e) {
+ // Can't delete file.
+ if (DEBUG) Log.d(TAG, "Failed to delete app triggers persist file", e);
+ }
+ }
+
private static boolean createDir(File dir) throws SecurityException {
if (dir.mkdir()) {
return true;
@@ -572,7 +714,7 @@
// NOTIFIED_REQUESTER and the only potential remaining work to be repeated will be
// cleanup. If the callback failed, then we won't have recursed here and we'll pick
// back up this stage next time thereby minimizing repeated work.
- maybePersistQueueToDisk();
+ maybePersistToDisk();
break;
case ERROR_OCCURRED:
// An error has occurred, proceed to callback.
@@ -584,7 +726,7 @@
// NOTIFIED_REQUESTER and the only potential remaining work to be repeated will be
// cleanup. If the callback failed, then we won't have recursed here and we'll pick
// back up this stage next time thereby minimizing repeated work.
- maybePersistQueueToDisk();
+ maybePersistToDisk();
break;
case NOTIFIED_REQUESTER:
// Callback has been completed successfully, start cleanup.
@@ -1191,11 +1333,19 @@
/** Start a trace to be used for system triggered profiling. */
private void startSystemTriggeredTrace() {
- if (!android.os.profiling.Flags.systemTriggeredProfiling()) {
+ if (!Flags.systemTriggeredProfilingNew()) {
// Flag disabled.
return;
}
+ if (!mAppTriggersLoaded) {
+ // Until the triggers are loaded we can't create a proper config so just return.
+ if (DEBUG) {
+ Log.d(TAG, "System triggered trace not started due to app triggers not loaded.");
+ }
+ return;
+ }
+
String[] packageNames = getActiveTriggerPackageNames();
if (packageNames.length == 0) {
// No apps have registered interest in system triggered profiling, so don't bother to
@@ -1210,7 +1360,8 @@
String uniqueSessionName = SYSTEM_TRIGGERED_SESSION_NAME_PREFIX
+ System.currentTimeMillis();
- byte[] config = Configs.generateSystemTriggeredTraceConfig(uniqueSessionName, packageNames);
+ byte[] config = Configs.generateSystemTriggeredTraceConfig(uniqueSessionName, packageNames,
+ mTestPackageName != null);
String outputFile = TEMP_TRACE_PATH + SYSTEM_TRIGGERED_SESSION_NAME_PREFIX
+ OUTPUT_FILE_IN_PROGRESS + OUTPUT_FILE_UNREDACTED_TRACE_SUFFIX;
@@ -1255,7 +1406,7 @@
*/
@VisibleForTesting
public void processTrigger(int uid, @NonNull String packageName, int triggerType) {
- if (!android.os.profiling.Flags.systemTriggeredProfiling()) {
+ if (!Flags.systemTriggeredProfilingNew()) {
// Flag disabled.
return;
}
@@ -1263,10 +1414,7 @@
if (mSystemTriggeredTraceUniqueSessionName == null) {
// If we don't have the session name then we don't know how to clone the trace so stop
// it if it's still running and then return.
- if (mSystemTriggeredTraceProcess != null && mSystemTriggeredTraceProcess.isAlive()) {
- mSystemTriggeredTraceProcess.destroyForcibly();
- mSystemTriggeredTraceProcess = null;
- }
+ stopSystemTriggeredTrace();
// There is no active system triggered trace so there's nothing to clone. Return.
if (DEBUG) {
@@ -1318,17 +1466,20 @@
return;
}
- int systemRateLimiterResult = getRateLimiter().isProfilingRequestAllowed(uid,
- ProfilingManager.PROFILING_TYPE_SYSTEM_TRACE, true, null);
- if (systemRateLimiterResult != RateLimiter.RATE_LIMIT_RESULT_ALLOWED) {
- // Blocked by system rate limiter, return. Since this is system triggered there is no
- // callback and therefore no need to distinguish between per app and system denials
- // within the system rate limiter.
- if (DEBUG) {
- Log.d(TAG, String.format("Profiling triggered for uid %d and trigger %d but blocked"
- + " by system rate limiting ", uid, triggerType));
+ // If this is from the test package, skip system rate limiting.
+ if (!packageName.equals(mTestPackageName)) {
+ int systemRateLimiterResult = getRateLimiter().isProfilingRequestAllowed(uid,
+ ProfilingManager.PROFILING_TYPE_SYSTEM_TRACE, true, null);
+ if (systemRateLimiterResult != RateLimiter.RATE_LIMIT_RESULT_ALLOWED) {
+ // Blocked by system rate limiter, return. Since this is system triggered there is
+ // no callback and therefore no need to distinguish between per app and system
+ // denials within the system rate limiter.
+ if (DEBUG) {
+ Log.d(TAG, String.format("Profiling triggered for uid %d and trigger %d but "
+ + "blocked by system rate limiting ", uid, triggerType));
+ }
+ return;
}
- return;
}
// Now that it's approved by both rate limiters, update their values.
@@ -1346,13 +1497,30 @@
try {
// Try to clone the running trace.
- Runtime.getRuntime().exec(new String[] {
+ Process clone = Runtime.getRuntime().exec(new String[] {
"/system/bin/perfetto",
"--clone-by-name",
mSystemTriggeredTraceUniqueSessionName,
"--out",
TEMP_TRACE_PATH + unredactedFullName});
- } catch (IOException e) {
+
+ // Wait for cloned process to stop.
+ if (!clone.waitFor(mPerfettoDestroyTimeoutMs, TimeUnit.MILLISECONDS)) {
+ // Cloned process did not stop, try to stop it forcibly.
+ if (DEBUG) {
+ Log.d(TAG, "Cloned system triggered trace didn't stop on its own, trying to "
+ + "stop it forcibly.");
+ }
+ clone.destroyForcibly();
+
+ // Wait again to see if it stops now.
+ if (!clone.waitFor(mPerfettoDestroyTimeoutMs, TimeUnit.MILLISECONDS)) {
+ // Nothing more to do, result won't be ready so return.
+ if (DEBUG) Log.d(TAG, "Cloned system triggered trace timed out.");
+ return;
+ }
+ }
+ } catch (IOException | InterruptedException e) {
// Failed. There's nothing to clean up as we haven't created a session for this clone
// yet so just fail quietly. The result for this trigger instance combo will be lost.
if (DEBUG) Log.d(TAG, "Failed to clone running system triggered trace.", e);
@@ -1367,30 +1535,47 @@
session.setFileName(unredactedFullName);
moveSessionToQueue(session, true);
advanceTracingSession(session, TracingState.PROFILING_FINISHED);
+
+ maybePersistToDisk();
}
/** Add a profiling trigger to the supporting data structure. */
@VisibleForTesting
public void addTrigger(int uid, @NonNull String packageName, int triggerType,
int rateLimitingPeriodHours) {
- if (!android.os.profiling.Flags.systemTriggeredProfiling()) {
+ addTrigger(new ProfilingTrigger(uid, packageName, triggerType, rateLimitingPeriodHours),
+ true);
+ }
+
+ /**
+ * Add a profiling trigger to the supporting data structure.
+ *
+ * @param trigger The trigger to add.
+ * @param maybePersist Whether to persist to disk, if eligible based on frequency. This is
+ * intended to be set to false only when loading triggers from disk.
+ */
+ @VisibleForTesting
+ public void addTrigger(ProfilingTrigger trigger, boolean maybePersist) {
+ if (!Flags.systemTriggeredProfilingNew()) {
// Flag disabled.
return;
}
- ProfilingTrigger trigger = new ProfilingTrigger(
- uid, packageName, triggerType, rateLimitingPeriodHours);
-
- SparseArray<ProfilingTrigger> perProcessTriggers = mAppTriggers.get(packageName, uid);
+ SparseArray<ProfilingTrigger> perProcessTriggers = mAppTriggers.get(
+ trigger.getPackageName(), trigger.getUid());
if (perProcessTriggers == null) {
perProcessTriggers = new SparseArray<ProfilingTrigger>();
- mAppTriggers.put(packageName, uid, perProcessTriggers);
+ mAppTriggers.put(trigger.getPackageName(), trigger.getUid(), perProcessTriggers);
}
// Only 1 trigger is allowed per uid + trigger type so this will override any previous
// triggers of this type registered for this uid.
- perProcessTriggers.put(triggerType, trigger);
+ perProcessTriggers.put(trigger.getTriggerType(), trigger);
+
+ if (maybePersist) {
+ maybePersistToDisk();
+ }
}
/** Get a list of all package names which have registered profiling triggers. */
@@ -1883,7 +2068,7 @@
mActiveTracingSessions.remove(session.getKey());
if (maybePersist) {
- maybePersistQueueToDisk();
+ maybePersistToDisk();
}
}
@@ -1964,49 +2149,60 @@
}
/**
- * Persist queued results to disk following the following rules:
+ * Persist service data to disk following the following rules:
* - If a persist is already scheduled, do nothing.
- * - If a persist happened within the last {@link #mPersistQueueFrequencyMs} then schedule a
- * persist for {@link #mPersistQueueFrequencyMs} after the last persist.
+ * - If a persist happened within the last {@link #mPersistFrequencyMs} then schedule a
+ * persist for {@link #mPersistFrequencyMs} after the last persist.
* - If no persist has occurred yet or the most recent persist was more than
- * {@link #mPersistQueueFrequencyMs} ago, persist immediately.
+ * {@link #mPersistFrequencyMs} ago, persist immediately.
*/
@VisibleForTesting
- public void maybePersistQueueToDisk() {
- if (!Flags.persistQueue()) {
+ public void maybePersistToDisk() {
+ if (!Flags.persistQueue() && !Flags.systemTriggeredProfilingNew()) {
+ // No persisting is enabled.
return;
}
synchronized (mLock) {
- if (mPersistQueueScheduled) {
+ if (mPersistScheduled) {
// We're already waiting on a scheduled persist job, do nothing.
return;
}
- if (mPersistQueueFrequencyMs.get() != 0
- && (System.currentTimeMillis() - mLastPersistedQueueTimestampMs
- < mPersistQueueFrequencyMs.get())) {
+ if (mPersistFrequencyMs.get() != 0
+ && (System.currentTimeMillis() - mLastPersistedTimestampMs
+ < mPersistFrequencyMs.get())) {
// Schedule the persist job.
- if (mPersistQueueRunnable == null) {
- mPersistQueueRunnable = new Runnable() {
+ if (mPersistRunnable == null) {
+ mPersistRunnable = new Runnable() {
@Override
public void run() {
- persistQueueToDisk();
- mPersistQueueScheduled = false;
+ if (Flags.persistQueue()) {
+ persistQueueToDisk();
+ }
+ if (Flags.systemTriggeredProfilingNew()) {
+ persistAppTriggersToDisk();
+ }
+ mPersistScheduled = false;
}
};
}
- mPersistQueueScheduled = true;
- long persistDelay = mLastPersistedQueueTimestampMs + mPersistQueueFrequencyMs.get()
+ mPersistScheduled = true;
+ long persistDelay = mLastPersistedTimestampMs + mPersistFrequencyMs.get()
- System.currentTimeMillis();
- getHandler().postDelayed(mPersistQueueRunnable, persistDelay);
+ getHandler().postDelayed(mPersistRunnable, persistDelay);
return;
}
}
// If we got here then either persist frequency is 0 or it has already been longer than
// persist frequency since the last persist. Persist immediately.
- persistQueueToDisk();
+ if (Flags.persistQueue()) {
+ persistQueueToDisk();
+ }
+ if (Flags.systemTriggeredProfilingNew()) {
+ persistAppTriggersToDisk();
+ }
}
/** Persist the current queue to disk after cleaning it up. */
@@ -2077,7 +2273,7 @@
out.write(protoBytes);
persistFile.finishWrite(out);
synchronized (mLock) {
- mLastPersistedQueueTimestampMs = System.currentTimeMillis();
+ mLastPersistedTimestampMs = System.currentTimeMillis();
}
} catch (IOException e) {
if (DEBUG) Log.e(TAG, "Exception writing queued results", e);
@@ -2085,6 +2281,125 @@
}
}
+ /** Persist the current app triggers to disk. */
+ @VisibleForTesting
+ public void persistAppTriggersToDisk() {
+ // Check if file exists
+ try {
+ if (mPersistAppTriggersFile == null) {
+ // Try again to create the necessary files.
+ if (!setupPersistAppTriggerFiles()) {
+ // No file, nowhere to save.
+ if (DEBUG) {
+ Log.d(TAG, "Failed setting up app triggers persist files so nowhere to save"
+ + " to.");
+ }
+ return;
+ }
+ }
+
+ if (!mPersistAppTriggersFile.exists()) {
+ // File doesn't exist, try to create it.
+ mPersistAppTriggersFile.createNewFile();
+ }
+ } catch (Exception e) {
+ if (DEBUG) Log.e(TAG, "Exception accessing persisted app triggers store.", e);
+ return;
+ }
+
+ // Generate proto for queue.
+ ProfilingTriggersWrapper.Builder builder = ProfilingTriggersWrapper.newBuilder();
+
+ forEachTrigger(mAppTriggers.getMap(), (trigger) -> builder.addTriggers(trigger.toProto()));
+
+ ProfilingTriggersWrapper queuedTriggersWrapper = builder.build();
+
+ // Write to disk
+ byte[] protoBytes = queuedTriggersWrapper.toByteArray();
+ AtomicFile persistFile = new AtomicFile(mPersistAppTriggersFile);
+ FileOutputStream out = null;
+ try {
+ out = persistFile.startWrite();
+ out.write(protoBytes);
+ persistFile.finishWrite(out);
+ synchronized (mLock) {
+ mLastPersistedTimestampMs = System.currentTimeMillis();
+ }
+ } catch (IOException e) {
+ if (DEBUG) Log.e(TAG, "Exception writing app triggers", e);
+ persistFile.failWrite(out);
+ }
+ }
+
+ /** Receive a callback with each of the tracked profiling triggers. */
+ private void forEachTrigger(
+ ArrayMap<String, SparseArray<SparseArray<ProfilingTrigger>>> triggersOuterMap,
+ Consumer<ProfilingTrigger> callback) {
+
+ for (int i = 0; i < triggersOuterMap.size(); i++) {
+ SparseArray<SparseArray<ProfilingTrigger>> triggerUidList = triggersOuterMap.valueAt(i);
+
+ for (int j = 0; j < triggerUidList.size(); j++) {
+ int uidKey = triggerUidList.keyAt(j);
+ SparseArray<ProfilingTrigger> triggersList = triggerUidList.get(uidKey);
+
+ if (triggersList != null) {
+ for (int k = 0; k < triggersList.size(); k++) {
+ int triggerTypeKey = triggersList.keyAt(k);
+ ProfilingTrigger trigger = triggersList.get(triggerTypeKey);
+
+ if (trigger != null) {
+ callback.accept(trigger);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /** Handle updates to test package config value. */
+ @GuardedBy("mLock")
+ private void handleTestPackageChangeLocked(String newTestPackageName) {
+ if (newTestPackageName == null) {
+
+ // Test package has been set to null, check whether it was null previously.
+ if (mTestPackageName != null) {
+
+ // New null state is a changed from previous state, disable test mode.
+ mTestPackageName = null;
+ stopSystemTriggeredTrace();
+ }
+ // If new state is unchanged from previous null state, do nothing.
+ } else {
+
+ // Test package has been set with a value. Stop running system triggered trace if
+ // applicable so we can start a new one that will have most up to date package names.
+ // This should not be called when the new test package name matches the old one as
+ // device config should not be sending an update for a value change when the value
+ // remains the same, but no need to check as the best experience for caller is to always
+ // stop the current trace and start a new one for most up to date package list.
+ stopSystemTriggeredTrace();
+
+ // Now update the test package name and start the system triggered trace.
+ mTestPackageName = newTestPackageName;
+ startSystemTriggeredTrace();
+ }
+ }
+
+ /** Stop the system triggered trace. */
+ private void stopSystemTriggeredTrace() {
+ // If the trace is alive, stop it.
+ if (mSystemTriggeredTraceProcess != null) {
+ if (mSystemTriggeredTraceProcess.isAlive()) {
+ mSystemTriggeredTraceProcess.destroyForcibly();
+ }
+ mSystemTriggeredTraceProcess = null;
+ }
+
+ // Set session name to null.
+ mSystemTriggeredTraceUniqueSessionName = null;
+ }
+
private class ProfilingDeathRecipient implements IBinder.DeathRecipient {
private final int mUid;
diff --git a/service/java/com/android/os/profiling/RateLimiter.java b/service/java/com/android/os/profiling/RateLimiter.java
index 0038aa0..a57b840 100644
--- a/service/java/com/android/os/profiling/RateLimiter.java
+++ b/service/java/com/android/os/profiling/RateLimiter.java
@@ -177,8 +177,9 @@
public @RateLimitResult int isProfilingRequestAllowed(int uid,
int profilingType, boolean isTriggered, @Nullable Bundle params) {
synchronized (mLock) {
- if (mRateLimiterDisabled) {
+ if (mRateLimiterDisabled && !isTriggered) {
// Rate limiter is disabled for testing, approve request and don't store cost.
+ // This mechanism applies only to direct requests, not system triggered ones.
Log.w(TAG, "Rate limiter disabled, request allowed.");
return RATE_LIMIT_RESULT_ALLOWED;
}
diff --git a/tests/cts/src/android/profiling/cts/ProfilingServiceTests.java b/tests/cts/src/android/profiling/cts/ProfilingServiceTests.java
index 5770137..ecd91b5 100644
--- a/tests/cts/src/android/profiling/cts/ProfilingServiceTests.java
+++ b/tests/cts/src/android/profiling/cts/ProfilingServiceTests.java
@@ -90,6 +90,9 @@
private static final String OVERRIDE_DEVICE_CONFIG_INT = "device_config put %s %s %d";
private static final String GET_DEVICE_CONFIG = "device_config get %s %s";
+ private static final String PERSIST_TEST_DIR = "testdir";
+ private static final String PERSIST_TEST_FILE = "testfile";
+
// Key most and least significant bits are used to generate a unique key specific to each
// request. Key is used to pair request back to caller and callbacks so test to keep consistent.
private static final long KEY_MOST_SIG_BITS = 456l;
@@ -127,15 +130,20 @@
// to our own file/directory in app storage, since the test app context can't access
// /data/system
doReturn(true).when(mRateLimiter).setupPersistFiles();
- mRateLimiter.mPersistStoreDir = new File(mContext.getFilesDir(), "testdir");
+ mRateLimiter.mPersistStoreDir = new File(mContext.getFilesDir(), PERSIST_TEST_DIR);
mRateLimiter.mPersistStoreDir.mkdir();
- mRateLimiter.mPersistFile = new File(mRateLimiter.mPersistStoreDir, "testfile");
+ mRateLimiter.mPersistFile = new File(mRateLimiter.mPersistStoreDir, PERSIST_TEST_FILE);
doReturn(true).when(mProfilingService).setupPersistQueueFiles();
- mProfilingService.mPersistQueueStoreDir = new File(mContext.getFilesDir(), "testdir");
+ mProfilingService.mPersistStoreDir =
+ new File(mContext.getFilesDir(), PERSIST_TEST_DIR);
// Same dir for both, no need to create the 2nd time.
mProfilingService.mPersistQueueFile =
- new File(mProfilingService.mPersistQueueStoreDir, "testfile");
+ new File(mProfilingService.mPersistStoreDir, PERSIST_TEST_FILE);
+
+ doReturn(true).when(mProfilingService).setupPersistAppTriggerFiles();
+ mProfilingService.mPersistAppTriggersFile =
+ new File(mProfilingService.mPersistStoreDir, PERSIST_TEST_FILE);
}
@After
@@ -908,13 +916,16 @@
/**
* Test that persisting queue respects the frequency defined, allowing the persist on the first
* instance but rejecting the subsequent persist.
+ *
+ * While this test focuses on queue persist, the logic for respect frequency is shared with
+ * triggers so this test covers both.
*/
@Test
@EnableFlags(android.os.profiling.Flags.FLAG_PERSIST_QUEUE)
public void testQueuePersist_RespectFrequency() throws Exception {
// Override persist frequency to something large.
updateDeviceConfigAndWaitForChange(DeviceConfigHelper.NAMESPACE,
- DeviceConfigHelper.PERSIST_QUEUE_TO_DISK_FREQUENCY_MS, 60 * 60 * 1000);
+ DeviceConfigHelper.PERSIST_TO_DISK_FREQUENCY_MS, 60 * 60 * 1000);
// Clear the queue.
mProfilingService.mQueuedTracingResults.clear();
@@ -948,7 +959,7 @@
mProfilingService.mQueuedTracingResults.put(FAKE_UID, sessionList);
// Trigger a persist.
- mProfilingService.maybePersistQueueToDisk();
+ mProfilingService.maybePersistToDisk();
// Confirm that it actually persisted.
verify(mProfilingService, times(1)).persistQueueToDisk();
@@ -958,7 +969,7 @@
assertTrue(mProfilingService.mPersistQueueFile.delete());
// Finally, trigger another persist.
- mProfilingService.maybePersistQueueToDisk();
+ mProfilingService.maybePersistToDisk();
// And confirm the persist did not immediately run.
assertFalse(mProfilingService.mPersistQueueFile.exists());
@@ -969,16 +980,19 @@
/**
* Test that persists that are scheduled for the future due to a persist having recently
* occurred, occur at a future time as expected.
+ *
+ * While this test focuses on queue persist, the logic for scheduling is shared with triggers so
+ * this test covers both.
*/
@Test
@EnableFlags(android.os.profiling.Flags.FLAG_PERSIST_QUEUE)
public void testQueuePersist_Scheduling() throws Exception {
// Override persist frequency to 5 seconds that way we can confirm both that the persist did
// not happen immediately and that it did eventually happen. This is the time from the first
- // call to maybePersistQueueToDisk until the next call to the same method for the scheduling
+ // call to maybePersistToDisk until the next call to the same method for the scheduling
// of the next persist to occur as expected, rather than immediately persisting.
updateDeviceConfigAndWaitForChange(DeviceConfigHelper.NAMESPACE,
- DeviceConfigHelper.PERSIST_QUEUE_TO_DISK_FREQUENCY_MS, 5 * 1000);
+ DeviceConfigHelper.PERSIST_TO_DISK_FREQUENCY_MS, 5 * 1000);
// Clear the queue.
mProfilingService.mQueuedTracingResults.clear();
@@ -1000,7 +1014,7 @@
mProfilingService.mQueuedTracingResults.put(FAKE_UID, sessionList);
// Trigger a persist.
- mProfilingService.maybePersistQueueToDisk();
+ mProfilingService.maybePersistToDisk();
// Confirm that it actually persisted.
assertTrue(mProfilingService.mPersistQueueFile.exists());
@@ -1010,7 +1024,7 @@
assertTrue(mProfilingService.mPersistQueueFile.delete());
// Trigger another persist.
- mProfilingService.maybePersistQueueToDisk();
+ mProfilingService.maybePersistToDisk();
// And confirm the persist did not immediately run.
assertFalse(mProfilingService.mPersistQueueFile.exists());
@@ -1022,6 +1036,137 @@
assertTrue(mProfilingService.mPersistQueueFile.exists());
}
+ /**
+ * Test that persisting app triggers and then reloading them from disk works correctly, loading
+ * all previous triggers.
+ */
+ @Test
+ @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
+ public void testAppTriggersPersist_PersistAndRestore() {
+ // First, clear the data structure.
+ mProfilingService.mAppTriggers.getMap().clear();
+
+ // Create 3 triggers belonging to 2 uids. Add a last triggered time to one of them.
+ ProfilingTrigger trigger1 = new ProfilingTrigger(FAKE_UID, APP_PACKAGE_NAME, 1, 0);
+
+ ProfilingTrigger trigger2 = new ProfilingTrigger(FAKE_UID, APP_PACKAGE_NAME, 2, 1);
+ trigger2.setLastTriggeredTimeMs(123L);
+
+ ProfilingTrigger trigger3 = new ProfilingTrigger(FAKE_UID_2, APP_PACKAGE_NAME, 1, 2);
+
+ // Group into sparse arrays by uid.
+ SparseArray<ProfilingTrigger> triggerArray1 = new SparseArray<ProfilingTrigger>();
+ triggerArray1.put(1, trigger1);
+ triggerArray1.put(2, trigger2);
+
+ SparseArray<ProfilingTrigger> triggerArray2 = new SparseArray<ProfilingTrigger>();
+ triggerArray2.put(1, trigger3);
+
+ mProfilingService.mAppTriggers.put(APP_PACKAGE_NAME, FAKE_UID, triggerArray1);
+ mProfilingService.mAppTriggers.put(APP_PACKAGE_NAME, FAKE_UID_2, triggerArray2);
+
+ // Trigger a persist.
+ mProfilingService.persistAppTriggersToDisk();
+
+ // Confirm file was written to
+ confirmNonEmptyFileExists(mProfilingService.mPersistAppTriggersFile);
+
+ // Clear app triggers so we can ensure it is reloaded properly.
+ mProfilingService.mAppTriggers.getMap().clear();
+ assertEquals(0, mProfilingService.mAppTriggers.getMap().size());
+
+ // Load app triggers from disk.
+ mProfilingService.loadAppTriggersFromPersistedData();
+
+ // Finally, verify the loaded contents match the ones that were persisted.
+ confirmProfilingTriggerEquals(trigger1,
+ mProfilingService.mAppTriggers.get(APP_PACKAGE_NAME, FAKE_UID).get(1));
+ confirmProfilingTriggerEquals(trigger2,
+ mProfilingService.mAppTriggers.get(APP_PACKAGE_NAME, FAKE_UID).get(2));
+ confirmProfilingTriggerEquals(trigger3,
+ mProfilingService.mAppTriggers.get(APP_PACKAGE_NAME, FAKE_UID_2).get(1));
+ }
+
+ /**
+ * Test that loading app triggers with no persist file works as intended with no triggers added,
+ * loaded set to true, and correct methods called.
+ */
+ @Test
+ @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
+ public void testAppTriggersPersist_NoPersistFile() {
+ // First, clear the data structure.
+ mProfilingService.mAppTriggers.getMap().clear();
+
+ // Ensure the file doesn't exist.
+ mProfilingService.mPersistAppTriggersFile.delete();
+ assertFalse(mProfilingService.mPersistAppTriggersFile.exists());
+
+ // Load app triggers from disk.
+ mProfilingService.loadAppTriggersFromPersistedData();
+
+ // Ensure that the triggers are still empty, that loaded was set to true, and that a delete
+ // was not attempted as there was no file to delete.
+ assertEquals(0, mProfilingService.mAppTriggers.getMap().size());
+ assertTrue(mProfilingService.mAppTriggersLoaded);
+ verify(mProfilingService, times(0)).deletePersistAppTriggersFile();
+ }
+
+ /**
+ * Test that loading app triggers with an empty persist file works as intended with no triggers
+ * added, loaded set to true, and correct methods called.
+ */
+ @Test
+ @EnableFlags(android.os.profiling.Flags.FLAG_PERSIST_QUEUE)
+ public void testAppTriggersPersist_EmptyPersistFile() throws Exception {
+ // First, clear the data structure.
+ mProfilingService.mAppTriggers.getMap().clear();
+
+ // Ensure the file exists and is empty.
+ mProfilingService.mPersistAppTriggersFile.delete();
+ assertFalse(mProfilingService.mPersistAppTriggersFile.exists());
+ mProfilingService.mPersistAppTriggersFile.createNewFile();
+ assertTrue(mProfilingService.mPersistAppTriggersFile.exists());
+ assertEquals(0L, mProfilingService.mPersistAppTriggersFile.length());
+
+ // Load app triggers from disk.
+ mProfilingService.loadAppTriggersFromPersistedData();
+
+ // Ensure that the triggers are still empty, that loaded was set to true, and that a delete
+ // was attempted as expected for the bad file state.
+ assertEquals(0, mProfilingService.mAppTriggers.getMap().size());
+ assertTrue(mProfilingService.mAppTriggersLoaded);
+ verify(mProfilingService, times(1)).deletePersistAppTriggersFile();
+ }
+
+ /**
+ * Test that loading app triggers with an invalid persist file works as intended with no
+ * triggers added, loaded set to true, and correct methods called.
+ */
+ @Test
+ @EnableFlags(android.os.profiling.Flags.FLAG_PERSIST_QUEUE)
+ public void testAppTriggersPersist_BadPersistFile() throws Exception {
+ // First, clear the data structure.
+ mProfilingService.mAppTriggers.getMap().clear();
+
+ // Ensure the file exists and contains some non proto contents.
+ mProfilingService.mPersistAppTriggersFile.delete();
+ mProfilingService.mPersistAppTriggersFile.createNewFile();
+ FileOutputStream fileOutputStream = new FileOutputStream(
+ mProfilingService.mPersistAppTriggersFile);
+ fileOutputStream.write("some text that is definitely not a proto".getBytes());
+ fileOutputStream.close();
+ confirmNonEmptyFileExists(mProfilingService.mPersistAppTriggersFile);
+
+ // Load app triggers from disk.
+ mProfilingService.loadAppTriggersFromPersistedData();
+
+ // Ensure that the triggers are still empty, that loaded was set to true, and that a delete
+ // was attempted as expected for the bad file state.
+ assertEquals(0, mProfilingService.mAppTriggers.getMap().size());
+ assertTrue(mProfilingService.mAppTriggersLoaded);
+ verify(mProfilingService, times(1)).deletePersistAppTriggersFile();
+ }
+
/** Test that adding a specific listener does not trigger handling queued results. */
@Test
public void testQueuedResult_RequestSpecificListener() {
@@ -1543,7 +1688,7 @@
* the same trigger, uid, and process name are used.
*/
@Test
- @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING)
+ @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
public void testAddTriggers() throws Exception {
// First, clear the data structure.
mProfilingService.mAppTriggers.getMap().clear();
@@ -1579,7 +1724,7 @@
/** Test that app level rate limiting works correctly in the allow case. */
@Test
- @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING)
+ @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
public void testProcessTrigger_appLevelRateLimit_allow() throws Exception {
// First, clear the data structure.
mProfilingService.mAppTriggers.getMap().clear();
@@ -1621,7 +1766,7 @@
/** Test that app level rate limiting works correctly in the deny case. */
@Test
- @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING)
+ @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
public void testProcessTrigger_appLevelRateLimit_deny() throws Exception {
// First, clear the data structure.
mProfilingService.mAppTriggers.getMap().clear();
@@ -1664,7 +1809,7 @@
/** Test that system level rate limiting works correctly in the allow case. */
@Test
- @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING)
+ @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
public void testProcessTrigger_systemLevelRateLimit_allow() throws Exception {
overrideRateLimiterDefaults();
@@ -1697,7 +1842,7 @@
/** Test that system level rate limiting works correctly in the deny case. */
@Test
- @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING)
+ @EnableFlags(android.os.profiling.Flags.FLAG_SYSTEM_TRIGGERED_PROFILING_NEW)
public void testProcessTrigger_systemLevelRateLimit_deny() throws Exception {
overrideRateLimiterDefaults();