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()