blob: c22aff675b743349f1235e5cf36d54a3886d63e6 [file] [edit]
#!/usr/bin/env python3
#
# Copyright (C) 2025 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.
#
from __future__ import annotations
import time
import sqlite3
from typing import Optional, TYPE_CHECKING
from adb import Adb, AdbError
from runnables.apply_device_configs import runnable_apply_device_configs
from runnables.prompt_device_serial import runnable_prompt_device_serial
from runnables.run_system_probe import runnable_run_system_probe
from runnables.run_workload import runnable_run_workload
from utils_terminal import TerminalPrintColors
if TYPE_CHECKING:
from run_config import RunConfig
class AndroidDevice:
"""A connected android device and its interactions."""
def __init__(
self,
database_connection: sqlite3.Connection,
device_name: str = "",
serial: str = "",
) -> None:
"""Initializes the AndroidDevice object."""
self.device_name = device_name
self._serial = ""
self._environ_prepared = False
self.page_size: int = 0
self.build_fingerprint = ""
self._adb: Optional[Adb] = None
self.serial = serial
self.database_connection = database_connection
self._record_device_details_in_sqlite_db()
self.post_boot_logcat_log: Optional[str] = None
@property
def serial(self) -> str:
"""The serial of the android device."""
return self._serial
@serial.setter
def serial(self, serial: str):
self._serial = serial
if self._serial:
self._adb = Adb(self._serial)
self._set_page_size()
self._set_build_fingerprint()
def _set_page_size(self) -> None:
if not self._adb:
return
try:
result = self._adb.adb_command(["shell", "getconf", "PAGESIZE"])
self.page_size = int(result.stdout)
except (AdbError, ValueError):
self.page_size = 0
def _set_build_fingerprint(self) -> None:
if not self._adb:
return
try:
result = self._adb.adb_command(
["shell", "getprop", "ro.build.fingerprint"])
self.build_fingerprint = result.stdout.strip()
except AdbError:
self.build_fingerprint = ""
def _record_device_details_in_sqlite_db(self):
cursor = self.database_connection.cursor()
cursor.execute(
"""\
CREATE TABLE IF NOT EXISTS device_details (
id INTEGER PRIMARY KEY,
device_name TEXT NOT NULL,
serial TEXT,
page_size INTEGER,
build_fingerprint TEXT
)""")
cursor.execute(
"""\
INSERT INTO device_details (
device_name,
serial,
page_size,
build_fingerprint
) VALUES (?, ?, ?, ?)
""",
(
self.device_name,
self.serial,
self.page_size,
self.build_fingerprint,
),
)
self.database_connection.commit()
def __str__(self) -> str:
return (
"AndroidDevice(\n"
f"\tdevice_name={self.device_name},\n"
f"\tserial='{self.serial}',\n"
f"\tpage_size={self.page_size}, \n"
f"\tbuild_fingerprint={self.build_fingerprint}, \n"
f")"
)
def reboot(self) -> None:
"""Reboots the device."""
if not self._adb:
raise AdbError("Cannot reboot device, ADB not initialized.")
print("Rebooting device and waiting 5 minutes...")
self._adb.adb_command(["reboot"], check=False)
self._adb.adb_command(["wait-for-device"], check=False)
def record_logcat_logs(self) -> None:
"""Records logcat logs."""
self._adb.root()
self.post_boot_logcat_log = self._adb.adb_command(
["shell", "logcat", "-b", "kernel", "-d"], check=False,
).stdout
def fetch_and_store_memory_data(self, run_config: RunConfig) -> None:
"""Runs specified workloads, system probes to generate results."""
from runnables.runnable_data import RunnableData # pylint: disable=import-outside-toplevel
runnable_data = RunnableData()
runnable_data.android_device = self
runnable_data.run_config = run_config
runnable_prompt_device_serial.process(runnable_data)
runnable_apply_device_configs.process(runnable_data)
# Circular reference errors caused by pylint's static analyzer.
# pylint: disable=no-member
print(
TerminalPrintColors.BOLD,
TerminalPrintColors.MAGENTA,
f"\n\n├────────── Starting Analysis for device "
f"{runnable_data.android_device_not_null.device_name} "
f"({runnable_data.android_device_not_null.serial}) "
f"(page size: {runnable_data.android_device_not_null.page_size:,}) "
"─────────┤",
TerminalPrintColors.RESET,
)
# pylint: enable=no-member
for reboot_count in range(
1,
runnable_data.run_config_not_null.num_reboots + 1
):
self.reboot()
self.record_logcat_logs()
time.sleep(300)
runnable_data.reboot_count = reboot_count
# After reboot, adb needs to be set to root again.
if not self._adb:
self._adb = Adb(self.serial)
while True:
try:
print("Running adb as root after reboot...")
self._adb.root()
break
except AdbError as e:
print(f"Failed to set root after reboot: {e}")
# pylint: disable=bad-builtin
input(
"Please check device connection and "
"press Enter to retry..."
)
print(
f" ├─ Reboot run #{reboot_count} of "
f"{runnable_data.run_config_not_null.num_reboots}"
)
for workload in runnable_data.run_config_not_null.workloads:
for probe_sample_count in range(
1,
runnable_data.run_config_not_null.runs_per_workload +1
):
runnable_data.workload = workload
runnable_data.probe_sample_count = probe_sample_count
system_probes_print_string = ",".join(
[
p.name for p in
runnable_data.run_config_not_null.system_probes
]
)
print(
f" └── [{system_probes_print_string}] + ",
f"{runnable_data.workload_not_null.name}",
)
# Run workload.
runnable_run_workload.process(runnable_data)
# Run system probes.
runnable_run_system_probe.process(runnable_data)
# Post process (typically just close the app).
runnable_run_workload.post_process(runnable_data)
# end for _
# end for workload