# Copyright 2021 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Symbolizes a perfetto trace file."""

import logging
import os
import shutil
import subprocess
import sys
import tempfile

sys.path.insert(
  0,
  os.path.join(
    os.path.dirname(__file__),
    os.pardir,
    os.pardir,
    'third_party',
    'catapult',
    'systrace',
  ),
)

import breakpad_file_extractor
import flag_utils
import metadata_extractor
import rename_breakpad
import symbol_fetcher


def SymbolizeTrace(trace_file, options):
  """Symbolizes a perfetto trace file.

  Args:
    trace_file: path to proto trace file to symbolize.
    options: The options set by the command line args.

  Raises:
    Exception: if breakpad_output_dir is not empty or if path to local breakpad
      directory is invalid.
  """
  print('Symbolizing file')

  need_cleanup = False
  if options.local_breakpad_dir is None:
    breakpad_file_extractor.EnsureDumpSymsBinary(
      options.dump_syms_path, options.local_build_dir
    )

    # Ensure valid |breakpad_output_dir|
    if options.breakpad_output_dir is None:
      # Create a temp dir if output dir is not provided.
      # Temp dir must be cleaned up later.
      options.breakpad_output_dir = tempfile.mkdtemp()
      need_cleanup = True
      flag_utils.GetTracingLogger().warning(
        'Created temporary directory to hold symbol files. These symbols '
        'will be cleaned up after symbolization. Please specify '
        '--breakpad_output_dir=<cached_dir> to save the symbols, if you need '
        'to profile multiple times. The future runs need to use '
        '--local_breakpad_dir=<cached_dir> flag so the symbolizer uses the '
        'cache.'
      )
    else:
      if os.path.isdir(options.breakpad_output_dir):
        if os.listdir(options.breakpad_output_dir):
          raise Exception(
            'Breakpad output directory is not empty: '
            + options.breakpad_output_dir
          )
      else:
        os.makedirs(options.breakpad_output_dir)
        flag_utils.GetTracingLogger().debug(
          'Created directory to hold symbol files.'
        )
  else:
    if not os.path.isdir(options.local_breakpad_dir):
      raise Exception('Local breakpad directory is not valid.')
    options.breakpad_output_dir = options.local_breakpad_dir

  _EnsureBreakpadSymbols(trace_file, options)

  # Set output file to write trace data and symbols to.
  if options.output_file is None:
    options.output_file = os.path.join(
      os.path.dirname(trace_file),
      os.path.basename(trace_file) + '_symbolized_trace',
    )

  _Symbolize(
    trace_file,
    options.symbolizer_path,
    options.breakpad_output_dir,
    options.output_file,
  )

  print('Symbolized trace saved to: ' + os.path.abspath(options.output_file))

  # Cleanup
  if need_cleanup:
    flag_utils.GetTracingLogger().debug('Cleaning up symbol files.')
    shutil.rmtree(options.breakpad_output_dir)


def _EnsureBreakpadSymbols(trace_file, options):
  """Ensures that there are breakpad symbols to symbolize with.

  Args:
    trace_file: The trace file to be symbolized.
    options: The options set by the command line args. This is used to check if
      symbols need to be fetched, extracted, or if they are already present.

  Raises:
    Exception: if no breakpad files could be extracted.
  """
  # If |options.local_breakpad_dir| is not None, then this can be skipped and
  # |trace_file| can be symbolized using those symbols.
  if options.local_breakpad_dir is not None:
    return

  # Extract Metadata
  flag_utils.GetTracingLogger().info('Extracting proto trace metadata.')
  trace_metadata = metadata_extractor.MetadataExtractor(
    options.trace_processor_path, trace_file
  )
  trace_metadata.Initialize()
  flag_utils.GetTracingLogger().info(trace_metadata)

  if options.local_build_dir is not None:
    # Extract breakpad symbol files from binaries in |options.local_build_dir|.
    if not breakpad_file_extractor.ExtractBreakpadFiles(
      options.dump_syms_path,
      options.local_build_dir,
      options.breakpad_output_dir,
      search_unstripped=True,
      module_ids=breakpad_file_extractor.GetModuleIdsToSymbolize(
        trace_metadata
      ),
    ):
      raise Exception(
        'No breakpad symbols could be extracted from files in: %s xor %s'
        % (
          options.local_build_dir,
          os.path.join(options.local_build_dir, 'lib.unstripped'),
        )
      )

    rename_breakpad.RenameBreakpadFiles(
      options.breakpad_output_dir, options.breakpad_output_dir
    )
    return

  # Fetch trace breakpad symbols from GCS
  flag_utils.GetTracingLogger().info(
    'Fetching and extracting trace breakpad symbols.'
  )
  symbol_fetcher.GetTraceBreakpadSymbols(
    options.cloud_storage_bucket,
    trace_metadata,
    options.breakpad_output_dir,
    options.dump_syms_path,
  )


def _Symbolize(trace_file, symbolizer_path, breakpad_output_dir, output_file):
  """ "Symbolizes a trace.

  Args:
    trace_file: The trace file to be symbolized.
    symbolizer_path: The path to the trace_to_text tool to use.
    breakpad_output_dir: Contains the breakpad symbols to use for symbolization.
    output_file: The path to the file to output symbols to.

  Raises:
    RuntimeError: If _RunSymbolizer() fails to execute the given command.
  """
  # Set environment variable as location of stored breakpad files.
  symbolize_env = os.environ.copy()
  symbolize_env['BREAKPAD_SYMBOL_DIR'] = os.path.join(breakpad_output_dir, '')
  cmd = [symbolizer_path, 'symbolize', trace_file]

  # Open temporary file where symbols can be stored.
  with tempfile.TemporaryFile(mode='wb+') as temp_symbol_file:
    _RunSymbolizer(cmd, symbolize_env, temp_symbol_file)

    # Write trace data and symbol data to the same file.
    temp_symbol_file.seek(0)
    symbol_data = temp_symbol_file.read()
    with open(trace_file, 'rb') as f:
      trace_data = f.read()
    with open(output_file, 'wb') as f:
      f.write(trace_data)
      f.write(symbol_data)
      flag_utils.GetTracingLogger().info(
        'Symbolized %s(%d bytes) with %d bytes of symbol data',
        os.path.abspath(trace_file),
        len(trace_data),
        len(symbol_data),
      )


def _RunSymbolizer(cmd, env, stdout):
  proc = subprocess.Popen(cmd, env=env, stdout=stdout, stderr=subprocess.PIPE)
  out, stderr = proc.communicate()
  flag_utils.GetTracingLogger().debug('STDOUT:%s', str(out))
  flag_utils.GetTracingLogger().debug('STDERR:%s', str(stderr))
  if proc.returncode != 0:
    raise RuntimeError(str(stderr))
