Support for treating logcat message as fully drawn signal
Change-Id: Id05fbf4c8edcea54cf35848a98de1ce0d53add80
diff --git a/tools/startup/adb_utils.py b/tools/startup/adb_utils.py
index 60a7ee3..c181682 100755
--- a/tools/startup/adb_utils.py
+++ b/tools/startup/adb_utils.py
@@ -150,6 +150,12 @@
'Expected stdout to end with ".apk", was: %s' % stdout)
return apk_path
+def get_component_name(app_id, activity):
+ if activity.startswith(app_id):
+ return '%s/.%s' % (app_id, activity[len(app_id)+1:])
+ else:
+ return '%s/%s' % (app_id, activity)
+
def get_profile_data(app_id, device_id=None):
with utils.TempDir() as temp:
source = get_profile_path(app_id)
@@ -268,18 +274,22 @@
time.sleep(sleep_in_seconds)
def launch_activity(
- app_id, activity, device_id=None, wait_for_activity_to_launch=False):
+ app_id,
+ activity,
+ device_id=None,
+ intent_data_uri=None,
+ wait_for_activity_to_launch=False):
args = ['shell', 'am', 'start', '-n', '%s/%s' % (app_id, activity)]
+ if intent_data_uri:
+ args.extend(['-d', intent_data_uri])
if wait_for_activity_to_launch:
args.append('-W')
cmd = create_adb_cmd(args, device_id)
stdout = subprocess.check_output(cmd).decode('utf-8').strip()
- if activity.startswith(app_id):
- expected_stdout = (
- 'Starting: Intent { cmp=%s/.%s }' % (app_id, activity[len(app_id)+1:]))
- else:
- expected_stdout = 'Starting: Intent { cmp=%s/%s }' % (app_id, activity)
- assert stdout.startswith(expected_stdout), 'was %s, expected %s' % (stdout, expected_stdout)
+ assert stdout.startswith('Starting: Intent {')
+ expected_component = 'cmp=%s' % get_component_name(app_id, activity)
+ assert expected_component in stdout, \
+ 'was %s, expected %s' % (stdout, expected_component)
lines = stdout.splitlines()
result = {}
for line in lines:
@@ -324,10 +334,12 @@
stdout = subprocess.check_output(cmd).decode('utf-8').strip()
assert len(stdout) == 0
-def start_logcat(device_id=None, format=None, filter=None):
+def start_logcat(device_id=None, format=None, filter=None, silent=False):
args = ['logcat']
if format:
args.extend(['--format', format])
+ if silent:
+ args.append('-s')
if filter:
args.append(filter)
cmd = create_adb_cmd(args, device_id)
diff --git a/tools/startup/measure_startup.py b/tools/startup/measure_startup.py
index e00797c..ef529b6 100755
--- a/tools/startup/measure_startup.py
+++ b/tools/startup/measure_startup.py
@@ -4,7 +4,9 @@
# BSD-style license that can be found in the LICENSE file.
import argparse
+import datetime
import os
+import re
import statistics
import sys
import time
@@ -141,6 +143,16 @@
def run(out_dir, options, tmp_dir):
assert adb_utils.get_screen_state(options.device_id).is_on_and_unlocked()
+ # Start logcat for time to fully drawn.
+ logcat_process = None
+ if options.fully_drawn_logcat_message:
+ adb_utils.clear_logcat(options.device_id)
+ logcat_process = adb_utils.start_logcat(
+ options.device_id,
+ format='time',
+ filter='%s ActivityTaskManager:I' % options.fully_drawn_logcat_filter,
+ silent=True)
+
# Start perfetto trace collector.
perfetto_process = None
perfetto_trace_path = None
@@ -153,17 +165,64 @@
options.app_id,
options.main_activity,
options.device_id,
+ intent_data_uri=options.intent_data_uri,
wait_for_activity_to_launch=True)
+ # Wait for app to be fully drawn.
+ logcat = None
+ if logcat_process is not None:
+ wait_until_fully_drawn(logcat_process, options)
+ logcat = adb_utils.stop_logcat(logcat_process)
+
# Wait for perfetto trace collector to stop.
if options.perfetto:
perfetto_utils.stop_record_android_trace(perfetto_process, out_dir)
# Get minor and major page faults from app process.
- data = compute_data(launch_activity_result, perfetto_trace_path, options)
+ data = compute_data(
+ launch_activity_result, logcat, perfetto_trace_path, options)
write_data(out_dir, data)
return data
+def wait_until_fully_drawn(logcat_process, options):
+ print('Waiting until app is fully drawn')
+ while True:
+ is_fully_drawn = any(
+ is_app_fully_drawn_logcat_message(line, options) \
+ for line in logcat_process.lines)
+ if is_fully_drawn:
+ break
+ time.sleep(1)
+ print('Done')
+
+def compute_time_to_fully_drawn_from_time_to_first_frame(logcat, options):
+ displayed_time = None
+ fully_drawn_time = None
+ for line in logcat:
+ if is_app_displayed_logcat_message(line, options):
+ displayed_time = get_timestamp_from_logcat_message(line)
+ elif is_app_fully_drawn_logcat_message(line, options):
+ fully_drawn_time = get_timestamp_from_logcat_message(line)
+ assert displayed_time is not None
+ assert fully_drawn_time is not None
+ assert fully_drawn_time > displayed_time
+ return fully_drawn_time - displayed_time
+
+def get_timestamp_from_logcat_message(line):
+ time_end_index = len('00-00 00:00:00.000')
+ time_format = '%m-%d %H:%M:%S.%f'
+ time_str = line[0:time_end_index] + '000'
+ time_seconds = datetime.datetime.strptime(time_str, time_format).timestamp()
+ return int(time_seconds * 1000)
+
+def is_app_displayed_logcat_message(line, options):
+ substring = 'Displayed %s' % adb_utils.get_component_name(
+ options.app_id, options.main_activity)
+ return substring in line
+
+def is_app_fully_drawn_logcat_message(line, options):
+ return re.search(options.fully_drawn_logcat_message, line)
+
def add_data(data_total, data):
for key, value in data.items():
if key == 'app_id':
@@ -183,7 +242,7 @@
assert isinstance(value, int), key
data_total[key] = [value]
-def compute_data(launch_activity_result, perfetto_trace_path, options):
+def compute_data(launch_activity_result, logcat, perfetto_trace_path, options):
minfl, majfl = adb_utils.get_minor_major_page_faults(
options.app_id, options.device_id)
data = {
@@ -193,13 +252,23 @@
'majfl': majfl
}
startup_data = compute_startup_data(
- launch_activity_result, perfetto_trace_path, options)
+ launch_activity_result, logcat, perfetto_trace_path, options)
return data | startup_data
-def compute_startup_data(launch_activity_result, perfetto_trace_path, options):
+def compute_startup_data(
+ launch_activity_result, logcat, perfetto_trace_path, options):
+ time_to_first_frame = launch_activity_result.get('total_time')
startup_data = {
- 'adb_startup': launch_activity_result.get('total_time')
+ 'adb_startup': time_to_first_frame
}
+
+ # Time to fully drawn.
+ if options.fully_drawn_logcat_message:
+ startup_data['time_to_fully_drawn'] = \
+ compute_time_to_fully_drawn_from_time_to_first_frame(logcat, options) \
+ + time_to_first_frame
+
+ # Perfetto stats.
perfetto_startup_data = {}
if options.perfetto:
trace_processor = TraceProcessor(file_path=perfetto_trace_path)
@@ -272,10 +341,19 @@
help='Device id (e.g., emulator-5554).')
result.add_argument('--device-pin',
help='Device pin code (e.g., 1234)')
+ result.add_argument('--fully-drawn-logcat-filter',
+ help='Logcat filter for the fully drawn message '
+ '(e.g., "tag:I")')
+ result.add_argument('--fully-drawn-logcat-message',
+ help='Logcat message that indicates that the app is '
+ 'fully drawn (regexp)')
result.add_argument('--hot-startup',
help='Measure hot startup instead of cold startup',
default=False,
action='store_true')
+ result.add_argument('--intent-data-uri',
+ help='Value to use for the -d argument to the intent '
+ 'that is used to launch the app')
result.add_argument('--iterations',
help='Number of traces to generate',
default=1,
@@ -315,6 +393,10 @@
# Profile is only used with --aot.
assert options.aot or not options.baseline_profile
+ # Fully drawn logcat filter and message is absent or both present.
+ assert (options.fully_drawn_logcat_filter is None) == \
+ (options.fully_drawn_logcat_message is None)
+
return options, args
def global_setup(options):