benchmarking/platforms/android/android_platform.py (313 lines of code) (raw):

#!/usr/bin/env python ############################################################################## # Copyright 2017-present, Facebook, Inc. # All rights reserved. # # This source code is licensed under the license found in the # LICENSE file in the root directory of this source tree. ############################################################################## import json import os import re import shlex import shutil import time from degrade.degrade_base import DegradeBase, getDegrade from platforms.platform_base import PlatformBase from profilers.perfetto.perfetto import Perfetto from profilers.profilers import getProfilerByUsage from six import string_types from utils.custom_logger import getLogger from utils.utilities import getRunStatus, setRunStatus class AndroidPlatform(PlatformBase): def __init__(self, tempdir, adb, args, usb_controller=None): super(AndroidPlatform, self).__init__( tempdir, args.android_dir, adb, args.hash_platform_mapping, args.device_name_mapping, ) self.args = args self.rel_version = adb.getprop("ro.build.version.release") self.build_version = adb.getprop("ro.build.version.sdk") platform = ( adb.getprop("ro.product.model") + "-" + self.rel_version + "-" + self.build_version ) self.platform_abi = adb.getprop("ro.product.cpu.abi") self.os_version = "{}-{}".format(self.rel_version, self.build_version) self.type = "android" self.setPlatform(platform) self.setPlatformHash(adb.device) self.usb_controller = usb_controller self._setLogCatSize() self.app = None self.degrade_constraints = None self.degrade: DegradeBase = getDegrade(self.type) if self.args.set_freq: self.util.setFrequency(self.args.set_freq) def getKind(self): if self.platform_model and self.platform_os_version: return "{}-{}".format(self.platform_model, self.platform_os_version) return self.platform def getOS(self): return "Android {} sdk {}".format(self.rel_version, self.build_version) def _setLogCatSize(self): repeat = True size = 131072 while repeat and size > 256: repeat = False # We know this command may fail. Avoid propogating this # failure to the upstream success = getRunStatus() ret = self.util.logcat("-G", str(size) + "K") setRunStatus(success, overwrite=True) if len(ret) > 0 and ret[0].find("failed to") >= 0: repeat = True size = int(size / 2) def fileExistsOnPlatform(self, files): if isinstance(files, string_types): exists = self.util.shell( "test -e {} && echo True || echo False".format(files).split(" ") ) if "True" not in exists: return False return True elif isinstance(files, list): for f in files: if not self.fileExistsOnPlatform(f): return False return True raise TypeError( "fileExistsOnPlatform takes either a string or list of strings." ) def preprocess(self, *args, **kwargs): assert "programs" in kwargs, "Must have programs specified" programs = kwargs["programs"] benchmark = kwargs["benchmark"] # find the first zipped app file assert "program" in programs, "program is not specified" if "platform" in benchmark["model"] and benchmark["model"][ "platform" ].startswith("android"): if "app" in benchmark["model"]: self.app = benchmark["model"]["app"] self.degrade_constraints = benchmark["tests"][0].get("degrade") if not self.app: if "intent.txt" in programs: # temporary to rename the program with adb suffix with open(programs["intent.txt"], "r") as f: self.app = json.load(f) else: return # Uninstall if exist package = self.util.shell(["pm", "list", "packages", self.app["package"]]) if len(package) > 0 and package[0].strip() == "package:" + self.app["package"]: self.util.shell(["pm", "uninstall", self.app["package"]]) # temporary fix to allow install apk files if not programs["program"].endswith(".apk"): new_name = programs["program"] + ".apk" shutil.copyfile(programs["program"], new_name) programs["program"] = new_name self.util.run(["install", programs["program"]]) del programs["program"] def root(self): return self.util.root() def unroot(self): return self.util.unroot() def user_is_root(self): return self.util.user_is_root() def rebootDevice(self): self.util.reboot() self.waitForDevice(180) # Need to wait a bit more after the device is rebooted time.sleep(20) # may need to set log size again after reboot self._setLogCatSize() if self.args.set_freq: self.util.setFrequency(self.args.set_freq) def runBenchmark(self, cmd, *args, **kwargs): if not isinstance(cmd, list): cmd = shlex.split(cmd) # meta is used to store any data about the benchmark run # that is not the output of the command meta = {} with self.degrade(self.util, self.degrade_constraints): # We know this command may fail. Avoid propogating this # failure to the upstream success = getRunStatus() self.util.logcat("-b", "all", "-c") setRunStatus(success, overwrite=True) if self.app: log, meta = self.runAppBenchmark(cmd, *args, **kwargs) else: log, meta = self.runBinaryBenchmark(cmd, *args, **kwargs) return log, meta def runAppBenchmark(self, cmd, *args, **kwargs): arguments = self.getPairedArguments(cmd) argument_filename = os.path.join(self.tempdir, "benchmark.json") arguments_json = json.dumps(arguments, indent=2, sort_keys=True) with open(argument_filename, "w") as f: f.write(arguments_json) tgt_argument_filename = os.path.join(self.tgt_dir, "benchmark.json") activity = os.path.join(self.app["package"], self.app["activity"]) self.util.push(argument_filename, tgt_argument_filename) platform_args = {} if "platform_args" in kwargs: platform_args = kwargs["platform_args"] if "power" in platform_args and platform_args["power"]: platform_args["non_blocking"] = True self.util.shell(["am", "start", "-S", activity]) return [] if platform_args.get("enable_profiling", False): getLogger().warn("Profiling for app benchmarks is not implemented.") patterns = [] pattern = re.compile( r".*{}.*{}.*BENCHMARK_DONE".format( self.app["package"], self.app["activity"] ) ) patterns.append(pattern) pattern = re.compile( r".*ActivityManager: Killing .*{}".format(self.app["package"]) ) patterns.append(pattern) platform_args["patterns"] = patterns self.util.shell(["am", "start", "-S", "-W", activity]) log_logcat = self.util.run(["logcat"], **platform_args) self.util.shell(["am", "force-stop", self.app["package"]]) return log_logcat def runBinaryBenchmark(self, cmd, *args, **kwargs): log_to_screen_only = ( "log_to_screen_only" in kwargs and kwargs["log_to_screen_only"] ) platform_args = {} if "platform_args" in kwargs: platform_args = kwargs["platform_args"] if "taskset" in platform_args: taskset = platform_args["taskset"] cmd = ["taskset", taskset] + cmd del platform_args["taskset"] if "sleep_before_run" in platform_args: sleep_before_run = str(platform_args["sleep_before_run"]) cmd = ["sleep", sleep_before_run, "&&"] + cmd del platform_args["sleep_before_run"] if "power" in platform_args and platform_args["power"]: # launch settings page to prevent the phone # to go into sleep mode self.util.shell(["am", "start", "-a", "android.settings.SETTINGS"]) time.sleep(1) cmd = ( ["nohup"] + ["sh", "-c", "'" + " ".join(cmd) + "'"] + [">", "/dev/null", "2>&1"] ) platform_args["non_blocking"] = True del platform_args["power"] enable_profiling = platform_args.get("profiling_args", {}).get( "enabled", False ) if enable_profiling: profiler = platform_args["profiling_args"]["profiler"] profiling_types = platform_args["profiling_args"]["types"] if profiler == "simpleperf": assert profiling_types == [ "cpu" ], "Only cpu profiling is supported for SimplePerf" try: # attempt to run with cpu profiling, else fallback to standard run return self._runBenchmarkWithSimpleperf( cmd, log_to_screen_only, **platform_args ) except Exception: # if this has not succeeded for some reason reset run status and run without profiling. getLogger().critical( f"An error has occurred when running Simpleperf profiler on device {self.platform} {self.platform_hash}.", exc_info=True, ) elif profiler == "perfetto": assert ( "cpu" not in profiling_types ), "cpu profiling is not yet implemented for Perfetto" try: # attempt Perfetto profiling return self._runBenchmarkWithPerfetto( cmd, log_to_screen_only, **platform_args ) except Exception: # if this has not succeeded for some reason reset run status and run without profiling. getLogger().critical( f"An error has occurred when running Perfetto profiler on device {self.platform} {self.platform_hash}.", exc_info=True, ) else: getLogger().error( f"Ignoring unsupported profiler setting: {profiler}: {profiling_types}.", ) # Run without profiling return self._runBinaryBenchmark(cmd, log_to_screen_only, **platform_args) def _runBinaryBenchmark(self, cmd, log_to_screen_only: bool, **platform_args): log_screen = self.util.shell(cmd, **platform_args) log_logcat = [] if not log_to_screen_only: log_logcat = self.util.logcat("-d") return log_screen + log_logcat, {} def _runBenchmarkWithSimpleperf( self, cmd, log_to_screen_only: bool, **platform_args ): simpleperf = getProfilerByUsage( "android", None, platform=self, model_name=platform_args.get("model_name", None), cmd=cmd, ) if simpleperf: f = simpleperf.start() output, meta = f.result() if not output or not meta: raise RuntimeError("No data returned from Simpleperf profiler.") log_logcat = [] if not log_to_screen_only: log_logcat = self.util.logcat("-d") return output + log_logcat, meta def _runBenchmarkWithPerfetto(self, cmd, log_to_screen_only: bool, **platform_args): # attempt Perfetto profiling if not self.util.isRootedDevice(silent=True): raise RuntimeError( "Attempted to perform Perfetto profiling on unrooted device {self.util.device}." ) with Perfetto( platform=self, types=platform_args["profiling_args"]["types"], options=platform_args["profiling_args"]["options"], ) as perfetto: getLogger().info("Invoked with Perfetto.") log_screen = self.util.shell(cmd, **platform_args) log_logcat = [] if not log_to_screen_only: log_logcat = self.util.logcat("-d") meta = perfetto.getResults() return log_screen + log_logcat, meta def collectMetaData(self, info): meta = super(AndroidPlatform, self).collectMetaData(info) meta["platform_hash"] = self.platform_hash return meta def killProgram(self, program): basename = os.path.basename(program) # if the program doesn't exist, the grep may fail # do not update status code success = getRunStatus() res = self.util.shell(["ps", "|", "grep", basename]) setRunStatus(success, overwrite=True) if len(res) == 0: return results = res[0].split("\n") pattern = re.compile(r"^shell\s+(\d+)\s+") for result in results: match = pattern.match(result) if match: pid = match.group(1) self.util.shell(["kill", pid]) def waitForDevice(self, timeout): period = int(timeout / 20) + 1 num = int(timeout / period) count = 0 ls = [] while len(ls) == 0 and count < num: ls = self.util.shell(["ls", self.tgt_dir]) time.sleep(period) if len(ls) == 0: getLogger().critical( f"Cannot reach device {self.platform} {self.platform_hash} after {timeout}s." ) def currentPower(self): try: result = self.util.shell(["dumpsys", "battery"], retry=10, retry_sleep=2) for line in result: if "Charge counter" in line: result_line = line return int(result_line.split(": ")[-1]) except Exception: getLogger().critical( f"Could not read battery level for device {self.platform} {self.platform_hash}", exc_info=True, ) return -1 @property def powerInfo(self): return {"unit": "mAh", "metric": "batteryLevel"}