Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 06f54882 authored by Yan Wang's avatar Yan Wang
Browse files

startop: Refactor app running.

Collecting and app running share lots of common code.
Create a new class to run app and allow callbacks to specify
preprocess and postprocess and metrics selection.

Test: pytest run_app_with_prefetch_test.py
Test: pytest app_runner_test.py
Bug: 138233615
Change-Id: I972c82fb9ff3a0f6cc7661bc3dc47b342716c26c
parent 89d954c3
Loading
Loading
Loading
Loading
+15 −6
Original line number Diff line number Diff line
@@ -33,12 +33,12 @@ import os
import sys
import tempfile
from typing import Any, Callable, Iterable, List, NamedTuple, TextIO, Tuple, \
    TypeVar, Union
    TypeVar, Union, Optional

# local import
DIR = os.path.abspath(os.path.dirname(__file__))
sys.path.append(os.path.dirname(DIR))
import app_startup.run_app_with_prefetch as run_app_with_prefetch
from app_startup.run_app_with_prefetch import PrefetchAppRunner
import app_startup.lib.args_utils as args_utils
from app_startup.lib.data_frame import DataFrame
import lib.cmd_utils as cmd_utils
@@ -62,6 +62,16 @@ _COLLECTOR_TIMEOUT_MULTIPLIER = 10 # take the regular --timeout and multiply
_UNLOCK_SCREEN_SCRIPT = os.path.join(
    os.path.dirname(os.path.realpath(__file__)), 'unlock_screen')

RunCommandArgs = NamedTuple('RunCommandArgs',
                            [('package', str),
                             ('readahead', str),
                             ('activity', Optional[str]),
                             ('compiler_filter', Optional[str]),
                             ('timeout', Optional[int]),
                             ('debug', bool),
                             ('simulate', bool),
                             ('input', Optional[str])])

# This must be the only mutable global variable. All other global variables are constants to avoid magic literals.
_debug = False  # See -d/--debug flag.
_DEBUG_FORCE = None  # Ignore -d/--debug if this is not none.
@@ -207,8 +217,7 @@ def parse_run_script_csv_file(csv_file: TextIO) -> DataFrame:
  return DataFrame(d)

def execute_run_combos(
    grouped_run_combos: Iterable[Tuple[CollectorPackageInfo, Iterable[
      run_app_with_prefetch.RunCommandArgs]]],
    grouped_run_combos: Iterable[Tuple[CollectorPackageInfo, Iterable[RunCommandArgs]]],
    simulate: bool,
    inodes_path: str,
    timeout: int):
@@ -229,7 +238,7 @@ def execute_run_combos(
        combos = combos._replace(input=collector_tmp_output_file.name)

      print_utils.debug_print(combos)
      output = run_app_with_prefetch.run_test(combos)
      output = PrefetchAppRunner(**combos._asdict()).run()

      yield DataFrame(dict((x, [y]) for x, y in output)) if output else None

@@ -307,7 +316,7 @@ def main():
  output_file = opts.output and open(opts.output, 'w') or sys.stdout

  combos = lambda: args_utils.generate_run_combinations(
      run_app_with_prefetch.RunCommandArgs,
      RunCommandArgs,
      coerce_to_list(vars(opts)),
      opts.loop_count)
  print_utils.debug_print_gen("run combinations: ", combos())
+265 −0
Original line number Diff line number Diff line
# 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".
        """
    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

  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
+104 −0
Original line number Diff line number Diff line
# 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.
#

"""Unit tests for the AppRunner."""
import os
import sys
from pathlib import Path

from app_runner import AppRunner, AppRunnerListener
from mock import Mock, call, patch

# The path is "frameworks/base/startop/scripts/"
sys.path.append(Path(os.path.realpath(__file__)).parents[2])
import lib.cmd_utils as cmd_utils

class AppRunnerTestListener(AppRunnerListener):
  def preprocess(self) -> None:
    cmd_utils.run_shell_command('pre'),

  def postprocess(self, pre_launch_timestamp: str) -> None:
    cmd_utils.run_shell_command('post'),

  def metrics_selector(self, am_start_output: str,
                       pre_launch_timestamp: str) -> None:
    return 'TotalTime=123\n'

RUNNER = AppRunner(package='music',
                   activity='MainActivity',
                   compiler_filter='speed',
                   timeout=None,
                   simulate=False)



def test_configure_compiler_filter():
  with patch('lib.cmd_utils.run_shell_command',
             new_callable=Mock) as mock_run_shell_command:
    mock_run_shell_command.return_value = (True, 'speed arm64 kUpToDate')

    RUNNER.configure_compiler_filter()

    calls = [call(os.path.realpath(
        os.path.join(RUNNER.DIR,
                     '../query_compiler_filter.py')) + ' --package music')]
    mock_run_shell_command.assert_has_calls(calls)

def test_parse_metrics_output():
  input = 'a1=b1\nc1=d1\ne1=f1'
  ret = RUNNER.parse_metrics_output(input)

  assert ret == [('a1', 'b1'), ('c1', 'd1'), ('e1', 'f1')]

def _mocked_run_shell_command(*args, **kwargs):
  if args[0] == 'adb shell "date -u +\'%Y-%m-%d %H:%M:%S.%N\'"':
    return (True, "2019-07-02 23:20:06.972674825")
  elif args[0] == 'adb shell ps | grep "music" | awk \'{print $2;}\'':
    return (True, '9999')
  else:
    return (True, 'a1=b1\nc1=d1=d2\ne1=f1')

@patch('app_startup.lib.adb_utils.blocking_wait_for_logcat_displayed_time')
@patch('lib.cmd_utils.run_shell_command')
def test_run(mock_run_shell_command,
             mock_blocking_wait_for_logcat_displayed_time):
  mock_run_shell_command.side_effect = _mocked_run_shell_command
  mock_blocking_wait_for_logcat_displayed_time.return_value = 123

  test_listener = AppRunnerTestListener()
  RUNNER.add_callbacks(test_listener)

  result = RUNNER.run()

  RUNNER.remove_callbacks(test_listener)

  calls = [call('pre'),
           call(os.path.realpath(
               os.path.join(RUNNER.DIR,
                            '../query_compiler_filter.py')) +
                ' --package music'),
           call('adb shell "date -u +\'%Y-%m-%d %H:%M:%S.%N\'"'),
           call(
               'timeout {timeout} "{DIR}/launch_application" "{package}" "{activity}"'
                 .format(timeout=30,
                         DIR=os.path.realpath(os.path.dirname(RUNNER.DIR)),
                         package='music',
                         activity='MainActivity',
                         timestamp='2019-07-02 23:20:06.972674825')),
           call('post')
           ]
  mock_run_shell_command.assert_has_calls(calls)
  assert result == [('TotalTime', '123')]
  assert len(RUNNER.listeners) == 0
 No newline at end of file
+130 −277

File changed.

Preview size limit exceeded, changes collapsed.

+102 −99

File changed.

Preview size limit exceeded, changes collapsed.