1#!/usr/bin/env python
2# Copyright (C) 2015 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""Record the event logs during boot and output them to a file.
16
17This script repeats the record of each event log during Android boot specified
18times. By default, interval between measurements is adjusted in such a way that
19CPUs are cooled down sufficiently to avoid boot time slowdown caused by CPU
20thermal throttling. The result is output in a tab-separated value format.
21
22Examples:
23
24Repeat measurements 10 times. Interval between iterations is adjusted based on
25CPU temperature of the device.
26
27$ ./perfboot.py --iterations=10
28
29Repeat measurements 20 times. 60 seconds interval is taken between each
30iteration.
31
32$ ./perfboot.py --iterations=20 --interval=60
33
34Repeat measurements 20 times, show verbose output, output the result to
35data.tsv, and read event tags from eventtags.txt.
36
37$ ./perfboot.py --iterations=30 -v --output=data.tsv --tags=eventtags.txt
38"""
39
40import argparse
41import atexit
42import cStringIO
43import glob
44import inspect
45import logging
46import math
47import os
48import re
49import subprocess
50import sys
51import threading
52import time
53
54sys.path.append(os.path.dirname(os.path.dirname(__file__)))
55import adb
56
57# The default event tags to record.
58_DEFAULT_EVENT_TAGS = [
59    'boot_progress_start',
60    'boot_progress_preload_start',
61    'boot_progress_preload_end',
62    'boot_progress_system_run',
63    'boot_progress_pms_start',
64    'boot_progress_pms_system_scan_start',
65    'boot_progress_pms_data_scan_start',
66    'boot_progress_pms_scan_end',
67    'boot_progress_pms_ready',
68    'boot_progress_ams_ready',
69    'boot_progress_enable_screen',
70    'sf_stop_bootanim',
71    'wm_boot_animation_done',
72]
73
74
75class IntervalAdjuster(object):
76    """A helper class to take suffficient interval between iterations."""
77
78    # CPU temperature values per product used to decide interval
79    _CPU_COOL_DOWN_THRESHOLDS = {
80        'flo': 40,
81        'flounder': 40000,
82        'razor': 40,
83        'volantis': 40000,
84    }
85    # The interval between CPU temperature checks
86    _CPU_COOL_DOWN_WAIT_INTERVAL = 10
87    # The wait time used when the value of _CPU_COOL_DOWN_THRESHOLDS for
88    # the product is not defined.
89    _CPU_COOL_DOWN_WAIT_TIME_DEFAULT = 120
90
91    def __init__(self, interval, device):
92        self._interval = interval
93        self._device = device
94        self._temp_paths = device.shell(
95            ['ls', '/sys/class/thermal/thermal_zone*/temp'])[0].splitlines()
96        self._product = device.get_prop('ro.build.product')
97        self._waited = False
98
99    def wait(self):
100        """Waits certain amount of time for CPUs cool-down."""
101        if self._interval is None:
102            self._wait_cpu_cool_down(self._product, self._temp_paths)
103        else:
104            if self._waited:
105                print 'Waiting for %d seconds' % self._interval
106                time.sleep(self._interval)
107        self._waited = True
108
109    def _get_cpu_temp(self, threshold):
110        max_temp = 0
111        for temp_path in self._temp_paths:
112            temp = int(self._device.shell(['cat', temp_path])[0].rstrip())
113            max_temp = max(max_temp, temp)
114            if temp >= threshold:
115                return temp
116        return max_temp
117
118    def _wait_cpu_cool_down(self, product, temp_paths):
119        threshold = IntervalAdjuster._CPU_COOL_DOWN_THRESHOLDS.get(
120            self._product)
121        if threshold is None:
122            print 'No CPU temperature threshold is set for ' + self._product
123            print ('Just wait %d seconds' %
124                   IntervalAdjuster._CPU_COOL_DOWN_WAIT_TIME_DEFAULT)
125            time.sleep(IntervalAdjuster._CPU_COOL_DOWN_WAIT_TIME_DEFAULT)
126            return
127        while True:
128            temp = self._get_cpu_temp(threshold)
129            if temp < threshold:
130                logging.info('Current CPU temperature %s' % temp)
131                return
132            print 'Waiting until CPU temperature (%d) falls below %d' % (
133                temp, threshold)
134            time.sleep(IntervalAdjuster._CPU_COOL_DOWN_WAIT_INTERVAL)
135
136
137class WatchdogTimer(object):
138    """A timer that makes is_timedout() return true in |timeout| seconds."""
139    def __init__(self, timeout):
140        self._timedout = False
141
142        def notify_timeout():
143            self._timedout = True
144        self._timer = threading.Timer(timeout, notify_timeout)
145        self._timer.daemon = True
146        self._timer.start()
147
148    def is_timedout(self):
149        return self._timedout
150
151    def cancel(self):
152        self._timer.cancel()
153
154
155def readlines_unbuffered(proc):
156    """Read lines from |proc|'s standard out without buffering."""
157    while True:
158        buf = []
159        c = proc.stdout.read(1)
160        if c == '' and proc.poll() is not None:
161            break
162        while c != '\n':
163            if c == '' and proc.poll() is not None:
164                break
165            buf.append(c)
166            c = proc.stdout.read(1)
167        yield ''.join(buf)
168
169
170def disable_dropbox(device):
171    """Removes the files created by Dropbox and avoids creating the files."""
172    device.root()
173    device.wait()
174    device.shell(['rm', '-rf', '/system/data/dropbox'])
175    original_dropbox_max_files = device.shell(
176        ['settings', 'get', 'global', 'dropbox_max_files'])[0].rstrip()
177    device.shell(['settings', 'put', 'global', 'dropbox_max_files', '0'])
178    return original_dropbox_max_files
179
180
181def restore_dropbox(device, original_dropbox_max_files):
182    """Restores the dropbox_max_files setting."""
183    device.root()
184    device.wait()
185    if original_dropbox_max_files == 'null':
186        device.shell(['settings', 'delete', 'global', 'dropbox_max_files'])
187    else:
188        device.shell(['settings', 'put', 'global', 'dropbox_max_files',
189                      original_dropbox_max_files])
190
191
192def init_perf(device, output, record_list, tags):
193    device.wait()
194    debuggable = device.get_prop('ro.debuggable')
195    original_dropbox_max_files = None
196    if debuggable == '1':
197        # Workaround for Dropbox issue (http://b/20890386).
198        original_dropbox_max_files = disable_dropbox(device)
199
200    def cleanup():
201        try:
202            if record_list:
203                print_summary(record_list, tags[-1])
204                output_results(output, record_list, tags)
205            if original_dropbox_max_files is not None:
206                restore_dropbox(device, original_dropbox_max_files)
207        except (subprocess.CalledProcessError, RuntimeError):
208            pass
209    atexit.register(cleanup)
210
211
212def check_dm_verity_settings(device):
213    device.wait()
214    for partition in ['system', 'vendor']:
215        verity_mode = device.get_prop('partition.%s.verified' % partition)
216        if verity_mode is None:
217            logging.warning('dm-verity is not enabled for /%s. Did you run '
218                            'adb disable-verity? That may skew the result.',
219                            partition)
220
221
222def read_event_tags(tags_file):
223    """Reads event tags from |tags_file|."""
224    if not tags_file:
225        return _DEFAULT_EVENT_TAGS
226    tags = []
227    with open(tags_file) as f:
228        for line in f:
229            if '#' in line:
230                line = line[:line.find('#')]
231            line = line.strip()
232            if line:
233                tags.append(line)
234    return tags
235
236
237def make_event_tags_re(tags):
238    """Makes a regular expression object that matches event logs of |tags|."""
239    return re.compile(r'(?P<pid>[0-9]+) +[0-9]+ I (?P<tag>%s): (?P<time>\d+)' %
240                      '|'.join(tags))
241
242
243def filter_event_tags(tags, device):
244    """Drop unknown tags not listed in device's event-log-tags file."""
245    device.wait()
246    supported_tags = set()
247    for l in device.shell(
248        ['cat', '/system/etc/event-log-tags'])[0].splitlines():
249        tokens = l.split(' ')
250        if len(tokens) >= 2:
251            supported_tags.add(tokens[1])
252    filtered = []
253    for tag in tags:
254        if tag in supported_tags:
255            filtered.append(tag)
256        else:
257            logging.warning('Unknown tag \'%s\'. Ignoring...', tag)
258    return filtered
259
260
261def get_values(record, tag):
262    """Gets values that matches |tag| from |record|."""
263    keys = [key for key in record.keys() if key[0] == tag]
264    return [record[k] for k in sorted(keys)]
265
266
267def get_last_value(record, tag):
268    """Gets the last value that matches |tag| from |record|."""
269    values = get_values(record, tag)
270    if not values:
271        return 0
272    return values[-1]
273
274
275def output_results(filename, record_list, tags):
276    """Outputs |record_list| into |filename| in a TSV format."""
277    # First, count the number of the values of each tag.
278    # This is for dealing with events that occur multiple times.
279    # For instance, boot_progress_preload_start and boot_progress_preload_end
280    # are recorded twice on 64-bit system. One is for 64-bit zygote process
281    # and the other is for 32-bit zygote process.
282    values_counter = {}
283    for record in record_list:
284        for tag in tags:
285            # Some record might lack values for some tags due to unanticipated
286            # problems (e.g. timeout), so take the maximum count among all the
287            # record.
288            values_counter[tag] = max(values_counter.get(tag, 1),
289                                      len(get_values(record, tag)))
290
291    # Then creates labels for the data. If there are multiple values for one
292    # tag, labels for these values are numbered except the first one as
293    # follows:
294    #
295    # event_tag event_tag2 event_tag3
296    #
297    # The corresponding values are sorted in an ascending order of PID.
298    labels = []
299    for tag in tags:
300        for i in range(1, values_counter[tag] + 1):
301            labels.append('%s%s' % (tag, '' if i == 1 else str(i)))
302
303    # Finally write the data into the file.
304    with open(filename, 'w') as f:
305        f.write('\t'.join(labels) + '\n')
306        for record in record_list:
307            line = cStringIO.StringIO()
308            invalid_line = False
309            for i, tag in enumerate(tags):
310                if i != 0:
311                    line.write('\t')
312                values = get_values(record, tag)
313                if len(values) < values_counter[tag]:
314                    invalid_line = True
315                    # Fill invalid record with 0
316                    values += [0] * (values_counter[tag] - len(values))
317                line.write('\t'.join(str(t) for t in values))
318            if invalid_line:
319                logging.error('Invalid record found: ' + line.getvalue())
320            line.write('\n')
321            f.write(line.getvalue())
322    print 'Wrote: ' + filename
323
324
325def median(data):
326    """Calculates the median value from |data|."""
327    data = sorted(data)
328    n = len(data)
329    if n % 2 == 1:
330        return data[n / 2]
331    else:
332        n2 = n / 2
333        return (data[n2 - 1] + data[n2]) / 2.0
334
335
336def mean(data):
337    """Calculates the mean value from |data|."""
338    return float(sum(data)) / len(data)
339
340
341def stddev(data):
342    """Calculates the standard deviation value from |value|."""
343    m = mean(data)
344    return math.sqrt(sum((x - m) ** 2 for x in data) / len(data))
345
346
347def print_summary(record_list, end_tag):
348    """Prints the summary of |record_list|."""
349    # Filter out invalid data.
350    end_times = [get_last_value(record, end_tag) for record in record_list
351                 if get_last_value(record, end_tag) != 0]
352    print 'mean: ', mean(end_times)
353    print 'median:', median(end_times)
354    print 'standard deviation:', stddev(end_times)
355
356
357def do_iteration(device, interval_adjuster, event_tags_re, end_tag):
358    """Measures the boot time once."""
359    device.wait()
360    interval_adjuster.wait()
361    device.reboot()
362    print 'Rebooted the device'
363    record = {}
364    booted = False
365    while not booted:
366        device.wait()
367        # Stop the iteration if it does not finish within 120 seconds.
368        timeout = 120
369        t = WatchdogTimer(timeout)
370        p = subprocess.Popen(
371                ['adb', 'logcat', '-b', 'events', '-v', 'threadtime'],
372                stdout=subprocess.PIPE)
373        for line in readlines_unbuffered(p):
374            if t.is_timedout():
375                print '*** Timed out ***'
376                return record
377            m = event_tags_re.search(line)
378            if not m:
379                continue
380            tag = m.group('tag')
381            event_time = int(m.group('time'))
382            pid = m.group('pid')
383            record[(tag, pid)] = event_time
384            print 'Event log recorded: %s (%s) - %d ms' % (
385                tag, pid, event_time)
386            if tag == end_tag:
387                booted = True
388                t.cancel()
389                break
390    return record
391
392
393def parse_args():
394    """Parses the command line arguments."""
395    parser = argparse.ArgumentParser(
396        description=inspect.getdoc(sys.modules[__name__]),
397        formatter_class=argparse.RawDescriptionHelpFormatter)
398    parser.add_argument('--iterations', type=int, default=5,
399                        help='Number of times to repeat boot measurements.')
400    parser.add_argument('--interval', type=int,
401                        help=('Duration between iterations. If this is not '
402                              'set explicitly, durations are determined '
403                              'adaptively based on CPUs temperature.'))
404    parser.add_argument('-o', '--output', help='File name of output data.')
405    parser.add_argument('-v', '--verbose', action='store_true',
406                        help='Show verbose output.')
407    parser.add_argument('-s', '--serial', default=os.getenv('ANDROID_SERIAL'),
408                        help='Adb device serial number.')
409    parser.add_argument('-t', '--tags', help='Specify the filename from which '
410                        'event tags are read. Every line contains one event '
411                        'tag and the last event tag is used to detect that '
412                        'the device has finished booting unless --end-tag is '
413                        'specified.')
414    parser.add_argument('--end-tag', help='An event tag on which the script '
415                        'stops measuring the boot time.')
416    parser.add_argument('--apk-dir', help='Specify the directory which contains '
417                        'APK files to be installed before measuring boot time.')
418    return parser.parse_args()
419
420
421def install_apks(device, apk_dir):
422    for apk in glob.glob(os.path.join(apk_dir, '*.apk')):
423        print 'Installing: ' + apk
424        device.install(apk, replace=True)
425
426
427def main():
428    args = parse_args()
429    if args.verbose:
430        logging.getLogger().setLevel(logging.INFO)
431
432    device = adb.get_device(args.serial)
433
434    if not args.output:
435        device.wait()
436        args.output = 'perf-%s-%s.tsv' % (
437            device.get_prop('ro.build.flavor'),
438            device.get_prop('ro.build.version.incremental'))
439    check_dm_verity_settings(device)
440
441    if args.apk_dir:
442        install_apks(device, args.apk_dir)
443
444    record_list = []
445    event_tags = filter_event_tags(read_event_tags(args.tags), device)
446    end_tag = args.end_tag or event_tags[-1]
447    if end_tag not in event_tags:
448        sys.exit('%s is not a valid tag.' % end_tag)
449    event_tags = event_tags[0 : event_tags.index(end_tag) + 1]
450    init_perf(device, args.output, record_list, event_tags)
451    interval_adjuster = IntervalAdjuster(args.interval, device)
452    event_tags_re = make_event_tags_re(event_tags)
453
454    for i in range(args.iterations):
455        print 'Run #%d ' % i
456        record = do_iteration(
457            device, interval_adjuster, event_tags_re, end_tag)
458        record_list.append(record)
459
460
461if __name__ == '__main__':
462    main()
463