Developer Profiling APIs Impl
Still pending: getting perfetto configs to perfetto, trace redaction, moving result to requesting apps storage, stay connected to each request with a dedicated thread.
Test: submit request to service from app, can access perfetto, calls back to called with correct result
Bug: 293957254
Change-Id: I89cc2ce4b86f24db14a20babb4742fbca412b950
diff --git a/aidl/Android.bp b/aidl/Android.bp
index bcb6da9..54903b3 100644
--- a/aidl/Android.bp
+++ b/aidl/Android.bp
@@ -35,11 +35,7 @@
enabled: false,
},
ndk: {
- enabled: true,
- apex_available: [
- "com.android.profiling",
- ],
- min_sdk_version: "34",
+ enabled: false,
},
},
}
diff --git a/aidl/android/os/IProfilingResultCallback.aidl b/aidl/android/os/IProfilingResultCallback.aidl
new file mode 100644
index 0000000..dc4a700
--- /dev/null
+++ b/aidl/android/os/IProfilingResultCallback.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}
+ */
+interface IProfilingResultCallback {
+
+ oneway void sendResult(long keyMostSigBits, long keyLeastSigBits, int status, String filePath, String tag, String error);
+}
diff --git a/aidl/android/os/IProfilingService.aidl b/aidl/android/os/IProfilingService.aidl
index 02bdf4d..d93aea4 100644
--- a/aidl/android/os/IProfilingService.aidl
+++ b/aidl/android/os/IProfilingService.aidl
@@ -16,9 +16,16 @@
package android.os;
+import android.os.IProfilingResultCallback;
+
/**
* {@hide}
*/
interface IProfilingService {
-}
\ No newline at end of file
+ void requestProfiling(in byte[] profilingRequest, String tag, long keyMostSigBits, long keyLeastSigBits);
+
+ void registerResultsCallback(IProfilingResultCallback callback);
+
+ void requestCancel(long keyMostSigBits, long keyLeastSigBits);
+}
diff --git a/framework/Android.bp b/framework/Android.bp
index 6a5842a..c5df7c7 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -30,15 +30,20 @@
java_library {
name: "framework-profiling-proto",
proto: {
- type: "stream",
+ type: "lite",
canonical_path_from_root: false,
include_dirs: [
"external/protobuf/src",
+ "external/protobuf/java",
],
},
srcs: [
"proto/**/*.proto",
],
+ visibility: [
+ "//packages/modules/Profiling/tests:__subpackages__",
+ ],
+ jarjar_rules: "jarjar-rules.txt",
installable: false,
min_sdk_version: "34",
sdk_version: "system_34",
@@ -83,12 +88,9 @@
hostdex: true, // for hiddenapi check
- lint: {
- strict_updatability_linting: true,
- },
-
impl_library_visibility: [
"//packages/modules/Profiling/service:__subpackages__",
+ "//packages/modules/Profiling/tests:__subpackages__",
],
apex_available: [
diff --git a/framework/jarjar-rules.txt b/framework/jarjar-rules.txt
index 967a7dd..1a0c208 100644
--- a/framework/jarjar-rules.txt
+++ b/framework/jarjar-rules.txt
@@ -1 +1,2 @@
-rule com.android.modules.utils.** com.android.internal.profiling.@0
\ No newline at end of file
+rule com.android.modules.utils.** com.android.internal.profiling.@0
+rule com.google.protobuf.** android.os.protobuf.@1
\ No newline at end of file
diff --git a/framework/java/android/os/ProfilingManager.java b/framework/java/android/os/ProfilingManager.java
index 9052567..e37b005 100644
--- a/framework/java/android/os/ProfilingManager.java
+++ b/framework/java/android/os/ProfilingManager.java
@@ -19,10 +19,20 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
-import android.os.OutcomeReceiver;
+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;
@@ -31,9 +41,26 @@
*/
@FlaggedApi(Flags.FLAG_TELEMETRY_APIS)
public class ProfilingManager {
+ private static final String TAG = ProfilingManager.class.getSimpleName();
+ private static final boolean DEBUG = false;
- /** @hide */
- public ProfilingManager(Context context) {}
+ 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.
@@ -61,7 +88,54 @@
@Nullable CancellationSignal cancellationSignal,
@Nullable Executor executor,
@Nullable Consumer<ProfilingResult> listener) {
- // TODO b/293957254
+ 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.");
+ }
+ }
}
/**
@@ -73,7 +147,9 @@
public void registerForProfilingResults(
@NonNull Executor executor,
@NonNull Consumer<ProfilingResult> listener) {
- // TODO b/293957254
+ synchronized (sLock) {
+ mCallbacks.add(new ProfilingRequestCallbackWrapper(executor, listener, null));
+ }
}
/**
@@ -85,6 +161,101 @@
*/
public void unregisterForProfilingResults(
@Nullable Consumer<ProfilingResult> listener) {
- // TODO b/293957254
+ 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;
+ }
}
}
diff --git a/framework/proto/android/os/profiling_request.proto b/framework/proto/android/os/profiling_request.proto
new file mode 100644
index 0000000..fcf21ec
--- /dev/null
+++ b/framework/proto/android/os/profiling_request.proto
@@ -0,0 +1,27 @@
+syntax = "proto2";
+
+package android.os;
+
+option java_outer_classname = "ProfilingRequestProto";
+option java_multiple_files = true;
+
+message ProfilingRequest {
+ message JavaHeapDump {}
+ message HeapProfile {
+ optional bool art = 1;
+ optional uint64 sampling_interval_bytes = 2;
+ }
+ message StackSampling {
+ optional uint32 frequency = 1;
+ }
+ message SystemTrace {}
+ message Config {
+ oneof cfg_type {
+ JavaHeapDump java_heap_dump = 1;
+ HeapProfile heap_profile = 2;
+ StackSampling stack_sampling = 3;
+ SystemTrace system_trace = 4;
+ }
+ }
+ optional Config config = 1;
+}
diff --git a/framework/proto/android/os/profiling_requests.proto b/framework/proto/android/os/profiling_requests.proto
deleted file mode 100644
index 769ed0c..0000000
--- a/framework/proto/android/os/profiling_requests.proto
+++ /dev/null
@@ -1,5 +0,0 @@
-syntax = "proto2";
-
-package android.os;
-
-message Placeholder {}
diff --git a/service/Android.bp b/service/Android.bp
index bd875c5..8d3db29 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -20,6 +20,7 @@
name: "service-profiling-sources",
srcs: [
"java/**/*.java",
+ ":framework-statsd-aidl-sources",
],
}
diff --git a/service/java/com/android/os/profiling/Configs.java b/service/java/com/android/os/profiling/Configs.java
new file mode 100644
index 0000000..19714cd
--- /dev/null
+++ b/service/java/com/android/os/profiling/Configs.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2023 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.profiling;
+
+import android.os.ProfilingRequest;
+
+import java.lang.IllegalArgumentException;
+
+public final class Configs {
+ static final String CONFIG_HEAP_PROFILE = "buffers {\n"
+ + " size_kb: 65536\n"
+ + "}\n"
+ + "\n"
+ + "data_sources {\n"
+ + " config {\n"
+ + " name: \"android.heapprofd\"\n"
+ + " heapprofd_config {\n"
+ + " # 8Mb\n"
+ + " shmem_size_bytes: 8388608\n"
+ + " sampling_interval_bytes: {{sampling_interval}}\n"
+ + " process_cmdline: \"{{package_name}}\"\n"
+ + " {% if {{art}} %}\n"
+ + " heaps: \"com.android.art\"\n"
+ + " {% endif %}\n"
+ + " }\n"
+ + " }\n"
+ + "}\n"
+ + "\n"
+ + "flush_timeout_ms: 30000";
+ static final String CONFIG_JAVA_HEAP_DUMP = "buffers {\n"
+ + " # This is the maximum size of the trace. The buffer will be mmap'd but, only\n"
+ + " # the non empty pages contribute to RSS.\n"
+ + " size_kb: 256000\n"
+ + " fill_policy: DISCARD\n"
+ + "}\n"
+ + "\n"
+ + "data_sources {\n"
+ + " config {\n"
+ + " name: \"android.java_hprof\"\n"
+ + " java_hprof_config {\n"
+ + " process_cmdline: \"{{package_name}}\"\n"
+ + " dump_smaps: true\n"
+ + " }\n"
+ + " }\n"
+ + "}\n"
+ + "\n"
+ + "# Wait 1s for the dump to start\n"
+ + "duration_ms: 1000\n"
+ + "# Wait up to 100s for the dump to finish\n"
+ + "data_source_stop_timeout_ms: 100000";
+ static final String CONFIG_STACK_SAMPLING = "buffers {\n"
+ + " size_kb: 65536\n"
+ + " fill_policy: DISCARD\n"
+ + "}\n"
+ + "\n"
+ + "data_sources {\n"
+ + " config {\n"
+ + " name: \"linux.perf\"\n"
+ + " perf_event_config {\n"
+ + " timebase {\n"
+ + " counter: SW_CPU_CLOCK\n"
+ + " frequency: {{frequency}}\n"
+ + " timestamp_clock: PERF_CLOCK_MONOTONIC\n"
+ + " }\n"
+ + " callstack_sampling {\n"
+ + " scope {\n"
+ + " target_cmdline: \"{{package_name}}\"\n"
+ + " }\n"
+ + " # TODO: Do kernel frame disclose sensitive info?\n"
+ + " kernel_frames: true\n"
+ + " }\n"
+ + " }\n"
+ + " }\n"
+ + "}\n"
+ + "\n"
+ + "flush_timeout_ms: 30000";
+ static final String CONFIG_SYSTEM_TRACE = "buffers {\n"
+ + " size_kb: 32768\n"
+ + " fill_policy: RING_BUFFER\n"
+ + "}\n"
+ + "\n"
+ + "data_sources {\n"
+ + " config {\n"
+ + " name: \"linux.process_stats\"\n"
+ + " target_buffer: 0\n"
+ + " process_stats_config {\n"
+ + " scan_all_processes_on_start: true\n"
+ + " }\n"
+ + " }\n"
+ + "}\n"
+ + "\n"
+ + "data_sources {\n"
+ + " config {\n"
+ + " name: \"linux.ftrace\"\n"
+ + " target_buffer: 0\n"
+ + " ftrace_config {\n"
+ + " throttle_rss_stat: true\n"
+ + " disable_generic_events: true\n"
+ + " compact_sched {\n"
+ + " enabled: true\n"
+ + " }\n"
+ + "\n"
+ + " # RSS and ION buffer events:\n"
+ + " ftrace_events: \"gpu_mem/gpu_mem_total\"\n"
+ + " ftrace_events: \"dmabuf_heap/dma_heap_stat\"\n"
+ + " ftrace_events: \"ion/ion_stat\"\n"
+ + " ftrace_events: \"kmem/ion_heap_grow\"\n"
+ + " ftrace_events: \"kmem/ion_heap_shrink\"\n"
+ + " ftrace_events: \"rss_stat\"\n"
+ + " ftrace_events: \"fastrpc/fastrpc_dma_stat\"\n"
+ + "\n"
+ + " # Scheduling information & process tracking. Useful for:\n"
+ + " # - what is happening on each CPU at each moment\n"
+ + " # - why a thread was descheduled\n"
+ + " # - parent/child relationships between processes and threads.\n"
+ + " ftrace_events: \"power/suspend_resume\"\n"
+ + " ftrace_events: \"sched/sched_blocked_reason\"\n"
+ + " ftrace_events: \"sched/sched_process_free\"\n"
+ + " ftrace_events: \"sched/sched_switch\"\n"
+ + " ftrace_events: \"task/task_newtask\"\n"
+ + " ftrace_events: \"task/task_rename\"\n"
+ + "\n"
+ + " # These two give us kernel wakelocks.\n"
+ + " ftrace_events: \"power/wakeup_source_activate\"\n"
+ + " ftrace_events: \"power/wakeup_source_deactivate\"\n"
+ + "\n"
+ + " # Wakeup info. Allows you to compute how long a task was\n"
+ + " # blocked due to CPU contention.\n"
+ + " ftrace_events: \"sched/sched_waking\"\n"
+ + " ftrace_events: \"sched/sched_wakeup_new\"\n"
+ + "\n"
+ + " # Workqueue events.\n"
+ + " ftrace_events: \"workqueue/workqueue_activate_work\"\n"
+ + " ftrace_events: \"workqueue/workqueue_execute_end\"\n"
+ + " ftrace_events: \"workqueue/workqueue_execute_start\"\n"
+ + " ftrace_events: \"workqueue/workqueue_queue_work\"\n"
+ + "\n"
+ + " # vmscan and mm_compaction events.\n"
+ + " ftrace_events: \"vmscan/mm_vmscan_kswapd_wake\"\n"
+ + " ftrace_events: \"vmscan/mm_vmscan_kswapd_sleep\"\n"
+ + " ftrace_events: \"vmscan/mm_vmscan_direct_reclaim_begin\"\n"
+ + " ftrace_events: \"vmscan/mm_vmscan_direct_reclaim_end\"\n"
+ + " ftrace_events: \"compaction/mm_compaction_begin\"\n"
+ + " ftrace_events: \"compaction/mm_compaction_end\"\n"
+ + "\n"
+ + " # CMA events.\n"
+ + " ftrace_events: \"cma/cma_alloc_start\"\n"
+ + " ftrace_events: \"cma/cma_alloc_info\"\n"
+ + "\n"
+ + " # Atrace activity manager:\n"
+ + " atrace_categories: \"am\"\n"
+ + " # Atrace system_server:\n"
+ + " atrace_categories: \"ss\"\n"
+ + "\n"
+ + " # Java and C:\n"
+ + " atrace_categories: \"dalvik\"\n"
+ + " atrace_categories: \"bionic\"\n"
+ + "\n"
+ + " atrace_categories: \"binder_driver\"\n"
+ + "\n"
+ + " atrace_apps: \"{{package_name}}\"\n"
+ + " }\n"
+ + " }\n"
+ + "}\n"
+ + "\n"
+ + "data_sources {\n"
+ + " config {\n"
+ + " name: \"android.surfaceflinger.frametimeline\"\n"
+ + " target_buffer: 0\n"
+ + " }\n"
+ + "}";
+
+ /** This method transforms a request into a useable config for perfetto. */
+ public static String generateConfigForRequest(ProfilingRequest request, String packageName)
+ throws IllegalArgumentException {
+ if (!request.hasConfig()) {
+ // Proto has no config, not requesting anything.
+ throw new IllegalArgumentException("Proto config is missing");
+ }
+
+ ProfilingRequest.Config config = request.getConfig();
+ String result = null;
+
+ // Config can have at most one collection type, find out which and then process parameters.
+ if (config.hasJavaHeapDump()) {
+ result = CONFIG_JAVA_HEAP_DUMP;
+ } else if (config.hasHeapProfile()) {
+ ProfilingRequest.HeapProfile heapProfile = config.getHeapProfile();
+ boolean art = heapProfile.hasArt() ? heapProfile.getArt() : false;
+ long samplingIntervalBytes = heapProfile.hasSamplingIntervalBytes()
+ ? heapProfile.getSamplingIntervalBytes() : 0;
+ result = CONFIG_HEAP_PROFILE
+ .replace("{{sampling_interval}}", String.valueOf(samplingIntervalBytes))
+ .replace("{{art}}", String.valueOf(art));
+ } else if (config.hasStackSampling()) {
+ ProfilingRequest.StackSampling stackSampling = config.getStackSampling();
+ int frequency = stackSampling.hasFrequency()
+ ? stackSampling.getFrequency() : 0;
+ result = CONFIG_STACK_SAMPLING.replace("{{frequency}}", String.valueOf(frequency));
+ } else if (config.hasSystemTrace()) {
+ result = CONFIG_SYSTEM_TRACE;
+ }
+
+ if (result == null) {
+ // Proto config has no type, we don't know what the app wants.
+ throw new IllegalArgumentException("Proto config type is missing");
+ }
+
+ // Fill in package name and return config.
+ return result.replace("{{package_name}}", packageName);
+ }
+}
\ No newline at end of file
diff --git a/service/java/com/android/os/profiling/ProfilingService.java b/service/java/com/android/os/profiling/ProfilingService.java
index 2843777..74de23a 100644
--- a/service/java/com/android/os/profiling/ProfilingService.java
+++ b/service/java/com/android/os/profiling/ProfilingService.java
@@ -16,6 +16,252 @@
package android.os.profiling;
-public class ProfilingService {
- public void placeholder() {}
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Binder;
+import android.os.IProfilingService;
+import android.os.OutcomeReceiver;
+import android.os.ProfilingRequest;
+import android.os.ProfilingResult;
+import android.os.IProfilingResultCallback;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemService;
+
+import java.io.IOException;
+import java.lang.Process;
+import java.lang.Exception;
+import java.lang.IllegalArgumentException;
+import java.lang.ProcessBuilder;
+import java.lang.RuntimeException;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.Executor;
+
+public class ProfilingService extends IProfilingService.Stub {
+ private static final String TAG = ProfilingService.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ private static final String TEMP_TRACE_DIR_KEY = "TMPDIR";
+ private static final String TEMP_TRACE_DIR =
+ "/data/local/traces/.profiling-trace-in-progress.trace";
+ private static final String PERFETTO_TAG = "profiling";
+
+ private final Context mContext;
+
+ // uid indexed collecion of JNI callbacks for results.
+ private @Nullable SparseArray<IProfilingResultCallback> mResultCallbacks = new SparseArray<>();
+
+ @VisibleForTesting
+ public ProfilingService(Context context) {
+ mContext = context;
+ RateLimiter.loadFromDisk();
+ }
+
+ /**
+ * This method validates the request, arguments, whether the app is allowed to profile now,
+ * and if so, starts the profiling.
+ */
+ public void requestProfiling(byte[] profilingRequestBytes, String tag,
+ long keyMostSigBits, long keyLeastSigBits) {
+ int uid = Binder.getCallingUid();
+
+ // Check if we're running another trace so we don't run multiple at once.
+ try {
+ if (isTraceRunning()) {
+ processResultCallback(uid, keyMostSigBits, keyLeastSigBits,
+ ProfilingResult.ERROR_FAILED_PROFILING_IN_PROGRESS, null, tag,
+ null);
+ return;
+ }
+ } catch (RuntimeException e) {
+ if (DEBUG) Log.d(TAG, "Perfetto error", e);
+ processResultCallback(uid, keyMostSigBits, keyLeastSigBits,
+ ProfilingResult.ERROR_UNKNOWN, null, tag, "Perfetto error");
+ return;
+ }
+
+ // Process the request from the byte array it was provided as.
+ ProfilingRequest request;
+ try {
+ request = ProfilingRequest.parseFrom(profilingRequestBytes);
+ } catch (Exception e) {
+ if (DEBUG) Log.d(TAG, "Exception parsing request", e);
+ processResultCallback(uid, keyMostSigBits, keyLeastSigBits,
+ ProfilingResult.ERROR_FAILED_INVALID_REQUEST, null, tag,
+ "Request parsing failed");
+ return;
+ }
+
+ // Get package name for requesting process. We can't request the trace without it.
+ String packageName = mContext.getPackageManager().getNameForUid(uid);
+ if (packageName == null) {
+ if (DEBUG) Log.d(TAG, "Could not get package name for UID: " + uid);
+ processResultCallback(uid, keyMostSigBits, keyLeastSigBits,
+ ProfilingResult.ERROR_UNKNOWN, null, tag, "Couldn't determine package name");
+ return;
+ }
+
+ // Check with rate limiter if this request is allowed.
+ final int status = RateLimiter.isProfilingRequestAllowed(Binder.getCallingUid(), request);
+ if (status == RateLimiter.RATE_LIMIT_RESULT_ALLOWED) {
+ // Rate limiter approved, try to start the request.
+ try {
+ startProfiling(Configs.generateConfigForRequest(request, packageName), uid,
+ packageName, keyMostSigBits, keyLeastSigBits, tag);
+ } catch (IllegalArgumentException e) {
+ // Issue with the request. Apps fault.
+ if (DEBUG) Log.d(TAG, "Invalid request", e);
+ processResultCallback(uid, keyMostSigBits, keyLeastSigBits,
+ ProfilingResult.ERROR_FAILED_INVALID_REQUEST, null, tag, e.getMessage());
+ return;
+ } catch (RuntimeException e) {
+ // Perfetto error. Systems fault.
+ if (DEBUG) Log.d(TAG, "Perfetto error", e);
+ processResultCallback(uid, keyMostSigBits, keyLeastSigBits,
+ ProfilingResult.ERROR_UNKNOWN, null, tag, "Perfetto error");
+ return;
+ }
+ } else {
+ // Rate limiter denied, notify caller.
+ if (DEBUG) Log.d(TAG, "Request denied with status: " + status);
+ processResultCallback(uid, keyMostSigBits, keyLeastSigBits,
+ RateLimiter.statusToResult(status), null, tag, null);
+ }
+ }
+
+ public void registerResultsCallback(IProfilingResultCallback callback) {
+ mResultCallbacks.put(Binder.getCallingUid(), callback);
+ }
+
+ public void requestCancel(long keyMostSigBits, long keyLeastSigBits) {
+ // TODO: b/293957254
+ }
+
+ private void processResultCallback(int uid, long keyMostSigBits, long keyLeastSigBits,
+ int status, @Nullable String filePath, @Nullable String tag, @Nullable String error) {
+ if (!mResultCallbacks.contains(uid)) {
+ // No callback, nowhere to notify with result or this failure.
+ if (DEBUG) Log.d(TAG, "No callback to ProfilingManager, callback dropped.");
+ return;
+ }
+ try {
+ mResultCallbacks.get(uid).sendResult(keyMostSigBits, keyLeastSigBits, status, filePath,
+ tag, error);
+ } catch (RemoteException e) {
+ // Failed to send result. Ignore.
+ if (DEBUG) Log.d(TAG, "Exception processing result callback", e);
+ }
+ }
+
+ private void startProfiling(String config, int uid, String packageName, long keyMostSigBits,
+ long keyLeastSigBits, @Nullable String tag) throws RuntimeException {
+ try {
+ ProcessBuilder pb = new ProcessBuilder("/system/bin/perfetto", "--detach="
+ + PERFETTO_TAG + " -o " + TEMP_TRACE_DIR_KEY
+ + " -c - --txt << PERFETTO_ARGUMENTS\n" + config + "\nPERFETTO_ARGUMENTS");
+ // Set temp directory for result
+ pb.environment().put(TEMP_TRACE_DIR_KEY, TEMP_TRACE_DIR);
+ Process process = pb.start();
+ if (!process.waitFor(10000 /* TODO b/324885858 make configureable */,
+ TimeUnit.MILLISECONDS)) {
+ process.destroyForcibly();
+ processResultCallback(uid, keyMostSigBits, keyLeastSigBits,
+ ProfilingResult.ERROR_FAILED_EXECUTING, null, tag, "perfetto timeout");
+ return;
+ }
+ if (process.exitValue() != 0) {
+ processResultCallback(uid, keyMostSigBits, keyLeastSigBits,
+ ProfilingResult.ERROR_FAILED_EXECUTING, null, tag,
+ "perfetto exitvalue: " + process.exitValue());
+ return;
+ }
+ } catch (Exception e) {
+ processResultCallback(uid, keyMostSigBits, keyLeastSigBits,
+ ProfilingResult.ERROR_FAILED_EXECUTING, null, tag, null);
+ throw new RuntimeException(e);
+ }
+ // If we got here then the trace has been started successfully.
+ spawnRedactionProcess(uid, packageName, keyMostSigBits, keyLeastSigBits, tag);
+ }
+
+ public void stopProfiling() throws RuntimeException {
+ if (!isTraceRunning()) {
+ // No trace running, nothing to stop.
+ if (DEBUG) Log.d(TAG, "Exited stopTrace without stopping due to no trace running.");
+ return;
+ }
+
+ try {
+ Process process = new ProcessBuilder("/system/bin/perfetto", "--stop", "--attach="
+ + PERFETTO_TAG).start();
+ if (process.waitFor() != 0) {
+ // failed to stop
+ if (DEBUG) Log.d(TAG, "Failed to stop trace");
+ }
+ // TODO: b/293957254 spawn redaction if results available
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public boolean isTraceRunning() throws RuntimeException {
+ try {
+ Process process = new ProcessBuilder("/system/bin/perfetto", "--is_detached="
+ + PERFETTO_TAG).start();
+ int result = process.waitFor();
+ if (result == 0) {
+ // running
+ if (DEBUG) Log.d(TAG, "A trace is currently running");
+ return true;
+ } else if (result == 2) {
+ // not running
+ if (DEBUG) Log.d(TAG, "A trace is not currently running");
+ return false;
+ } else {
+ throw new RuntimeException("Perfetto error: " + result);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void spawnRedactionProcess(int uid, String packageName, long keyMostSigBits,
+ long keyLeastSigBits, String tag) {
+ // todo: start redaction in its own process.
+ processResultCallback(uid, keyMostSigBits, keyLeastSigBits, ProfilingResult.ERROR_NONE,
+ null, tag, null);
+ }
+
+ public static final class Lifecycle extends SystemService {
+ final ProfilingService mService;
+
+ public Lifecycle(Context context) {
+ this(context, new ProfilingService(context));
+ }
+
+ @VisibleForTesting
+ public Lifecycle(Context context, ProfilingService service) {
+ super(context);
+ mService = service;
+ }
+
+ @Override
+ public void onStart() {
+ try {
+ publishBinderService("profiling_service", mService);
+ } catch (Exception e) {
+ if (DEBUG) Log.d(TAG, "Failed to publish service", e);
+ }
+ }
+
+ @Override
+ public void onBootPhase(int phase) {
+ super.onBootPhase(phase);
+ }
+ }
}
diff --git a/service/java/com/android/os/profiling/RateLimiter.java b/service/java/com/android/os/profiling/RateLimiter.java
new file mode 100644
index 0000000..2fdcf42
--- /dev/null
+++ b/service/java/com/android/os/profiling/RateLimiter.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2023 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.profiling;
+
+import android.annotation.IntDef;
+import android.os.ProfilingRequest;
+import android.os.ProfilingResult;
+import android.util.SparseIntArray;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayDeque;
+import java.util.Queue;
+
+public final class RateLimiter {
+
+ private static final String DEVICE_CONFIG_NAMESPACE = "profiling";
+
+ private static final long PERSIST_TO_DISK_FREQUENCY_MS;
+
+ private static final long TIME_1_HOUR_MS = 60 * 60 * 1000;
+ private static final long TIME_24_HOUR_MS = 24 * 60 * 60 * 1000;
+ private static final long TIME_7_DAY_MS = 7 * 24 * 60 * 60 * 1000;
+
+ public static final int RATE_LIMIT_RESULT_ALLOWED = 0;
+ public static final int RATE_LIMIT_RESULT_BLOCKED_PROCESS = 1;
+ public static final int RATE_LIMIT_RESULT_BLOCKED_SYSTEM = 2;
+
+ /** To be disabled for testing only. */
+ private static boolean sRateLimiterEnabled = true;
+
+ /** Collection of run costs and entries from the last hour. */
+ private static final EntryGroupWrapper sPastRuns1Hour;
+ /** Collection of run costs and entries from the 24 hours. */
+ private static final EntryGroupWrapper sPastRuns24Hour;
+ /** Collection of run costs and entries from the 7 days. */
+ private static final EntryGroupWrapper sPastRuns7Day;
+
+ private static long sLastPersistedTimestampMs;
+
+ @IntDef(value={
+ RATE_LIMIT_RESULT_ALLOWED,
+ RATE_LIMIT_RESULT_BLOCKED_PROCESS,
+ RATE_LIMIT_RESULT_BLOCKED_SYSTEM,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface RateLimitResult {}
+
+ static {
+ // TODO: b/324885858 use DeviceConfig for adjustable values.
+ sPastRuns1Hour = new EntryGroupWrapper(10, 10, TIME_1_HOUR_MS);
+ sPastRuns24Hour = new EntryGroupWrapper(100, 100, TIME_24_HOUR_MS);
+ sPastRuns7Day = new EntryGroupWrapper(1000, 1000, TIME_7_DAY_MS);
+ PERSIST_TO_DISK_FREQUENCY_MS = 0;
+ sLastPersistedTimestampMs = System.currentTimeMillis();
+ }
+
+ public static @RateLimitResult int isProfilingRequestAllowed(int uid,
+ ProfilingRequest request) {
+ if (!sRateLimiterEnabled) {
+ // Rate limiter is disabled for testing, approve request and don't store cost.
+ return RATE_LIMIT_RESULT_ALLOWED;
+ }
+ final int cost = 1; // TODO: compute cost b/293957254
+ final long currentTimeMillis = System.currentTimeMillis();
+ int status = sPastRuns1Hour.isProfilingAllowed(uid, cost, currentTimeMillis);
+ if (status == RATE_LIMIT_RESULT_ALLOWED) {
+ status = sPastRuns24Hour.isProfilingAllowed(uid, cost, currentTimeMillis);
+ }
+ if (status == RATE_LIMIT_RESULT_ALLOWED) {
+ status = sPastRuns7Day.isProfilingAllowed(uid, cost, currentTimeMillis);
+ }
+ if (status == RATE_LIMIT_RESULT_ALLOWED) {
+ sPastRuns1Hour.add(uid, cost, currentTimeMillis);
+ sPastRuns24Hour.add(uid, cost, currentTimeMillis);
+ sPastRuns7Day.add(uid, cost, currentTimeMillis);
+ maybePersistToDisk();
+ return RATE_LIMIT_RESULT_ALLOWED;
+ }
+ return status;
+ }
+
+ static void maybePersistToDisk() {
+ if (PERSIST_TO_DISK_FREQUENCY_MS == 0
+ || System.currentTimeMillis() - sLastPersistedTimestampMs
+ >= PERSIST_TO_DISK_FREQUENCY_MS) {
+ persistToDisk();
+ } else {
+ // TODO: queue persist job b/293957254
+ }
+ }
+
+ static void persistToDisk() {
+ // TODO: b/293957254
+ }
+
+ static void loadFromDisk() {
+ // TODO: b/293957254
+ }
+
+ static int statusToResult(@RateLimitResult int resultStatus) {
+ switch (resultStatus) {
+ case RATE_LIMIT_RESULT_BLOCKED_PROCESS:
+ return ProfilingResult.ERROR_FAILED_RATE_LIMIT_PROCESS;
+ case RATE_LIMIT_RESULT_BLOCKED_SYSTEM:
+ return ProfilingResult.ERROR_FAILED_RATE_LIMIT_SYSTEM;
+ default:
+ return ProfilingResult.ERROR_UNKNOWN;
+ }
+ }
+
+ final static class EntryGroupWrapper {
+ final Queue<CollectionEntry> mEntries;
+ int mTotalCost;
+ // uid indexed
+ final SparseIntArray mPerUidCost;
+ final int mMaxCost;
+ final int mMaxCostPerUid;
+ final long mTimeRangeMs;
+
+ EntryGroupWrapper(final int maxCost, final int maxPerUidCost, final long timeRangeMs) {
+ mMaxCost = maxCost;
+ mMaxCostPerUid = maxPerUidCost;
+ mTimeRangeMs = timeRangeMs;
+ mEntries = new ArrayDeque<>();
+ mPerUidCost = new SparseIntArray();
+ }
+
+ void add(final int uid, final int cost, final long timestamp) {
+ mTotalCost += cost;
+ final int index = mPerUidCost.indexOfKey(uid);
+ if (index < 0) {
+ mPerUidCost.put(uid, cost);
+ } else {
+ mPerUidCost.put(uid, mPerUidCost.valueAt(index) + cost);
+ }
+ mEntries.offer(new CollectionEntry(uid, cost, timestamp));
+ }
+
+ /**
+ * Clean up the queue by removing entries that are too old.
+ *
+ * @param olderThanTimestamp timestamp to remove record which are older than.
+ */
+ void removeOlderThan(final long olderThanTimestamp) {
+ while (mEntries.peek() != null && mEntries.peek().mTimestamp <= olderThanTimestamp) {
+ final CollectionEntry entry = mEntries.poll();
+ if (entry == null) {
+ return;
+ }
+ mTotalCost -= entry.mCost;
+ if (mTotalCost < 0) {
+ mTotalCost = 0;
+ }
+ final int index = mPerUidCost.indexOfKey(entry.mUid);
+ if (index >= 0) {
+ mPerUidCost.setValueAt(index, mPerUidCost.valueAt(index) - entry.mCost);
+ }
+ }
+ }
+
+ /**
+ * Check if the requested profiling is allowed by the limits of this collection after
+ * ensuring the collection is up to date.
+ *
+ * @param uid of requesting process
+ * @param cost calculated perf cost of running this query
+ * @param currentTimeMillis cache time and keep consistent across checks
+ * @return status indicating whether request is allowed, or which rate limiting applied to
+ * deny it.
+ */
+ @RateLimitResult int isProfilingAllowed(final int uid, final int cost,
+ final long currentTimeMillis) {
+ removeOlderThan(currentTimeMillis - mTimeRangeMs);
+ if (mTotalCost + cost > mMaxCost) {
+ return RATE_LIMIT_RESULT_BLOCKED_SYSTEM;
+ }
+ final int index = mPerUidCost.indexOfKey(uid);
+ return ((index < 0 ? 0 : mPerUidCost.valueAt(index)) + cost < mMaxCostPerUid)
+ ? RATE_LIMIT_RESULT_ALLOWED : RATE_LIMIT_RESULT_BLOCKED_PROCESS;
+ }
+ }
+ final static class CollectionEntry {
+ final int mUid;
+ final int mCost;
+ final Long mTimestamp;
+
+ CollectionEntry(final int uid, final int cost, final Long timestamp) {
+ mUid = uid;
+ mCost = cost;
+ mTimestamp = timestamp;
+ }
+ }
+}
\ No newline at end of file