1#!/usr/bin/env python3
2#
3# Copyright 2019, The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Runner of one test given a setting.
18
19Run app and gather the measurement in a certain configuration.
20Print the result to stdout.
21See --help for more details.
22
23Sample usage:
24  $> ./python run_app_with_prefetch.py  -p com.android.settings -a
25     com.android.settings.Settings -r fadvise -i input
26
27"""
28
29import argparse
30import os
31import sys
32import time
33from typing import List, Tuple, Optional
34
35# local imports
36import lib.adb_utils as adb_utils
37from lib.app_runner import AppRunner, AppRunnerListener
38
39# global variables
40DIR = os.path.abspath(os.path.dirname(__file__))
41
42sys.path.append(os.path.dirname(DIR))
43import lib.print_utils as print_utils
44import lib.cmd_utils as cmd_utils
45import iorap.lib.iorapd_utils as iorapd_utils
46
47class PrefetchAppRunner(AppRunnerListener):
48  def __init__(self,
49               package: str,
50               activity: Optional[str],
51               readahead: str,
52               compiler_filter: Optional[str],
53               timeout: Optional[int],
54               simulate: bool,
55               debug: bool,
56               input:Optional[str],
57               **kwargs):
58    self.app_runner = AppRunner(package,
59                                activity,
60                                compiler_filter,
61                                timeout,
62                                simulate)
63    self.app_runner.add_callbacks(self)
64
65    self.simulate = simulate
66    self.readahead = readahead
67    self.debug = debug
68    self.input = input
69    print_utils.DEBUG = self.debug
70    cmd_utils.SIMULATE = self.simulate
71
72
73  def run(self) -> Optional[List[Tuple[str]]]:
74    """Runs an app.
75
76    Returns:
77      A list of (metric, value) tuples.
78    """
79    return self.app_runner.run()
80
81  def preprocess(self):
82    passed = self.validate_options()
83    if not passed:
84      return
85
86    # Sets up adb environment.
87    adb_utils.root()
88    adb_utils.disable_selinux()
89    time.sleep(1)
90
91    # Kill any existing process of this app
92    adb_utils.pkill(self.app_runner.package)
93
94    if self.readahead != 'warm':
95      print_utils.debug_print('Drop caches for non-warm start.')
96      # Drop all caches to get cold starts.
97      adb_utils.vm_drop_cache()
98
99    if self.readahead != 'warm' and self.readahead != 'cold':
100      iorapd_utils.enable_iorapd_readahead()
101
102  def postprocess(self, pre_launch_timestamp: str):
103    passed = self._perform_post_launch_cleanup(pre_launch_timestamp)
104    if not passed and not self.app_runner.simulate:
105      print_utils.error_print('Cannot perform post launch cleanup!')
106      return None
107
108    # Kill any existing process of this app
109    adb_utils.pkill(self.app_runner.package)
110
111  def _perform_post_launch_cleanup(self, logcat_timestamp: str) -> bool:
112    """Performs cleanup at the end of each loop iteration.
113
114    Returns:
115      A bool indicates whether the cleanup succeeds or not.
116    """
117    if self.readahead != 'warm' and self.readahead != 'cold':
118      passed = iorapd_utils.wait_for_iorapd_finish(self.app_runner.package,
119                                                   self.app_runner.activity,
120                                                   self.app_runner.timeout,
121                                                   self.debug,
122                                                   logcat_timestamp)
123
124      if not passed:
125        return passed
126
127      return iorapd_utils.disable_iorapd_readahead()
128
129    # Don't need to do anything for warm or cold.
130    return True
131
132  def metrics_selector(self, am_start_output: str,
133                       pre_launch_timestamp: str) -> str:
134    """Parses the metric after app startup by reading from logcat in a blocking
135    manner until all metrics have been found".
136
137    Returns:
138      the total time and displayed time of app startup.
139      For example: "TotalTime=123\nDisplayedTime=121
140    """
141    total_time = AppRunner.parse_total_time(am_start_output)
142    displayed_time = adb_utils.blocking_wait_for_logcat_displayed_time(
143        pre_launch_timestamp, self.app_runner.package, self.app_runner.timeout)
144
145    return 'TotalTime={}\nDisplayedTime={}'.format(total_time, displayed_time)
146
147  def validate_options(self) -> bool:
148    """Validates the activity and trace file if needed.
149
150    Returns:
151      A bool indicates whether the activity is valid.
152    """
153    needs_trace_file = self.readahead != 'cold' and self.readahead != 'warm'
154    if needs_trace_file and (self.input is None or
155                             not os.path.exists(self.input)):
156      print_utils.error_print('--input not specified!')
157      return False
158
159    # Install necessary trace file. This must be after the activity checking.
160    if needs_trace_file:
161      passed = iorapd_utils.iorapd_compiler_install_trace_file(
162          self.app_runner.package, self.app_runner.activity, self.input)
163      if not cmd_utils.SIMULATE and not passed:
164        print_utils.error_print('Failed to install compiled TraceFile.pb for '
165                                '"{}/{}"'.
166                                    format(self.app_runner.package,
167                                           self.app_runner.activity))
168        return False
169
170    return True
171
172
173
174def parse_options(argv: List[str] = None):
175  """Parses command line arguments and return an argparse Namespace object."""
176  parser = argparse.ArgumentParser(
177      description='Run an Android application once and measure startup time.'
178  )
179
180  required_named = parser.add_argument_group('required named arguments')
181  required_named.add_argument('-p', '--package', action='store', dest='package',
182                              help='package of the application', required=True)
183
184  # optional arguments
185  # use a group here to get the required arguments to appear 'above' the
186  # optional arguments in help.
187  optional_named = parser.add_argument_group('optional named arguments')
188  optional_named.add_argument('-a', '--activity', action='store',
189                              dest='activity',
190                              help='launch activity of the application')
191  optional_named.add_argument('-s', '--simulate', dest='simulate',
192                              action='store_true',
193                              help='simulate the process without executing '
194                                   'any shell commands')
195  optional_named.add_argument('-d', '--debug', dest='debug',
196                              action='store_true',
197                              help='Add extra debugging output')
198  optional_named.add_argument('-i', '--input', action='store', dest='input',
199                              help='perfetto trace file protobuf',
200                              default='TraceFile.pb')
201  optional_named.add_argument('-r', '--readahead', action='store',
202                              dest='readahead',
203                              help='which readahead mode to use',
204                              default='cold',
205                              choices=('warm', 'cold', 'mlock', 'fadvise'))
206  optional_named.add_argument('-t', '--timeout', dest='timeout', action='store',
207                              type=int,
208                              help='Timeout after this many seconds when '
209                                   'executing a single run.',
210                              default=10)
211  optional_named.add_argument('--compiler-filter', dest='compiler_filter',
212                              action='store',
213                              help='Which compiler filter to use.',
214                              default=None)
215
216  return parser.parse_args(argv)
217
218def main():
219  opts = parse_options()
220  runner = PrefetchAppRunner(**vars(opts))
221  result = runner.run()
222
223  if result is None:
224    return 1
225
226  print(result)
227  return 0
228
229if __name__ == '__main__':
230  sys.exit(main())
231