www_server.py revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
1# Copyright 2014 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 5"""This module implements a simple WSGI server for the memory_inspector Web UI. 6 7The WSGI server essentially handles two kinds of requests: 8 - /ajax/foo/bar: The AJAX endpoints which exchange JSON data with the JS. 9 Requests routing is achieved using a simple @uri decorator which simply 10 performs regex matching on the request path. 11 - /static/content: Anything not matching the /ajax/ prefix is treated as a 12 static content request (for serving the index.html and JS/CSS resources). 13 14The following HTTP status code are returned by the server: 15 - 200 - OK: The request was handled correctly. 16 - 404 - Not found: None of the defined handlers did match the /request/path. 17 - 410 - Gone: The path was matched but the handler returned an empty response. 18 This typically happens when the target device is disconnected. 19""" 20 21import cgi 22import collections 23import datetime 24import dateutil.parser 25import glob 26import json 27import memory_inspector 28import mimetypes 29import os 30import posixpath 31import re 32import urlparse 33import uuid 34import wsgiref.simple_server 35 36from memory_inspector import constants 37from memory_inspector.core import backends 38from memory_inspector.core import memory_map 39from memory_inspector.classification import mmap_classifier 40from memory_inspector.classification import native_heap_classifier 41from memory_inspector.data import serialization 42from memory_inspector.data import file_storage 43from memory_inspector.frontends import background_tasks 44 45 46_HTTP_OK = '200 - OK' 47_HTTP_GONE = '410 - Gone' 48_HTTP_NOT_FOUND = '404 - Not Found' 49_PERSISTENT_STORAGE_PATH = os.path.join( 50 os.path.expanduser('~'), '.config', 'memory_inspector') 51_CONTENT_DIR = os.path.abspath(os.path.join( 52 os.path.dirname(__file__), 'www_content')) 53_APP_PROCESS_RE = r'^[\w.:]+$' # Regex for matching app processes. 54_STATS_HIST_SIZE = 120 # Keep at most 120 samples of stats per process. 55_CACHE_LEN = 10 # Max length of |_cached_objs|. 56 57# |_cached_objs| keeps the state of short-lived objects that the client needs to 58# _cached_objs subsequent AJAX calls. 59_cached_objs = collections.OrderedDict() 60_persistent_storage = file_storage.Storage(_PERSISTENT_STORAGE_PATH) 61_proc_stats_history = {} # /Android/device/PID -> deque([stats@T=0, stats@T=1]) 62 63 64class UriHandler(object): 65 """Base decorator used to automatically route /requests/by/path. 66 67 Each handler is called with the following args: 68 args: a tuple of the matching regex groups. 69 req_vars: a dictionary of request args (querystring for GET, body for POST). 70 Each handler must return a tuple with the following elements: 71 http_code: a string with the HTTP status code (e.g., '200 - OK') 72 headers: a list of HTTP headers (e.g., [('Content-Type': 'foo/bar')]) 73 body: the HTTP response body. 74 """ 75 _handlers = [] 76 77 def __init__(self, path_regex, verb='GET', output_filter=None): 78 self._path_regex = path_regex 79 self._verb = verb 80 default_output_filter = lambda *x: x # Just return the same args unchanged. 81 self._output_filter = output_filter or default_output_filter 82 83 def __call__(self, handler): 84 UriHandler._handlers += [( 85 self._verb, self._path_regex, self._output_filter, handler)] 86 87 @staticmethod 88 def Handle(method, path, req_vars): 89 """Finds a matching handler and calls it (or returns a 404 - Not Found).""" 90 for (match_method, path_regex, output_filter, fn) in UriHandler._handlers: 91 if method != match_method: 92 continue 93 m = re.match(path_regex, path) 94 if not m: 95 continue 96 (http_code, headers, body) = fn(m.groups(), req_vars) 97 return output_filter(http_code, headers, body) 98 return (_HTTP_NOT_FOUND, [], 'No AJAX handlers found') 99 100 101class AjaxHandler(UriHandler): 102 """Decorator for routing AJAX requests. 103 104 This decorator essentially groups the JSON serialization and the cache headers 105 which is shared by most of the handlers defined below. 106 """ 107 def __init__(self, path_regex, verb='GET'): 108 super(AjaxHandler, self).__init__( 109 path_regex, verb, AjaxHandler.AjaxOutputFilter) 110 111 @staticmethod 112 def AjaxOutputFilter(http_code, headers, body): 113 serialized_content = json.dumps(body, cls=serialization.Encoder) 114 extra_headers = [('Cache-Control', 'no-cache'), 115 ('Expires', 'Fri, 19 Sep 1986 05:00:00 GMT')] 116 return http_code, headers + extra_headers, serialized_content 117 118 119@AjaxHandler('/ajax/backends') 120def _ListBackends(args, req_vars): # pylint: disable=W0613 121 return _HTTP_OK, [], [backend.name for backend in backends.ListBackends()] 122 123 124@AjaxHandler('/ajax/devices') 125def _ListDevices(args, req_vars): # pylint: disable=W0613 126 resp = [] 127 for device in backends.ListDevices(): 128 # The device settings must loaded at discovery time (i.e. here), not during 129 # startup, because it might have been plugged later. 130 for k, v in _persistent_storage.LoadSettings(device.id).iteritems(): 131 device.settings[k] = v 132 133 resp += [{'backend': device.backend.name, 134 'id': device.id, 135 'name': device.name}] 136 return _HTTP_OK, [], resp 137 138 139@AjaxHandler(r'/ajax/dump/mmap/(\w+)/(\w+)/(\d+)') 140def _DumpMmapsForProcess(args, req_vars): # pylint: disable=W0613 141 """Dumps memory maps for a process. 142 143 The response is formatted according to the Google Charts DataTable format. 144 """ 145 process = _GetProcess(args) 146 if not process: 147 return _HTTP_GONE, [], 'Device not found or process died' 148 mmap = process.DumpMemoryMaps() 149 table = _ConvertMmapToGTable(mmap) 150 151 # Store the dump in the cache. The client might need it later for profiling. 152 cache_id = _CacheObject(mmap) 153 return _HTTP_OK, [], {'table': table, 'id': cache_id} 154 155 156@AjaxHandler('/ajax/initialize/(\w+)/(\w+)$', 'POST') 157def _InitializeDevice(args, req_vars): # pylint: disable=W0613 158 device = _GetDevice(args) 159 if not device: 160 return _HTTP_GONE, [], 'Device not found' 161 device.Initialize() 162 if req_vars['enableNativeTracing']: 163 device.EnableNativeTracing(True) 164 return _HTTP_OK, [], { 165 'isNativeTracingEnabled': device.IsNativeTracingEnabled()} 166 167 168@AjaxHandler(r'/ajax/profile/create', 'POST') 169def _CreateProfile(args, req_vars): # pylint: disable=W0613 170 """Creates (and caches) a profile from a set of dumps. 171 172 The profiling data can be retrieved afterwards using the /profile/{PROFILE_ID} 173 endpoints (below). 174 """ 175 classifier = None # A classifier module (/classification/*_classifier.py). 176 dumps = {} # dump-time -> obj. to classify (e.g., |memory_map.Map|). 177 for arg in 'type', 'source', 'ruleset': 178 assert(arg in req_vars), 'Expecting %s argument in POST data' % arg 179 180 # Step 1: collect the memory dumps, according to what the client specified in 181 # the 'type' and 'source' POST arguments. 182 183 # Case 1a: The client requests to load data from an archive. 184 if req_vars['source'] == 'archive': 185 archive = _persistent_storage.OpenArchive(req_vars['archive']) 186 if not archive: 187 return _HTTP_GONE, [], 'Cannot open archive %s' % req_vars['archive'] 188 first_timestamp = None 189 for timestamp_str in req_vars['snapshots']: 190 timestamp = dateutil.parser.parse(timestamp_str) 191 first_timestamp = first_timestamp or timestamp 192 time_delta = int((timestamp - first_timestamp).total_seconds()) 193 if req_vars['type'] == 'mmap': 194 dumps[time_delta] = archive.LoadMemMaps(timestamp) 195 elif req_vars['type'] == 'nheap': 196 dumps[time_delta] = archive.LoadNativeHeap(timestamp) 197 198 # Case 1b: Use a dump recently cached (only mmap, via _DumpMmapsForProcess). 199 elif req_vars['source'] == 'cache': 200 assert(req_vars['type'] == 'mmap'), 'Only cached mmap dumps are supported.' 201 dumps[0] = _GetCacheObject(req_vars['id']) 202 203 if not dumps: 204 return _HTTP_GONE, [], 'No memory dumps could be retrieved' 205 206 # Initialize the classifier (mmap or nheap) and prepare symbols for nheap. 207 if req_vars['type'] == 'mmap': 208 classifier = mmap_classifier 209 elif req_vars['type'] == 'nheap': 210 classifier = native_heap_classifier 211 if not archive.HasSymbols(): 212 return _HTTP_GONE, [], 'No symbols in archive %s' % req_vars['archive'] 213 symbols = archive.LoadSymbols() 214 for nheap in dumps.itervalues(): 215 nheap.SymbolizeUsingSymbolDB(symbols) 216 217 if not classifier: 218 return _HTTP_GONE, [], 'Classifier %s not supported.' % req_vars['type'] 219 220 # Step 2: Load the rule-set specified by the client in the 'ruleset' POST arg. 221 if req_vars['ruleset'] == 'heuristic': 222 assert(req_vars['type'] == 'nheap'), ( 223 'heuristic rules are supported only for nheap') 224 rules = native_heap_classifier.InferHeuristicRulesFromHeap(dumps[0]) 225 else: 226 rules_path = os.path.join(constants.CLASSIFICATION_RULES_PATH, 227 req_vars['ruleset']) 228 if not os.path.isfile(rules_path): 229 return _HTTP_GONE, [], 'Cannot find the rule-set %s' % rules_path 230 with open(rules_path) as f: 231 rules = classifier.LoadRules(f.read()) 232 233 # Step 3: Aggregate the dump data using the classifier and generate the 234 # profile data (which will be kept cached here in the server). 235 # The resulting profile will consist of 1+ snapshots (depending on the number 236 # dumps the client has requested to process) and a number of 1+ metrics 237 # (depending on the buckets' keys returned by the classifier). 238 239 # Converts the {time: dump_obj} dict into a {time: |AggregatedResult|} dict. 240 # using the classifier. 241 snapshots = collections.OrderedDict((time, classifier.Classify(dump, rules)) 242 for time, dump in sorted(dumps.iteritems())) 243 244 # Add the profile to the cache (and eventually discard old items). 245 # |profile_id| is the key that the client will use in subsequent requests 246 # (to the /ajax/profile/{ID}/ endpoints) to refer to this particular profile. 247 profile_id = _CacheObject(snapshots) 248 249 first_snapshot = next(snapshots.itervalues()) 250 return _HTTP_OK, [], {'id': profile_id, 251 'times': snapshots.keys(), 252 'metrics': first_snapshot.keys, 253 'rootBucket': first_snapshot.total.name + '/'} 254 255 256@AjaxHandler(r'/ajax/profile/(\w+)/tree/(\d+)/(\d+)') 257def _GetProfileTreeDataForSnapshot(args, req_vars): # pylint: disable=W0613 258 """Gets the data for the tree chart for a given time and metric. 259 260 The response is formatted according to the Google Charts DataTable format. 261 """ 262 snapshot_id = args[0] 263 metric_index = int(args[1]) 264 time = int(args[2]) 265 snapshots = _GetCacheObject(snapshot_id) 266 if not snapshots: 267 return _HTTP_GONE, [], 'Cannot find the selected profile.' 268 if time not in snapshots: 269 return _HTTP_GONE, [], 'Cannot find snapshot at T=%d.' % time 270 snapshot = snapshots[time] 271 if metric_index >= len(snapshot.keys): 272 return _HTTP_GONE, [], 'Invalid metric id %d' % metric_index 273 274 resp = {'cols': [{'label': 'bucket', 'type': 'string'}, 275 {'label': 'parent', 'type': 'string'}], 276 'rows': []} 277 278 def VisitBucketAndAddRows(bucket, parent_id=''): 279 """Recursively creates the (node, parent) visiting |ResultTree| in DFS.""" 280 node_id = parent_id + bucket.name + '/' 281 node_label = '<dl><dt>%s</dt><dd>%s</dd></dl>' % ( 282 bucket.name, _StrMem(bucket.values[metric_index])) 283 resp['rows'] += [{'c': [ 284 {'v': node_id, 'f': node_label}, 285 {'v': parent_id, 'f': None}, 286 ]}] 287 for child in bucket.children: 288 VisitBucketAndAddRows(child, node_id) 289 290 VisitBucketAndAddRows(snapshot.total) 291 return _HTTP_OK, [], resp 292 293 294@AjaxHandler(r'/ajax/profile/(\w+)/time_serie/(\d+)/(.*)$') 295def _GetTimeSerieForSnapshot(args, req_vars): # pylint: disable=W0613 296 """Gets the data for the area chart for a given metric and bucket. 297 298 The response is formatted according to the Google Charts DataTable format. 299 """ 300 snapshot_id = args[0] 301 metric_index = int(args[1]) 302 bucket_path = args[2] 303 snapshots = _GetCacheObject(snapshot_id) 304 if not snapshots: 305 return _HTTP_GONE, [], 'Cannot find the selected profile.' 306 if metric_index >= len(next(snapshots.itervalues()).keys): 307 return _HTTP_GONE, [], 'Invalid metric id %d' % metric_index 308 309 def FindBucketByPath(bucket, path, parent_path=''): # Essentially a DFS. 310 cur_path = parent_path + bucket.name + '/' 311 if cur_path == path: 312 return bucket 313 for child in bucket.children: 314 res = FindBucketByPath(child, path, cur_path) 315 if res: 316 return res 317 return None 318 319 # The resulting data table will look like this (assuming len(metrics) == 2): 320 # Time Ashmem Dalvik Other 321 # 0 (1024,0) (4096,1024) (0,0) 322 # 30 (512,512) (1024,1024) (0,512) 323 # 60 (0,512) (1024,0) (512,0) 324 resp = {'cols': [], 'rows': []} 325 for time, aggregated_result in snapshots.iteritems(): 326 bucket = FindBucketByPath(aggregated_result.total, bucket_path) 327 if not bucket: 328 return _HTTP_GONE, [], 'Bucket %s not found' % bucket_path 329 330 # If the user selected a non-leaf bucket, display the breakdown of its 331 # direct children. Otherwise just the leaf bucket. 332 children_buckets = bucket.children if bucket.children else [bucket] 333 334 # Create the columns (form the buckets) when processing the first snapshot. 335 if not resp['cols']: 336 resp['cols'] += [{'label': 'Time', 'type': 'string'}] 337 for child_bucket in children_buckets: 338 resp['cols'] += [{'label': child_bucket.name, 'type': 'number'}] 339 340 row = [{'v': str(time), 'f': None}] 341 for child_bucket in children_buckets: 342 row += [{'v': child_bucket.values[metric_index] / 1024, 'f': None}] 343 resp['rows'] += [{'c': row}] 344 345 return _HTTP_OK, [], resp 346 347@AjaxHandler(r'/ajax/profile/rules') 348def _ListProfilingRules(args, req_vars): # pylint: disable=W0613 349 """Lists the classification rule files available for profiling.""" 350 rules = glob.glob(constants.CLASSIFICATION_RULES_PATH + 351 os.sep + '*' + os.sep + '*.py') 352 rules = [x.replace(constants.CLASSIFICATION_RULES_PATH, '')[1:] # Strip /. 353 for x in rules] 354 resp = {'mmap': filter(lambda x: 'mmap-' in x, rules), 355 'nheap': filter(lambda x: 'nheap-' in x, rules)} 356 resp['nheap'].insert(0, 'heuristic') 357 return _HTTP_OK, [], resp 358 359 360@AjaxHandler(r'/ajax/ps/(\w+)/(\w+)$') # /ajax/ps/Android/a0b1c2[?all=1] 361def _ListProcesses(args, req_vars): # pylint: disable=W0613 362 """Lists processes and their CPU / mem stats. 363 364 The response is formatted according to the Google Charts DataTable format. 365 """ 366 device = _GetDevice(args) 367 if not device: 368 return _HTTP_GONE, [], 'Device not found' 369 resp = { 370 'cols': [ 371 {'label': 'Pid', 'type':'number'}, 372 {'label': 'Name', 'type':'string'}, 373 {'label': 'Cpu %', 'type':'number'}, 374 {'label': 'Mem RSS Kb', 'type':'number'}, 375 {'label': '# Threads', 'type':'number'}, 376 ], 377 'rows': []} 378 for process in device.ListProcesses(): 379 # Exclude system apps if the request didn't contain the ?all=1 arg. 380 if not req_vars.get('all') and not re.match(_APP_PROCESS_RE, process.name): 381 continue 382 stats = process.GetStats() 383 resp['rows'] += [{'c': [ 384 {'v': process.pid, 'f': None}, 385 {'v': process.name, 'f': None}, 386 {'v': stats.cpu_usage, 'f': None}, 387 {'v': stats.vm_rss, 'f': None}, 388 {'v': stats.threads, 'f': None}, 389 ]}] 390 return _HTTP_OK, [], resp 391 392 393@AjaxHandler(r'/ajax/stats/(\w+)/(\w+)$') # /ajax/stats/Android/a0b1c2 394def _GetDeviceStats(args, req_vars): # pylint: disable=W0613 395 """Lists device CPU / mem stats. 396 397 The response is formatted according to the Google Charts DataTable format. 398 """ 399 device = _GetDevice(args) 400 if not device: 401 return _HTTP_GONE, [], 'Device not found' 402 device_stats = device.GetStats() 403 404 cpu_stats = { 405 'cols': [ 406 {'label': 'CPU', 'type':'string'}, 407 {'label': 'Usr %', 'type':'number'}, 408 {'label': 'Sys %', 'type':'number'}, 409 {'label': 'Idle %', 'type':'number'}, 410 ], 411 'rows': []} 412 413 for cpu_idx in xrange(len(device_stats.cpu_times)): 414 cpu = device_stats.cpu_times[cpu_idx] 415 cpu_stats['rows'] += [{'c': [ 416 {'v': '# %d' % cpu_idx, 'f': None}, 417 {'v': cpu['usr'], 'f': None}, 418 {'v': cpu['sys'], 'f': None}, 419 {'v': cpu['idle'], 'f': None}, 420 ]}] 421 422 mem_stats = { 423 'cols': [ 424 {'label': 'Section', 'type':'string'}, 425 {'label': 'MB', 'type':'number', 'pattern': ''}, 426 ], 427 'rows': []} 428 429 for key, value in device_stats.memory_stats.iteritems(): 430 mem_stats['rows'] += [{'c': [ 431 {'v': key, 'f': None}, 432 {'v': value / 1024, 'f': None} 433 ]}] 434 435 return _HTTP_OK, [], {'cpu': cpu_stats, 'mem': mem_stats} 436 437 438@AjaxHandler(r'/ajax/stats/(\w+)/(\w+)/(\d+)$') # /ajax/stats/Android/a0b1c2/42 439def _GetProcessStats(args, req_vars): # pylint: disable=W0613 440 """Lists CPU / mem stats for a given process (and keeps history). 441 442 The response is formatted according to the Google Charts DataTable format. 443 """ 444 process = _GetProcess(args) 445 if not process: 446 return _HTTP_GONE, [], 'Device not found' 447 448 proc_uri = '/'.join(args) 449 cur_stats = process.GetStats() 450 if proc_uri not in _proc_stats_history: 451 _proc_stats_history[proc_uri] = collections.deque(maxlen=_STATS_HIST_SIZE) 452 history = _proc_stats_history[proc_uri] 453 history.append(cur_stats) 454 455 cpu_stats = { 456 'cols': [ 457 {'label': 'T', 'type':'string'}, 458 {'label': 'CPU %', 'type':'number'}, 459 {'label': '# Threads', 'type':'number'}, 460 ], 461 'rows': [] 462 } 463 464 mem_stats = { 465 'cols': [ 466 {'label': 'T', 'type':'string'}, 467 {'label': 'Mem RSS Kb', 'type':'number'}, 468 {'label': 'Page faults', 'type':'number'}, 469 ], 470 'rows': [] 471 } 472 473 for stats in history: 474 cpu_stats['rows'] += [{'c': [ 475 {'v': str(datetime.timedelta(seconds=stats.run_time)), 'f': None}, 476 {'v': stats.cpu_usage, 'f': None}, 477 {'v': stats.threads, 'f': None}, 478 ]}] 479 mem_stats['rows'] += [{'c': [ 480 {'v': str(datetime.timedelta(seconds=stats.run_time)), 'f': None}, 481 {'v': stats.vm_rss, 'f': None}, 482 {'v': stats.page_faults, 'f': None}, 483 ]}] 484 485 return _HTTP_OK, [], {'cpu': cpu_stats, 'mem': mem_stats} 486 487 488@AjaxHandler(r'/ajax/settings/(\w+)/?(\w+)?$') # /ajax/settings/Android[/id] 489def _GetDeviceOrBackendSettings(args, req_vars): # pylint: disable=W0613 490 backend = backends.GetBackend(args[0]) 491 if not backend: 492 return _HTTP_GONE, [], 'Backend not found' 493 if args[1]: 494 device = _GetDevice(args) 495 if not device: 496 return _HTTP_GONE, [], 'Device not found' 497 settings = device.settings 498 else: 499 settings = backend.settings 500 501 assert(isinstance(settings, backends.Settings)) 502 resp = {} 503 for key in settings.expected_keys: 504 resp[key] = {'description': settings.expected_keys[key], 505 'value': settings.values[key]} 506 return _HTTP_OK, [], resp 507 508 509@AjaxHandler(r'/ajax/settings/(\w+)/?(\w+)?$', 'POST') 510def _SetDeviceOrBackendSettings(args, req_vars): # pylint: disable=W0613 511 backend = backends.GetBackend(args[0]) 512 if not backend: 513 return _HTTP_GONE, [], 'Backend not found' 514 if args[1]: 515 device = _GetDevice(args) 516 if not device: 517 return _HTTP_GONE, [], 'Device not found' 518 settings = device.settings 519 storage_name = device.id 520 else: 521 settings = backend.settings 522 storage_name = backend.name 523 524 for key in req_vars.iterkeys(): 525 settings[key] = req_vars[key] 526 _persistent_storage.StoreSettings(storage_name, settings.values) 527 return _HTTP_OK, [], '' 528 529 530@AjaxHandler(r'/ajax/storage/list') 531def _ListStorage(args, req_vars): # pylint: disable=W0613 532 resp = { 533 'cols': [ 534 {'label': 'Archive', 'type':'string'}, 535 {'label': 'Snapshot', 'type':'string'}, 536 {'label': 'Mem maps', 'type':'boolean'}, 537 {'label': 'N. Heap', 'type':'boolean'}, 538 ], 539 'rows': []} 540 for archive_name in _persistent_storage.ListArchives(): 541 archive = _persistent_storage.OpenArchive(archive_name) 542 first_timestamp = None 543 for timestamp in archive.ListSnapshots(): 544 first_timestamp = timestamp if not first_timestamp else first_timestamp 545 time_delta = '%d s.' % (timestamp - first_timestamp).total_seconds() 546 resp['rows'] += [{'c': [ 547 {'v': archive_name, 'f': None}, 548 {'v': timestamp.isoformat(), 'f': time_delta}, 549 {'v': archive.HasMemMaps(timestamp), 'f': None}, 550 {'v': archive.HasNativeHeap(timestamp), 'f': None}, 551 ]}] 552 return _HTTP_OK, [], resp 553 554 555@AjaxHandler(r'/ajax/storage/(.+)/(.+)/mmaps') 556def _LoadMmapsFromStorage(args, req_vars): # pylint: disable=W0613 557 archive = _persistent_storage.OpenArchive(args[0]) 558 if not archive: 559 return _HTTP_GONE, [], 'Cannot open archive %s' % req_vars['archive'] 560 561 timestamp = dateutil.parser.parse(args[1]) 562 if not archive.HasMemMaps(timestamp): 563 return _HTTP_GONE, [], 'No mmaps for snapshot %s' % timestamp 564 mmap = archive.LoadMemMaps(timestamp) 565 return _HTTP_OK, [], {'table': _ConvertMmapToGTable(mmap)} 566 567 568@AjaxHandler(r'/ajax/storage/(.+)/(.+)/nheap') 569def _LoadNheapFromStorage(args, req_vars): 570 """Returns a Google Charts DataTable dictionary for the nheap.""" 571 archive = _persistent_storage.OpenArchive(args[0]) 572 if not archive: 573 return _HTTP_GONE, [], 'Cannot open archive %s' % req_vars['archive'] 574 575 timestamp = dateutil.parser.parse(args[1]) 576 if not archive.HasNativeHeap(timestamp): 577 return _HTTP_GONE, [], 'No native heap dump for snapshot %s' % timestamp 578 579 nheap = archive.LoadNativeHeap(timestamp) 580 symbols = archive.LoadSymbols() 581 nheap.SymbolizeUsingSymbolDB(symbols) 582 583 resp = { 584 'cols': [ 585 {'label': 'Allocated', 'type':'number'}, 586 {'label': 'Resident', 'type':'number'}, 587 {'label': 'Flags', 'type':'number'}, 588 {'label': 'Stack Trace', 'type':'string'}, 589 ], 590 'rows': []} 591 for alloc in nheap.allocations: 592 strace = '<dl>' 593 for frame in alloc.stack_trace.frames: 594 # Use the fallback libname.so+0xaddr if symbol info is not available. 595 symbol_name = frame.symbol.name if frame.symbol else '??' 596 source_info = (str(frame.symbol.source_info[0]) if 597 frame.symbol and frame.symbol.source_info else frame.raw_address) 598 strace += '<dd title="%s">%s</dd><dt>%s</dt>' % ( 599 cgi.escape(source_info), 600 cgi.escape(posixpath.basename(source_info)), 601 cgi.escape(symbol_name)) 602 strace += '</dl>' 603 604 resp['rows'] += [{'c': [ 605 {'v': alloc.size, 'f': _StrMem(alloc.size)}, 606 {'v': alloc.resident_size, 'f': _StrMem(alloc.resident_size)}, 607 {'v': alloc.flags, 'f': None}, 608 {'v': strace, 'f': None}, 609 ]}] 610 return _HTTP_OK, [], resp 611 612 613# /ajax/tracer/start/Android/device-id/pid 614@AjaxHandler(r'/ajax/tracer/start/(\w+)/(\w+)/(\d+)', 'POST') 615def _StartTracer(args, req_vars): 616 for arg in 'interval', 'count', 'traceNativeHeap': 617 assert(arg in req_vars), 'Expecting %s argument in POST data' % arg 618 process = _GetProcess(args) 619 if not process: 620 return _HTTP_GONE, [], 'Device not found or process died' 621 task_id = background_tasks.StartTracer( 622 storage_path=_PERSISTENT_STORAGE_PATH, 623 process=process, 624 interval=int(req_vars['interval']), 625 count=int(req_vars['count']), 626 trace_native_heap=req_vars['traceNativeHeap']) 627 return _HTTP_OK, [], task_id 628 629 630@AjaxHandler(r'/ajax/tracer/status/(\d+)') # /ajax/tracer/status/{task_id} 631def _GetTracerStatus(args, req_vars): # pylint: disable=W0613 632 task = background_tasks.Get(int(args[0])) 633 if not task: 634 return _HTTP_GONE, [], 'Task not found' 635 return _HTTP_OK, [], task.GetProgress() 636 637 638@UriHandler(r'^(?!/ajax)/(.*)$') 639def _StaticContent(args, req_vars): # pylint: disable=W0613 640 # Give the browser a 1-day TTL cache to minimize the start-up time. 641 cache_headers = [('Cache-Control', 'max-age=86400, public')] 642 req_path = args[0] if args[0] else 'index.html' 643 file_path = os.path.abspath(os.path.join(_CONTENT_DIR, req_path)) 644 if (os.path.isfile(file_path) and 645 os.path.commonprefix([file_path, _CONTENT_DIR]) == _CONTENT_DIR): 646 mtype = 'text/plain' 647 guessed_mime = mimetypes.guess_type(file_path) 648 if guessed_mime and guessed_mime[0]: 649 mtype = guessed_mime[0] 650 with open(file_path, 'rb') as f: 651 body = f.read() 652 return _HTTP_OK, cache_headers + [('Content-Type', mtype)], body 653 return _HTTP_NOT_FOUND, cache_headers, file_path + ' not found' 654 655 656def _GetDevice(args): 657 """Returns a |backends.Device| instance from a /backend/device URI.""" 658 assert(len(args) >= 2), 'Malformed request. Expecting /backend/device' 659 return backends.GetDevice(backend_name=args[0], device_id=args[1]) 660 661 662def _GetProcess(args): 663 """Returns a |backends.Process| instance from a /backend/device/pid URI.""" 664 assert(len(args) >= 3 and args[2].isdigit()), ( 665 'Malformed request. Expecting /backend/device/pid') 666 device = _GetDevice(args) 667 if not device: 668 return None 669 return device.GetProcess(int(args[2])) 670 671def _ConvertMmapToGTable(mmap): 672 """Returns a Google Charts DataTable dictionary for the given mmap.""" 673 assert(isinstance(mmap, memory_map.Map)) 674 table = { 675 'cols': [ 676 {'label': 'Start', 'type':'string'}, 677 {'label': 'End', 'type':'string'}, 678 {'label': 'Length Kb', 'type':'number'}, 679 {'label': 'Prot', 'type':'string'}, 680 {'label': 'RSS Kb', 'type':'number'}, 681 {'label': 'Priv. Dirty Kb', 'type':'number'}, 682 {'label': 'Priv. Clean Kb', 'type':'number'}, 683 {'label': 'Shared Dirty Kb', 'type':'number'}, 684 {'label': 'Shared Clean Kb', 'type':'number'}, 685 {'label': 'File', 'type':'string'}, 686 {'label': 'Offset', 'type':'number'}, 687 {'label': 'Resident Pages', 'type':'string'}, 688 ], 689 'rows': []} 690 for entry in mmap.entries: 691 table['rows'] += [{'c': [ 692 {'v': '%08x' % entry.start, 'f': None}, 693 {'v': '%08x' % entry.end, 'f': None}, 694 {'v': entry.len / 1024, 'f': None}, 695 {'v': entry.prot_flags, 'f': None}, 696 {'v': entry.rss_bytes / 1024, 'f': None}, 697 {'v': entry.priv_dirty_bytes / 1024, 'f': None}, 698 {'v': entry.priv_clean_bytes / 1024, 'f': None}, 699 {'v': entry.shared_dirty_bytes / 1024, 'f': None}, 700 {'v': entry.shared_clean_bytes / 1024, 'f': None}, 701 {'v': entry.mapped_file, 'f': None}, 702 {'v': entry.mapped_offset, 'f': None}, 703 {'v': '[%s]' % (','.join(map(str, entry.resident_pages))), 'f': None}, 704 ]}] 705 return table 706 707def _CacheObject(obj_to_store): 708 """Stores an object in the server-side cache and returns its unique id.""" 709 if len(_cached_objs) >= _CACHE_LEN: 710 _cached_objs.popitem(last=False) 711 obj_id = uuid.uuid4().hex 712 _cached_objs[obj_id] = obj_to_store 713 return str(obj_id) 714 715 716def _GetCacheObject(obj_id): 717 """Retrieves an object in the server-side cache by its id.""" 718 return _cached_objs.get(obj_id) 719 720 721def _StrMem(nbytes): 722 """Converts a number (of bytes) into a human readable string (kb, mb).""" 723 UNITS = ['B', 'K', 'M', 'G'] 724 for unit in UNITS: 725 if abs(nbytes) < 1024.0 or unit == UNITS[-1]: 726 return ('%3.1f' % nbytes).replace('.0','') + ' ' + unit 727 nbytes /= 1024.0 728 729 730def _HttpRequestHandler(environ, start_response): 731 """Parses a single HTTP request and delegates the handling through UriHandler. 732 733 This essentially wires up wsgiref.simple_server with our @UriHandler(s). 734 """ 735 path = environ['PATH_INFO'] 736 method = environ['REQUEST_METHOD'] 737 if method == 'POST': 738 req_body_size = int(environ.get('CONTENT_LENGTH', 0)) 739 req_body = environ['wsgi.input'].read(req_body_size) 740 req_vars = json.loads(req_body) 741 else: 742 req_vars = urlparse.parse_qs(environ['QUERY_STRING']) 743 (http_code, headers, body) = UriHandler.Handle(method, path, req_vars) 744 start_response(http_code, headers) 745 return [body] 746 747 748def Start(http_port): 749 # Load the saved backends' settings (some of them might be needed to bootstrap 750 # as, for instance, the adb path for the Android backend). 751 memory_inspector.RegisterAllBackends() 752 for backend in backends.ListBackends(): 753 for k, v in _persistent_storage.LoadSettings(backend.name).iteritems(): 754 backend.settings[k] = v 755 756 httpd = wsgiref.simple_server.make_server('', http_port, _HttpRequestHandler) 757 try: 758 httpd.serve_forever() 759 except KeyboardInterrupt: 760 pass # Don't print useless stack traces when the user hits CTRL-C. 761 background_tasks.TerminateAll()