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

# Helper functions for Perfetto profiling (i.e. callstack sampling) in Telemetry
# benchmarks.

import logging
import os
import shutil
import subprocess
import threading

# traceconv is used for symbolization and generating pprof profiles.
_TRACECONV_PATH = os.path.normpath(
  os.path.join(
    os.path.abspath(__file__),
    '../../../../../third_party/perfetto/tools/traceconv',
  )
)
# traceconv is not guaranteed to be thread-safe.
_TRACECONV_LOCK = threading.Lock()


def _ConcatenateFiles(files_to_concatenate, output_path):
  """Concatenates files in the order provided.

  Args:
    files_to_concatenate: Paths for input files to concatenate.
    output_path: Path to the resultant output file.
  """
  with open(output_path, 'wb') as output_file:
    for input_path in files_to_concatenate:
      with open(input_path, 'rb') as input_file:
        shutil.copyfileobj(input_file, output_file)


def _CopyFiles(source_directory_path, destination_directory_path):
  """Copies all files in from the source to the destination directory."""
  file_names = os.listdir(source_directory_path)
  if file_names is None:
    return
  for file_name in file_names:
    shutil.copy(
      os.path.join(source_directory_path, file_name), destination_directory_path
    )


def SymbolizeTrace(trace_path):
  """Attempts symbolization of a Perfetto proto trace, if symbols are available.

  If symbolization is successful, the original trace file is replace with the
  symbolized one.

  Args:
    trace_path: The path to the trace file.
  """
  binary_path = os.getenv('PERFETTO_BINARY_PATH')
  if binary_path is None:
    logging.warning(
      'Not symbolizing trace at %s since PERFETTO_BINARY_PATH is not set.',
      trace_path,
    )
    return

  symbols = None
  # Symbolize the trace.
  with _TRACECONV_LOCK:
    popen = subprocess.Popen(
      [_TRACECONV_PATH, 'symbolize', trace_path],
      stdout=subprocess.PIPE,
      stderr=subprocess.PIPE,
    )
    stdout, stderr = popen.communicate()

    if popen.return_code == 0:
      symbols = stdout
    else:
      logging.error(
        'Failed to symbolize trace at %s: %s',
        trace_path,
        stderr.decode('utf-8'),
      )
  if symbols is None:
    return

  parent_dir = os.path.dirname(trace_path)
  symbols_path = os.path.join(parent_dir, 'symbols')
  symbolized_trace_path = os.path.join(
    parent_dir, 'symbolized_{}'.format(os.path.basename(trace_path))
  )

  # We write the symbols to a file in case they are useful for debugging, etc.
  with open(symbols_path, 'wb') as symbols_file:
    symbols_file.write(symbols)

  # Add symbols to the trace file.
  _ConcatenateFiles([trace_path, symbols_path], symbolized_trace_path)
  # Replace the original trace file.
  os.remove(trace_path)
  shutil.move(symbolized_trace_path, trace_path)
  logging.info('Successfully symbolized trace at %s', trace_path)


def GenerateProfiles(trace_path):
  """Generates pprof profiles from a Perfetto proto trace.

  Args:
    trace_path: The path to the potentially symbolized Perfetto trace file.
  """
  traceconv_output = None
  with _TRACECONV_LOCK:
    popen = subprocess.Popen(
      [_TRACECONV_PATH, 'profile', '--perf', trace_path],
      stdout=subprocess.PIPE,
      stderr=subprocess.PIPE,
    )
    stdout, stderr = popen.communicate()
    if popen.return_code == 0:
      traceconv_output = stdout.decode('utf-8')
    else:
      logging.error(
        'Unable to extract pprof profiles from trace at %s: %s',
        trace_path,
        stderr.decode('utf-8'),
      )
  if traceconv_output is None:
    return

  # Copy profiles to the same directory as the source trace.
  profiles_output_directory = None
  for word in traceconv_output.split():
    if 'perf_profile-' in word:
      profiles_output_directory = word
  if profiles_output_directory is None:
    logging.error('No profiles were extracted from trace at %s.', trace_path)
  else:
    _CopyFiles(profiles_output_directory, os.path.dirname(trace_path))
    logging.info(
      'Successfully generated pprof profiles from trace at %s', trace_path
    )
