Sync to latest from aosp master
bug:22636165
Change-Id: Iccf99b34047273f277eae53ef9614e3e562ece4d
diff --git a/systrace.py b/systrace.py
index 516c0af..38443fe 100755
--- a/systrace.py
+++ b/systrace.py
@@ -10,73 +10,35 @@
the kernel. It creates an HTML file for visualizing the trace.
"""
-import errno, optparse, os, re, select, subprocess, sys, time, zlib
+import sys
-default_categories = 'sched gfx view dalvik webview input disk am wm'.split()
+# Make sure we're using a new enough version of Python.
+# The flags= parameter of re.sub() is new in Python 2.7.
+if sys.version_info[:2] < (2, 7):
+ print >> sys.stderr, '\nThis script requires Python 2.7 or newer.'
+ sys.exit(1)
-class OptionParserIgnoreErrors(optparse.OptionParser):
- def error(self, msg):
- pass
+# pylint: disable=g-bad-import-order,g-import-not-at-top
+import imp
+import optparse
+import os
- def exit(self):
- pass
+import util
- def print_usage(self):
- pass
- def print_help(self):
- pass
+# The default agent directory.
+DEFAULT_AGENT_DIR = 'agents'
- def print_version(self):
- pass
-def get_device_sdk_version():
- getprop_args = ['adb', 'shell', 'getprop', 'ro.build.version.sdk']
+def parse_options(argv):
+ """Parses and checks the command-line options.
- parser = OptionParserIgnoreErrors()
- parser.add_option('-e', '--serial', dest='device_serial', type='string')
- options, args = parser.parse_args()
- if options.device_serial is not None:
- getprop_args[1:1] = ['-s', options.device_serial]
-
- try:
- adb = subprocess.Popen(getprop_args, stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- except OSError:
- print 'Missing adb?'
- sys.exit(1)
- out, err = adb.communicate()
- if adb.returncode != 0:
- print >> sys.stderr, 'Error querying device SDK-version:'
- print >> sys.stderr, err
- sys.exit(1)
-
- version = int(out)
- return version
-
-def add_adb_serial(command, serial):
- if serial is not None:
- command.insert(1, serial)
- command.insert(1, '-s')
-
-def get_default_categories():
- list_command = ['adb', 'shell', 'atrace', '--list_categories']
- try:
- categories_output = subprocess.check_output(list_command)
- categories = [c.split('-')[0].strip() for c in categories_output.splitlines()]
-
- return [c for c in categories if c in default_categories]
- except:
- return []
-
-def main():
- device_sdk_version = get_device_sdk_version()
- if device_sdk_version < 18:
- legacy_script = os.path.join(os.path.dirname(sys.argv[0]), 'systrace-legacy.py')
- os.execv(legacy_script, sys.argv)
-
- usage = "Usage: %prog [options] [category1 [category2 ...]]"
- desc = "Example: %prog -b 32768 -t 15 gfx input view sched freq"
+ Returns:
+ A tuple containing the options structure and a list of categories to
+ be traced.
+ """
+ usage = 'Usage: %prog [options] [category1 [category2 ...]]'
+ desc = 'Example: %prog -b 32768 -t 15 gfx input view sched freq'
parser = optparse.OptionParser(usage=usage, description=desc)
parser.add_option('-o', dest='output_file', help='write HTML to FILE',
default='trace.html', metavar='FILE')
@@ -85,236 +47,153 @@
parser.add_option('-b', '--buf-size', dest='trace_buf_size', type='int',
help='use a trace buffer size of N KB', metavar='N')
parser.add_option('-k', '--ktrace', dest='kfuncs', action='store',
- help='specify a comma-separated list of kernel functions to trace')
- parser.add_option('-l', '--list-categories', dest='list_categories', default=False,
- action='store_true', help='list the available categories and exit')
+ help='specify a comma-separated list of kernel functions '
+ 'to trace')
+ parser.add_option('-l', '--list-categories', dest='list_categories',
+ default=False, action='store_true',
+ help='list the available categories and exit')
parser.add_option('-a', '--app', dest='app_name', default=None, type='string',
- action='store', help='enable application-level tracing for comma-separated ' +
+ action='store',
+ help='enable application-level tracing for comma-separated '
'list of app cmdlines')
parser.add_option('--no-fix-threads', dest='fix_threads', default=True,
- action='store_false', help='don\'t fix missing or truncated thread names')
+ action='store_false',
+ help='don\'t fix missing or truncated thread names')
parser.add_option('--no-fix-circular', dest='fix_circular', default=True,
- action='store_false', help='don\'t fix truncated circular traces')
-
+ action='store_false',
+ help='don\'t fix truncated circular traces')
+ parser.add_option('--no-compress', dest='compress_trace_data',
+ default=True, action='store_false',
+ help='Tell the device not to send the trace data in '
+ 'compressed form.')
parser.add_option('--link-assets', dest='link_assets', default=False,
- action='store_true', help='link to original CSS or JS resources '
- 'instead of embedding them')
+ action='store_true',
+ help='(deprecated)')
parser.add_option('--from-file', dest='from_file', action='store',
- help='read the trace from a file (compressed) rather than running a live trace')
+ help='read the trace from a file (compressed) rather than '
+ 'running a live trace')
parser.add_option('--asset-dir', dest='asset_dir', default='trace-viewer',
- type='string', help='')
+ type='string', help='(deprecated)')
parser.add_option('-e', '--serial', dest='device_serial', type='string',
help='adb device serial number')
+ parser.add_option('--agent-dirs', dest='agent_dirs', type='string',
+ help='the directories of additional systrace agent modules.'
+ ' The directories should be comma separated, e.g., '
+ '--agent-dirs=dir1,dir2,dir3. Directory |%s| is the default'
+ ' agent directory and will always be checked.'
+ % DEFAULT_AGENT_DIR)
- options, categories = parser.parse_args()
+ options, categories = parser.parse_args(argv[1:])
if options.link_assets or options.asset_dir != 'trace-viewer':
- parser.error('--link-assets and --asset-dir is deprecated.')
+ parser.error('--link-assets and --asset-dir are deprecated.')
- if options.list_categories:
- tracer_args = ['adb', 'shell', 'atrace --list_categories']
- expect_trace = False
- elif options.from_file is not None:
- tracer_args = ['cat', options.from_file]
- expect_trace = True
- else:
- atrace_args = ['atrace', '-z']
- expect_trace = True
+ if (options.trace_time is not None) and (options.trace_time <= 0):
+ parser.error('the trace time must be a positive number')
- if options.trace_time is not None:
- if options.trace_time > 0:
- atrace_args.extend(['-t', str(options.trace_time)])
- else:
- parser.error('the trace time must be a positive number')
+ if (options.trace_buf_size is not None) and (options.trace_buf_size <= 0):
+ parser.error('the trace buffer size must be a positive number')
- if options.trace_buf_size is not None:
- if options.trace_buf_size > 0:
- atrace_args.extend(['-b', str(options.trace_buf_size)])
- else:
- parser.error('the trace buffer size must be a positive number')
+ return (options, categories)
- if options.app_name is not None:
- atrace_args.extend(['-a', options.app_name])
- if options.kfuncs is not None:
- atrace_args.extend(['-k', options.kfuncs])
+def write_trace_html(html_filename, script_dir, agents):
+ """Writes out a trace html file.
- if not categories:
- categories = get_default_categories()
- atrace_args.extend(categories)
+ Args:
+ html_filename: The name of the file to write.
+ script_dir: The directory containing this script.
+ agents: The systrace agents.
+ """
+ html_prefix = read_asset(script_dir, 'prefix.html')
+ html_suffix = read_asset(script_dir, 'suffix.html')
+ trace_viewer_html = read_asset(script_dir, 'systrace_trace_viewer.html')
- if options.fix_threads:
- atrace_args.extend([';', 'ps', '-t'])
- tracer_args = ['adb', 'shell', ' '.join(atrace_args)]
+ # Open the file in binary mode to prevent python from changing the
+ # line endings.
+ html_file = open(html_filename, 'wb')
+ html_file.write(html_prefix.replace('{{SYSTRACE_TRACE_VIEWER_HTML}}',
+ trace_viewer_html))
- if tracer_args[0] == 'adb':
- add_adb_serial(tracer_args, options.device_serial)
+ html_file.write('<!-- BEGIN TRACE -->\n')
+ for a in agents:
+ html_file.write(' <script class="')
+ html_file.write(a.get_class_name())
+ html_file.write('" type="application/text">\n')
+ html_file.write(a.get_trace_data())
+ html_file.write(' </script>\n')
+ html_file.write('<!-- END TRACE -->\n')
+
+ html_file.write(html_suffix)
+ html_file.close()
+ print '\n wrote file://%s\n' % os.path.abspath(html_filename)
+
+
+def create_agents(options, categories):
+ """Create systrace agents.
+
+ This function will search systrace agent modules in agent directories and
+ create the corresponding systrace agents.
+ Args:
+ options: The command-line options.
+ categories: The trace categories to capture.
+ Returns:
+ The list of systrace agents.
+ """
+ agent_dirs = [os.path.join(os.path.dirname(__file__), DEFAULT_AGENT_DIR)]
+ if options.agent_dirs:
+ agent_dirs.extend(options.agent_dirs.split(','))
+
+ agents = []
+ for agent_dir in agent_dirs:
+ if not agent_dir:
+ continue
+ for filename in os.listdir(agent_dir):
+ (module_name, ext) = os.path.splitext(filename)
+ if ext != '.py' or module_name == '__init__':
+ continue
+ (f, pathname, data) = imp.find_module(module_name, [agent_dir])
+ try:
+ module = imp.load_module(module_name, f, pathname, data)
+ finally:
+ if f:
+ f.close()
+ if module:
+ agent = module.try_create_agent(options, categories)
+ if not agent:
+ continue
+ agents.append(agent)
+ return agents
+
+
+def main():
+ options, categories = parse_options(sys.argv)
+ agents = create_agents(options, categories)
+
+ if not agents:
+ dirs = DEFAULT_AGENT_DIR
+ if options.agent_dirs:
+ dirs += ',' + options.agent_dirs
+ print >> sys.stderr, ('No systrace agent is available in directories |%s|.'
+ % dirs)
+ sys.exit(1)
+
+ for a in agents:
+ a.start()
+
+ for a in agents:
+ a.collect_result()
+ if not a.expect_trace():
+ # Nothing more to do.
+ return
script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
+ write_trace_html(options.output_file, script_dir, agents)
- html_filename = options.output_file
-
- adb = subprocess.Popen(tracer_args, stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
-
- result = None
- data = []
-
- # Read the text portion of the output and watch for the 'TRACE:' marker that
- # indicates the start of the trace data.
- while result is None:
- ready = select.select([adb.stdout, adb.stderr], [], [adb.stdout, adb.stderr])
- if adb.stderr in ready[0]:
- err = os.read(adb.stderr.fileno(), 4096)
- sys.stderr.write(err)
- sys.stderr.flush()
- if adb.stdout in ready[0]:
- out = os.read(adb.stdout.fileno(), 4096)
- parts = out.split('\nTRACE:', 1)
-
- txt = parts[0].replace('\r', '')
- if len(parts) == 2:
- # The '\nTRACE:' match stole the last newline from the text, so add it
- # back here.
- txt += '\n'
- sys.stdout.write(txt)
- sys.stdout.flush()
-
- if len(parts) == 2:
- data.append(parts[1])
- sys.stdout.write("downloading trace...")
- sys.stdout.flush()
- break
-
- result = adb.poll()
-
- # Read and buffer the data portion of the output.
- while True:
- ready = select.select([adb.stdout, adb.stderr], [], [adb.stdout, adb.stderr])
- keepReading = False
- if adb.stderr in ready[0]:
- err = os.read(adb.stderr.fileno(), 4096)
- if len(err) > 0:
- keepReading = True
- sys.stderr.write(err)
- sys.stderr.flush()
- if adb.stdout in ready[0]:
- out = os.read(adb.stdout.fileno(), 4096)
- if len(out) > 0:
- keepReading = True
- data.append(out)
-
- if result is not None and not keepReading:
- break
-
- result = adb.poll()
-
- if result == 0:
- if expect_trace:
- data = ''.join(data)
-
- # Collapse CRLFs that are added by adb shell.
- if data.startswith('\r\n'):
- data = data.replace('\r\n', '\n')
-
- # Skip the initial newline.
- data = data[1:]
-
- if not data:
- print >> sys.stderr, ('No data was captured. Output file was not ' +
- 'written.')
- sys.exit(1)
- else:
- # Indicate to the user that the data download is complete.
- print " done\n"
-
- # Extract the thread list dumped by ps.
- threads = {}
- if options.fix_threads:
- parts = re.split('USER +PID +PPID +VSIZE +RSS +WCHAN +PC +NAME', data, 1)
- if len(parts) == 2:
- data = parts[0]
- for line in parts[1].splitlines():
- cols = line.split(None, 8)
- if len(cols) == 9:
- tid = int(cols[1])
- name = cols[8]
- threads[tid] = name
-
- # Decompress and preprocess the data.
- out = zlib.decompress(data)
- if options.fix_threads:
- def repl(m):
- tid = int(m.group(2))
- if tid > 0:
- name = threads.get(tid)
- if name is None:
- name = m.group(1)
- if name == '<...>':
- name = '<' + str(tid) + '>'
- threads[tid] = name
- return name + '-' + m.group(2)
- else:
- return m.group(0)
- out = re.sub(r'^\s*(\S+)-(\d+)', repl, out, flags=re.MULTILINE)
-
- if options.fix_circular:
- out = fix_circular_traces(out)
-
- html_prefix = read_asset(script_dir, 'prefix.html')
- html_suffix = read_asset(script_dir, 'suffix.html')
- trace_viewer_html = read_asset(script_dir, 'systrace_trace_viewer.html')
-
- html_file = open(html_filename, 'w')
- html_file.write(
- html_prefix.replace("{{SYSTRACE_TRACE_VIEWER_HTML}}", trace_viewer_html))
-
- html_file.write('<!-- BEGIN TRACE -->\n' +
- ' <script class="trace-data" type="application/text">\n')
- html_file.write(out)
- html_file.write(' </script>\n<!-- END TRACE -->\n')
-
- html_file.write(html_suffix)
- html_file.close()
- print "\n wrote file://%s\n" % os.path.abspath(options.output_file)
-
- else: # i.e. result != 0
- print >> sys.stderr, 'adb returned error code %d' % result
- sys.exit(1)
def read_asset(src_dir, filename):
return open(os.path.join(src_dir, filename)).read()
-def fix_circular_traces(out):
- """Fix inconsistentcies in traces due to circular buffering.
-
- The circular buffers are kept per CPU, so it is not guaranteed that the
- beginning of a slice is overwritten before the end. To work around this, we
- throw away the prefix of the trace where not all CPUs have events yet."""
-
- # If any of the CPU's buffers have filled up and
- # older events have been dropped, the kernel
- # emits markers of the form '##### CPU 2 buffer started ####' on
- # the line before the first event in the trace on that CPU.
- #
- # No such headers are emitted if there were no overflows or the trace
- # was captured with non-circular buffers.
- buffer_start_re = re.compile(r'^#+ CPU \d+ buffer started', re.MULTILINE)
-
- start_of_full_trace = 0
-
- while True:
- result = buffer_start_re.search(out, start_of_full_trace + 1)
- if result:
- start_of_full_trace = result.start()
- else:
- break
-
- if start_of_full_trace > 0:
- # Need to keep the header intact to make the importer happy.
- end_of_header = re.search(r'^[^#]', out, re.MULTILINE).start()
- out = out[:end_of_header] + out[start_of_full_trace:]
- return out
if __name__ == '__main__':
main()