summaryrefslogtreecommitdiff
path: root/startop/scripts/app_startup/lib/app_runner.py
blob: 78873fa51ab5ca4e57baf06de22706d8a9b90945 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# 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