diff --git a/benchkit/adb/__init__.py b/benchkit/adb/__init__.py index cf9a066..b89068d 100644 --- a/benchkit/adb/__init__.py +++ b/benchkit/adb/__init__.py @@ -8,11 +8,13 @@ import subprocess import sys import time -from typing import Iterable, Optional +from typing import Callable, Iterable, Optional from benchkit.adb.usb import usb_down_up +from benchkit.communication import CommunicationLayer +from benchkit.communication.utils import command_with_env, remote_shell_command from benchkit.shell.shell import get_args, shell_out -from benchkit.utils.types import Command, PathType +from benchkit.utils.types import Command, Environment, PathType, SplitCommand def _identifier_from(ip_addr: str, port: int) -> str: @@ -48,18 +50,34 @@ class AndroidDebugBridge: # TODO add commlayer for "host" def __init__( self, - ip_addr: str, - port: int = 5555, + identifier: str, + # ip_addr: str, + # port: int = 5555, keep_connected: bool = False, wait_connected: bool = False, expected_os: Optional[str] = None, ) -> None: - self._ip = ip_addr - self._port = port + self.identifier = identifier + # self._ip = ip_addr + # self._port = port self._keep_connected = keep_connected self._wait_connected = wait_connected self._expected_os = expected_os + @staticmethod + def from_device( + device: ADBDevice, + keep_connected: bool = False, + wait_connected: bool = False, + expected_os: Optional[str] = None, + ) -> "AndroidDebugBridge": + return AndroidDebugBridge( + identifier=device.identifier, + keep_connected=keep_connected, + wait_connected=wait_connected, + expected_os=expected_os, + ) + def __enter__(self) -> "AndroidDebugBridge": if not self.is_connected(): self._connect_daemon() @@ -74,14 +92,15 @@ def __exit__(self, exc_type, exc_value, exc_tb) -> None: if not self._keep_connected and self.is_connected(): self._disconnect() - @property - def identifier(self) -> str: - """Get adb identifier of current device. + # TODO: this may or may not be enabled again, right now I don't see an easy way to connect to non-ip devices? + # @property + # def identifier(self) -> str: + # """Get adb identifier of current device. - Returns: - str: adb identifier of current device. - """ - return _identifier_from(ip_addr=self._ip, port=self._port) + # Returns: + # str: adb identifier of current device. + # """ + # return _identifier_from(ip_addr=self._ip, port=self._port) def is_connected(self) -> bool: """Returns whether the device is connected to adb. @@ -220,6 +239,20 @@ def _devices() -> Iterable[ADBDevice]: return devices + def query_devices( + self, + filter_callback: Callable[[ADBDevice], bool] = lambda _: True, + ) -> Iterable[ADBDevice]: + """Get filtered list of devices recognized by adb. + + Returns: + Iterable[ADBDevice]: filtered list of devices recognized by adb. + """ + devices = self._devices() + filtered = [dev for dev in devices if filter_callback(dev)] + return filtered + + @staticmethod def _host_shell_out( command: Command, @@ -440,3 +473,125 @@ def is_installed(self, activity_name: str) -> bool: output = self._target_shell_out(command) is_installed = f"package:{activity_name}" == output.strip() return is_installed + + def enable_ftracing(self) -> None: + self.shell_out("echo 1 > /sys/kernel/tracing/events/irq/enable") + self.shell_out("echo 1 > /sys/kernel/tracing/events/sched/sched_wakeup/enable") + + def disable_ftracing(self) -> None: + self.shell_out("echo 0 > /sys/kernel/tracing/events/irq/enable") + self.shell_out("echo 0 > /sys/kernel/tracing/events/sched/sched_wakeup/enable") + + def start_ftracing(self) -> None: + self.shell_out("echo 1 > /sys/kernel/tracing/tracing_on") + + def stop_ftracing(self) -> None: + self.shell_out("echo 0 > /sys/kernel/tracing/tracing_on") + + def dump_ftrace(self, output: str) -> None: + self.shell_out(f"rm {output} || true") # delete file or ignore rm if fails (file not exists) + self.shell_out(f"cat /sys/kernel/tracing/trace > {output}") + + def clear_ftrace_buffer(self) -> None: + self.shell_out("cat /dev/null > /sys/kernel/tracing/trace") + + def set_ftrace_buffer_size(self, size_kb: int) -> None: + self.shell_out(f"echo {size_kb} > /sys/kernel/tracing/buffer_size_kb") + + + +class AndroidCommLayer(CommunicationLayer): + def __init__( + self, + bridge: AndroidDebugBridge, + environment: Optional[Environment] = None, + ) -> None: + super().__init__() + self._bridge = bridge + self._additional_environment = environment if environment is not None else {} + self._command_prefix = None + + @property + def remote_host(self) -> Optional[str]: + return self._bridge.identifier + + @property + def is_local(self) -> bool: + return False + + def copy_from_host(self, source: PathType, destination: PathType) -> None: + self._bridge.push(source, destination) + + def copy_to_host(self, source: PathType, destination: PathType) -> None: + self._bridge.pull(source, destination) + + def shell( + self, + command: Command, + std_input: str | None = None, + current_dir: PathType | None = None, + environment: Environment = None, + shell: bool = False, + print_input: bool = True, + print_output: bool = True, + print_curdir: bool = True, + timeout: int | None = None, + output_is_log: bool = False, + ignore_ret_codes: Iterable[int] = (), + ignore_any_error_code: bool = False + ) -> str: + env_command = command_with_env( + command=command, + environment=environment, + additional_environment=self._additional_environment, + ) + output = self._bridge.shell_out( + command=env_command, + current_dir=current_dir, + output_is_log=output_is_log, + ) + return output + + def pipe_shell( + self, + command: Command, + current_dir: Optional[PathType] = None, + shell: bool = False, + ignore_ret_codes: Iterable[int] = () + ): + raise NotImplementedError("TODO") + + + def background_subprocess( + self, + command: Command, + stdout: PathType, + stderr: PathType, + cwd: PathType | None, + env: dict | None, + establish_new_connection: bool = False + ) -> subprocess.Popen: + dir_args = ["cd", f"{cwd}", "&&"] if cwd is not None else [] + command_args = dir_args + get_args(command) + + adb_command = [ + "adb", + "-s", + f"{self._bridge.identifier}", + "shell", + ] + command_args + + return subprocess.Popen( + adb_command, + stdout=stdout, + stderr=stderr, + env=env, + preexec_fn=os.setsid, + ) + + def get_process_status(self, process_handle: subprocess.Popen) -> str: + raise NotImplementedError("TODO") + + def get_process_nb_threads(self, process_handle: subprocess.Popen) -> int: + raise NotImplementedError("TODO") + diff --git a/benchkit/communication/adb.py b/benchkit/communication/adb.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/benchkit/communication/adb.py @@ -0,0 +1 @@ + diff --git a/benchkit/hdc/__init__.py b/benchkit/hdc/__init__.py index c33940a..3c948f2 100644 --- a/benchkit/hdc/__init__.py +++ b/benchkit/hdc/__init__.py @@ -5,12 +5,15 @@ """ from enum import Enum from platform import system as os_system +import subprocess from typing import Callable, Iterable, List, Optional +from benchkit.communication import CommunicationLayer +from benchkit.communication.utils import command_with_env from benchkit.dependencies.executables import ExecutableDependency from benchkit.dependencies.packages import Dependency from benchkit.shell.shell import get_args, shell_out -from benchkit.utils.types import Command, PathType +from benchkit.utils.types import Command, Environment, PathType class HDCError(Exception): @@ -217,3 +220,99 @@ def pull( f"{local_path}", ] self._host_shell_out(command=command) + + +class OpenHarmonyCommLayer(CommunicationLayer): + def __init__( + self, + conn: OpenHarmonyDeviceConnector, + environment: Optional[Environment] = None, + ) -> None: + super().__init__() + self._conn = conn + self._additional_environment = environment if environment is not None else {} + self._command_prefix = None + + @property + def remote_host(self) -> Optional[str]: + return self._conn.identifier + + @property + def is_local(self) -> bool: + return False + + def copy_from_host(self, source: PathType, destination: PathType) -> None: + self._conn.push(source, destination) + + def copy_to_host(self, source: PathType, destination: PathType) -> None: + self._conn.pull(source, destination) + + def shell( + self, + command: Command, + std_input: str | None = None, + current_dir: PathType | None = None, + environment: Environment = None, + shell: bool = False, + print_input: bool = True, + print_output: bool = True, + print_curdir: bool = True, + timeout: int | None = None, + output_is_log: bool = False, + ignore_ret_codes: Iterable[int] = (), + ignore_any_error_code: bool = False + ) -> str: + env_command = command_with_env( + command=command, + environment=environment, + additional_environment=self._additional_environment, + ) + output = self._conn.shell_out( + command=env_command, + current_dir=current_dir, + output_is_log=output_is_log, + ) + return output + + def pipe_shell( + self, + command: Command, + current_dir: Optional[PathType] = None, + shell: bool = False, + ignore_ret_codes: Iterable[int] = () + ): + raise NotImplementedError("TODO") + + def background_subprocess( + self, + command: Command, + stdout: PathType, + stderr: PathType, + cwd: PathType | None, + env: dict | None, + establish_new_connection: bool = False + ) -> subprocess.Popen: + dir_args = ["cd", f"{cwd}", "&&"] if cwd is not None else [] + command_args = dir_args + get_args(command) + + adb_command = [ + "adb", + "-s", + f"{self._conn.identifier}", + "shell", + ] + command_args + + return subprocess.Popen( + adb_command, + stdout=stdout, + stderr=stderr, + env=env, + preexec_fn=os.setsid, + ) + + def get_process_status(self, process_handle: subprocess.Popen) -> str: + raise NotImplementedError("TODO") + + def get_process_nb_threads(self, process_handle: subprocess.Popen) -> int: + raise NotImplementedError("TODO") + diff --git a/benchkit/utils/ftrace.py b/benchkit/utils/ftrace.py new file mode 100644 index 0000000..5cba8d8 --- /dev/null +++ b/benchkit/utils/ftrace.py @@ -0,0 +1,83 @@ +import time +from perfetto.trace_processor import TraceProcessor + +from typing import Iterable, NamedTuple, Optional, Protocol, cast + +from perfetto.trace_processor.api import QueryResultIterator + +from benchkit.utils.types import Command, PathType + +__default_span_query__ = """ + SELECT id, ts as timestamp, dur as duration, name + FROM slice +""" +__default_track_query__ = """ + SELECT id AS track_id, name + FROM counter_track +""" +__default_counter_query__ = """ + SELECT id, ts as timestamp, value, track_id + FROM counter +""" + + +class SpanEvent(NamedTuple): + id: int + timestamp: int + duration: float + name: str + + +class TrackMapping(NamedTuple): + track_id: int + name: str + + +class RawCountEvent(NamedTuple): + id: int + timestamp: int + value: int + track_id: int + + +class CountEvent(NamedTuple): + id: int + timestamp: int + name: str + value: int + + +class FTrace: + def __init__( + self, + path: str + ) -> None: + self.tp = TraceProcessor(trace=path) + + def query_raw(self, query: str) -> QueryResultIterator: + result = self.tp.query(query) + return result + + def query_spans(self) -> Iterable[SpanEvent]: + events = self.query_raw(__default_span_query__) + spans = [cast(SpanEvent, e) for e in events] + return spans + + def query_counts(self) -> Iterable[CountEvent]: + track_events = self.query_raw(__default_track_query__) + tracks = [cast(TrackMapping, t) for t in track_events] + mappings = {track.track_id: track.name for track in tracks} + raw_events = self.query_raw(__default_counter_query__) + raw_counts = [cast(RawCountEvent, e) for e in raw_events] + + events = [] + for rc in raw_counts: + event = CountEvent( + id=rc.id, + timestamp=rc.timestamp, + name=mappings.get(rc.track_id, "Unknown"), + value=rc.value, + ) + events.append(event) + return events + diff --git a/examples/ftrace/ftrace_bench.py b/examples/ftrace/ftrace_bench.py new file mode 100644 index 0000000..d0b33a5 --- /dev/null +++ b/examples/ftrace/ftrace_bench.py @@ -0,0 +1,75 @@ +import pathlib +from typing import Iterable, List +from benchkit.adb import AndroidDebugBridge +from benchkit.commandwrappers import CommandWrapper +from benchkit.platforms.generic import Platform +from benchkit.sharedlibs import SharedLib +from benchkit.utils.ftrace import FTrace +from benchkit.benchmark import Benchmark, CommandAttachment, PostRunHook, PreRunHook +from benchkit.utils.types import PathType + + +BUILD_VARIABLES = [] +RUN_VARIABLES = [] +TILT_VARIABLES = [] + +class FTraceBenchmark(Benchmark): + def __init__( + self, + bench_dir: PathType, + mobile: bool = False, + command_wrappers: Iterable[CommandWrapper] = [], + command_attachments: Iterable[CommandAttachment] = [], + shared_libs: Iterable[SharedLib] = [], + pre_run_hooks: Iterable[PreRunHook] = [], + post_run_hooks: Iterable[PostRunHook] = [], + platform: Platform | None = None, + adb: AndroidDebugBridge | None = None, + ) -> None: + super().__init__( + command_wrappers=command_wrappers, + command_attachments=command_attachments, + shared_libs=shared_libs, + pre_run_hooks=pre_run_hooks, + post_run_hooks=post_run_hooks, + ) + self.bench_dir = bench_dir + self.mobile = mobile + + if platform is not None: + self.platform = platform + if adb is not None: + self.adb = adb + + + @property + def bench_src_dir(self) -> pathlib.Path: + return pathlib.Path(self.bench_dir) + + @staticmethod + def get_build_var_names() -> List[str]: + return BUILD_VARIABLES + + @staticmethod + def get_run_var_names() -> List[str]: + return RUN_VARIABLES + + @staticmethod + def get_tilt_var_names() -> List[str]: + return TILT_VARIABLES + + + +def main() -> None: + path = "./tests/tmp/ftrace_dump" + trace = FTrace(path) + spans = trace.query_spans() + counts =trace.query_counts() + for span in spans: + print(span) + for count in counts: + print(count) + + +if __name__ == "__main__": + main() diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..3fec32c --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1 @@ +tmp/ diff --git a/tests/test_adb_api.py b/tests/test_adb_api.py new file mode 100644 index 0000000..b6e0d06 --- /dev/null +++ b/tests/test_adb_api.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +""" +Module to test the ADB api basics +""" + +from benchkit.adb import ADBDevice, AndroidCommLayer, AndroidDebugBridge +from benchkit.platforms.generic import Platform + +def main() -> None: + devices = list(AndroidDebugBridge._devices()) + device: ADBDevice + if devices: + device = devices[0] + else: + exit("No device found") + + bridge = AndroidDebugBridge.from_device(device) + + comm = AndroidCommLayer(bridge) + platform = Platform(comm_layer=comm) + + output = platform.comm.shell(command="ls") + print(output) + + +if __name__ == "__main__": + main() diff --git a/tests/test_android_ftrace.py b/tests/test_android_ftrace.py new file mode 100644 index 0000000..ac261fc --- /dev/null +++ b/tests/test_android_ftrace.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +""" +Module to test reading ftrace from android devices +""" + +import time +from perfetto.trace_processor import TraceProcessor + +from benchkit.adb import AndroidDebugBridge, ADBDevice + + +TRACE_BUFFER_SIZE_KB: int = 96000 +DEVICE_DUMP_PATH: str = "/sdcard/tmp/ftrace_dump" +HOST_DUMP_PATH: str = "./tests/tmp/ftrace_dump" +EXAMPLE_WEBSITE: str = "https://github.com/open-s4c/benchkit" +HOST_SLEEP_TIME: float = 5 + + +def enable_tracing(bridge: AndroidDebugBridge) -> None: + # per category tracing + bridge.shell_out("echo 1 > /sys/kernel/tracing/events/irq/enable") + # per event tracing + bridge.shell_out("echo 1 > /sys/kernel/tracing/events/sched/sched_wakeup/enable") + + +def disable_tracing(bridge: AndroidDebugBridge) -> None: + bridge.shell_out("echo 0 > /sys/kernel/tracing/events/irq/enable") + bridge.shell_out("echo 0 > /sys/kernel/tracing/events/sched/sched_wakeup/enable") + + +def start_tracing(bridge: AndroidDebugBridge) -> None: + bridge.shell_out("echo 1 > /sys/kernel/tracing/tracing_on") + + +def stop_tracing(bridge: AndroidDebugBridge) -> None: + bridge.shell_out("echo 0 > /sys/kernel/tracing/tracing_on") + + +def dump_trace(bridge: AndroidDebugBridge, output: str) -> None: + bridge.shell_out(f"rm {output} || true") # delete file or ignore rm if fails (file not exists) + bridge.shell_out(f"cat /sys/kernel/tracing/trace > {output}") + + +def clear_trace_buffer(bridge: AndroidDebugBridge) -> None: + bridge.shell_out("cat /dev/null > /sys/kernel/tracing/trace") + + +def set_trace_buffer_size(bridge: AndroidDebugBridge, size_kb: int) -> None: + bridge.shell_out(f"echo {size_kb} > /sys/kernel/tracing/buffer_size_kb") + + +def open_website(bridge: AndroidDebugBridge, url: str) -> None: + bridge.shell_out(f"am start -a android.intent.action.VIEW -d \"{url}\" com.android.chrome") + + +def print_ftrace_events(path: str) -> None: + tp = TraceProcessor(trace=path) + + NS_TO_MS = 1e-6 + + dutation_query = """ + SELECT id, ts, dur, name + FROM slice + """ + + track_query = """ + SELECT id AS track_id, name + FROM counter_track + """ + + counter_query = """ + SELECT ts, value, track_id + FROM counter + """ + + print("Duration Events:") + duration_events = tp.query(dutation_query) + for row in duration_events: + ts = row.ts + name = row.name + duration = row.dur * NS_TO_MS + print(f"Duration Event ({ts}): {name}, Duration: {duration:.6f}ms") + + print() + + print("Counter Events:") + track_events = tp.query(track_query) + track_mapping = {row.track_id: row.name for row in track_events} + + counter_events = tp.query(counter_query) + + for row in counter_events: + ts = row.ts + track_id = row.track_id + value = row.value + counter_name = track_mapping.get(track_id, 'Unknown') + + print(f"Counter Event ({ts}): {counter_name}, Value: {value}") + + +def main() -> None: + devices = list(AndroidDebugBridge._devices()) + device: ADBDevice + if devices: + device = devices[0] + else: + exit("No device found") + bridge = AndroidDebugBridge.from_device(device) + + # enable and start tracing + enable_tracing(bridge) + set_trace_buffer_size(bridge, TRACE_BUFFER_SIZE_KB) + clear_trace_buffer(bridge) + start_tracing(bridge) + + # demo code to be traced (touch your device for more actions) + bridge.screen_tap(50, 50) + open_website(bridge, EXAMPLE_WEBSITE) + time.sleep(2) + bridge.push_button_home() + time.sleep(HOST_SLEEP_TIME) + + # end tracing and pull data to pc + stop_tracing(bridge) + disable_tracing(bridge) + dump_trace(bridge, DEVICE_DUMP_PATH) + bridge.pull(DEVICE_DUMP_PATH, HOST_DUMP_PATH) + + print_ftrace_events(HOST_DUMP_PATH) + + +if __name__ == "__main__": + main() diff --git a/tests/test_ftrace.py b/tests/test_ftrace.py new file mode 100644 index 0000000..1166c29 --- /dev/null +++ b/tests/test_ftrace.py @@ -0,0 +1,16 @@ +from benchkit.utils.ftrace import FTrace + + +def main() -> None: + path = "./tests/tmp/ftrace_dump" + trace = FTrace(path) + spans = trace.query_spans() + counts = trace.query_counts() + for span in spans: + print(span) + for count in counts: + print(count) + + +if __name__ == "__main__": + main()