Implement torq simpleperf symbolization validation
Add logic to check for and download device symbols, and to check for
simpleperf scripts. This logic will be used in a subsequent CL to
process the simpleperf traces. Add tests for this logic.
Test: atest validate_simpleperf_unit_test
Test: atest torq_unit_test
Bug: 365985190
Change-Id: Ie4eeb655acf457e1028a3bef256cd68a15e6b027
diff --git a/torq/Android.bp b/torq/Android.bp
index 6bc39d4..e3245df 100644
--- a/torq/Android.bp
+++ b/torq/Android.bp
@@ -28,6 +28,7 @@
"config_builder.py",
"open_ui.py",
"utils.py",
+ "validate_simpleperf.py",
],
}
@@ -100,3 +101,19 @@
unit_test: true,
},
}
+
+python_test_host {
+ name: "validate_simpleperf_unit_test",
+ main: "tests/validate_simpleperf_unit_test.py",
+ srcs: ["tests/validate_simpleperf_unit_test.py"],
+ defaults: ["torq_defaults"],
+ version: {
+ py3: {
+ enabled: true,
+ embedded_launcher: false,
+ },
+ },
+ test_options: {
+ unit_test: true,
+ },
+}
diff --git a/torq/tests/torq_unit_test.py b/torq/tests/torq_unit_test.py
index 112bf58..5a0e4a9 100644
--- a/torq/tests/torq_unit_test.py
+++ b/torq/tests/torq_unit_test.py
@@ -24,6 +24,7 @@
TEST_USER_ID = 10
TEST_PACKAGE = "com.android.contacts"
TEST_FILE = "file.pbtxt"
+SYMBOLS_PATH = "/folder/symbols"
class TorqUnitTest(unittest.TestCase):
@@ -90,7 +91,11 @@
with self.assertRaises(SystemExit):
parser.parse_args()
- def test_create_parser_valid_profiler_names(self):
+ @mock.patch.object(os.path, "exists", autospec=True)
+ @mock.patch.object(os.path, "isdir", autospec=True)
+ def test_create_parser_valid_profiler_names(self, mock_isdir, mock_exists):
+ mock_isdir.return_value = True
+ mock_exists.return_value = True
parser = self.set_up_parser("torq.py -p perfetto")
args = parser.parse_args()
@@ -99,7 +104,8 @@
self.assertEqual(error, None)
self.assertEqual(args.profiler, "perfetto")
- parser = self.set_up_parser("torq.py -p simpleperf")
+ parser = self.set_up_parser("torq.py -p simpleperf --symbols %s"
+ % SYMBOLS_PATH)
args = parser.parse_args()
args, error = verify_args(args)
@@ -482,8 +488,14 @@
" torq --event app-startup --app"
" <package-name>"))
- def test_verify_args_profiler_and_simpleperf_event_valid_dependencies(self):
- parser = self.set_up_parser("torq.py -p simpleperf")
+ @mock.patch.object(os.path, "exists", autospec=True)
+ @mock.patch.object(os.path, "isdir", autospec=True)
+ def test_verify_args_profiler_and_simpleperf_event_valid_dependencies(self,
+ mock_isdir, mock_exists):
+ mock_isdir.return_value = True
+ mock_exists.return_value = True
+ parser = self.set_up_parser("torq.py -p simpleperf --symbols %s"
+ % SYMBOLS_PATH)
args = parser.parse_args()
args, error = verify_args(args)
@@ -492,7 +504,8 @@
self.assertEqual(len(args.simpleperf_event), 1)
self.assertEqual(args.simpleperf_event[0], "cpu-cycles")
- parser = self.set_up_parser("torq.py -p simpleperf -s cpu-cycles")
+ parser = self.set_up_parser("torq.py -p simpleperf -s cpu-cycles "
+ "--symbols %s" % SYMBOLS_PATH)
args = parser.parse_args()
args, error = verify_args(args)
@@ -707,9 +720,15 @@
" include power/cpu_idle in the"
" config."))
- def test_verify_args_multiple_valid_simpleperf_events(self):
+ @mock.patch.object(os.path, "exists", autospec=True)
+ @mock.patch.object(os.path, "isdir", autospec=True)
+ def test_verify_args_multiple_valid_simpleperf_events(self, mock_isdir,
+ mock_exists):
+ mock_isdir.return_value = True
+ mock_exists.return_value = True
parser = self.set_up_parser(("torq.py -p simpleperf -s cpu-cycles"
- " -s instructions"))
+ " -s instructions --symbols %s"
+ % SYMBOLS_PATH))
args = parser.parse_args()
args, error = verify_args(args)
diff --git a/torq/tests/validate_simpleperf_unit_test.py b/torq/tests/validate_simpleperf_unit_test.py
new file mode 100644
index 0000000..3eec988
--- /dev/null
+++ b/torq/tests/validate_simpleperf_unit_test.py
@@ -0,0 +1,229 @@
+#
+# 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 builtins
+import unittest
+import sys
+import os
+import subprocess
+from unittest import mock
+from torq import create_parser, verify_args
+
+TORQ_TEMP_DIR = "/tmp/.torq"
+ANDROID_BUILD_TOP = "/folder"
+ANDROID_PRODUCT_OUT = "/folder/out/product/seahawk"
+SYMBOLS_PATH = "/folder/symbols"
+
+
+class ValidateSimpleperfUnitTest(unittest.TestCase):
+
+ def set_up_parser(self, command_string):
+ parser = create_parser()
+ sys.argv = command_string.split()
+ return parser
+
+ @mock.patch.object(os.path, "exists", autospec=True)
+ @mock.patch.object(os.path, "isdir", autospec=True)
+ @mock.patch.dict(os.environ, {"ANDROID_BUILD_TOP": ANDROID_BUILD_TOP,
+ "ANDROID_PRODUCT_OUT": ANDROID_PRODUCT_OUT},
+ clear=True)
+ def test_create_parser_valid_symbols(self, mock_isdir, mock_exists):
+ mock_isdir.return_value = True
+ mock_exists.return_value = True
+ parser = self.set_up_parser("torq.py -p simpleperf "
+ "--symbols %s" % SYMBOLS_PATH)
+
+ args = parser.parse_args()
+ args, error = verify_args(args)
+
+ self.assertEqual(error, None)
+ self.assertEqual(args.symbols, SYMBOLS_PATH)
+ self.assertEqual(args.scripts_path, "%s/system/extras/simpleperf/scripts"
+ % ANDROID_BUILD_TOP)
+
+ @mock.patch.object(os.path, "exists", autospec=True)
+ @mock.patch.object(os.path, "isdir", autospec=True)
+ @mock.patch.dict(os.environ, {"ANDROID_BUILD_TOP": ANDROID_BUILD_TOP,
+ "ANDROID_PRODUCT_OUT": ANDROID_PRODUCT_OUT},
+ clear=True)
+ def test_create_parser_valid_android_product_out_no_symbols(self,
+ mock_isdir, mock_exists):
+ mock_isdir.return_value = True
+ mock_exists.return_value = True
+ parser = self.set_up_parser("torq.py -p simpleperf")
+
+ args = parser.parse_args()
+ args, error = verify_args(args)
+
+ self.assertEqual(error, None)
+ self.assertEqual(args.symbols, ANDROID_PRODUCT_OUT)
+ self.assertEqual(args.scripts_path, "%s/system/extras/simpleperf/scripts"
+ % ANDROID_BUILD_TOP)
+
+ @mock.patch.dict(os.environ, {"ANDROID_PRODUCT_OUT": ANDROID_PRODUCT_OUT},
+ clear=True)
+ @mock.patch.object(os.path, "exists", autospec=True)
+ @mock.patch.object(os.path, "isdir", autospec=True)
+ def test_create_parser_invalid_android_product_no_symbols(self,
+ mock_isdir, mock_exists):
+ mock_isdir.return_value = False
+ mock_exists.return_value = False
+ parser = self.set_up_parser("torq.py -p simpleperf")
+
+ args = parser.parse_args()
+ args, error = verify_args(args)
+
+ self.assertEqual(error.message, ("%s is not a valid $ANDROID_PRODUCT_OUT."
+ % ANDROID_PRODUCT_OUT))
+ self.assertEqual(error.suggestion, "Set --symbols to a valid symbols lib "
+ "path or set $ANDROID_PRODUCT_OUT to "
+ "your android product out directory "
+ "(<ANDROID_BUILD_TOP>/out/target/product"
+ "/<TARGET>).")
+
+ @mock.patch.dict(os.environ, {},
+ clear=True)
+ @mock.patch.object(os.path, "exists", autospec=True)
+ @mock.patch.object(os.path, "isdir", autospec=True)
+ def test_create_parser_invalid_symbols_no_android_product_out(self,
+ mock_isdir, mock_exists):
+ mock_isdir.return_value = False
+ mock_exists.return_value = False
+ parser = self.set_up_parser("torq.py -p simpleperf "
+ "--symbols %s" % SYMBOLS_PATH)
+
+ args = parser.parse_args()
+ args, error = verify_args(args)
+
+ self.assertEqual(error.message, ("%s is not a valid path." % SYMBOLS_PATH))
+ self.assertEqual(error.suggestion, "Set --symbols to a valid symbols lib "
+ "path or set $ANDROID_PRODUCT_OUT to "
+ "your android product out directory "
+ "(<ANDROID_BUILD_TOP>/out/target/product"
+ "/<TARGET>).")
+
+ @mock.patch.dict(os.environ, {}, clear=True)
+ @mock.patch.object(os.path, "exists", autospec=True)
+ @mock.patch.object(os.path, "isdir", autospec=True)
+ def test_create_parser_no_android_product_out_no_symbols(self, mock_isdir,
+ mock_exists):
+ mock_isdir.return_value = False
+ mock_exists.return_value = False
+ parser = self.set_up_parser("torq.py -p simpleperf")
+
+ args = parser.parse_args()
+ args, error = verify_args(args)
+
+ self.assertEqual(error.message, "ANDROID_PRODUCT_OUT is not set.")
+ self.assertEqual(error.suggestion, "Set --symbols to a valid symbols lib "
+ "path or set $ANDROID_PRODUCT_OUT to "
+ "your android product out directory "
+ "(<ANDROID_BUILD_TOP>/out/target/"
+ "product/<TARGET>).")
+
+ @mock.patch.dict(os.environ, {"ANDROID_PRODUCT_OUT": ANDROID_PRODUCT_OUT},
+ clear=True)
+ @mock.patch.object(os.path, "exists", autospec=True)
+ @mock.patch.object(os.path, "isdir", autospec=True)
+ @mock.patch.object(subprocess, "run", autospec=True)
+ @mock.patch.object(builtins, "input")
+ def test_create_parser_successfully_download_scripts(self, mock_input,
+ mock_subprocess_run, mock_isdir, mock_exists):
+ mock_isdir.return_value = True
+ mock_input.return_value = "y"
+ mock_exists.side_effect = [False, True]
+ mock_subprocess_run.return_value = None
+ parser = self.set_up_parser("torq.py -p simpleperf")
+
+ args = parser.parse_args()
+ args, error = verify_args(args)
+
+ self.assertEqual(error, None)
+ self.assertEqual(args.symbols, ANDROID_PRODUCT_OUT)
+ self.assertEqual(args.scripts_path, TORQ_TEMP_DIR)
+
+ @mock.patch.dict(os.environ, {"ANDROID_BUILD_TOP": ANDROID_BUILD_TOP},
+ clear=True)
+ @mock.patch.object(os.path, "exists", autospec=True)
+ @mock.patch.object(os.path, "isdir", autospec=True)
+ @mock.patch.object(subprocess, "run", autospec=True)
+ @mock.patch.object(builtins, "input")
+ def test_create_parser_failed_to_download_scripts(self, mock_input,
+ mock_subprocess_run, mock_isdir, mock_exists):
+ mock_isdir.return_value = True
+ mock_input.return_value = "y"
+ mock_exists.side_effect = [False, False, False]
+ mock_subprocess_run.return_value = None
+ parser = self.set_up_parser("torq.py -p simpleperf --symbols %s"
+ % SYMBOLS_PATH)
+
+ args = parser.parse_args()
+ with self.assertRaises(Exception) as e:
+ args, error = verify_args(args)
+
+ self.assertEqual(str(e.exception),
+ "Error while downloading simpleperf scripts. Try "
+ "again or set $ANDROID_BUILD_TOP to your android root "
+ "path and make sure you have $ANDROID_BUILD_TOP/system"
+ "/extras/simpleperf/scripts downloaded.")
+
+ @mock.patch.dict(os.environ, {"ANDROID_BUILD_TOP": ANDROID_BUILD_TOP},
+ clear=True)
+ @mock.patch.object(os.path, "exists", autospec=True)
+ @mock.patch.object(os.path, "isdir", autospec=True)
+ @mock.patch.object(builtins, "input")
+ def test_create_parser_download_scripts_wrong_input(self, mock_input,
+ mock_isdir, mock_exists):
+ mock_isdir.return_value = True
+ mock_input.return_value = "bad-input"
+ mock_exists.side_effect = [False, False]
+ parser = self.set_up_parser("torq.py -p simpleperf --symbols %s"
+ % SYMBOLS_PATH)
+
+ args = parser.parse_args()
+ args, error = verify_args(args)
+
+ self.assertEqual(error.message, "Invalid inputs.")
+ self.assertEqual(error.suggestion, "Set $ANDROID_BUILD_TOP to your android "
+ "root path and make sure you have "
+ "$ANDROID_BUILD_TOP/system/extras/"
+ "simpleperf/scripts downloaded.")
+
+ @mock.patch.dict(os.environ, {"ANDROID_BUILD_TOP": ANDROID_BUILD_TOP},
+ clear=True)
+ @mock.patch.object(os.path, "exists", autospec=True)
+ @mock.patch.object(os.path, "isdir", autospec=True)
+ @mock.patch.object(builtins, "input")
+ def test_create_parser_download_scripts_refuse_download(self, mock_input,
+ mock_isdir, mock_exists):
+ mock_isdir.return_value = True
+ mock_input.return_value = "n"
+ mock_exists.side_effect = [False, False]
+ parser = self.set_up_parser("torq.py -p simpleperf --symbols %s"
+ % SYMBOLS_PATH)
+
+ args = parser.parse_args()
+ args, error = verify_args(args)
+
+ self.assertEqual(error.message, "Did not download simpleperf scripts.")
+ self.assertEqual(error.suggestion, "Set $ANDROID_BUILD_TOP to your android "
+ "root path and make sure you have "
+ "$ANDROID_BUILD_TOP/system/extras/"
+ "simpleperf/scripts downloaded.")
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/torq/torq.py b/torq/torq.py
index 0137326..a01dadb 100644
--- a/torq/torq.py
+++ b/torq/torq.py
@@ -20,7 +20,8 @@
from device import AdbDevice
from validation_error import ValidationError
from config_builder import PREDEFINED_PERFETTO_CONFIGS
-from utils import does_path_exist
+from utils import path_exists
+from validate_simpleperf import verify_simpleperf_args
DEFAULT_DUR_MS = 10000
MIN_DURATION_MS = 3000
@@ -72,6 +73,8 @@
parser.add_argument('--serial',
help=(('Specifies serial of the device that will be'
' used.')))
+ parser.add_argument('--symbols',
+ help='Specifies path to symbols library.')
subparsers = parser.add_subparsers(dest='subcommands', help='Subcommands')
config_parser = subparsers.add_parser('config',
help=('The config subcommand used'
@@ -324,11 +327,18 @@
"\t torq pull lightweight to copy to ./lightweight.pbtxt\n"
"\t torq pull memory to copy to ./memory.pbtxt"))
- if args.subcommands == "open" and not does_path_exist(args.file_path):
+ if args.subcommands == "open" and not path_exists(args.file_path):
return None, ValidationError(
"Command is invalid because %s is an invalid file path."
% args.file_path, "Make sure your file exists.")
+ if args.profiler == "simpleperf":
+ args, error = verify_simpleperf_args(args)
+ if error is not None:
+ return None, error
+ else:
+ args.scripts_path = None
+
return args, None
diff --git a/torq/utils.py b/torq/utils.py
index 8ef0662..8716d9f 100644
--- a/torq/utils.py
+++ b/torq/utils.py
@@ -16,7 +16,12 @@
import os
-def does_path_exist(path: str):
+def path_exists(path: str):
if path is None:
return False
return os.path.exists(os.path.expanduser(path))
+
+def dir_exists(path: str):
+ if path is None:
+ return False
+ return os.path.isdir(os.path.expanduser(path))
diff --git a/torq/validate_simpleperf.py b/torq/validate_simpleperf.py
new file mode 100644
index 0000000..4b8c41c
--- /dev/null
+++ b/torq/validate_simpleperf.py
@@ -0,0 +1,106 @@
+#
+# 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 os
+import subprocess
+from utils import path_exists, dir_exists
+from validation_error import ValidationError
+
+TORQ_TEMP_DIR = "/tmp/.torq"
+TEMP_CACHE_BUILDER_SCRIPT = TORQ_TEMP_DIR + "/binary_cache_builder.py"
+SIMPLEPERF_SCRIPTS_DIR = "/system/extras/simpleperf/scripts"
+BUILDER_SCRIPT = SIMPLEPERF_SCRIPTS_DIR + "/binary_cache_builder.py"
+
+def verify_simpleperf_args(args):
+ args.scripts_path = TORQ_TEMP_DIR
+ if ("ANDROID_BUILD_TOP" in os.environ
+ and path_exists(os.environ["ANDROID_BUILD_TOP"] + BUILDER_SCRIPT)):
+ args.scripts_path = (os.environ["ANDROID_BUILD_TOP"]
+ + SIMPLEPERF_SCRIPTS_DIR)
+
+ if args.symbols is None or not dir_exists(args.symbols):
+ if args.symbols is not None:
+ return None, ValidationError(
+ ("%s is not a valid path." % args.symbols),
+ "Set --symbols to a valid symbols lib path or set "
+ "$ANDROID_PRODUCT_OUT to your android product out directory "
+ "(<ANDROID_BUILD_TOP>/out/target/product/<TARGET>).")
+ if "ANDROID_PRODUCT_OUT" not in os.environ:
+ return None, ValidationError(
+ "ANDROID_PRODUCT_OUT is not set.",
+ "Set --symbols to a valid symbols lib path or set "
+ "$ANDROID_PRODUCT_OUT to your android product out directory "
+ "(<ANDROID_BUILD_TOP>/out/target/product/<TARGET>).")
+ if not dir_exists(os.environ["ANDROID_PRODUCT_OUT"]):
+ return None, ValidationError(
+ ("%s is not a valid $ANDROID_PRODUCT_OUT."
+ % (os.environ["ANDROID_PRODUCT_OUT"])),
+ "Set --symbols to a valid symbols lib path or set "
+ "$ANDROID_PRODUCT_OUT to your android product out directory "
+ "(<ANDROID_BUILD_TOP>/out/target/product/<TARGET>).")
+ args.symbols = os.environ["ANDROID_PRODUCT_OUT"]
+
+ if (args.scripts_path != TORQ_TEMP_DIR or
+ path_exists(TEMP_CACHE_BUILDER_SCRIPT)):
+ return args, None
+
+ error = download_simpleperf_scripts()
+
+ if error is not None:
+ return None, error
+
+ return args, None
+
+def download_simpleperf_scripts():
+ i = 0
+ while i <= 3:
+ i += 1
+ confirmation = input("You do not have an Android Root configured with "
+ "the simpleperf directory. To use simpleperf, torq "
+ "will download simpleperf scripts to '%s'. "
+ "Are you ok with this download? [Y/N]: "
+ % TORQ_TEMP_DIR)
+
+ if confirmation.lower() == "y":
+ break
+ elif confirmation.lower() == "n":
+ return ValidationError("Did not download simpleperf scripts.",
+ "Set $ANDROID_BUILD_TOP to your android root "
+ "path and make sure you have $ANDROID_BUILD_TOP"
+ "/system/extras/simpleperf/scripts "
+ "downloaded.")
+ if i == 3:
+ return ValidationError("Invalid inputs.",
+ "Set $ANDROID_BUILD_TOP to your android root "
+ "path and make sure you have $ANDROID_BUILD_TOP"
+ "/system/extras/simpleperf/scripts "
+ "downloaded.")
+
+ subprocess.run(("mkdir -p %s && wget -P %s "
+ "https://android.googlesource.com/platform/system/extras"
+ "/+archive/refs/heads/main/simpleperf/scripts.tar.gz "
+ "&& tar -xvzf %s/scripts.tar.gz -C %s"
+ % (TORQ_TEMP_DIR, TORQ_TEMP_DIR, TORQ_TEMP_DIR,
+ TORQ_TEMP_DIR)),
+ shell=True)
+
+ if not path_exists(TEMP_CACHE_BUILDER_SCRIPT):
+ raise Exception("Error while downloading simpleperf scripts. Try again "
+ "or set $ANDROID_BUILD_TOP to your android root path and "
+ "make sure you have $ANDROID_BUILD_TOP/system/extras/"
+ "simpleperf/scripts downloaded.")
+
+ return None