diff options
Diffstat (limited to 'startop/scripts/app_startup/lib')
-rw-r--r-- | startop/scripts/app_startup/lib/adb_utils.py | 126 | ||||
-rw-r--r-- | startop/scripts/app_startup/lib/adb_utils_test.py | 16 | ||||
-rw-r--r-- | startop/scripts/app_startup/lib/app_runner.py | 266 | ||||
-rw-r--r-- | startop/scripts/app_startup/lib/app_runner_test.py | 104 | ||||
-rw-r--r-- | startop/scripts/app_startup/lib/args_utils.py | 77 | ||||
-rw-r--r-- | startop/scripts/app_startup/lib/args_utils_test.py | 58 | ||||
-rwxr-xr-x | startop/scripts/app_startup/lib/common | 145 | ||||
-rw-r--r-- | startop/scripts/app_startup/lib/data_frame.py | 201 | ||||
-rw-r--r-- | startop/scripts/app_startup/lib/data_frame_test.py | 128 | ||||
-rw-r--r-- | startop/scripts/app_startup/lib/perfetto_trace_collector.py | 166 | ||||
-rw-r--r-- | startop/scripts/app_startup/lib/perfetto_trace_collector_test.py | 101 |
11 files changed, 1388 insertions, 0 deletions
diff --git a/startop/scripts/app_startup/lib/adb_utils.py b/startop/scripts/app_startup/lib/adb_utils.py new file mode 100644 index 000000000000..3cebc9a97a50 --- /dev/null +++ b/startop/scripts/app_startup/lib/adb_utils.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# +# 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. + +"""Helper util libraries for calling adb command line.""" + +import datetime +import os +import re +import sys +import time +from typing import Optional + +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname( + os.path.abspath(__file__))))) +import lib.cmd_utils as cmd_utils +import lib.logcat_utils as logcat_utils + + +def logcat_save_timestamp() -> str: + """Gets the current logcat timestamp. + + Returns: + A string of timestamp. + """ + _, output = cmd_utils.run_adb_shell_command( + "date -u +\'%Y-%m-%d %H:%M:%S.%N\'") + return output + +def vm_drop_cache(): + """Free pagecache and slab object.""" + cmd_utils.run_adb_shell_command('echo 3 > /proc/sys/vm/drop_caches') + # Sleep a little bit to provide enough time for cache cleanup. + time.sleep(1) + +def root(): + """Roots adb and successive adb commands will run under root.""" + cmd_utils.run_shell_command('adb root') + +def disable_selinux(): + """Disables selinux setting.""" + _, output = cmd_utils.run_adb_shell_command('getenforce') + if output == 'Permissive': + return + + print('Disable selinux permissions and restart framework.') + cmd_utils.run_adb_shell_command('setenforce 0') + cmd_utils.run_adb_shell_command('stop') + cmd_utils.run_adb_shell_command('start') + cmd_utils.run_shell_command('adb wait-for-device') + +def pkill(procname: str): + """Kills a process on device specified by the substring pattern in procname""" + _, pids = cmd_utils.run_shell_command('adb shell ps | grep "{}" | ' + 'awk \'{{print $2;}}\''. + format(procname)) + + for pid in pids.split('\n'): + pid = pid.strip() + if pid: + passed,_ = cmd_utils.run_adb_shell_command('kill {}'.format(pid)) + time.sleep(1) + +def parse_time_to_milliseconds(time: str) -> int: + """Parses the time string to milliseconds.""" + # Example: +1s56ms, +56ms + regex = r'\+((?P<second>\d+?)s)?(?P<millisecond>\d+?)ms' + result = re.search(regex, time) + second = 0 + if result.group('second'): + second = int(result.group('second')) + ms = int(result.group('millisecond')) + return second * 1000 + ms + +def blocking_wait_for_logcat_displayed_time(timestamp: datetime.datetime, + package: str, + timeout: int) -> Optional[int]: + """Parses the displayed time in the logcat. + + Returns: + the displayed time. + """ + pattern = re.compile('.*ActivityTaskManager: Displayed {}.*'.format(package)) + # 2019-07-02 22:28:34.469453349 -> 2019-07-02 22:28:34.469453 + timestamp = datetime.datetime.strptime(timestamp[:-3], + '%Y-%m-%d %H:%M:%S.%f') + timeout_dt = timestamp + datetime.timedelta(0, timeout) + # 2019-07-01 14:54:21.946 27365 27392 I ActivityTaskManager: + # Displayed com.android.settings/.Settings: +927ms + result = logcat_utils.blocking_wait_for_logcat_pattern(timestamp, + pattern, + timeout_dt) + if not result or not '+' in result: + return None + displayed_time = result[result.rfind('+'):] + + return parse_time_to_milliseconds(displayed_time) + +def delete_file_on_device(file_path: str) -> None: + """ Deletes a file on the device. """ + cmd_utils.run_adb_shell_command( + "[[ -f '{file_path}' ]] && rm -f '{file_path}' || " + "exit 0".format(file_path=file_path)) + +def set_prop(property: str, value: str) -> None: + """ Sets property using adb shell. """ + cmd_utils.run_adb_shell_command('setprop "{property}" "{value}"'.format( + property=property, value=value)) + +def pull_file(device_file_path: str, output_file_path: str) -> None: + """ Pulls file from device to output """ + cmd_utils.run_shell_command('adb pull "{device_file_path}" "{output_file_path}"'. + format(device_file_path=device_file_path, + output_file_path=output_file_path)) diff --git a/startop/scripts/app_startup/lib/adb_utils_test.py b/startop/scripts/app_startup/lib/adb_utils_test.py new file mode 100644 index 000000000000..e590fed568e3 --- /dev/null +++ b/startop/scripts/app_startup/lib/adb_utils_test.py @@ -0,0 +1,16 @@ +import adb_utils + +# pip imports +import pytest + +def test_parse_time_to_milliseconds(): + # Act + result1 = adb_utils.parse_time_to_milliseconds('+1s7ms') + result2 = adb_utils.parse_time_to_milliseconds('+523ms') + + # Assert + assert result1 == 1007 + assert result2 == 523 + +if __name__ == '__main__': + pytest.main() diff --git a/startop/scripts/app_startup/lib/app_runner.py b/startop/scripts/app_startup/lib/app_runner.py new file mode 100644 index 000000000000..78873fa51ab5 --- /dev/null +++ b/startop/scripts/app_startup/lib/app_runner.py @@ -0,0 +1,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 + diff --git a/startop/scripts/app_startup/lib/app_runner_test.py b/startop/scripts/app_startup/lib/app_runner_test.py new file mode 100644 index 000000000000..33d233b03aab --- /dev/null +++ b/startop/scripts/app_startup/lib/app_runner_test.py @@ -0,0 +1,104 @@ +# 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 diff --git a/startop/scripts/app_startup/lib/args_utils.py b/startop/scripts/app_startup/lib/args_utils.py new file mode 100644 index 000000000000..080f3b53157b --- /dev/null +++ b/startop/scripts/app_startup/lib/args_utils.py @@ -0,0 +1,77 @@ +import itertools +import os +import sys +from typing import Any, Callable, Dict, Iterable, List, NamedTuple, Tuple, \ + TypeVar, Optional + +# local import +sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname( + os.path.abspath(__file__))))) +import lib.print_utils as print_utils + +T = TypeVar('T') +NamedTupleMeta = Callable[ + ..., T] # approximation of a (S : NamedTuple<T> where S() == T) metatype. +FilterFuncType = Callable[[NamedTuple], bool] + +def dict_lookup_any_key(dictionary: dict, *keys: List[Any]): + for k in keys: + if k in dictionary: + return dictionary[k] + + + print_utils.debug_print("None of the keys {} were in the dictionary".format( + keys)) + return [None] + +def generate_run_combinations(named_tuple: NamedTupleMeta[T], + opts_dict: Dict[str, List[Optional[object]]], + loop_count: int = 1) -> Iterable[T]: + """ + Create all possible combinations given the values in opts_dict[named_tuple._fields]. + + :type T: type annotation for the named_tuple type. + :param named_tuple: named tuple type, whose fields are used to make combinations for + :param opts_dict: dictionary of keys to value list. keys correspond to the named_tuple fields. + :param loop_count: number of repetitions. + :return: an iterable over named_tuple instances. + """ + combinations_list = [] + for k in named_tuple._fields: + # the key can be either singular or plural , e.g. 'package' or 'packages' + val = dict_lookup_any_key(opts_dict, k, k + "s") + + # treat {'x': None} key value pairs as if it was [None] + # otherwise itertools.product throws an exception about not being able to iterate None. + combinations_list.append(val or [None]) + + print_utils.debug_print("opts_dict: ", opts_dict) + print_utils.debug_print_nd("named_tuple: ", named_tuple) + print_utils.debug_print("combinations_list: ", combinations_list) + + for i in range(loop_count): + for combo in itertools.product(*combinations_list): + yield named_tuple(*combo) + +def filter_run_combinations(named_tuple: NamedTuple, + filters: List[FilterFuncType]) -> bool: + for filter in filters: + if filter(named_tuple): + return False + return True + +def generate_group_run_combinations(run_combinations: Iterable[NamedTuple], + dst_nt: NamedTupleMeta[T]) \ + -> Iterable[Tuple[T, Iterable[NamedTuple]]]: + def group_by_keys(src_nt): + src_d = src_nt._asdict() + # now remove the keys that aren't legal in dst. + for illegal_key in set(src_d.keys()) - set(dst_nt._fields): + if illegal_key in src_d: + del src_d[illegal_key] + + return dst_nt(**src_d) + + for args_list_it in itertools.groupby(run_combinations, group_by_keys): + (group_key_value, args_it) = args_list_it + yield (group_key_value, args_it) diff --git a/startop/scripts/app_startup/lib/args_utils_test.py b/startop/scripts/app_startup/lib/args_utils_test.py new file mode 100644 index 000000000000..4b7e0fa20627 --- /dev/null +++ b/startop/scripts/app_startup/lib/args_utils_test.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright 2018, 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 args_utils.py script.""" + +import typing + +import args_utils + +def generate_run_combinations(*args): + # expand out the generator values so that assert x == y works properly. + return [i for i in args_utils.generate_run_combinations(*args)] + +def test_generate_run_combinations(): + blank_nd = typing.NamedTuple('Blank') + assert generate_run_combinations(blank_nd, {}, 1) == [()], "empty" + assert generate_run_combinations(blank_nd, {'a': ['a1', 'a2']}) == [ + ()], "empty filter" + a_nd = typing.NamedTuple('A', [('a', str)]) + assert generate_run_combinations(a_nd, {'a': None}) == [(None,)], "None" + assert generate_run_combinations(a_nd, {'a': ['a1', 'a2']}) == [('a1',), ( + 'a2',)], "one item" + assert generate_run_combinations(a_nd, + {'a': ['a1', 'a2'], 'b': ['b1', 'b2']}) == [ + ('a1',), ('a2',)], \ + "one item filter" + assert generate_run_combinations(a_nd, {'a': ['a1', 'a2']}, 2) == [('a1',), ( + 'a2',), ('a1',), ('a2',)], "one item" + ab_nd = typing.NamedTuple('AB', [('a', str), ('b', str)]) + assert generate_run_combinations(ab_nd, + {'a': ['a1', 'a2'], + 'b': ['b1', 'b2']}) == [ab_nd('a1', 'b1'), + ab_nd('a1', 'b2'), + ab_nd('a2', 'b1'), + ab_nd('a2', 'b2')], \ + "two items" + + assert generate_run_combinations(ab_nd, + {'as': ['a1', 'a2'], + 'bs': ['b1', 'b2']}) == [ab_nd('a1', 'b1'), + ab_nd('a1', 'b2'), + ab_nd('a2', 'b1'), + ab_nd('a2', 'b2')], \ + "two items plural" diff --git a/startop/scripts/app_startup/lib/common b/startop/scripts/app_startup/lib/common index 043d8550b64b..bedaa1e10288 100755 --- a/startop/scripts/app_startup/lib/common +++ b/startop/scripts/app_startup/lib/common @@ -1,4 +1,17 @@ #!/bin/bash +# Copyright 2018, 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. if [[ -z $ANDROID_BUILD_TOP ]]; then echo "Please run source build/envsetup.sh first" >&2 @@ -49,5 +62,137 @@ get_activity_name() { local activity_line="$(adb shell cmd package query-activities --brief -a android.intent.action.MAIN -c android.intent.category.LAUNCHER | grep "$package/")" IFS="/" read -a array <<< "$activity_line" local activity_name="${array[1]}" + + # Activities starting with '.' are shorthand for having their package name prefixed. + if [[ $activity_name == .* ]]; then + activity_name="${package}${activity_name}" + fi echo "$activity_name" } + +# Use with logcat_from_timestamp to skip all past log-lines. +logcat_save_timestamp() { + adb shell 'date -u +"%Y-%m-%d %H:%M:%S.%N"' +} + +# Roll forward logcat to only show events +# since the specified timestamp. +# +# i.e. don't look at historical logcat, +# only look at FUTURE logcat. +# +# First use 'logcat_save_timestamp' +# Then do whatever action you want. +# Then use 'logcat_from_timestamp_bg $timestamp' +logcat_from_timestamp_bg() { + local timestamp="$1" + shift # drop timestamp from args. + verbose_print adb logcat -T \"$timestamp\" \"$@\" + adb logcat -v UTC -T "$timestamp" "$@" & + logcat_from_timestamp_pid=$! +} + +# Starting at timestamp $2, wait until we seen pattern $3 +# or until a timeout happens in $1 seconds. +# If successful, also echo the line that matched the pattern. +# +# Set VERBOSE_LOGCAT=1 to debug every line of logcat it tries to parse. +logcat_select_pattern() { + local timeout="$1" + local timestamp="$2" + local pattern="$3" + + local logcat_fd + + coproc logcat_fd { + kill_children_quietly() { + kill "$logcat_pidd" + wait "$logcat_pidd" 2>/dev/null + } + + trap 'kill_children_quietly' EXIT # kill logcat when this coproc is killed. + + # run logcat in the background so it can be killed. + logcat_from_timestamp_bg "$timestamp" + logcat_pidd=$logcat_from_timestamp_pid + wait "$logcat_pidd" + } + local logcat_pid="$!" + verbose_print "[LOGCAT] Spawn pid $logcat_pid" + + local timeout_ts="$(date -d "now + ${timeout} seconds" '+%s')" + local now_ts="0" + + local return_code=1 + + verbose_print "logcat_wait_for_pattern begin" + + while read -t "$timeout" -r -u "${logcat_fd[0]}" logcat_output; do + if (( $VERBOSE_LOGCAT )); then + verbose_print "LOGCAT: $logcat_output" + fi + if [[ "$logcat_output:" == *"$pattern"* ]]; then + verbose_print "LOGCAT: " "$logcat_output" + verbose_print "WE DID SEE PATTERN" '<<' "$pattern" '>>.' + echo "$logcat_output" + return_code=0 + break + fi + now_ts="$(date -d "now" '+%s')" + if (( now_ts >= timeout_ts )); then + verbose_print "DID TIMEOUT BEFORE SEEING ANYTHING (timeout=$timeout seconds) " '<<' "$pattern" '>>.' + break + fi + done + + # Don't leave logcat lying around since it will keep going. + kill "$logcat_pid" + # Suppress annoying 'Terminated...' message. + wait "$logcat_pid" 2>/dev/null + + verbose_print "[LOGCAT] $logcat_pid should be killed" + + return $return_code +} + +# Starting at timestamp $2, wait until we seen pattern $3 +# or until a timeout happens in $1 seconds. +# +# Set VERBOSE_LOGCAT=1 to debug every line of logcat it tries to parse. +logcat_wait_for_pattern() { + logcat_select_pattern "$@" > /dev/null +} + +# Starting at timestamp $2, wait until we seen pattern $3 +# or until a timeout happens in $1 seconds. +# If successful, extract with the regular expression pattern in #4 +# and return the first capture group. +# +# Set VERBOSE_LOGCAT=1 to debug every line of logcat it tries to parse. +logcat_extract_pattern() { + local timeout="$1" + local timestamp="$2" + local pattern="$3" + local re_pattern="$4" + + local result + local exit_code + + result="$(logcat_select_pattern "$@")" + exit_code=$? + + if [[ $exit_code -ne 0 ]]; then + return $exit_code + fi + + echo "$result" | sed 's/'"$re_pattern"'/\1/g' +} + +# Join array +# FOO=(a b c) +# join_by , "${FOO[@]}" #a,b,c +join_by() { + local IFS="$1" + shift + echo "$*" +} diff --git a/startop/scripts/app_startup/lib/data_frame.py b/startop/scripts/app_startup/lib/data_frame.py new file mode 100644 index 000000000000..20a2308637f2 --- /dev/null +++ b/startop/scripts/app_startup/lib/data_frame.py @@ -0,0 +1,201 @@ +import itertools +from typing import Dict, List + +class DataFrame: + """Table-like class for storing a 2D cells table with named columns.""" + def __init__(self, data: Dict[str, List[object]] = {}): + """ + Create a new DataFrame from a dictionary (keys = headers, + values = columns). + """ + self._headers = [i for i in data.keys()] + self._rows = [] + + row_num = 0 + + def get_data_row(idx): + r = {} + for header, header_data in data.items(): + + if not len(header_data) > idx: + continue + + r[header] = header_data[idx] + + return r + + while True: + row_dict = get_data_row(row_num) + if len(row_dict) == 0: + break + + self._append_row(row_dict.keys(), row_dict.values()) + row_num = row_num + 1 + + def concat_rows(self, other: 'DataFrame') -> None: + """ + In-place concatenate rows of other into the rows of the + current DataFrame. + + None is added in pre-existing cells if new headers + are introduced. + """ + other_datas = other._data_only() + + other_headers = other.headers + + for d in other_datas: + self._append_row(other_headers, d) + + def _append_row(self, headers: List[str], data: List[object]): + new_row = {k:v for k,v in zip(headers, data)} + self._rows.append(new_row) + + for header in headers: + if not header in self._headers: + self._headers.append(header) + + def __repr__(self): +# return repr(self._rows) + repr = "" + + header_list = self._headers_only() + + row_format = u"" + for header in header_list: + row_format = row_format + u"{:>%d}" %(len(header) + 1) + + repr = row_format.format(*header_list) + "\n" + + for v in self._data_only(): + repr = repr + row_format.format(*v) + "\n" + + return repr + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self.headers == other.headers and self.data_table == other.data_table + else: + print("wrong instance", other.__class__) + return False + + @property + def headers(self) -> List[str]: + return [i for i in self._headers_only()] + + @property + def data_table(self) -> List[List[object]]: + return list(self._data_only()) + + @property + def data_table_transposed(self) -> List[List[object]]: + return list(self._transposed_data()) + + @property + def data_row_len(self) -> int: + return len(self._rows) + + def data_row_at(self, idx) -> List[object]: + """ + Return a single data row at the specified index (0th based). + + Accepts negative indices, e.g. -1 is last row. + """ + row_dict = self._rows[idx] + l = [] + + for h in self._headers_only(): + l.append(row_dict.get(h)) # Adds None in blank spots. + + return l + + def copy(self) -> 'DataFrame': + """ + Shallow copy of this DataFrame. + """ + return self.repeat(count=0) + + def repeat(self, count: int) -> 'DataFrame': + """ + Returns a new DataFrame where each row of this dataframe is repeated count times. + A repeat of a row is adjacent to other repeats of that same row. + """ + df = DataFrame() + df._headers = self._headers.copy() + + rows = [] + for row in self._rows: + for i in range(count): + rows.append(row.copy()) + + df._rows = rows + + return df + + def merge_data_columns(self, other: 'DataFrame'): + """ + Merge self and another DataFrame by adding the data from other column-wise. + For any headers that are the same, data from 'other' is preferred. + """ + for h in other._headers: + if not h in self._headers: + self._headers.append(h) + + append_rows = [] + + for self_dict, other_dict in itertools.zip_longest(self._rows, other._rows): + if not self_dict: + d = {} + append_rows.append(d) + else: + d = self_dict + + d_other = other_dict + if d_other: + for k,v in d_other.items(): + d[k] = v + + for r in append_rows: + self._rows.append(r) + + def data_row_reduce(self, fnc) -> 'DataFrame': + """ + Reduces the data row-wise by applying the fnc to each row (column-wise). + Empty cells are skipped. + + fnc(Iterable[object]) -> object + fnc is applied over every non-empty cell in that column (descending row-wise). + + Example: + DataFrame({'a':[1,2,3]}).data_row_reduce(sum) == DataFrame({'a':[6]}) + + Returns a new single-row DataFrame. + """ + df = DataFrame() + df._headers = self._headers.copy() + + def yield_by_column(header_key): + for row_dict in self._rows: + val = row_dict.get(header_key) + if val: + yield val + + new_row_dict = {} + for h in df._headers: + cell_value = fnc(yield_by_column(h)) + new_row_dict[h] = cell_value + + df._rows = [new_row_dict] + return df + + def _headers_only(self): + return self._headers + + def _data_only(self): + row_len = len(self._rows) + + for i in range(row_len): + yield self.data_row_at(i) + + def _transposed_data(self): + return zip(*self._data_only())
\ No newline at end of file diff --git a/startop/scripts/app_startup/lib/data_frame_test.py b/startop/scripts/app_startup/lib/data_frame_test.py new file mode 100644 index 000000000000..1cbc1cbe45cb --- /dev/null +++ b/startop/scripts/app_startup/lib/data_frame_test.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# +# Copyright 2018, 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 data_frame.py script.""" + +from data_frame import DataFrame + +def test_data_frame(): + # trivial empty data frame + df = DataFrame() + assert df.headers == [] + assert df.data_table == [] + assert df.data_table_transposed == [] + + # common case, same number of values in each place. + df = DataFrame({'TotalTime_ms': [1, 2, 3], 'Displayed_ms': [4, 5, 6]}) + assert df.headers == ['TotalTime_ms', 'Displayed_ms'] + assert df.data_table == [[1, 4], [2, 5], [3, 6]] + assert df.data_table_transposed == [(1, 2, 3), (4, 5, 6)] + + # varying num values. + df = DataFrame({'many': [1, 2], 'none': []}) + assert df.headers == ['many', 'none'] + assert df.data_table == [[1, None], [2, None]] + assert df.data_table_transposed == [(1, 2), (None, None)] + + df = DataFrame({'many': [], 'none': [1, 2]}) + assert df.headers == ['many', 'none'] + assert df.data_table == [[None, 1], [None, 2]] + assert df.data_table_transposed == [(None, None), (1, 2)] + + # merge multiple data frames + df = DataFrame() + df.concat_rows(DataFrame()) + assert df.headers == [] + assert df.data_table == [] + assert df.data_table_transposed == [] + + df = DataFrame() + df2 = DataFrame({'TotalTime_ms': [1, 2, 3], 'Displayed_ms': [4, 5, 6]}) + + df.concat_rows(df2) + assert df.headers == ['TotalTime_ms', 'Displayed_ms'] + assert df.data_table == [[1, 4], [2, 5], [3, 6]] + assert df.data_table_transposed == [(1, 2, 3), (4, 5, 6)] + + df = DataFrame({'TotalTime_ms': [1, 2]}) + df2 = DataFrame({'Displayed_ms': [4, 5]}) + + df.concat_rows(df2) + assert df.headers == ['TotalTime_ms', 'Displayed_ms'] + assert df.data_table == [[1, None], [2, None], [None, 4], [None, 5]] + + df = DataFrame({'TotalTime_ms': [1, 2]}) + df2 = DataFrame({'TotalTime_ms': [3, 4], 'Displayed_ms': [5, 6]}) + + df.concat_rows(df2) + assert df.headers == ['TotalTime_ms', 'Displayed_ms'] + assert df.data_table == [[1, None], [2, None], [3, 5], [4, 6]] + + # data_row_at + df = DataFrame({'TotalTime_ms': [1, 2, 3], 'Displayed_ms': [4, 5, 6]}) + assert df.data_row_at(-1) == [3, 6] + assert df.data_row_at(2) == [3, 6] + assert df.data_row_at(1) == [2, 5] + + # repeat + df = DataFrame({'TotalTime_ms': [1], 'Displayed_ms': [4]}) + df2 = DataFrame({'TotalTime_ms': [1, 1, 1], 'Displayed_ms': [4, 4, 4]}) + assert df.repeat(3) == df2 + + # repeat + df = DataFrame({'TotalTime_ms': [1, 1, 1], 'Displayed_ms': [4, 4, 4]}) + assert df.data_row_len == 3 + df = DataFrame({'TotalTime_ms': [1, 1]}) + assert df.data_row_len == 2 + + # repeat + df = DataFrame({'TotalTime_ms': [1, 1, 1], 'Displayed_ms': [4, 4, 4]}) + assert df.data_row_len == 3 + df = DataFrame({'TotalTime_ms': [1, 1]}) + assert df.data_row_len == 2 + + # data_row_reduce + df = DataFrame({'TotalTime_ms': [1, 1, 1], 'Displayed_ms': [4, 4, 4]}) + df_sum = DataFrame({'TotalTime_ms': [3], 'Displayed_ms': [12]}) + assert df.data_row_reduce(sum) == df_sum + + # merge_data_columns + df = DataFrame({'TotalTime_ms': [1, 2, 3]}) + df2 = DataFrame({'Displayed_ms': [3, 4, 5, 6]}) + + df.merge_data_columns(df2) + assert df == DataFrame( + {'TotalTime_ms': [1, 2, 3], 'Displayed_ms': [3, 4, 5, 6]}) + + df = DataFrame({'TotalTime_ms': [1, 2, 3]}) + df2 = DataFrame({'Displayed_ms': [3, 4]}) + + df.merge_data_columns(df2) + assert df == DataFrame( + {'TotalTime_ms': [1, 2, 3], 'Displayed_ms': [3, 4]}) + + df = DataFrame({'TotalTime_ms': [1, 2, 3]}) + df2 = DataFrame({'TotalTime_ms': [10, 11]}) + + df.merge_data_columns(df2) + assert df == DataFrame({'TotalTime_ms': [10, 11, 3]}) + + df = DataFrame({'TotalTime_ms': []}) + df2 = DataFrame({'TotalTime_ms': [10, 11]}) + + df.merge_data_columns(df2) + assert df == DataFrame({'TotalTime_ms': [10, 11]}) diff --git a/startop/scripts/app_startup/lib/perfetto_trace_collector.py b/startop/scripts/app_startup/lib/perfetto_trace_collector.py new file mode 100644 index 000000000000..9ffb3494da49 --- /dev/null +++ b/startop/scripts/app_startup/lib/perfetto_trace_collector.py @@ -0,0 +1,166 @@ +# 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 collector perfetto trace.""" +import datetime +import os +import re +import sys +import time +from datetime import timedelta +from typing import Optional, List, Tuple + +# global variables +DIR = os.path.abspath(os.path.dirname(__file__)) + +sys.path.append(os.path.dirname(os.path.dirname(DIR))) + +import app_startup.lib.adb_utils as adb_utils +from app_startup.lib.app_runner import AppRunner, AppRunnerListener +import lib.print_utils as print_utils +import lib.logcat_utils as logcat_utils +import iorap.lib.iorapd_utils as iorapd_utils + +class PerfettoTraceCollector(AppRunnerListener): + """ Class to collect perfetto trace. + + To set trace duration of perfetto, change the 'trace_duration_ms'. + To pull the generated perfetto trace on device, set the 'output'. + """ + TRACE_FILE_SUFFIX = 'perfetto_trace.pb' + TRACE_DURATION_PROP = 'iorapd.perfetto.trace_duration_ms' + MS_PER_SEC = 1000 + DEFAULT_TRACE_DURATION = timedelta(milliseconds=5000) # 5 seconds + _COLLECTOR_TIMEOUT_MULTIPLIER = 10 # take the regular timeout and multiply + + def __init__(self, + package: str, + activity: Optional[str], + compiler_filter: Optional[str], + timeout: Optional[int], + simulate: bool, + trace_duration: timedelta = DEFAULT_TRACE_DURATION, + save_destination_file_path: Optional[str] = None): + """ Initialize the perfetto trace collector. """ + self.app_runner = AppRunner(package, + activity, + compiler_filter, + timeout, + simulate) + self.app_runner.add_callbacks(self) + + self.trace_duration = trace_duration + self.save_destination_file_path = save_destination_file_path + + def purge_file(self, suffix: str) -> None: + print_utils.debug_print('iorapd-perfetto: purge file in ' + + self._get_remote_path()) + adb_utils.delete_file_on_device(self._get_remote_path()) + + def run(self) -> Optional[List[Tuple[str]]]: + """Runs an app. + + Returns: + A list of (metric, value) tuples. + """ + return self.app_runner.run() + + def preprocess(self): + # Sets up adb environment. + adb_utils.root() + adb_utils.disable_selinux() + time.sleep(1) + + # Kill any existing process of this app + adb_utils.pkill(self.app_runner.package) + + # Remove existing trace and compiler files + self.purge_file(PerfettoTraceCollector.TRACE_FILE_SUFFIX) + + # Set perfetto trace duration prop to milliseconds. + adb_utils.set_prop(PerfettoTraceCollector.TRACE_DURATION_PROP, + int(self.trace_duration.total_seconds()* + PerfettoTraceCollector.MS_PER_SEC)) + + if not iorapd_utils.stop_iorapd(): + raise RuntimeError('Cannot stop iorapd!') + + if not iorapd_utils.enable_iorapd_perfetto(): + raise RuntimeError('Cannot enable perfetto!') + + if not iorapd_utils.disable_iorapd_readahead(): + raise RuntimeError('Cannot disable readahead!') + + if not iorapd_utils.start_iorapd(): + raise RuntimeError('Cannot start iorapd!') + + # Drop all caches to get cold starts. + adb_utils.vm_drop_cache() + + def postprocess(self, pre_launch_timestamp: str): + # Kill any existing process of this app + adb_utils.pkill(self.app_runner.package) + + iorapd_utils.disable_iorapd_perfetto() + + if self.save_destination_file_path is not None: + adb_utils.pull_file(self._get_remote_path(), + self.save_destination_file_path) + + def metrics_selector(self, am_start_output: str, + pre_launch_timestamp: str) -> str: + """Parses the metric after app startup by reading from logcat in a blocking + manner until all metrics have been found". + + Returns: + An empty string because the metric needs no further parsing. + """ + if not self._wait_for_perfetto_trace(pre_launch_timestamp): + raise RuntimeError('Could not save perfetto app trace file!') + + return '' + + def _wait_for_perfetto_trace(self, pre_launch_timestamp) -> Optional[str]: + """ Waits for the perfetto trace being saved to file. + + The string is in the format of r".*Perfetto TraceBuffer saved to file: + <file path>.*" + + Returns: + the string what the program waits for. If the string doesn't show up, + return None. + """ + pattern = re.compile(r'.*Perfetto TraceBuffer saved to file: {}.*'. + format(self._get_remote_path())) + + # The pre_launch_timestamp is longer than what the datetime can parse. Trim + # last three digits to make them align. For example: + # 2019-07-02 23:20:06.972674825999 -> 2019-07-02 23:20:06.972674825 + assert len(pre_launch_timestamp) == len('2019-07-02 23:20:06.972674825') + timestamp = datetime.datetime.strptime(pre_launch_timestamp[:-3], + '%Y-%m-%d %H:%M:%S.%f') + + # The timeout of perfetto trace is longer than the normal app run timeout. + timeout_dt = self.app_runner.timeout * PerfettoTraceCollector._COLLECTOR_TIMEOUT_MULTIPLIER + timeout_end = timestamp + datetime.timedelta(seconds=timeout_dt) + + return logcat_utils.blocking_wait_for_logcat_pattern(timestamp, + pattern, + timeout_end) + + def _get_remote_path(self): + # For example: android.music%2Fmusic.TopLevelActivity.perfetto_trace.pb + return iorapd_utils._iorapd_path_to_data_file(self.app_runner.package, + self.app_runner.activity, + PerfettoTraceCollector.TRACE_FILE_SUFFIX) diff --git a/startop/scripts/app_startup/lib/perfetto_trace_collector_test.py b/startop/scripts/app_startup/lib/perfetto_trace_collector_test.py new file mode 100644 index 000000000000..8d94fc58bede --- /dev/null +++ b/startop/scripts/app_startup/lib/perfetto_trace_collector_test.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# +# 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 data_frame.py script.""" +import os +import sys +from pathlib import Path +from datetime import timedelta + +from mock import call, patch +from perfetto_trace_collector import PerfettoTraceCollector + +sys.path.append(Path(os.path.realpath(__file__)).parents[2]) +from app_startup.lib.app_runner import AppRunner + +RUNNER = PerfettoTraceCollector(package='music', + activity='MainActivity', + compiler_filter=None, + timeout=10, + simulate=False, + trace_duration = timedelta(milliseconds=1000), + # No actual file will be created. Just to + # check the command. + save_destination_file_path='/tmp/trace.pb') + +def _mocked_run_shell_command(*args, **kwargs): + if args[0] == 'adb shell ps | grep "music" | awk \'{print $2;}\'': + return (True, '9999') + else: + return (True, '') + +@patch('lib.logcat_utils.blocking_wait_for_logcat_pattern') +@patch('lib.cmd_utils.run_shell_command') +def test_perfetto_trace_collector_preprocess(mock_run_shell_command, + mock_blocking_wait_for_logcat_pattern): + mock_run_shell_command.side_effect = _mocked_run_shell_command + mock_blocking_wait_for_logcat_pattern.return_value = "Succeed!" + + RUNNER.preprocess() + + calls = [call('adb root'), + call('adb shell "getenforce"'), + call('adb shell "setenforce 0"'), + call('adb shell "stop"'), + call('adb shell "start"'), + call('adb wait-for-device'), + call('adb shell ps | grep "music" | awk \'{print $2;}\''), + call('adb shell "kill 9999"'), + call( + 'adb shell "[[ -f \'/data/misc/iorapd/music%2FMainActivity.perfetto_trace.pb\' ]] ' + '&& rm -f \'/data/misc/iorapd/music%2FMainActivity.perfetto_trace.pb\' || exit 0"'), + call('adb shell "setprop "iorapd.perfetto.trace_duration_ms" "1000""'), + call( + 'bash -c "source {}; iorapd_stop"'.format( + AppRunner.IORAP_COMMON_BASH_SCRIPT)), + call( + 'bash -c "source {}; iorapd_perfetto_enable"'.format( + AppRunner.IORAP_COMMON_BASH_SCRIPT)), + call( + 'bash -c "source {}; iorapd_readahead_disable"'.format( + AppRunner.IORAP_COMMON_BASH_SCRIPT)), + call( + 'bash -c "source {}; iorapd_start"'.format( + AppRunner.IORAP_COMMON_BASH_SCRIPT)), + call('adb shell "echo 3 > /proc/sys/vm/drop_caches"')] + + mock_run_shell_command.assert_has_calls(calls) + +@patch('lib.logcat_utils.blocking_wait_for_logcat_pattern') +@patch('lib.cmd_utils.run_shell_command') +def test_perfetto_trace_collector_postprocess(mock_run_shell_command, + mock_blocking_wait_for_logcat_pattern): + mock_run_shell_command.side_effect = _mocked_run_shell_command + mock_blocking_wait_for_logcat_pattern.return_value = "Succeed!" + + RUNNER.postprocess('2019-07-02 23:20:06.972674825') + + calls = [call('adb shell ps | grep "music" | awk \'{print $2;}\''), + call('adb shell "kill 9999"'), + call( + 'bash -c "source {}; iorapd_perfetto_disable"'.format( + AppRunner.IORAP_COMMON_BASH_SCRIPT)), + call('adb pull ' + '"/data/misc/iorapd/music%2FMainActivity.perfetto_trace.pb" ' + '"/tmp/trace.pb"')] + + mock_run_shell_command.assert_has_calls(calls) |