1# Copyright 2019, The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Class to run an app."""
16import os
17import sys
18from typing import Optional, List, Tuple
19
20# local import
21sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(
22    os.path.abspath(__file__)))))
23
24import app_startup.lib.adb_utils as adb_utils
25import lib.cmd_utils as cmd_utils
26import lib.print_utils as print_utils
27
28class AppRunnerListener(object):
29  """Interface for lisenter of AppRunner. """
30
31  def preprocess(self) -> None:
32    """Preprocess callback to initialized before the app is running. """
33    pass
34
35  def postprocess(self, pre_launch_timestamp: str) -> None:
36    """Postprocess callback to cleanup after the app is running.
37
38      param:
39        'pre_launch_timestamp': indicates the timestamp when the app is
40        launching.. """
41    pass
42
43  def metrics_selector(self, am_start_output: str,
44                       pre_launch_timestamp: str) -> None:
45    """A metrics selection callback that waits for the desired metrics to
46      show up in logcat.
47      params:
48        'am_start_output': indicates the output of app startup.
49        'pre_launch_timestamp': indicates the timestamp when the app is
50                        launching.
51      returns:
52        a string in the format of "<metric>=<value>\n<metric>=<value>\n..."
53        for further parsing. For example "TotalTime=123\nDisplayedTime=121".
54        Return an empty string if no metrics need to be parsed further.
55        """
56    pass
57
58class AppRunner(object):
59  """ Class to run an app. """
60  # static variables
61  DIR = os.path.abspath(os.path.dirname(__file__))
62  APP_STARTUP_DIR = os.path.dirname(DIR)
63  IORAP_COMMON_BASH_SCRIPT = os.path.realpath(os.path.join(DIR,
64                                                           '../../iorap/common'))
65  DEFAULT_TIMEOUT = 30 # seconds
66
67  def __init__(self,
68               package: str,
69               activity: Optional[str],
70               compiler_filter: Optional[str],
71               timeout: Optional[int],
72               simulate: bool):
73    self.package = package
74    self.simulate = simulate
75
76    # If the argument activity is None, try to set it.
77    self.activity = activity
78    if self.simulate:
79      self.activity = 'act'
80    if self.activity is None:
81      self.activity = AppRunner.get_activity(self.package)
82
83    self.compiler_filter = compiler_filter
84    self.timeout = timeout if timeout else AppRunner.DEFAULT_TIMEOUT
85
86    self.listeners = []
87
88  def add_callbacks(self, listener: AppRunnerListener):
89    self.listeners.append(listener)
90
91  def remove_callbacks(self, listener: AppRunnerListener):
92    self.listeners.remove(listener)
93
94  @staticmethod
95  def get_activity(package: str) -> str:
96    """ Tries to set the activity based on the package. """
97    passed, activity = cmd_utils.run_shell_func(
98        AppRunner.IORAP_COMMON_BASH_SCRIPT,
99        'get_activity_name',
100        [package])
101
102    if not passed or not activity:
103      raise ValueError(
104          'Activity name could not be found, invalid package name?!')
105
106    return activity
107
108  def configure_compiler_filter(self) -> bool:
109    """Configures compiler filter (e.g. speed).
110
111    Returns:
112      A bool indicates whether configure of compiler filer succeeds or not.
113    """
114    if not self.compiler_filter:
115      print_utils.debug_print('No --compiler-filter specified, don\'t'
116                              ' need to force it.')
117      return True
118
119    passed, current_compiler_filter_info = \
120      cmd_utils.run_shell_command(
121          '{} --package {}'.format(os.path.join(AppRunner.APP_STARTUP_DIR,
122                                                'query_compiler_filter.py'),
123                                   self.package))
124
125    if passed != 0:
126      return passed
127
128    # TODO: call query_compiler_filter directly as a python function instead of
129    #  these shell calls.
130    current_compiler_filter, current_reason, current_isa = \
131      current_compiler_filter_info.split(' ')
132    print_utils.debug_print('Compiler Filter={} Reason={} Isa={}'.format(
133        current_compiler_filter, current_reason, current_isa))
134
135    # Don't trust reasons that aren't 'unknown' because that means
136    #  we didn't manually force the compilation filter.
137    # (e.g. if any automatic system-triggered compilations are not unknown).
138    if current_reason != 'unknown' or \
139        current_compiler_filter != self.compiler_filter:
140      passed, _ = adb_utils.run_shell_command('{}/force_compiler_filter '
141                                              '--compiler-filter "{}" '
142                                              '--package "{}"'
143                                              ' --activity "{}'.
144                                                format(AppRunner.APP_STARTUP_DIR,
145                                                       self.compiler_filter,
146                                                       self.package,
147                                                       self.activity))
148    else:
149      adb_utils.debug_print('Queried compiler-filter matched requested '
150                            'compiler-filter, skip forcing.')
151      passed = False
152    return passed
153
154  def run(self) -> Optional[List[Tuple[str]]]:
155    """Runs an app.
156
157    Returns:
158      A list of (metric, value) tuples.
159    """
160    print_utils.debug_print('==========================================')
161    print_utils.debug_print('=====             START              =====')
162    print_utils.debug_print('==========================================')
163    # Run the preprocess.
164    for listener in self.listeners:
165      listener.preprocess()
166
167    # Ensure the APK is currently compiled with whatever we passed in
168    # via --compiler-filter.
169    # No-op if this option was not passed in.
170    if not self.configure_compiler_filter():
171      print_utils.error_print('Compiler filter configuration failed!')
172      return None
173
174    pre_launch_timestamp = adb_utils.logcat_save_timestamp()
175    # Launch the app.
176    results = self.launch_app(pre_launch_timestamp)
177
178    # Run the postprocess.
179    for listener in self.listeners:
180      listener.postprocess(pre_launch_timestamp)
181
182    return results
183
184  def launch_app(self, pre_launch_timestamp: str) -> Optional[List[Tuple[str]]]:
185    """ Launches the app.
186
187        Returns:
188          A list of (metric, value) tuples.
189    """
190    print_utils.debug_print('Running with timeout {}'.format(self.timeout))
191
192    passed, am_start_output = cmd_utils.run_shell_command('timeout {timeout} '
193                                                 '"{DIR}/launch_application" '
194                                                 '"{package}" '
195                                                 '"{activity}"'.
196                                                   format(timeout=self.timeout,
197                                                          DIR=AppRunner.APP_STARTUP_DIR,
198                                                          package=self.package,
199                                                          activity=self.activity))
200    if not passed and not self.simulate:
201      return None
202
203    return self.wait_for_app_finish(pre_launch_timestamp, am_start_output)
204
205  def wait_for_app_finish(self,
206                          pre_launch_timestamp: str,
207                          am_start_output:  str) -> Optional[List[Tuple[str]]]:
208    """ Wait for app finish and all metrics are shown in logcat.
209
210    Returns:
211      A list of (metric, value) tuples.
212    """
213    if self.simulate:
214      return [('TotalTime', '123')]
215
216    ret = []
217    for listener in self.listeners:
218      output = listener.metrics_selector(am_start_output,
219                                         pre_launch_timestamp)
220      ret = ret + AppRunner.parse_metrics_output(output)
221
222    return ret
223
224  @staticmethod
225  def parse_metrics_output(input: str) -> List[
226    Tuple[str, str, str]]:
227    """Parses output of app startup to metrics and corresponding values.
228
229    It converts 'a=b\nc=d\ne=f\n...' into '[(a,b,''),(c,d,''),(e,f,'')]'
230
231    Returns:
232      A list of tuples that including metric name, metric value and rest info.
233    """
234    all_metrics = []
235    for line in input.split('\n'):
236      if not line:
237        continue
238      splits = line.split('=')
239      if len(splits) < 2:
240        print_utils.error_print('Bad line "{}"'.format(line))
241        continue
242      metric_name = splits[0]
243      metric_value = splits[1]
244      rest = splits[2] if len(splits) > 2 else ''
245      if rest:
246        print_utils.error_print('Corrupt line "{}"'.format(line))
247      print_utils.debug_print('metric: "{metric_name}", '
248                              'value: "{metric_value}" '.
249                                format(metric_name=metric_name,
250                                     metric_value=metric_value))
251
252      all_metrics.append((metric_name, metric_value))
253    return all_metrics
254
255  @staticmethod
256  def parse_total_time( am_start_output: str) -> Optional[str]:
257    """Parses the total time from 'adb shell am start pkg' output.
258
259    Returns:
260      the total time of app startup.
261    """
262    for line in am_start_output.split('\n'):
263      if 'TotalTime:' in line:
264        return line[len('TotalTime:'):].strip()
265    return None
266
267