# Copyright 2014 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 logging
import os
import signal
import subprocess
import sys
import tempfile

from profile_chrome import controllers
from profile_chrome import ui

from pylib import android_commands
from pylib import constants
from pylib.perf import perf_control

sys.path.append(os.path.join(constants.DIR_SOURCE_ROOT,
                             'tools',
                             'telemetry'))
try:
  # pylint: disable=F0401
  from telemetry.core.platform.profiler import android_profiling_helper
  from telemetry.util import support_binaries
except ImportError:
  android_profiling_helper = None
  support_binaries = None


_PERF_OPTIONS = [
    # Sample across all processes and CPUs to so that the current CPU gets
    # recorded to each sample.
    '--all-cpus',
    # In perf 3.13 --call-graph requires an argument, so use the -g short-hand
    # which does not.
    '-g',
    # Increase priority to avoid dropping samples. Requires root.
    '--realtime', '80',
    # Record raw samples to get CPU information.
    '--raw-samples',
    # Increase sampling frequency for better coverage.
    '--freq', '2000',
]


class _PerfProfiler(object):
  def __init__(self, device, perf_binary, categories):
    self._device = device
    self._output_file = android_commands.DeviceTempFile(
        self._device.old_interface, prefix='perf_output')
    self._log_file = tempfile.TemporaryFile()

    # TODO(jbudorick) Look at providing a way to unhandroll this once the
    #                 adb rewrite has fully landed.
    device_param = (['-s', str(self._device)] if str(self._device) else [])
    cmd = ['adb'] + device_param + \
          ['shell', perf_binary, 'record',
           '--output', self._output_file.name] + _PERF_OPTIONS
    if categories:
      cmd += ['--event', ','.join(categories)]
    self._perf_control = perf_control.PerfControl(self._device)
    self._perf_control.SetPerfProfilingMode()
    self._perf_process = subprocess.Popen(cmd,
                                          stdout=self._log_file,
                                          stderr=subprocess.STDOUT)

  def SignalAndWait(self):
    self._device.KillAll('perf', signum=signal.SIGINT)
    self._perf_process.wait()
    self._perf_control.SetDefaultPerfMode()

  def _FailWithLog(self, msg):
    self._log_file.seek(0)
    log = self._log_file.read()
    raise RuntimeError('%s. Log output:\n%s' % (msg, log))

  def PullResult(self, output_path):
    if not self._device.FileExists(self._output_file.name):
      self._FailWithLog('Perf recorded no data')

    perf_profile = os.path.join(output_path,
                                os.path.basename(self._output_file.name))
    self._device.PullFile(self._output_file.name, perf_profile)
    if not os.stat(perf_profile).st_size:
      os.remove(perf_profile)
      self._FailWithLog('Perf recorded a zero-sized file')

    self._log_file.close()
    self._output_file.close()
    return perf_profile


class PerfProfilerController(controllers.BaseController):
  def __init__(self, device, categories):
    controllers.BaseController.__init__(self)
    self._device = device
    self._categories = categories
    self._perf_binary = self._PrepareDevice(device)
    self._perf_instance = None

  def __repr__(self):
    return 'perf profile'

  @staticmethod
  def IsSupported():
    return bool(android_profiling_helper)

  @staticmethod
  def _PrepareDevice(device):
    if not 'BUILDTYPE' in os.environ:
      os.environ['BUILDTYPE'] = 'Release'
    return android_profiling_helper.PrepareDeviceForPerf(device)

  @classmethod
  def GetCategories(cls, device):
    perf_binary = cls._PrepareDevice(device)
    return device.RunShellCommand('%s list' % perf_binary)

  def StartTracing(self, _):
    self._perf_instance = _PerfProfiler(self._device,
                                        self._perf_binary,
                                        self._categories)

  def StopTracing(self):
    if not self._perf_instance:
      return
    self._perf_instance.SignalAndWait()

  @staticmethod
  def _GetInteractivePerfCommand(perfhost_path, perf_profile, symfs_dir,
                                 required_libs, kallsyms):
    cmd = '%s report -n -i %s --symfs %s --kallsyms %s' % (
        os.path.relpath(perfhost_path, '.'), perf_profile, symfs_dir, kallsyms)
    for lib in required_libs:
      lib = os.path.join(symfs_dir, lib[1:])
      if not os.path.exists(lib):
        continue
      objdump_path = android_profiling_helper.GetToolchainBinaryPath(
          lib, 'objdump')
      if objdump_path:
        cmd += ' --objdump %s' % os.path.relpath(objdump_path, '.')
        break
    return cmd

  def PullTrace(self):
    symfs_dir = os.path.join(tempfile.gettempdir(),
                             os.path.expandvars('$USER-perf-symfs'))
    if not os.path.exists(symfs_dir):
      os.makedirs(symfs_dir)
    required_libs = set()

    # Download the recorded perf profile.
    perf_profile = self._perf_instance.PullResult(symfs_dir)
    required_libs = \
        android_profiling_helper.GetRequiredLibrariesForPerfProfile(
            perf_profile)
    if not required_libs:
      logging.warning('No libraries required by perf trace. Most likely there '
                      'are no samples in the trace.')

    # Build a symfs with all the necessary libraries.
    kallsyms = android_profiling_helper.CreateSymFs(self._device,
                                                    symfs_dir,
                                                    required_libs,
                                                    use_symlinks=False)
    perfhost_path = support_binaries.FindPath(
        android_profiling_helper.GetPerfhostName(), 'linux')

    ui.PrintMessage('\nNote: to view the profile in perf, run:')
    ui.PrintMessage('  ' + self._GetInteractivePerfCommand(perfhost_path,
        perf_profile, symfs_dir, required_libs, kallsyms))

    # Convert the perf profile into JSON.
    perf_script_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                                    'third_party', 'perf_to_tracing.py')
    json_file_name = os.path.basename(perf_profile)
    with open(os.devnull, 'w') as dev_null, \
        open(json_file_name, 'w') as json_file:
      cmd = [perfhost_path, 'script', '-s', perf_script_path, '-i',
             perf_profile, '--symfs', symfs_dir, '--kallsyms', kallsyms]
      if subprocess.call(cmd, stdout=json_file, stderr=dev_null):
        logging.warning('Perf data to JSON conversion failed. The result will '
                        'not contain any perf samples. You can still view the '
                        'perf data manually as shown above.')
        return None

    return json_file_name