Implement torq simpleperf symbolization

Add logic to build device binary caches and to convert simpleperf files
to gecko json files. This will allow the symbols in the generated
simpleperf traces to be human-readable. Add tests for this logic and
update the relevant tests.

Test: atest command_executor_unit_test \ torq_unit_test \
device_unit_test \ config_builder_unit_test \
validate_simpleperf_unit_test \ utils_unit_test
Fixes: 365985190

Change-Id: I7575bb490452e07a271208fef93335fed6911f83
diff --git a/torq/Android.bp b/torq/Android.bp
index 818a279..c618f24 100644
--- a/torq/Android.bp
+++ b/torq/Android.bp
@@ -118,3 +118,19 @@
         unit_test: true,
     },
 }
+
+python_test_host {
+    name: "utils_unit_test",
+    main: "tests/utils_unit_test.py",
+    srcs: ["tests/utils_unit_test.py"],
+    defaults: ["torq_defaults"],
+    version: {
+        py3: {
+            enabled: true,
+            embedded_launcher: false,
+        },
+    },
+    test_options: {
+        unit_test: true,
+    },
+}
diff --git a/torq/README.md b/torq/README.md
index 128669f..4536586 100644
--- a/torq/README.md
+++ b/torq/README.md
@@ -73,6 +73,7 @@
 | `-d, --dur-ms`                          | The duration (ms) of the event. Determines when to stop collecting performance data.                                                                                                                                                                                               | Float >= `3000`                                                                              | `10000`                              |
 | `-a, --app`                             | The package name of the app to start.<br/>(Requires use of `-e app-startup`)                                                                                                                                                                                                       | Any package on connected device                                                              |                                      |
 | `-r, --runs`                            | The amount of times to run the event and capture the performance data.                                                                                                                                                                                                             | Integer >= `1`                                                                               | `1`                                  |
+| `-s, --simpleperf-event`                | Simpleperf supported events that should be collected. Can be defined multiple times in a command. (Requires use of `-p simpleperf`).                                                                                                                                               | Any supported simpleperf event<br/>(e.g., `cpu-cycles`, `instructions`)                      | `cpu-clock`                          |
 | `--serial`                              | The serial of the connected device that you want to use.<br/>(If not provided, the ANDROID_SERIAL environment variable is used. If ANDROID_SERIAL is also not set and there is only one device connected, the device is chosen.)                                                   |                                                                                              |                                      |
 | `--perfetto-config`                     | The local file path of the user's Perfetto config or used to specify a predefined Perfetto configs.                                                                                                                                                                                | `default`, any local perfetto config,<br/>(`lightweight`, `memory` coming soon)              | `default`                            |
 | `--between-dur-ms`                      | The amount of time (ms) to wait between different runs.<br/>(Requires that `--r` is set to a value greater than 1)                                                                                                                                                                 | Float >= `3000`                                                                              | `10000`                              |
@@ -81,13 +82,8 @@
 | `--include-ftrace-event`                | Includes the ftrace event in the Perfetto config. Can be defined multiple times in a command.<br/>(Requires use of `-p perfetto`)<br/>(Currently only works with `--perfetto-config default`,<br/>support for any local Perfetto configs, `lightweight`, and `memory` coming soon) | Any supported perfetto ftrace event<br/>(e.g., `power/cpu_idle`, `sched/sched_process_exit`) | Empty list                           |
 | `--from-user`                           | The user ID from which to start the user switch. (Requires use of `-e user-switch`)                                                                                                                                                                                                | ID of any user on connected device                                                           | Current user on the device           |
 | `--to-user`                             | The user ID of user that device is switching to. (Requires use of `-e user-switch`).                                                                                                                                                                                               | ID of any user on connected device                                                           |                                      |
+| `--symbols`                             | The device symbols library. (Requires use of `-p simpleperf`).                                                                                                                                                                                                                     | Path to a device symbols library                                                             |                                      |
 | `config list`                           | Subcommand to list the predefined Perfetto configs (`default`, `lightweight`, `memory`).                                                                                                                                                                                           |                                                                                              |                                      |
 | `config show <config-name>`             | Subcommand to print the contents of a predefined Perfetto config to the terminal.                                                                                                                                                                                                  | `default`, `lightweight`, `memory`                                                           |                                      |
 | `config pull <config-name> [file-path]` | Subcommand to download a predefined Perfetto config to a specified local file path.                                                                                                                                                                                                | <config-name>: `default`, `lightweight`, `memory`<br/> [file-path]: Any local file path      | [file-path]: `./<config-name>.pbtxt` |
 | `open <file-path>`                      | Subcommand to open a Perfetto or Simpleperf trace in the Perfetto UI.                                                                                                                                                                                                              | Any local path to a Perfetto or Simpleperf trace file                                        |                                      |
-
-## Functionality Coming Soon
-
-| Argument                                | Description                                                                                                                                                   | Accepted Values                                                                         | Default                               |
-|-----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|---------------------------------------|
-| `-s, --simpleperf-event`                | Simpleperf supported events that should be collected. Can be defined multiple times in a command.                                                             | Any supported simpleperf event<br/>(e.g., `cpu-cycles`, `instructions`)                 | Empty list                            |
diff --git a/torq/command.py b/torq/command.py
index a2a7262..542e4cc 100644
--- a/torq/command.py
+++ b/torq/command.py
@@ -48,7 +48,8 @@
   """
   def __init__(self, type, event, profiler, out_dir, dur_ms, app, runs,
       simpleperf_event, perfetto_config, between_dur_ms, ui,
-      excluded_ftrace_events, included_ftrace_events, from_user, to_user):
+      excluded_ftrace_events, included_ftrace_events, from_user, to_user,
+      scripts_path, symbols):
     super().__init__(type)
     self.event = event
     self.profiler = profiler
@@ -64,6 +65,8 @@
     self.included_ftrace_events = included_ftrace_events
     self.from_user = from_user
     self.to_user = to_user
+    self.scripts_path = scripts_path
+    self.symbols = symbols
     match event:
       case "custom":
         self.command_executor = ProfilerCommandExecutor()
diff --git a/torq/command_executor.py b/torq/command_executor.py
index 20ce3a4..78ee314 100644
--- a/torq/command_executor.py
+++ b/torq/command_executor.py
@@ -21,6 +21,7 @@
 from config_builder import PREDEFINED_PERFETTO_CONFIGS, build_custom_config
 from open_ui import open_trace
 from device import SIMPLEPERF_TRACE_FILE
+from utils import convert_simpleperf_to_gecko
 
 PERFETTO_TRACE_FILE = "/data/misc/perfetto-traces/trace.perfetto-trace"
 PERFETTO_BOOT_TRACE_FILE = "/data/misc/perfetto-traces/boottrace.perfetto-trace"
@@ -62,19 +63,22 @@
     if error is not None:
       return error
     host_file = None
+    host_gecko_file = None
     for run in range(1, command.runs + 1):
       timestamp = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
       if command.profiler == "perfetto":
         host_file = f"{command.out_dir}/trace-{timestamp}.perfetto-trace"
       else:
         host_file = f"{command.out_dir}/perf-{timestamp}.data"
+        host_gecko_file = f"{command.out_dir}/perf-{timestamp}.json"
       error = self.prepare_device_for_run(command, device)
       if error is not None:
         return error
       error = self.execute_run(command, device, config, run)
       if error is not None:
         return error
-      error = self.retrieve_perf_data(command, device, host_file)
+      error = self.retrieve_perf_data(command, device, host_file,
+                                      host_gecko_file)
       if error is not None:
         return error
       if command.runs != run:
@@ -83,7 +87,10 @@
     if error is not None:
       return error
     if command.use_ui:
-      open_trace(host_file, WEB_UI_ADDRESS)
+      if command.profiler == "perfetto":
+        open_trace(host_file, WEB_UI_ADDRESS)
+      else:
+        open_trace(host_gecko_file, WEB_UI_ADDRESS)
     return None
 
   @staticmethod
@@ -119,11 +126,13 @@
   def trigger_system_event(self, command, device):
     return None
 
-  def retrieve_perf_data(self, command, device, host_file):
+  def retrieve_perf_data(self, command, device, host_file, host_gecko_file):
     if command.profiler == "perfetto":
       device.pull_file(PERFETTO_TRACE_FILE, host_file)
     else:
       device.pull_file(SIMPLEPERF_TRACE_FILE, host_file)
+      convert_simpleperf_to_gecko(command.scripts_path, host_file,
+                                  host_gecko_file, command.symbols)
 
   def cleanup(self, command, device):
     return None
@@ -181,7 +190,7 @@
   def trigger_system_event(self, command, device):
     device.reboot()
 
-  def retrieve_perf_data(self, command, device, host_file):
+  def retrieve_perf_data(self, command, device, host_file, host_gecko_file):
     device.pull_file(PERFETTO_BOOT_TRACE_FILE, host_file)
 
 
diff --git a/torq/tests/command_executor_unit_test.py b/torq/tests/command_executor_unit_test.py
index 19226bf..4c931c3 100644
--- a/torq/tests/command_executor_unit_test.py
+++ b/torq/tests/command_executor_unit_test.py
@@ -14,6 +14,7 @@
 # limitations under the License.
 #
 
+import os
 import unittest
 import subprocess
 import sys
@@ -25,7 +26,6 @@
 from torq import DEFAULT_DUR_MS, DEFAULT_OUT_DIR, PREDEFINED_PERFETTO_CONFIGS
 
 PROFILER_COMMAND_TYPE = "profiler"
-PROFILER_TYPE = "perfetto"
 TEST_ERROR_MSG = "test-error"
 TEST_EXCEPTION = Exception(TEST_ERROR_MSG)
 TEST_VALIDATION_ERROR = ValidationError(TEST_ERROR_MSG, None)
@@ -374,9 +374,9 @@
 
   def setUp(self):
     self.command = ProfilerCommand(
-        PROFILER_COMMAND_TYPE, "custom", PROFILER_TYPE, DEFAULT_OUT_DIR, DEFAULT_DUR_MS,
+        PROFILER_COMMAND_TYPE, "custom", "perfetto", DEFAULT_OUT_DIR, DEFAULT_DUR_MS,
         None, 1, None, DEFAULT_PERFETTO_CONFIG, None, False, None, None, None,
-        None)
+        None, None, None)
     self.mock_device = mock.create_autospec(AdbDevice, instance=True,
                                             serial=TEST_SERIAL)
     self.mock_device.check_device_connection.return_value = None
@@ -393,6 +393,46 @@
       self.assertEqual(error, None)
       self.assertEqual(self.mock_device.pull_file.call_count, 1)
 
+  @mock.patch.object(subprocess, "run", autospec=True)
+  @mock.patch.object(subprocess, "Popen", autospec=True)
+  @mock.patch.object(os.path, "exists", autospec=True)
+  def test_execute_one_simpleperf_run_success(self,
+      mock_exists, mock_process, mock_run):
+    with mock.patch("command_executor.open_trace", autospec=True):
+      self.mock_device.start_simpleperf_trace.return_value = mock_process
+      mock_exists.return_value = True
+      mock_run.return_value = None
+      simpleperf_command = ProfilerCommand(
+          PROFILER_COMMAND_TYPE, "custom", "simpleperf", DEFAULT_OUT_DIR,
+          DEFAULT_DUR_MS, None, 1, None, DEFAULT_PERFETTO_CONFIG, None, False,
+          None, None, None, None, "/", "/")
+      simpleperf_command.use_ui = True
+
+      error = simpleperf_command.execute(self.mock_device)
+
+      self.assertEqual(error, None)
+      self.assertEqual(self.mock_device.pull_file.call_count, 1)
+
+  @mock.patch.object(subprocess, "run", autospec=True)
+  @mock.patch.object(subprocess, "Popen", autospec=True)
+  @mock.patch.object(os.path, "exists", autospec=True)
+  def test_execute_one_simpleperf_run_failure(self,
+      mock_exists, mock_process, mock_run):
+    with mock.patch("command_executor.open_trace", autospec=True):
+      self.mock_device.start_simpleperf_trace.return_value = mock_process
+      mock_exists.return_value = False
+      mock_run.return_value = None
+      simpleperf_command = ProfilerCommand(
+          PROFILER_COMMAND_TYPE, "custom", "simpleperf", DEFAULT_OUT_DIR,
+          DEFAULT_DUR_MS, None, 1, None, DEFAULT_PERFETTO_CONFIG, None, False,
+          None, None, None, None, "/", "/")
+      simpleperf_command.use_ui = True
+
+      with self.assertRaises(Exception) as e:
+        simpleperf_command.execute(self.mock_device)
+
+        self.assertEqual(str(e.exception), "Gecko file was not created.")
+
   @mock.patch.object(subprocess, "Popen", autospec=True)
   def test_execute_one_run_no_ui_success(self, mock_process):
     self.mock_device.start_perfetto_trace.return_value = mock_process
@@ -563,9 +603,9 @@
 
   def setUp(self):
     self.command = ProfilerCommand(
-        PROFILER_COMMAND_TYPE, "user-switch", PROFILER_TYPE, DEFAULT_OUT_DIR,
+        PROFILER_COMMAND_TYPE, "user-switch", "perfetto", DEFAULT_OUT_DIR,
         DEFAULT_DUR_MS, None, 1, None, DEFAULT_PERFETTO_CONFIG, None, False,
-        None, None, None, None)
+        None, None, None, None, None, None)
     self.mock_device = mock.create_autospec(AdbDevice, instance=True,
                                             serial=TEST_SERIAL)
     self.mock_device.check_device_connection.return_value = None
@@ -687,9 +727,9 @@
 
   def setUp(self):
     self.command = ProfilerCommand(
-        PROFILER_COMMAND_TYPE, "boot", PROFILER_TYPE, DEFAULT_OUT_DIR,
+        PROFILER_COMMAND_TYPE, "boot", "perfetto", DEFAULT_OUT_DIR,
         TEST_DURATION, None, 1, None, DEFAULT_PERFETTO_CONFIG, TEST_DURATION,
-        False, None, None, None, None)
+        False, None, None, None, None, None, None)
     self.mock_device = mock.create_autospec(AdbDevice, instance=True,
                                             serial=TEST_SERIAL)
     self.mock_device.check_device_connection.return_value = None
@@ -801,9 +841,9 @@
 
   def setUp(self):
     self.command = ProfilerCommand(
-        PROFILER_COMMAND_TYPE, "app-startup", PROFILER_TYPE, DEFAULT_OUT_DIR,
+        PROFILER_COMMAND_TYPE, "app-startup", "perfetto", DEFAULT_OUT_DIR,
         DEFAULT_DUR_MS, TEST_PACKAGE_1, 1, None, DEFAULT_PERFETTO_CONFIG, None,
-        False, None, None, None, None)
+        False, None, None, None, None, None, None)
     self.mock_device = mock.create_autospec(AdbDevice, instance=True,
                                             serial=TEST_SERIAL)
     self.mock_device.check_device_connection.return_value = None
diff --git a/torq/tests/config_builder_unit_test.py b/torq/tests/config_builder_unit_test.py
index 42e9f4e..4dd1210 100644
--- a/torq/tests/config_builder_unit_test.py
+++ b/torq/tests/config_builder_unit_test.py
@@ -339,7 +339,7 @@
   def setUp(self):
     self.command = ProfilerCommand(
         None, "custom", None, None, DEFAULT_DUR_MS, None, None, "test-path",
-        None, None, None, None, None, None, None)
+        None, None, None, None, None, None, None, None, None)
 
   def test_build_default_config_setting_valid_dur_ms(self):
     self.command.dur_ms = TEST_DUR_MS
diff --git a/torq/tests/device_unit_test.py b/torq/tests/device_unit_test.py
index da0c372..d79f092 100644
--- a/torq/tests/device_unit_test.py
+++ b/torq/tests/device_unit_test.py
@@ -356,7 +356,7 @@
     adbDevice = AdbDevice(TEST_DEVICE_SERIAL)
     command = ProfilerCommand("profiler", "custom", None, None,
                               10000, None, None, ["cpu-cycles"], None, None,
-                              None, None, None, None, None)
+                              None, None, None, None, None, None, None)
     mock_process = adbDevice.start_simpleperf_trace(command)
 
     # No exception is expected to be thrown
@@ -369,7 +369,7 @@
 
     command = ProfilerCommand("profiler", "custom", None, None,
                               10000, None, None, ["cpu-cycles"], None, None,
-                              None, None, None, None, None)
+                              None, None, None, None, None, None, None)
     with self.assertRaises(Exception) as e:
       adbDevice.start_simpleperf_trace(command)
 
diff --git a/torq/tests/utils_unit_test.py b/torq/tests/utils_unit_test.py
new file mode 100644
index 0000000..3e72aa6
--- /dev/null
+++ b/torq/tests/utils_unit_test.py
@@ -0,0 +1,53 @@
+#
+# 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.
+#
+
+import unittest
+import subprocess
+import os
+from device import AdbDevice
+from unittest import mock
+from utils import convert_simpleperf_to_gecko
+
+
+class UtilsUnitTest(unittest.TestCase):
+
+  @mock.patch.object(subprocess, "run", autospec=True)
+  @mock.patch.object(os.path, "exists", autospec=True)
+  def test_convert_simpleperf_to_gecko_success(self, mock_exists,
+      mock_subprocess_run):
+    mock_exists.return_value = True
+    mock_subprocess_run.return_value = None
+
+    # No exception is expected to be thrown
+    convert_simpleperf_to_gecko("/scripts", "/path/file.data",
+                                "/path/file.json", "/symbols")
+
+  @mock.patch.object(subprocess, "run", autospec=True)
+  @mock.patch.object(os.path, "exists", autospec=True)
+  def test_convert_simpleperf_to_gecko_failure(self, mock_exists,
+      mock_subprocess_run):
+    mock_exists.return_value = False
+    mock_subprocess_run.return_value = None
+
+    with self.assertRaises(Exception) as e:
+      convert_simpleperf_to_gecko("/scripts", "/path/file.data",
+                                  "/path/file.json", "/symbols")
+
+      self.assertEqual(str(e.exception), "Gecko file was not created.")
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/torq/torq.py b/torq/torq.py
index a01dadb..5f1b6ed 100644
--- a/torq/torq.py
+++ b/torq/torq.py
@@ -349,7 +349,7 @@
                          args.perfetto_config, args.between_dur_ms,
                          args.ui, args.excluded_ftrace_events,
                          args.included_ftrace_events, args.from_user,
-                         args.to_user)
+                         args.to_user, args.scripts_path, args.symbols)
 
 
 def create_config_command(args):
diff --git a/torq/utils.py b/torq/utils.py
index 8716d9f..e30795f 100644
--- a/torq/utils.py
+++ b/torq/utils.py
@@ -15,6 +15,7 @@
 #
 
 import os
+import subprocess
 
 def path_exists(path: str):
   if path is None:
@@ -25,3 +26,20 @@
   if path is None:
     return False
   return os.path.isdir(os.path.expanduser(path))
+
+def convert_simpleperf_to_gecko(scripts_path, host_raw_trace_filename,
+    host_gecko_trace_filename, symbols):
+  expanded_symbols = os.path.expanduser(symbols)
+  expanded_scripts_path = os.path.expanduser(scripts_path)
+  print("Building binary cache, please wait. If no samples were recorded,"
+        " the trace will be empty.")
+  subprocess.run(("%s/binary_cache_builder.py -i %s -lib %s"
+                  % (expanded_scripts_path, host_raw_trace_filename,
+                     expanded_symbols)),
+                 shell=True)
+  subprocess.run(("%s/gecko_profile_generator.py -i %s > %s"
+                  % (expanded_scripts_path, host_raw_trace_filename,
+                     host_gecko_trace_filename)),
+                 shell=True)
+  if not path_exists(host_gecko_trace_filename):
+    raise Exception("Gecko file was not created.")