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