text: # Copyright 2013 The Chromium Authors. All rights reserved.
  # Use of this source code is governed by a BSD-style license that can be
  # found in the LICENSE file.
  
  import json
  import logging
  import re
  import signal
  import subprocess
  import sys
  import tempfile
  
  from telemetry.internal.platform import profiler
  from telemetry.timeline import model
  from tracing.trace_data import trace_data as trace_data_module
  
  
  # Parses one line of strace output, for example:
  # 6052  1311456063.159722 read(8, "\1\0\0\0\0\0\0\0", 8) = 8 <0.000022>
  _STRACE_LINE_RE = re.compile(
      r'^(?P<tid>\d+)\s+'
      r'(?P<ts>\d+)'
      r'(?P<micro>.\d+)\s+'
      r'(?P<func>.*?)'
      r'[(](?P<args>.*?)[)]\s+=\s+'
      r'(?P<ret>.*?)\s+'
      r'<(?P<dur>[\d.]+)>$')
  
  _UNFINISHED_LINE_RE = re.compile(
      r'^(?P<tid>\d+)\s+'
      r'(?P<line>.*?)'
      r'<unfinished ...>$')
  
  _RESUMED_LINE_RE = re.compile(
      r'^(?P<tid>\d+)\s+'
      r'(?P<ts>\d+)'
      r'(?P<micro>.\d+)\s+'
      r'<[.][.][.]\s(?P<func>.*?)\sresumed>'
      r'(?P<line>.*?)$')
  
  _KILLED_LINE_RE = re.compile(
      r'^(?P<tid>\d+)\s+'
      r'(?P<ts>\d+)'
      r'(?P<micro>.\d+)\s+'
      r'[+][+][+] killed by SIGKILL [+][+][+]$')
  
  
  def _StraceToChromeTrace(pid, infile):
    """Returns chrometrace json format for |infile| strace output."""
    # Map of fd:file_name for open file descriptors. Useful for displaying
    # file name instead of the descriptor number.
    fd_map = {}
  
    # Map of tid:interrupted_call for the interrupted call on each thread. It is
    # possible to context switch during a system call. In this case we must
    # match up the lines.
    interrupted_call_map = {}
  
    out = []
    with open(infile, 'r') as f:
      for line in f.readlines():
        # Ignore kill lines for now.
        m = _KILLED_LINE_RE.match(line)
        if m:
          continue
  
        # If this line is interrupted, then remember it and continue.
        m = _UNFINISHED_LINE_RE.match(line)
        if m:
          assert m.group('tid') not in interrupted_call_map
          interrupted_call_map[m.group('tid')] = line
          continue
  
        # If this is a resume of a previous line, stitch it together.
        interrupted = False
        m = _RESUMED_LINE_RE.match(line)
        if m:
          interrupted = True
          assert m.group('tid') in interrupted_call_map
          line = interrupted_call_map[m.group('tid')].replace(
              '<unfinished ...>', m.group('line'))
          del interrupted_call_map[m.group('tid')]
  
        # At this point we can do a normal match.
        m = _STRACE_LINE_RE.match(line)
        if not m:
          if ('exit' not in line and
              'Profiling timer expired' not in line and
              '<unavailable>' not in line):
            logging.warn('Failed to parse line: %s' % line)
          continue
  
        ts_begin = int(1000000 * (int(m.group('ts')) + float(m.group('micro'))))
        ts_end = ts_begin + int(1000000 * float(m.group('dur')))
        tid = int(m.group('tid'))
        function_name = unicode(m.group('func'), errors='ignore')
        function_args = unicode(m.group('args'), errors='ignore')
        ret = unicode(m.group('ret'), errors='ignore')
        cat = 'strace'
  
        possible_fd_arg = None
        first_arg = function_args.split(',')[0]
        if first_arg and first_arg.strip().isdigit():
          possible_fd_arg = first_arg.strip()
  
        if function_name == 'open' and ret.isdigit():
          # 1918  1311606151.649379 open("/foo/bar.so", O_RDONLY) = 7 <0.000088>
          fd_map[ret] = first_arg
  
        args = {
            'args': function_args,
            'ret': ret,
            }
        if interrupted:
          args['interrupted'] = True
        if possible_fd_arg and possible_fd_arg in fd_map:
          args['fd%s' % first_arg] = fd_map[possible_fd_arg]
  
        out.append({
            'cat': cat,
            'pid': pid,
            'tid': tid,
            'ts': ts_begin,
            'ph': 'B',  # Begin
            'name': function_name,
            })
        out.append({
            'cat': cat,
            'pid': pid,
            'tid': tid,
            'ts': ts_end,
            'ph': 'E',  # End
            'name': function_name,
            'args': args,
            })
  
    return out
  
  
  def _GenerateTraceMetadata(timeline_model):
    out = []
    for process in timeline_model.processes:
      out.append({
          'name': 'process_name',
          'ph': 'M',  # Metadata
          'pid': process,
          'args': {
            'name': timeline_model.processes[process].name
            }
          })
      for thread in timeline_model.processes[process].threads:
        out.append({
            'name': 'thread_name',
            'ph': 'M',  # Metadata
            'pid': process,
            'tid': thread,
            'args': {
              'name': timeline_model.processes[process].threads[thread].name
              }
            })
    return out
  
  
  class _SingleProcessStraceProfiler(object):
    """An internal class for using perf for a given process."""
    def __init__(self, pid, output_file, platform_backend):
      self._pid = pid
      self._platform_backend = platform_backend
      self._output_file = output_file
      self._tmp_output_file = tempfile.NamedTemporaryFile('w', 0)
      self._proc = subprocess.Popen(
          ['strace', '-ttt', '-f', '-T', '-p', str(pid), '-o', output_file],
          stdout=self._tmp_output_file, stderr=subprocess.STDOUT)
  
    def CollectProfile(self):
      if ('renderer' in self._output_file and
          not self._platform_backend.GetCommandLine(self._pid)):
        logging.warning('Renderer was swapped out during profiling. '
                        'To collect a full profile rerun with '
                        '"--extra-browser-args=--single-process"')
      self._proc.send_signal(signal.SIGINT)
      exit_code = self._proc.wait()
      try:
        if exit_code:
          raise Exception('strace failed with exit code %d. Output:\n%s' % (
                          exit_code, self._GetStdOut()))
      finally:
        self._tmp_output_file.close()
  
      return _StraceToChromeTrace(self._pid, self._output_file)
  
    def _GetStdOut(self):
      self._tmp_output_file.flush()
      try:
        with open(self._tmp_output_file.name) as f:
          return f.read()
      except IOError:
        return ''
  
  
  class StraceProfiler(profiler.Profiler):
  
    def __init__(self, browser_backend, platform_backend, output_path, state):
      super(StraceProfiler, self).__init__(
          browser_backend, platform_backend, output_path, state)
      assert self._browser_backend.supports_tracing
      self._browser_backend.browser.StartTracing(None, timeout=10)
      process_output_file_map = self._GetProcessOutputFileMap()
      self._process_profilers = []
      self._output_file = output_path + '.json'
      for pid, output_file in process_output_file_map.iteritems():
        if 'zygote' in output_file:
          continue
        self._process_profilers.append(
            _SingleProcessStraceProfiler(pid, output_file, platform_backend))
  
    @classmethod
    def name(cls):
      return 'strace'
  
    @classmethod
    def is_supported(cls, browser_type):
      if sys.platform != 'linux2':
        return False
      # TODO(tonyg): This should be supported on android and cros.
      if (browser_type.startswith('android') or
         browser_type.startswith('cros')):
        return False
      return True
  
    @classmethod
    def CustomizeBrowserOptions(cls, browser_type, options):
      options.AppendExtraBrowserArgs([
          '--no-sandbox',
          '--allow-sandbox-debugging'
      ])
  
    def CollectProfile(self):
      print 'Processing trace...'
  
      out_json = []
  
      for single_process in self._process_profilers:
        out_json.extend(single_process.CollectProfile())
  
      trace_data_builder = trace_data_module.TraceDataBuilder()
      self._browser_backend.browser.StopTracing(trace_data_builder)
      timeline_model = model.TimelineModel(trace_data_builder.AsData())
      out_json.extend(_GenerateTraceMetadata(timeline_model))
  
      with open(self._output_file, 'w') as f:
        f.write(json.dumps(out_json, separators=(',', ':')))
  
      print 'Trace saved as %s' % self._output_file
      print 'To view, open in chrome://tracing'
      return [self._output_file]
  