summaryrefslogtreecommitdiff
path: root/startop/scripts/app_startup/lib
diff options
context:
space:
mode:
Diffstat (limited to 'startop/scripts/app_startup/lib')
-rw-r--r--startop/scripts/app_startup/lib/adb_utils.py126
-rw-r--r--startop/scripts/app_startup/lib/adb_utils_test.py16
-rw-r--r--startop/scripts/app_startup/lib/app_runner.py266
-rw-r--r--startop/scripts/app_startup/lib/app_runner_test.py104
-rw-r--r--startop/scripts/app_startup/lib/args_utils.py77
-rw-r--r--startop/scripts/app_startup/lib/args_utils_test.py58
-rwxr-xr-xstartop/scripts/app_startup/lib/common145
-rw-r--r--startop/scripts/app_startup/lib/data_frame.py201
-rw-r--r--startop/scripts/app_startup/lib/data_frame_test.py128
-rw-r--r--startop/scripts/app_startup/lib/perfetto_trace_collector.py166
-rw-r--r--startop/scripts/app_startup/lib/perfetto_trace_collector_test.py101
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)