blob: 78873fa51ab5ca4e57baf06de22706d8a9b90945 [file] [log] [blame]
# Copyright 2019, 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.
"""Class to run an app."""
import os
import sys
from typing import Optional, List, Tuple
# local import
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__)))))
import app_startup.lib.adb_utils as adb_utils
import lib.cmd_utils as cmd_utils
import lib.print_utils as print_utils
class AppRunnerListener(object):
"""Interface for lisenter of AppRunner. """
def preprocess(self) -> None:
"""Preprocess callback to initialized before the app is running. """
pass
def postprocess(self, pre_launch_timestamp: str) -> None:
"""Postprocess callback to cleanup after the app is running.
param:
'pre_launch_timestamp': indicates the timestamp when the app is
launching.. """
pass
def metrics_selector(self, am_start_output: str,
pre_launch_timestamp: str) -> None:
"""A metrics selection callback that waits for the desired metrics to
show up in logcat.
params:
'am_start_output': indicates the output of app startup.
'pre_launch_timestamp': indicates the timestamp when the app is
launching.
returns:
a string in the format of "<metric>=<value>\n<metric>=<value>\n..."
for further parsing. For example "TotalTime=123\nDisplayedTime=121".
Return an empty string if no metrics need to be parsed further.
"""
pass
class AppRunner(object):
""" Class to run an app. """
# static variables
DIR = os.path.abspath(os.path.dirname(__file__))
APP_STARTUP_DIR = os.path.dirname(DIR)
IORAP_COMMON_BASH_SCRIPT = os.path.realpath(os.path.join(DIR,
'../../iorap/common'))
DEFAULT_TIMEOUT = 30 # seconds
def __init__(self,
package: str,
activity: Optional[str],
compiler_filter: Optional[str],
timeout: Optional[int],
simulate: bool):
self.package = package
self.simulate = simulate
# If the argument activity is None, try to set it.
self.activity = activity
if self.simulate:
self.activity = 'act'
if self.activity is None:
self.activity = AppRunner.get_activity(self.package)
self.compiler_filter = compiler_filter
self.timeout = timeout if timeout else AppRunner.DEFAULT_TIMEOUT
self.listeners = []
def add_callbacks(self, listener: AppRunnerListener):
self.listeners.append(listener)
def remove_callbacks(self, listener: AppRunnerListener):
self.listeners.remove(listener)
@staticmethod
def get_activity(package: str) -> str:
""" Tries to set the activity based on the package. """
passed, activity = cmd_utils.run_shell_func(
AppRunner.IORAP_COMMON_BASH_SCRIPT,
'get_activity_name',
[package])
if not passed or not activity:
raise ValueError(
'Activity name could not be found, invalid package name?!')
return activity
def configure_compiler_filter(self) -> bool:
"""Configures compiler filter (e.g. speed).
Returns:
A bool indicates whether configure of compiler filer succeeds or not.
"""
if not self.compiler_filter:
print_utils.debug_print('No --compiler-filter specified, don\'t'
' need to force it.')
return True
passed, current_compiler_filter_info = \
cmd_utils.run_shell_command(
'{} --package {}'.format(os.path.join(AppRunner.APP_STARTUP_DIR,
'query_compiler_filter.py'),
self.package))
if passed != 0:
return passed
# TODO: call query_compiler_filter directly as a python function instead of
# these shell calls.
current_compiler_filter, current_reason, current_isa = \
current_compiler_filter_info.split(' ')
print_utils.debug_print('Compiler Filter={} Reason={} Isa={}'.format(
current_compiler_filter, current_reason, current_isa))
# Don't trust reasons that aren't 'unknown' because that means
# we didn't manually force the compilation filter.
# (e.g. if any automatic system-triggered compilations are not unknown).
if current_reason != 'unknown' or \
current_compiler_filter != self.compiler_filter:
passed, _ = adb_utils.run_shell_command('{}/force_compiler_filter '
'--compiler-filter "{}" '
'--package "{}"'
' --activity "{}'.
format(AppRunner.APP_STARTUP_DIR,
self.compiler_filter,
self.package,
self.activity))
else:
adb_utils.debug_print('Queried compiler-filter matched requested '
'compiler-filter, skip forcing.')
passed = False
return passed
def run(self) -> Optional[List[Tuple[str]]]:
"""Runs an app.
Returns:
A list of (metric, value) tuples.
"""
print_utils.debug_print('==========================================')
print_utils.debug_print('===== START =====')
print_utils.debug_print('==========================================')
# Run the preprocess.
for listener in self.listeners:
listener.preprocess()
# Ensure the APK is currently compiled with whatever we passed in
# via --compiler-filter.
# No-op if this option was not passed in.
if not self.configure_compiler_filter():
print_utils.error_print('Compiler filter configuration failed!')
return None
pre_launch_timestamp = adb_utils.logcat_save_timestamp()
# Launch the app.
results = self.launch_app(pre_launch_timestamp)
# Run the postprocess.
for listener in self.listeners:
listener.postprocess(pre_launch_timestamp)
return results
def launch_app(self, pre_launch_timestamp: str) -> Optional[List[Tuple[str]]]:
""" Launches the app.
Returns:
A list of (metric, value) tuples.
"""
print_utils.debug_print('Running with timeout {}'.format(self.timeout))
passed, am_start_output = cmd_utils.run_shell_command('timeout {timeout} '
'"{DIR}/launch_application" '
'"{package}" '
'"{activity}"'.
format(timeout=self.timeout,
DIR=AppRunner.APP_STARTUP_DIR,
package=self.package,
activity=self.activity))
if not passed and not self.simulate:
return None
return self.wait_for_app_finish(pre_launch_timestamp, am_start_output)
def wait_for_app_finish(self,
pre_launch_timestamp: str,
am_start_output: str) -> Optional[List[Tuple[str]]]:
""" Wait for app finish and all metrics are shown in logcat.
Returns:
A list of (metric, value) tuples.
"""
if self.simulate:
return [('TotalTime', '123')]
ret = []
for listener in self.listeners:
output = listener.metrics_selector(am_start_output,
pre_launch_timestamp)
ret = ret + AppRunner.parse_metrics_output(output)
return ret
@staticmethod
def parse_metrics_output(input: str) -> List[
Tuple[str, str, str]]:
"""Parses output of app startup to metrics and corresponding values.
It converts 'a=b\nc=d\ne=f\n...' into '[(a,b,''),(c,d,''),(e,f,'')]'
Returns:
A list of tuples that including metric name, metric value and rest info.
"""
all_metrics = []
for line in input.split('\n'):
if not line:
continue
splits = line.split('=')
if len(splits) < 2:
print_utils.error_print('Bad line "{}"'.format(line))
continue
metric_name = splits[0]
metric_value = splits[1]
rest = splits[2] if len(splits) > 2 else ''
if rest:
print_utils.error_print('Corrupt line "{}"'.format(line))
print_utils.debug_print('metric: "{metric_name}", '
'value: "{metric_value}" '.
format(metric_name=metric_name,
metric_value=metric_value))
all_metrics.append((metric_name, metric_value))
return all_metrics
@staticmethod
def parse_total_time( am_start_output: str) -> Optional[str]:
"""Parses the total time from 'adb shell am start pkg' output.
Returns:
the total time of app startup.
"""
for line in am_start_output.split('\n'):
if 'TotalTime:' in line:
return line[len('TotalTime:'):].strip()
return None