• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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