blob: 7e9773579c740423cae9efb6571782ba7ad9bff5 [file] [log] [blame]
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +01001#!/usr/bin/env python3
2# Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
3# for details. All rights reserved. Use of this source code is governed by a
4# BSD-style license that can be found in the LICENSE file.
5
6import argparse
Christoffer Quist Adamsen85f77482022-08-18 14:31:41 +02007import datetime
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +01008import os
Christoffer Quist Adamsen85f77482022-08-18 14:31:41 +02009import re
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +020010import statistics
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +010011import sys
12import time
13
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +010014sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
15
16import adb_utils
Christoffer Quist Adamsen3f7e4282022-04-21 13:12:31 +020017import apk_utils
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +010018import perfetto_utils
19import utils
20
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020021
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +010022def setup(options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020023 # Increase screen off timeout to avoid device screen turns off.
24 twenty_four_hours_in_millis = 24 * 60 * 60 * 1000
25 previous_screen_off_timeout = adb_utils.get_screen_off_timeout(
26 options.device_id)
27 adb_utils.set_screen_off_timeout(twenty_four_hours_in_millis,
28 options.device_id)
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +010029
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020030 # Unlock device.
31 adb_utils.unlock(options.device_id, options.device_pin)
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +010032
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020033 teardown_options = {
34 'previous_screen_off_timeout': previous_screen_off_timeout
35 }
36 return teardown_options
37
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +010038
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +020039def teardown(options, teardown_options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020040 # Reset screen off timeout.
41 adb_utils.set_screen_off_timeout(
42 teardown_options['previous_screen_off_timeout'], options.device_id)
43
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +010044
Christoffer Quist Adamsendab3b202022-08-15 09:36:28 +020045def run_all(apk_or_apks, options, tmp_dir):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020046 # Launch app while collecting information.
47 data_total = {}
48 for iteration in range(1, options.iterations + 1):
49 print('Starting iteration %i' % iteration)
50 out_dir = os.path.join(options.out_dir, str(iteration))
51 teardown_options = setup_for_run(apk_or_apks, out_dir, options)
52 data = run(out_dir, options, tmp_dir)
53 teardown_for_run(out_dir, options, teardown_options)
54 add_data(data_total, data)
55 print('Result:')
56 print(data)
57 print(compute_data_summary(data_total))
58 print('Done')
59 print('Average result:')
60 data_summary = compute_data_summary(data_total)
61 print(data_summary)
62 write_data_to_dir(options.out_dir, data_summary)
63 if options.out:
64 write_data_to_file(options.out, data_summary)
65
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +010066
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +020067def compute_data_summary(data_total):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020068 data_summary = {}
69 for key, value in data_total.items():
70 if not isinstance(value, list):
71 data_summary[key] = value
72 continue
73 data_summary['%s_avg' % key] = round(statistics.mean(value), 1)
74 data_summary['%s_med' % key] = statistics.median(value)
75 data_summary['%s_min' % key] = min(value)
76 data_summary['%s_max' % key] = max(value)
77 return data_summary
78
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +020079
Christoffer Quist Adamsendab3b202022-08-15 09:36:28 +020080def setup_for_run(apk_or_apks, out_dir, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020081 adb_utils.root(options.device_id)
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +020082
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020083 print('Installing')
84 adb_utils.uninstall(options.app_id, options.device_id)
85 if apk_or_apks['apk']:
86 adb_utils.install(apk_or_apks['apk'], options.device_id)
Christoffer Quist Adamsen3f7e4282022-04-21 13:12:31 +020087 else:
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020088 assert apk_or_apks['apks']
89 adb_utils.install_apks(apk_or_apks['apks'], options.device_id)
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +020090
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020091 os.makedirs(out_dir, exist_ok=True)
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +020092
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +020093 # Grant notifications.
94 if options.grant_post_notification_permission:
95 adb_utils.grant(options.app_id, 'android.permission.POST_NOTIFICATIONS',
96 options.device_id)
97
98 # AOT compile.
99 if options.aot:
100 print('AOT compiling')
101 if options.baseline_profile:
102 adb_utils.clear_profile_data(options.app_id, options.device_id)
103 if options.baseline_profile_install == 'adb':
104 adb_utils.install_profile_using_adb(options.app_id,
105 options.baseline_profile,
106 options.device_id)
107 else:
108 assert options.baseline_profile_install == 'profileinstaller'
109 adb_utils.install_profile_using_profileinstaller(
110 options.app_id, options.device_id)
111 else:
112 adb_utils.force_compilation(options.app_id, options.device_id)
113
114 # Cooldown and then unlock device.
115 if options.cooldown > 0:
116 print('Cooling down for %i seconds' % options.cooldown)
117 assert adb_utils.get_screen_state(options.device_id).is_off()
118 time.sleep(options.cooldown)
119 teardown_options = adb_utils.prepare_for_interaction_with_device(
120 options.device_id, options.device_pin)
121 else:
122 teardown_options = None
123
124 # Prelaunch for hot startup.
125 if options.hot_startup:
126 print('Prelaunching')
127 adb_utils.launch_activity(options.app_id,
128 options.main_activity,
129 options.device_id,
130 wait_for_activity_to_launch=False)
131 time.sleep(options.startup_duration)
132 adb_utils.navigate_to_home_screen(options.device_id)
133 time.sleep(1)
134
135 # Drop caches before run.
136 adb_utils.drop_caches(options.device_id)
137 return teardown_options
138
139
140def teardown_for_run(out_dir, options, teardown_options):
141 assert adb_utils.get_screen_state(options.device_id).is_on_and_unlocked()
142
143 if options.capture_screen:
144 target = os.path.join(out_dir, 'screen.png')
145 adb_utils.capture_screen(target, options.device_id)
146
147 if options.cooldown > 0:
148 adb_utils.teardown_after_interaction_with_device(
149 teardown_options, options.device_id)
150 adb_utils.ensure_screen_off(options.device_id)
151 else:
152 assert teardown_options is None
153
154
155def run(out_dir, options, tmp_dir):
156 assert adb_utils.get_screen_state(options.device_id).is_on_and_unlocked()
157
158 # Start logcat for time to fully drawn.
159 logcat_process = None
160 if options.fully_drawn_logcat_message:
161 adb_utils.clear_logcat(options.device_id)
162 logcat_process = adb_utils.start_logcat(
163 options.device_id,
164 format='time',
165 filter='%s ActivityTaskManager:I' %
166 options.fully_drawn_logcat_filter,
167 silent=True)
168
169 # Start perfetto trace collector.
170 perfetto_process = None
171 perfetto_trace_path = None
172 if options.perfetto:
173 perfetto_process, perfetto_trace_path = perfetto_utils.record_android_trace(
174 out_dir, tmp_dir, options.device_id)
175
176 # Launch main activity.
177 launch_activity_result = adb_utils.launch_activity(
Christoffer Quist Adamsen1786a592022-04-21 13:13:04 +0200178 options.app_id,
179 options.main_activity,
180 options.device_id,
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200181 intent_data_uri=options.intent_data_uri,
182 wait_for_activity_to_launch=True)
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200183
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200184 # Wait for app to be fully drawn.
185 logcat = None
186 if logcat_process is not None:
187 wait_until_fully_drawn(logcat_process, options)
188 logcat = adb_utils.stop_logcat(logcat_process)
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200189
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200190 # Wait for perfetto trace collector to stop.
191 if options.perfetto:
192 perfetto_utils.stop_record_android_trace(perfetto_process, out_dir)
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200193
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200194 # Get minor and major page faults from app process.
195 data = compute_data(launch_activity_result, logcat, perfetto_trace_path,
196 options)
197 write_data_to_dir(out_dir, data)
198 return data
Christoffer Quist Adamsened805c52022-08-08 14:46:29 +0200199
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100200
Christoffer Quist Adamsen85f77482022-08-18 14:31:41 +0200201def wait_until_fully_drawn(logcat_process, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200202 print('Waiting until app is fully drawn')
203 while True:
204 is_fully_drawn = any(
205 is_app_fully_drawn_logcat_message(line, options) \
206 for line in logcat_process.lines)
207 if is_fully_drawn:
208 break
209 time.sleep(1)
210 print('Done')
211
Christoffer Quist Adamsen85f77482022-08-18 14:31:41 +0200212
213def compute_time_to_fully_drawn_from_time_to_first_frame(logcat, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200214 displayed_time = None
215 fully_drawn_time = None
216 for line in logcat:
217 if is_app_displayed_logcat_message(line, options):
218 displayed_time = get_timestamp_from_logcat_message(line)
219 elif is_app_fully_drawn_logcat_message(line, options):
220 fully_drawn_time = get_timestamp_from_logcat_message(line)
221 assert displayed_time is not None
222 assert fully_drawn_time is not None
223 assert fully_drawn_time >= displayed_time
224 return fully_drawn_time - displayed_time
225
Christoffer Quist Adamsen85f77482022-08-18 14:31:41 +0200226
227def get_timestamp_from_logcat_message(line):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200228 time_end_index = len('00-00 00:00:00.000')
229 time_format = '%m-%d %H:%M:%S.%f'
230 time_str = line[0:time_end_index] + '000'
231 time_seconds = datetime.datetime.strptime(time_str, time_format).timestamp()
232 return int(time_seconds * 1000)
233
Christoffer Quist Adamsen85f77482022-08-18 14:31:41 +0200234
235def is_app_displayed_logcat_message(line, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200236 substring = 'Displayed %s' % adb_utils.get_component_name(
237 options.app_id, options.main_activity)
238 return substring in line
239
Christoffer Quist Adamsen85f77482022-08-18 14:31:41 +0200240
241def is_app_fully_drawn_logcat_message(line, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200242 return re.search(options.fully_drawn_logcat_message, line)
243
Christoffer Quist Adamsen85f77482022-08-18 14:31:41 +0200244
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200245def add_data(data_total, data):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200246 for key, value in data.items():
247 if key == 'app_id':
248 assert data_total.get(key, value) == value
249 data_total[key] = value
250 if key == 'time':
251 continue
252 if key in data_total:
253 if key == 'app_id':
254 assert data_total[key] == value
255 else:
256 existing_value = data_total[key]
257 assert isinstance(value, int)
258 assert isinstance(existing_value, list)
259 existing_value.append(value)
260 else:
261 assert isinstance(value, int), key
262 data_total[key] = [value]
263
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100264
Christoffer Quist Adamsen85f77482022-08-18 14:31:41 +0200265def compute_data(launch_activity_result, logcat, perfetto_trace_path, options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200266 minfl, majfl = adb_utils.get_minor_major_page_faults(
267 options.app_id, options.device_id)
268 meminfo = adb_utils.get_meminfo(options.app_id, options.device_id)
269 data = {
270 'app_id': options.app_id,
271 'time': time.ctime(time.time()),
272 'minfl': minfl,
273 'majfl': majfl
274 }
275 data.update(meminfo)
276 startup_data = compute_startup_data(launch_activity_result, logcat,
277 perfetto_trace_path, options)
278 return data | startup_data
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100279
Christoffer Quist Adamsen85f77482022-08-18 14:31:41 +0200280
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200281def compute_startup_data(launch_activity_result, logcat, perfetto_trace_path,
282 options):
283 time_to_first_frame = launch_activity_result.get('total_time')
284 startup_data = {'adb_startup': time_to_first_frame}
Christoffer Quist Adamsen85f77482022-08-18 14:31:41 +0200285
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200286 # Time to fully drawn.
287 if options.fully_drawn_logcat_message:
288 startup_data['time_to_fully_drawn'] = \
289 compute_time_to_fully_drawn_from_time_to_first_frame(logcat, options) \
290 + time_to_first_frame
Christoffer Quist Adamsenea091052022-03-16 13:28:20 +0100291
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200292 # Perfetto stats.
293 perfetto_startup_data = {}
294 if options.perfetto:
295 TraceProcessor = perfetto_utils.get_trace_processor()
296 trace_processor = TraceProcessor(file_path=perfetto_trace_path)
Christoffer Quist Adamsenea091052022-03-16 13:28:20 +0100297
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200298 # Compute time to first frame according to the builtin android_startup
299 # metric.
300 startup_metric = trace_processor.metric(['android_startup'])
301 time_to_first_frame_ms = \
302 startup_metric.android_startup.startup[0].to_first_frame.dur_ms
303 perfetto_startup_data['perfetto_startup'] = round(
304 time_to_first_frame_ms)
Christoffer Quist Adamsenea091052022-03-16 13:28:20 +0100305
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200306 if not options.hot_startup:
307 # Compute time to first and last doFrame event.
308 bind_application_slice = perfetto_utils.find_unique_slice_by_name(
309 'bindApplication', options, trace_processor)
310 activity_start_slice = perfetto_utils.find_unique_slice_by_name(
311 'activityStart', options, trace_processor)
312 do_frame_slices = perfetto_utils.find_slices_by_name(
313 'Choreographer#doFrame', options, trace_processor)
314 first_do_frame_slice = next(do_frame_slices)
315 *_, last_do_frame_slice = do_frame_slices
Christoffer Quist Adamsenea091052022-03-16 13:28:20 +0100316
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200317 perfetto_startup_data.update({
318 'time_to_first_choreographer_do_frame_ms':
319 round(
320 perfetto_utils.get_slice_end_since_start(
321 first_do_frame_slice, bind_application_slice)),
322 'time_to_last_choreographer_do_frame_ms':
323 round(
324 perfetto_utils.get_slice_end_since_start(
325 last_do_frame_slice, bind_application_slice))
326 })
327
328 # Return combined startup data.
329 return startup_data | perfetto_startup_data
330
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100331
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +0100332def write_data_to_dir(out_dir, data):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200333 data_path = os.path.join(out_dir, 'data.txt')
334 write_data_to_file(data_path, data)
335
Christoffer Quist Adamseneb29bfe2023-02-21 14:50:22 +0100336
337def write_data_to_file(out_file, data):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200338 with open(out_file, 'w') as f:
339 for key, value in data.items():
340 f.write('%s=%s\n' % (key, str(value)))
341
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100342
343def parse_options(argv):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200344 result = argparse.ArgumentParser(
345 description='Generate a perfetto trace file.')
346 result.add_argument('--app-id',
347 help='The application ID of interest',
348 required=True)
349 result.add_argument('--aot',
350 help='Enable force compilation',
351 default=False,
352 action='store_true')
353 result.add_argument('--apk', help='Path to the .apk')
354 result.add_argument('--apks', help='Path to the .apks')
355 result.add_argument('--bundle', help='Path to the .aab')
356 result.add_argument('--capture-screen',
357 help='Take a screenshot after each test',
358 default=False,
359 action='store_true')
360 result.add_argument('--cooldown',
361 help='Seconds to wait before running each iteration',
362 default=0,
363 type=int)
364 result.add_argument('--device-id', help='Device id (e.g., emulator-5554).')
365 result.add_argument('--device-pin', help='Device pin code (e.g., 1234)')
366 result.add_argument('--fully-drawn-logcat-filter',
367 help='Logcat filter for the fully drawn message '
368 '(e.g., "tag:I")')
369 result.add_argument('--fully-drawn-logcat-message',
370 help='Logcat message that indicates that the app is '
371 'fully drawn (regexp)')
372 result.add_argument('--grant-post-notification-permission',
373 help='Grants the android.permission.POST_NOTIFICATIONS '
374 'permission before launching the app',
375 default=False,
376 action='store_true')
377 result.add_argument('--hot-startup',
378 help='Measure hot startup instead of cold startup',
379 default=False,
380 action='store_true')
381 result.add_argument('--intent-data-uri',
382 help='Value to use for the -d argument to the intent '
383 'that is used to launch the app')
384 result.add_argument('--iterations',
385 help='Number of traces to generate',
386 default=1,
387 type=int)
388 result.add_argument('--main-activity',
389 help='Main activity class name',
390 required=True)
391 result.add_argument('--no-perfetto',
392 help='Disables perfetto trace generation',
393 action='store_true',
394 default=False)
395 result.add_argument('--out', help='File to store result in')
396 result.add_argument('--out-dir',
397 help='Directory to store trace files in',
398 required=True)
399 result.add_argument('--baseline-profile',
400 help='Baseline profile (.prof) in binary format')
401 result.add_argument('--baseline-profile-metadata',
402 help='Baseline profile metadata (.profm) in binary '
403 'format')
404 result.add_argument('--baseline-profile-install',
405 help='Whether to install profile using adb or '
406 'profileinstaller',
407 choices=['adb', 'profileinstaller'],
408 default='profileinstaller')
409 result.add_argument('--startup-duration',
410 help='Duration in seconds before shutting down app',
411 default=15,
412 type=int)
413 options, args = result.parse_known_args(argv)
414 setattr(options, 'perfetto', not options.no_perfetto)
Christoffer Quist Adamsendab3b202022-08-15 09:36:28 +0200415
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200416 paths = [
417 path for path in [options.apk, options.apks, options.bundle]
418 if path is not None
419 ]
420 assert len(paths) == 1, 'Expected exactly one .apk, .apks, or .aab file.'
Christoffer Quist Adamsendab3b202022-08-15 09:36:28 +0200421
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200422 # Build .apks file up front to avoid building the bundle upon each install.
423 if options.bundle:
424 os.makedirs(options.out_dir, exist_ok=True)
425 options.apks = os.path.join(options.out_dir, 'Bundle.apks')
426 adb_utils.build_apks_from_bundle(options.bundle,
427 options.apks,
428 overwrite=True)
429 del options.bundle
Christoffer Quist Adamsendab3b202022-08-15 09:36:28 +0200430
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200431 # Profile is only used with --aot.
432 assert options.aot or not options.baseline_profile
Christoffer Quist Adamsendab3b202022-08-15 09:36:28 +0200433
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200434 # Fully drawn logcat filter and message is absent or both present.
435 assert (options.fully_drawn_logcat_filter is None) == \
436 (options.fully_drawn_logcat_message is None)
Christoffer Quist Adamsen85f77482022-08-18 14:31:41 +0200437
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200438 return options, args
439
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100440
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200441def global_setup(options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200442 # If there is no cooldown then unlock the screen once. Otherwise we turn off
443 # the screen during the cooldown and unlock the screen before each iteration.
444 teardown_options = None
445 if options.cooldown == 0:
446 teardown_options = adb_utils.prepare_for_interaction_with_device(
447 options.device_id, options.device_pin)
448 assert adb_utils.get_screen_state(options.device_id).is_on()
449 else:
450 adb_utils.ensure_screen_off(options.device_id)
451 return teardown_options
452
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200453
454def global_teardown(options, teardown_options):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200455 if options.cooldown == 0:
456 adb_utils.teardown_after_interaction_with_device(
457 teardown_options, options.device_id)
458 else:
459 assert teardown_options is None
460
Christoffer Quist Adamsen1a459b72022-05-11 12:09:03 +0200461
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100462def main(argv):
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200463 (options, args) = parse_options(argv)
464 with utils.TempDir() as tmp_dir:
465 apk_or_apks = {'apk': options.apk, 'apks': options.apks}
466 if options.baseline_profile \
467 and options.baseline_profile_install == 'profileinstaller':
468 assert not options.apks, 'Unimplemented'
469 apk_or_apks['apk'] = apk_utils.add_baseline_profile_to_apk(
470 options.apk, options.baseline_profile,
471 options.baseline_profile_metadata, tmp_dir)
472 teardown_options = global_setup(options)
473 run_all(apk_or_apks, options, tmp_dir)
474 global_teardown(options, teardown_options)
475
Christoffer Quist Adamsenfad33a02022-03-14 14:45:09 +0100476
477if __name__ == '__main__':
Christoffer Quist Adamsen2434a4d2023-10-16 11:29:03 +0200478 sys.exit(main(sys.argv[1:]))