1# Copyright 2015 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import optparse 6import platform 7import re 8import sys 9import threading 10import zlib 11 12import py_utils 13 14from devil.android import device_utils 15from devil.android.sdk import version_codes 16from py_trace_event import trace_time as trace_time_module 17from systrace import trace_result 18from systrace import tracing_agents 19from systrace import util 20 21# Text that ADB sends, but does not need to be displayed to the user. 22ADB_IGNORE_REGEXP = r'^capturing trace\.\.\. done|^capturing trace\.\.\.' 23# The number of seconds to wait on output from ADB. 24ADB_STDOUT_READ_TIMEOUT = 0.2 25# The number of seconds to wait for large output from ADB. 26ADB_LARGE_OUTPUT_TIMEOUT = 600 27# The adb shell command to initiate a trace. 28ATRACE_BASE_ARGS = ['atrace'] 29# If a custom list of categories is not specified, traces will include 30# these categories (if available on the device). 31DEFAULT_CATEGORIES = 'am,binder_driver,camera,dalvik,freq,'\ 32 'gfx,hal,idle,input,memory,memreclaim,'\ 33 'res,sched,sync,view,webview,wm,workq' 34# The command to list trace categories. 35LIST_CATEGORIES_ARGS = ATRACE_BASE_ARGS + ['--list_categories'] 36# Minimum number of seconds between displaying status updates. 37MIN_TIME_BETWEEN_STATUS_UPDATES = 0.2 38# ADB sends this text to indicate the beginning of the trace data. 39TRACE_START_REGEXP = r'TRACE\:' 40# Plain-text trace data should always start with this string. 41TRACE_TEXT_HEADER = '# tracer' 42_FIX_MISSING_TGIDS = True 43_FIX_CIRCULAR_TRACES = True 44 45 46def list_categories(config): 47 """List the possible trace event categories. 48 49 This function needs the tracing config since it needs to get the serial 50 number of the device to send a command to. 51 52 Args: 53 config: Tracing config. 54 """ 55 devutils = device_utils.DeviceUtils(config.device_serial_number) 56 categories = devutils.RunShellCommand( 57 LIST_CATEGORIES_ARGS, check_return=True) 58 59 device_sdk_version = util.get_device_sdk_version() 60 if device_sdk_version < version_codes.MARSHMALLOW: 61 # work around platform bug where rs tag would corrupt trace until M(Api23) 62 categories = [c for c in categories if not re.match(r'^\s*rs\s*-', c)] 63 64 print '\n'.join(categories) 65 if not devutils.HasRoot(): 66 print '\nNOTE: more categories may be available with adb root\n' 67 68 69def get_available_categories(config, device_sdk_version): 70 """Gets the list of atrace categories available for tracing. 71 Args: 72 config: Tracing config. 73 device_sdk_version: Sdk version int of device to be queried. 74 """ 75 devutils = device_utils.DeviceUtils(config.device_serial_number) 76 categories_output = devutils.RunShellCommand( 77 LIST_CATEGORIES_ARGS, check_return=True) 78 categories = [c.split('-')[0].strip() for c in categories_output] 79 80 if device_sdk_version < version_codes.MARSHMALLOW: 81 # work around platform bug where rs tag would corrupt trace until M(Api23) 82 categories = [c for c in categories if c != 'rs'] 83 return categories 84 85 86def try_create_agent(config): 87 """Create an Atrace agent. 88 89 Args: 90 config: Command line config. 91 """ 92 if config.target != 'android': 93 return None 94 if config.from_file is not None: 95 return None 96 97 if not config.atrace_categories: 98 return None 99 100 # Check device SDK version. 101 device_sdk_version = util.get_device_sdk_version() 102 if device_sdk_version < version_codes.JELLY_BEAN_MR2: 103 print ('Device SDK versions < 18 (Jellybean MR2) not supported.\n' 104 'Your device SDK version is %d.' % device_sdk_version) 105 return None 106 107 return AtraceAgent(device_sdk_version, 108 util.get_tracing_path(config.device_serial_number)) 109 110def _construct_extra_atrace_args(config, categories): 111 """Construct extra arguments (-a, -k, categories) for atrace command. 112 113 Args: 114 config: Tracing config. 115 """ 116 extra_args = [] 117 118 if config.app_name is not None: 119 extra_args.extend(['-a', config.app_name]) 120 121 if config.kfuncs is not None: 122 extra_args.extend(['-k', config.kfuncs]) 123 124 extra_args.extend(categories) 125 return extra_args 126 127def _construct_atrace_args(config, categories): 128 """Builds the command used to invoke a trace process. 129 Returns: 130 A tuple where the first element is an array of command arguments, and 131 the second element is a boolean which will be true if the command will 132 stream trace data. 133 """ 134 atrace_args = ATRACE_BASE_ARGS[:] 135 136 if config.compress_trace_data: 137 atrace_args.extend(['-z']) 138 139 if (config.trace_time is not None) and (config.trace_time > 0): 140 atrace_args.extend(['-t', str(config.trace_time)]) 141 142 if (config.trace_buf_size is not None) and (config.trace_buf_size > 0): 143 atrace_args.extend(['-b', str(config.trace_buf_size)]) 144 145 elif 'webview' in categories and 'sched' in categories: 146 # https://crbug.com/814330: webview_startup sometimes exceeds the buffer 147 # limit, so doubling this. 148 atrace_args.extend(['-b', '8192']) 149 150 elif 'sched' in categories: 151 # 'sched' is a high-volume tag, double the default buffer size 152 # to accommodate that 153 atrace_args.extend(['-b', '4096']) 154 extra_args = _construct_extra_atrace_args(config, categories) 155 156 atrace_args.extend(extra_args) 157 return atrace_args 158 159 160class AtraceAgent(tracing_agents.TracingAgent): 161 162 def __init__(self, device_sdk_version, tracing_path): 163 super(AtraceAgent, self).__init__() 164 self._device_sdk_version = device_sdk_version 165 self._tracing_path = tracing_path 166 self._adb = None 167 self._trace_data = None 168 self._tracer_args = None 169 self._collection_thread = None 170 self._device_utils = None 171 self._device_serial_number = None 172 self._config = None 173 self._categories = None 174 175 def __repr__(self): 176 return 'atrace' 177 178 @py_utils.Timeout(tracing_agents.START_STOP_TIMEOUT) 179 def StartAgentTracing(self, config, timeout=None): 180 assert config.atrace_categories, 'Atrace categories are missing!' 181 self._config = config 182 self._categories = config.atrace_categories 183 if isinstance(self._categories, list): 184 self._categories = ','.join(self._categories) 185 avail_cats = get_available_categories(config, self._device_sdk_version) 186 unavailable = [x for x in self._categories.split(',') if 187 x not in avail_cats] 188 self._categories = [x for x in self._categories.split(',') if 189 x in avail_cats] 190 if unavailable: 191 print 'These categories are unavailable: ' + ' '.join(unavailable) 192 self._device_utils = device_utils.DeviceUtils(config.device_serial_number) 193 self._device_serial_number = config.device_serial_number 194 self._tracer_args = _construct_atrace_args(config, 195 self._categories) 196 self._device_utils.RunShellCommand( 197 self._tracer_args + ['--async_start'], check_return=True) 198 return True 199 200 def _collect_and_preprocess(self): 201 """Collects and preprocesses trace data. 202 203 Stores results in self._trace_data. 204 """ 205 trace_data = self._collect_trace_data() 206 self._trace_data = self._preprocess_trace_data(trace_data) 207 208 @py_utils.Timeout(tracing_agents.START_STOP_TIMEOUT) 209 def StopAgentTracing(self, timeout=None): 210 """Stops tracing and starts collecting results. 211 212 To synchronously retrieve the results after calling this function, 213 call GetResults(). 214 """ 215 self._collection_thread = threading.Thread( 216 target=self._collect_and_preprocess) 217 self._collection_thread.start() 218 return True 219 220 @py_utils.Timeout(tracing_agents.GET_RESULTS_TIMEOUT) 221 def GetResults(self, timeout=None): 222 """Waits for collection thread to finish and returns trace results.""" 223 self._collection_thread.join() 224 self._collection_thread = None 225 return trace_result.TraceResult('systemTraceEvents', self._trace_data) 226 227 def SupportsExplicitClockSync(self): 228 return True 229 230 def RecordClockSyncMarker(self, sync_id, did_record_sync_marker_callback): 231 """Records a clock sync marker. 232 233 Args: 234 sync_id: ID string for clock sync marker. 235 """ 236 cmd = 'echo trace_event_clock_sync: name=%s >' \ 237 ' %s/trace_marker' % (sync_id, self._tracing_path) 238 with self._device_utils.adb.PersistentShell( 239 self._device_serial_number) as shell: 240 t1 = trace_time_module.Now() 241 shell.RunCommand(cmd, close=True) 242 did_record_sync_marker_callback(t1, sync_id) 243 244 def _stop_collect_trace(self): 245 """Stops atrace. 246 247 Note that prior to Api 23, --async-stop isn't working correctly. It 248 doesn't stop tracing and clears trace buffer before dumping it rendering 249 results unusable.""" 250 if self._device_sdk_version < version_codes.MARSHMALLOW: 251 is_trace_enabled_file = '%s/tracing_on' % self._tracing_path 252 # Stop tracing first so new data won't arrive while dump is performed (it 253 # may take a non-trivial time and tracing buffer may overflow). 254 self._device_utils.WriteFile(is_trace_enabled_file, '0') 255 result = self._device_utils.RunShellCommand( 256 self._tracer_args + ['--async_dump'], raw_output=True, 257 large_output=True, check_return=True, 258 timeout=ADB_LARGE_OUTPUT_TIMEOUT) 259 # Run synchronous tracing for 0 seconds to stop tracing, clear buffers 260 # and other state. 261 self._device_utils.RunShellCommand( 262 self._tracer_args + ['-t 0'], check_return=True) 263 else: 264 # On M+ --async_stop does everything necessary 265 result = self._device_utils.RunShellCommand( 266 self._tracer_args + ['--async_stop'], raw_output=True, 267 large_output=True, check_return=True, 268 timeout=ADB_LARGE_OUTPUT_TIMEOUT) 269 270 return result 271 272 def _collect_trace_data(self): 273 """Reads the output from atrace and stops the trace.""" 274 result = self._stop_collect_trace() 275 276 data_start = re.search(TRACE_START_REGEXP, result) 277 if data_start: 278 data_start = data_start.end(0) 279 else: 280 raise IOError('Unable to get atrace data. Did you forget adb root?') 281 output = re.sub(ADB_IGNORE_REGEXP, '', result[data_start:]) 282 return output 283 284 def _preprocess_trace_data(self, trace_data): 285 """Performs various processing on atrace data. 286 287 Args: 288 trace_data: The raw trace data. 289 Returns: 290 The processed trace data. 291 """ 292 if trace_data: 293 trace_data = strip_and_decompress_trace(trace_data) 294 295 if not trace_data: 296 print >> sys.stderr, ('No data was captured. Output file was not ' 297 'written.') 298 sys.exit(1) 299 300 if _FIX_MISSING_TGIDS: 301 # Gather proc data from device and patch tgids 302 procfs_dump = self._device_utils.RunShellCommand( 303 'echo -n /proc/[0-9]*/task/[0-9]*', 304 shell=True, check_return=True)[0].split(' ') 305 pid2_tgid = extract_tgids(procfs_dump) 306 trace_data = fix_missing_tgids(trace_data, pid2_tgid) 307 308 if _FIX_CIRCULAR_TRACES: 309 trace_data = fix_circular_traces(trace_data) 310 311 return trace_data 312 313def extract_tgids(trace_lines): 314 """Removes the procfs dump from the given trace text 315 316 Args: 317 trace_lines: The text portion of the trace 318 319 Returns: 320 a map of pids to their tgid. 321 """ 322 tgid_2pid = {} 323 for line in trace_lines: 324 result = re.match('^/proc/([0-9]+)/task/([0-9]+)', line) 325 if result: 326 parent_pid, tgid = result.group(1, 2) 327 tgid_2pid[tgid] = parent_pid 328 329 return tgid_2pid 330 331 332def strip_and_decompress_trace(trace_data): 333 """Fixes new-lines and decompresses trace data. 334 335 Args: 336 trace_data: The trace data returned by atrace. 337 Returns: 338 The decompressed trace data. 339 """ 340 # Collapse CRLFs that are added by adb shell. 341 if trace_data.startswith('\r\n'): 342 trace_data = trace_data.replace('\r\n', '\n') 343 elif trace_data.startswith('\r\r\n'): 344 # On windows, adb adds an extra '\r' character for each line. 345 trace_data = trace_data.replace('\r\r\n', '\n') 346 347 # Skip the initial newline. 348 if trace_data[0] == '\n': 349 trace_data = trace_data[1:] 350 351 if not trace_data.startswith(TRACE_TEXT_HEADER): 352 # No header found, so assume the data is compressed. 353 trace_data = zlib.decompress(trace_data) 354 355 # Enforce Unix line-endings. 356 trace_data = trace_data.replace('\r', '') 357 358 # Skip any initial newlines. 359 while trace_data and trace_data[0] == '\n': 360 trace_data = trace_data[1:] 361 362 return trace_data 363 364 365def fix_missing_tgids(trace_data, pid2_tgid): 366 """Replaces missing TGIDs from the trace data with those found in procfs 367 368 Args: 369 trace_data: the atrace data 370 371 Returns: 372 The updated trace data with missing TGIDs replaced with the correct TGID 373 """ 374 375 def repl(m): 376 tid = m.group(2) 377 if (int(tid) > 0 and m.group(1) != '<idle>' and m.group(3) == '(-----)' 378 and tid in pid2_tgid): 379 # returns Proc_name-PID (TGID) 380 # Binder_2-381 (-----) becomes Binder_2-381 (128) 381 return m.group(1) + '-' + m.group(2) + ' ( ' + pid2_tgid[tid] + ')' 382 383 return m.group(0) 384 385 # matches something like: 386 # Binder_2-895 (-----) 387 trace_data = re.sub(r'^\s*(\S+)-(\d+)\s+(\(\S+\))', repl, trace_data, 388 flags=re.MULTILINE) 389 return trace_data 390 391 392def fix_circular_traces(out): 393 """Fix inconsistentcies in traces due to circular buffering. 394 395 The circular buffers are kept per CPU, so it is not guaranteed that the 396 beginning of a slice is overwritten before the end. To work around this, we 397 throw away the prefix of the trace where not all CPUs have events yet. 398 399 Args: 400 out: The data to fix. 401 Returns: 402 The updated trace data. 403 """ 404 # If any of the CPU's buffers have filled up and 405 # older events have been dropped, the kernel 406 # emits markers of the form '##### CPU 2 buffer started ####' on 407 # the line before the first event in the trace on that CPU. 408 # 409 # No such headers are emitted if there were no overflows or the trace 410 # was captured with non-circular buffers. 411 buffer_start_re = re.compile(r'^#+ CPU \d+ buffer started', re.MULTILINE) 412 413 start_of_full_trace = 0 414 415 while True: 416 result = buffer_start_re.search(out, start_of_full_trace + 1) 417 if result: 418 start_of_full_trace = result.start() 419 else: 420 break 421 422 if start_of_full_trace > 0: 423 # Need to keep the header intact to make the importer happy. 424 end_of_header = re.search(r'^[^#]', out, re.MULTILINE).start() 425 out = out[:end_of_header] + out[start_of_full_trace:] 426 return out 427 428 429class AtraceConfig(tracing_agents.TracingConfig): 430 def __init__(self, atrace_categories, trace_buf_size, kfuncs, 431 app_name, compress_trace_data, from_file, 432 device_serial_number, trace_time, target): 433 tracing_agents.TracingConfig.__init__(self) 434 self.atrace_categories = atrace_categories 435 self.trace_buf_size = trace_buf_size 436 self.kfuncs = kfuncs 437 self.app_name = app_name 438 # Trace compression is broken on Windows. 439 # TODO: Fix https://crbug.com/739751. 440 self.compress_trace_data = \ 441 compress_trace_data and platform.system() != 'Windows' 442 self.from_file = from_file 443 self.device_serial_number = device_serial_number 444 self.trace_time = trace_time 445 self.target = target 446 447 448def add_options(parser): 449 options = optparse.OptionGroup(parser, 'Atrace options') 450 options.add_option('--atrace-categories', dest='atrace_categories', 451 help='Select atrace categories with a comma-delimited ' 452 'list, e.g. --atrace-categories=cat1,cat2,cat3') 453 options.add_option('-k', '--ktrace', dest='kfuncs', action='store', 454 help='specify a comma-separated list of kernel functions ' 455 'to trace') 456 options.add_option('--no-compress', dest='compress_trace_data', 457 default=True, action='store_false', 458 help='Tell the device not to send the trace data in ' 459 'compressed form.') 460 options.add_option('-a', '--app', dest='app_name', default=None, 461 type='string', action='store', 462 help='enable application-level tracing for ' 463 'comma-separated list of app cmdlines') 464 options.add_option('--from-file', dest='from_file', 465 action='store', help='read the trace from a ' 466 'file (compressed) rather than running a ' 467 'live trace') 468 return options 469 470def get_config(options): 471 return AtraceConfig(options.atrace_categories, 472 options.trace_buf_size, options.kfuncs, 473 options.app_name, options.compress_trace_data, 474 options.from_file, options.device_serial_number, 475 options.trace_time, options.target) 476