blob: eb582f946958b49dd42a9b9fb55935de6ccd2a1f [file] [log] [blame]
Igor Murashkin25f394d2018-09-11 16:37:18 -07001#!/usr/bin/env python3
2#
3# Copyright 2018, The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17#
18#
19# Measure application start-up time by launching applications under various combinations.
20# See --help for more details.
21#
22#
23# Sample usage:
24# $> ./app_startup_runner.py -p com.google.android.calculator -r warm -r cold -lc 10 -o out.csv
25# $> ./analyze_metrics.py out.csv
26#
27#
28
29import argparse
30import csv
31import itertools
32import os
Igor Murashkin25f394d2018-09-11 16:37:18 -070033import sys
34import tempfile
Yan Wang6ddab322019-07-26 10:53:45 -070035from datetime import timedelta
Yan Wang7af01552019-07-02 15:46:34 -070036from typing import Any, Callable, Iterable, List, NamedTuple, TextIO, Tuple, \
Yan Wang06f54882019-07-23 18:09:41 -070037 TypeVar, Union, Optional
Yan Wang7af01552019-07-02 15:46:34 -070038
39# local import
40DIR = os.path.abspath(os.path.dirname(__file__))
41sys.path.append(os.path.dirname(DIR))
Yan Wang6ddab322019-07-26 10:53:45 -070042import lib.cmd_utils as cmd_utils
43import lib.print_utils as print_utils
44import iorap.compiler as compiler
Yan Wang06f54882019-07-23 18:09:41 -070045from app_startup.run_app_with_prefetch import PrefetchAppRunner
Yan Wang7af01552019-07-02 15:46:34 -070046import app_startup.lib.args_utils as args_utils
47from app_startup.lib.data_frame import DataFrame
Yan Wang6ddab322019-07-26 10:53:45 -070048from app_startup.lib.perfetto_trace_collector import PerfettoTraceCollector
Igor Murashkin25f394d2018-09-11 16:37:18 -070049
50# The following command line options participate in the combinatorial generation.
51# All other arguments have a global effect.
Yan Wang6ddab322019-07-26 10:53:45 -070052_COMBINATORIAL_OPTIONS = ['package', 'readahead', 'compiler_filter',
53 'activity', 'trace_duration']
Yan Wang7af01552019-07-02 15:46:34 -070054_TRACING_READAHEADS = ['mlock', 'fadvise']
55_FORWARD_OPTIONS = {'loop_count': '--count'}
56_RUN_SCRIPT = os.path.join(os.path.dirname(os.path.realpath(__file__)),
57 'run_app_with_prefetch.py')
Igor Murashkin25f394d2018-09-11 16:37:18 -070058
Yan Wang7af01552019-07-02 15:46:34 -070059CollectorPackageInfo = NamedTuple('CollectorPackageInfo',
60 [('package', str), ('compiler_filter', str)])
Yan Wang6ddab322019-07-26 10:53:45 -070061_COMPILER_SCRIPT = os.path.join(os.path.dirname(os.path.dirname(
62 os.path.realpath(__file__))), 'iorap/compiler.py')
Yan Wang7af01552019-07-02 15:46:34 -070063# by 2; systrace starts up slowly.
Igor Murashkin25f394d2018-09-11 16:37:18 -070064
Yan Wang7af01552019-07-02 15:46:34 -070065_UNLOCK_SCREEN_SCRIPT = os.path.join(
66 os.path.dirname(os.path.realpath(__file__)), 'unlock_screen')
Igor Murashkin25f394d2018-09-11 16:37:18 -070067
Yan Wang06f54882019-07-23 18:09:41 -070068RunCommandArgs = NamedTuple('RunCommandArgs',
69 [('package', str),
70 ('readahead', str),
71 ('activity', Optional[str]),
72 ('compiler_filter', Optional[str]),
73 ('timeout', Optional[int]),
74 ('debug', bool),
75 ('simulate', bool),
Yan Wang6ddab322019-07-26 10:53:45 -070076 ('input', Optional[str]),
77 ('trace_duration', Optional[timedelta])])
Yan Wang06f54882019-07-23 18:09:41 -070078
Igor Murashkin25f394d2018-09-11 16:37:18 -070079# This must be the only mutable global variable. All other global variables are constants to avoid magic literals.
80_debug = False # See -d/--debug flag.
81_DEBUG_FORCE = None # Ignore -d/--debug if this is not none.
Yan Wang6ddab322019-07-26 10:53:45 -070082_PERFETTO_TRACE_DURATION_MS = 5000 # milliseconds
83_PERFETTO_TRACE_DURATION = timedelta(milliseconds=_PERFETTO_TRACE_DURATION_MS)
Igor Murashkin25f394d2018-09-11 16:37:18 -070084
85# Type hinting names.
86T = TypeVar('T')
Yan Wang7af01552019-07-02 15:46:34 -070087NamedTupleMeta = Callable[
88 ..., T] # approximation of a (S : NamedTuple<T> where S() == T) metatype.
Igor Murashkin25f394d2018-09-11 16:37:18 -070089
90def parse_options(argv: List[str] = None):
91 """Parse command line arguments and return an argparse Namespace object."""
Yan Wang7af01552019-07-02 15:46:34 -070092 parser = argparse.ArgumentParser(description="Run one or more Android "
93 "applications under various "
94 "settings in order to measure "
95 "startup time.")
Igor Murashkin25f394d2018-09-11 16:37:18 -070096 # argparse considers args starting with - and -- optional in --help, even though required=True.
97 # by using a named argument group --help will clearly say that it's required instead of optional.
98 required_named = parser.add_argument_group('required named arguments')
Yan Wang7af01552019-07-02 15:46:34 -070099 required_named.add_argument('-p', '--package', action='append',
100 dest='packages',
101 help='package of the application', required=True)
102 required_named.add_argument('-r', '--readahead', action='append',
103 dest='readaheads',
104 help='which readahead mode to use',
105 choices=('warm', 'cold', 'mlock', 'fadvise'),
106 required=True)
Igor Murashkin25f394d2018-09-11 16:37:18 -0700107
108 # optional arguments
109 # use a group here to get the required arguments to appear 'above' the optional arguments in help.
110 optional_named = parser.add_argument_group('optional named arguments')
Yan Wang7af01552019-07-02 15:46:34 -0700111 optional_named.add_argument('-c', '--compiler-filter', action='append',
112 dest='compiler_filters',
113 help='which compiler filter to use. if omitted it does not enforce the app\'s compiler filter',
114 choices=('speed', 'speed-profile', 'quicken'))
115 optional_named.add_argument('-s', '--simulate', dest='simulate',
116 action='store_true',
117 help='Print which commands will run, but don\'t run the apps')
118 optional_named.add_argument('-d', '--debug', dest='debug',
119 action='store_true',
120 help='Add extra debugging output')
121 optional_named.add_argument('-o', '--output', dest='output', action='store',
122 help='Write CSV output to file.')
123 optional_named.add_argument('-t', '--timeout', dest='timeout', action='store',
124 type=int, default=10,
125 help='Timeout after this many seconds when executing a single run.')
126 optional_named.add_argument('-lc', '--loop-count', dest='loop_count',
127 default=1, type=int, action='store',
128 help='How many times to loop a single run.')
129 optional_named.add_argument('-in', '--inodes', dest='inodes', type=str,
130 action='store',
131 help='Path to inodes file (system/extras/pagecache/pagecache.py -d inodes)')
Yan Wang6ddab322019-07-26 10:53:45 -0700132 optional_named.add_argument('--compiler-trace-duration-ms',
133 dest='trace_duration',
134 type=lambda ms_str: timedelta(milliseconds=int(ms_str)),
135 action='append',
136 help='The trace duration (milliseconds) in '
137 'compilation')
Igor Murashkin25f394d2018-09-11 16:37:18 -0700138
139 return parser.parse_args(argv)
140
Igor Murashkin25f394d2018-09-11 16:37:18 -0700141def key_to_cmdline_flag(key: str) -> str:
142 """Convert key into a command line flag, e.g. 'foo-bars' -> '--foo-bar' """
143 if key.endswith("s"):
144 key = key[:-1]
145 return "--" + key.replace("_", "-")
146
147def as_run_command(tpl: NamedTuple) -> List[Union[str, Any]]:
148 """
149 Convert a named tuple into a command-line compatible arguments list.
150
151 Example: ABC(1, 2, 3) -> ['--a', 1, '--b', 2, '--c', 3]
152 """
153 args = []
154 for key, value in tpl._asdict().items():
155 if value is None:
156 continue
157 args.append(key_to_cmdline_flag(key))
158 args.append(value)
159 return args
160
Yan Wang6ddab322019-07-26 10:53:45 -0700161def run_perfetto_collector(collector_info: CollectorPackageInfo,
162 timeout: int,
163 simulate: bool) -> Tuple[bool, TextIO]:
164 """Run collector to collect prefetching trace.
Igor Murashkin25f394d2018-09-11 16:37:18 -0700165
Yan Wang6ddab322019-07-26 10:53:45 -0700166 Returns:
167 A tuple of whether the collection succeeds and the generated trace file.
168 """
169 tmp_output_file = tempfile.NamedTemporaryFile()
Igor Murashkin25f394d2018-09-11 16:37:18 -0700170
Yan Wang6ddab322019-07-26 10:53:45 -0700171 collector = PerfettoTraceCollector(package=collector_info.package,
172 activity=None,
173 compiler_filter=collector_info.compiler_filter,
174 timeout=timeout,
175 simulate=simulate,
176 trace_duration=_PERFETTO_TRACE_DURATION,
177 save_destination_file_path=tmp_output_file.name)
178 result = collector.run()
179
180 return result is not None, tmp_output_file
Igor Murashkin25f394d2018-09-11 16:37:18 -0700181
Igor Murashkinab37e6e2019-05-13 16:31:25 -0700182def parse_run_script_csv_file(csv_file: TextIO) -> DataFrame:
183 """Parse a CSV file full of integers into a DataFrame."""
184 csv_reader = csv.reader(csv_file)
185
186 try:
187 header_list = next(csv_reader)
188 except StopIteration:
189 header_list = []
190
191 if not header_list:
192 return None
193
194 headers = [i for i in header_list]
195
196 d = {}
197 for row in csv_reader:
198 header_idx = 0
199
200 for i in row:
201 v = i
202 if i:
203 v = int(i)
204
205 header_key = headers[header_idx]
206 l = d.get(header_key, [])
207 l.append(v)
208 d[header_key] = l
209
210 header_idx = header_idx + 1
211
212 return DataFrame(d)
213
Yan Wang6ddab322019-07-26 10:53:45 -0700214def compile_perfetto_trace(inodes_path: str,
215 perfetto_trace_file: str,
216 trace_duration: Optional[timedelta]) -> TextIO:
217 compiler_trace_file = tempfile.NamedTemporaryFile()
218 argv = [_COMPILER_SCRIPT, '-i', inodes_path, '--perfetto-trace',
219 perfetto_trace_file, '-o', compiler_trace_file.name]
220
221 if trace_duration is not None:
222 argv += ['--duration', str(int(trace_duration.total_seconds()
223 * PerfettoTraceCollector.MS_PER_SEC))]
224
225 print_utils.debug_print(argv)
226 compiler.main(argv)
227 return compiler_trace_file
228
229def execute_run_using_perfetto_trace(collector_info,
230 run_combos: Iterable[RunCommandArgs],
231 simulate: bool,
232 inodes_path: str,
233 timeout: int) -> DataFrame:
234 """ Executes run based on perfetto trace. """
235 passed, perfetto_trace_file = run_perfetto_collector(collector_info,
236 timeout,
237 simulate)
238 if not passed:
239 raise RuntimeError('Cannot run perfetto collector!')
240
241 with perfetto_trace_file:
242 for combos in run_combos:
243 if combos.readahead in _TRACING_READAHEADS:
244 if simulate:
245 compiler_trace_file = tempfile.NamedTemporaryFile()
246 else:
247 compiler_trace_file = compile_perfetto_trace(inodes_path,
248 perfetto_trace_file.name,
249 combos.trace_duration)
250 with compiler_trace_file:
251 combos = combos._replace(input=compiler_trace_file.name)
252 print_utils.debug_print(combos)
253 output = PrefetchAppRunner(**combos._asdict()).run()
254 else:
255 print_utils.debug_print(combos)
256 output = PrefetchAppRunner(**combos._asdict()).run()
257
258 yield DataFrame(dict((x, [y]) for x, y in output)) if output else None
259
Yan Wang7af01552019-07-02 15:46:34 -0700260def execute_run_combos(
Yan Wang06f54882019-07-23 18:09:41 -0700261 grouped_run_combos: Iterable[Tuple[CollectorPackageInfo, Iterable[RunCommandArgs]]],
Yan Wang7af01552019-07-02 15:46:34 -0700262 simulate: bool,
263 inodes_path: str,
264 timeout: int):
Igor Murashkin25f394d2018-09-11 16:37:18 -0700265 # nothing will work if the screen isn't unlocked first.
Yan Wang7af01552019-07-02 15:46:34 -0700266 cmd_utils.execute_arbitrary_command([_UNLOCK_SCREEN_SCRIPT],
267 timeout,
268 simulate=simulate,
269 shell=False)
Igor Murashkin25f394d2018-09-11 16:37:18 -0700270
271 for collector_info, run_combos in grouped_run_combos:
Yan Wang6ddab322019-07-26 10:53:45 -0700272 yield from execute_run_using_perfetto_trace(collector_info,
273 run_combos,
274 simulate,
275 inodes_path,
276 timeout)
Igor Murashkin25f394d2018-09-11 16:37:18 -0700277
Yan Wang7af01552019-07-02 15:46:34 -0700278def gather_results(commands: Iterable[Tuple[DataFrame]],
279 key_list: List[str], value_list: List[Tuple[str, ...]]):
280 print_utils.debug_print("gather_results: key_list = ", key_list)
Igor Murashkin25f394d2018-09-11 16:37:18 -0700281 stringify_none = lambda s: s is None and "<none>" or s
Yan Wang7af01552019-07-02 15:46:34 -0700282 # yield key_list + ["time(ms)"]
283 for (run_result_list, values) in itertools.zip_longest(commands, value_list):
284 print_utils.debug_print("run_result_list = ", run_result_list)
285 print_utils.debug_print("values = ", values)
Igor Murashkin25f394d2018-09-11 16:37:18 -0700286
Yan Wang7af01552019-07-02 15:46:34 -0700287 if not run_result_list:
Igor Murashkin25f394d2018-09-11 16:37:18 -0700288 continue
Igor Murashkin25f394d2018-09-11 16:37:18 -0700289
Igor Murashkinab37e6e2019-05-13 16:31:25 -0700290 # RunCommandArgs(package='com.whatever', readahead='warm', compiler_filter=None)
291 # -> {'package':['com.whatever'], 'readahead':['warm'], 'compiler_filter':[None]}
Yan Wang7af01552019-07-02 15:46:34 -0700292 values_dict = {}
293 for k, v in values._asdict().items():
294 if not k in key_list:
295 continue
296 values_dict[k] = [stringify_none(v)]
Igor Murashkinab37e6e2019-05-13 16:31:25 -0700297
298 values_df = DataFrame(values_dict)
299 # project 'values_df' to be same number of rows as run_result_list.
300 values_df = values_df.repeat(run_result_list.data_row_len)
301
302 # the results are added as right-hand-side columns onto the existing labels for the table.
303 values_df.merge_data_columns(run_result_list)
304
305 yield values_df
Igor Murashkin25f394d2018-09-11 16:37:18 -0700306
307def eval_and_save_to_csv(output, annotated_result_values):
Igor Murashkinab37e6e2019-05-13 16:31:25 -0700308 printed_header = False
309
Igor Murashkin25f394d2018-09-11 16:37:18 -0700310 csv_writer = csv.writer(output)
311 for row in annotated_result_values:
Igor Murashkinab37e6e2019-05-13 16:31:25 -0700312 if not printed_header:
313 headers = row.headers
314 csv_writer.writerow(headers)
315 printed_header = True
316 # TODO: what about when headers change?
317
318 for data_row in row.data_table:
Yan Wang7af01552019-07-02 15:46:34 -0700319 data_row = [d for d in data_row]
Igor Murashkinab37e6e2019-05-13 16:31:25 -0700320 csv_writer.writerow(data_row)
321
Yan Wang7af01552019-07-02 15:46:34 -0700322 output.flush() # see the output live.
323
324def coerce_to_list(opts: dict):
325 """Tranform values of the dictionary to list.
326 For example:
327 1 -> [1], None -> [None], [1,2,3] -> [1,2,3]
328 [[1],[2]] -> [[1],[2]], {1:1, 2:2} -> [{1:1, 2:2}]
329 """
330 result = {}
331 for key in opts:
332 val = opts[key]
333 result[key] = val if issubclass(type(val), list) else [val]
334 return result
Igor Murashkin25f394d2018-09-11 16:37:18 -0700335
336def main():
337 global _debug
338
339 opts = parse_options()
340 _debug = opts.debug
341 if _DEBUG_FORCE is not None:
342 _debug = _DEBUG_FORCE
Yan Wang7af01552019-07-02 15:46:34 -0700343
344 print_utils.DEBUG = _debug
345 cmd_utils.SIMULATE = opts.simulate
346
347 print_utils.debug_print("parsed options: ", opts)
Igor Murashkin25f394d2018-09-11 16:37:18 -0700348
349 output_file = opts.output and open(opts.output, 'w') or sys.stdout
350
Yan Wang7af01552019-07-02 15:46:34 -0700351 combos = lambda: args_utils.generate_run_combinations(
Yan Wang06f54882019-07-23 18:09:41 -0700352 RunCommandArgs,
Yan Wang7af01552019-07-02 15:46:34 -0700353 coerce_to_list(vars(opts)),
354 opts.loop_count)
355 print_utils.debug_print_gen("run combinations: ", combos())
Igor Murashkin25f394d2018-09-11 16:37:18 -0700356
Yan Wang7af01552019-07-02 15:46:34 -0700357 grouped_combos = lambda: args_utils.generate_group_run_combinations(combos(),
358 CollectorPackageInfo)
Igor Murashkin25f394d2018-09-11 16:37:18 -0700359
Yan Wang7af01552019-07-02 15:46:34 -0700360 print_utils.debug_print_gen("grouped run combinations: ", grouped_combos())
361 exec = execute_run_combos(grouped_combos(),
362 opts.simulate,
363 opts.inodes,
364 opts.timeout)
365
Igor Murashkin25f394d2018-09-11 16:37:18 -0700366 results = gather_results(exec, _COMBINATORIAL_OPTIONS, combos())
Yan Wang7af01552019-07-02 15:46:34 -0700367
Igor Murashkin25f394d2018-09-11 16:37:18 -0700368 eval_and_save_to_csv(output_file, results)
369
Yan Wang7af01552019-07-02 15:46:34 -0700370 return 1
Igor Murashkin25f394d2018-09-11 16:37:18 -0700371
372if __name__ == '__main__':
373 sys.exit(main())