1 #!/usr/bin/env python 2 3 # Copyright (c) 2011 The Chromium Authors. All rights reserved. 4 # Use of this source code is governed by a BSD-style license that can be 5 # found in the LICENSE file. 6 7 """Android system-wide tracing utility. 8 9 This is a tool for capturing a trace that includes data from both userland and 10 the kernel. It creates an HTML file for visualizing the trace. 11 """ 12 13 import errno, optparse, os, select, subprocess, sys, time, zlib 14 15 flattened_css_file = 'style.css' 16 flattened_js_file = 'script.js' 17 18 class OptionParserIgnoreErrors(optparse.OptionParser): 19 def error(self, msg): 20 pass 21 22 def exit(self): 23 pass 24 25 def print_usage(self): 26 pass 27 28 def print_help(self): 29 pass 30 31 def print_version(self): 32 pass 33 34 def get_device_sdk_version(): 35 getprop_args = ['adb', 'shell', 'getprop', 'ro.build.version.sdk'] 36 37 parser = OptionParserIgnoreErrors() 38 parser.add_option('-e', '--serial', dest='device_serial', type='string') 39 options, args = parser.parse_args() 40 if options.device_serial is not None: 41 getprop_args[1:1] = ['-s', options.device_serial] 42 43 adb = subprocess.Popen(getprop_args, stdout=subprocess.PIPE, 44 stderr=subprocess.PIPE) 45 out, err = adb.communicate() 46 if adb.returncode != 0: 47 print >> sys.stderr, 'Error querying device SDK-version:' 48 print >> sys.stderr, err 49 sys.exit(1) 50 51 version = int(out) 52 return version 53 54 def add_adb_serial(command, serial): 55 if serial is not None: 56 command.insert(1, serial) 57 command.insert(1, '-s') 58 59 def main(): 60 device_sdk_version = get_device_sdk_version() 61 if device_sdk_version < 18: 62 legacy_script = os.path.join(os.path.dirname(sys.argv[0]), 'systrace-legacy.py') 63 os.execv(legacy_script, sys.argv) 64 65 usage = "Usage: %prog [options] [category1 [category2 ...]]" 66 desc = "Example: %prog -b 32768 -t 15 gfx input view sched freq" 67 parser = optparse.OptionParser(usage=usage, description=desc) 68 parser.add_option('-o', dest='output_file', help='write HTML to FILE', 69 default='trace.html', metavar='FILE') 70 parser.add_option('-t', '--time', dest='trace_time', type='int', 71 help='trace for N seconds', metavar='N') 72 parser.add_option('-b', '--buf-size', dest='trace_buf_size', type='int', 73 help='use a trace buffer size of N KB', metavar='N') 74 parser.add_option('-k', '--ktrace', dest='kfuncs', action='store', 75 help='specify a comma-separated list of kernel functions to trace') 76 parser.add_option('-l', '--list-categories', dest='list_categories', default=False, 77 action='store_true', help='list the available categories and exit') 78 parser.add_option('-a', '--app', dest='app_name', default=None, type='string', 79 action='store', help='enable application-level tracing for comma-separated ' + 80 'list of app cmdlines') 81 82 parser.add_option('--link-assets', dest='link_assets', default=False, 83 action='store_true', help='link to original CSS or JS resources ' 84 'instead of embedding them') 85 parser.add_option('--from-file', dest='from_file', action='store', 86 help='read the trace from a file (compressed) rather than running a live trace') 87 parser.add_option('--asset-dir', dest='asset_dir', default='trace-viewer', 88 type='string', help='') 89 parser.add_option('-e', '--serial', dest='device_serial', type='string', 90 help='adb device serial number') 91 92 options, args = parser.parse_args() 93 94 if options.list_categories: 95 atrace_args = ['adb', 'shell', 'atrace', '--list_categories'] 96 expect_trace = False 97 elif options.from_file is not None: 98 atrace_args = ['cat', options.from_file] 99 expect_trace = True 100 else: 101 atrace_args = ['adb', 'shell', 'atrace', '-z'] 102 expect_trace = True 103 104 if options.trace_time is not None: 105 if options.trace_time > 0: 106 atrace_args.extend(['-t', str(options.trace_time)]) 107 else: 108 parser.error('the trace time must be a positive number') 109 110 if options.trace_buf_size is not None: 111 if options.trace_buf_size > 0: 112 atrace_args.extend(['-b', str(options.trace_buf_size)]) 113 else: 114 parser.error('the trace buffer size must be a positive number') 115 116 if options.app_name is not None: 117 atrace_args.extend(['-a', options.app_name]) 118 119 if options.kfuncs is not None: 120 atrace_args.extend(['-k', options.kfuncs]) 121 122 atrace_args.extend(args) 123 124 if atrace_args[0] == 'adb': 125 add_adb_serial(atrace_args, options.device_serial) 126 127 script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) 128 129 if options.link_assets: 130 src_dir = os.path.join(script_dir, options.asset_dir, 'src') 131 build_dir = os.path.join(script_dir, options.asset_dir, 'build') 132 133 js_files, js_flattenizer, css_files, templates = get_assets(src_dir, build_dir) 134 135 css = '\n'.join(linked_css_tag % (os.path.join(src_dir, f)) for f in css_files) 136 js = '<script language="javascript">\n%s</script>\n' % js_flattenizer 137 js += '\n'.join(linked_js_tag % (os.path.join(src_dir, f)) for f in js_files) 138 139 else: 140 css_filename = os.path.join(script_dir, flattened_css_file) 141 js_filename = os.path.join(script_dir, flattened_js_file) 142 css = compiled_css_tag % (open(css_filename).read()) 143 js = compiled_js_tag % (open(js_filename).read()) 144 templates = '' 145 146 html_filename = options.output_file 147 148 adb = subprocess.Popen(atrace_args, stdout=subprocess.PIPE, 149 stderr=subprocess.PIPE) 150 151 result = None 152 data = [] 153 154 # Read the text portion of the output and watch for the 'TRACE:' marker that 155 # indicates the start of the trace data. 156 while result is None: 157 ready = select.select([adb.stdout, adb.stderr], [], [adb.stdout, adb.stderr]) 158 if adb.stderr in ready[0]: 159 err = os.read(adb.stderr.fileno(), 4096) 160 sys.stderr.write(err) 161 sys.stderr.flush() 162 if adb.stdout in ready[0]: 163 out = os.read(adb.stdout.fileno(), 4096) 164 parts = out.split('\nTRACE:', 1) 165 166 txt = parts[0].replace('\r', '') 167 if len(parts) == 2: 168 # The '\nTRACE:' match stole the last newline from the text, so add it 169 # back here. 170 txt += '\n' 171 sys.stdout.write(txt) 172 sys.stdout.flush() 173 174 if len(parts) == 2: 175 data.append(parts[1]) 176 sys.stdout.write("downloading trace...") 177 sys.stdout.flush() 178 break 179 180 result = adb.poll() 181 182 # Read and buffer the data portion of the output. 183 while True: 184 ready = select.select([adb.stdout, adb.stderr], [], [adb.stdout, adb.stderr]) 185 keepReading = False 186 if adb.stderr in ready[0]: 187 err = os.read(adb.stderr.fileno(), 4096) 188 if len(err) > 0: 189 keepReading = True 190 sys.stderr.write(err) 191 sys.stderr.flush() 192 if adb.stdout in ready[0]: 193 out = os.read(adb.stdout.fileno(), 4096) 194 if len(out) > 0: 195 keepReading = True 196 data.append(out) 197 198 if result is not None and not keepReading: 199 break 200 201 result = adb.poll() 202 203 if result == 0: 204 if expect_trace: 205 data = ''.join(data) 206 207 # Collapse CRLFs that are added by adb shell. 208 if data.startswith('\r\n'): 209 data = data.replace('\r\n', '\n') 210 211 # Skip the initial newline. 212 data = data[1:] 213 214 if not data: 215 print >> sys.stderr, ('No data was captured. Output file was not ' + 216 'written.') 217 sys.exit(1) 218 else: 219 # Indicate to the user that the data download is complete. 220 print " done\n" 221 222 html_prefix = read_asset(script_dir, 'prefix.html') 223 html_suffix = read_asset(script_dir, 'suffix.html') 224 225 html_file = open(html_filename, 'w') 226 html_file.write(html_prefix % (css, js, templates)) 227 228 size = 4096 229 dec = zlib.decompressobj() 230 for chunk in (data[i:i+size] for i in xrange(0, len(data), size)): 231 decoded_chunk = dec.decompress(chunk) 232 html_chunk = decoded_chunk.replace('\n', '\\n\\\n') 233 html_file.write(html_chunk) 234 235 html_out = dec.flush().replace('\n', '\\n\\\n') 236 html_file.write(html_out) 237 html_file.write(html_suffix) 238 html_file.close() 239 print "\n wrote file://%s\n" % os.path.abspath(options.output_file) 240 241 else: # i.e. result != 0 242 print >> sys.stderr, 'adb returned error code %d' % result 243 sys.exit(1) 244 245 def read_asset(src_dir, filename): 246 return open(os.path.join(src_dir, filename)).read() 247 248 def get_assets(src_dir, build_dir): 249 sys.path.append(build_dir) 250 gen = __import__('generate_standalone_timeline_view', {}, {}) 251 parse_deps = __import__('parse_deps', {}, {}) 252 gen_templates = __import__('generate_template_contents', {}, {}) 253 filenames = gen._get_input_filenames() 254 load_sequence = parse_deps.calc_load_sequence(filenames, src_dir) 255 256 js_files = [] 257 js_flattenizer = "window.FLATTENED = {};\n" 258 js_flattenizer += "window.FLATTENED_RAW_SCRIPTS = {};\n" 259 css_files = [] 260 261 for module in load_sequence: 262 js_files.append(os.path.relpath(module.filename, src_dir)) 263 js_flattenizer += "window.FLATTENED['%s'] = true;\n" % module.name 264 for dependent_raw_script_name in module.dependent_raw_script_names: 265 js_flattenizer += ( 266 "window.FLATTENED_RAW_SCRIPTS['%s'] = true;\n" % 267 dependent_raw_script_name) 268 269 for style_sheet in module.style_sheets: 270 css_files.append(os.path.relpath(style_sheet.filename, src_dir)) 271 272 templates = gen_templates.generate_templates() 273 274 sys.path.pop() 275 276 return (js_files, js_flattenizer, css_files, templates) 277 278 279 compiled_css_tag = """<style type="text/css">%s</style>""" 280 compiled_js_tag = """<script language="javascript">%s</script>""" 281 282 linked_css_tag = """<link rel="stylesheet" href="%s"></link>""" 283 linked_js_tag = """<script language="javascript" src="%s"></script>""" 284 285 if __name__ == '__main__': 286 main() 287