#! /usr/bin/python
#
# Copyright 2016 the V8 project authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#

# for py2/py3 compatibility
from __future__ import print_function

import argparse
import collections
import os
import subprocess
import sys


__DESCRIPTION = """
Processes a perf.data sample file and annotates the hottest instructions in a
given bytecode handler.
"""


__HELP_EPILOGUE = """
Note:
  This tool uses the disassembly of interpreter's bytecode handler codegen
  from out/<arch>.debug/d8. you should ensure that this binary is in-sync with
  the version used to generate the perf profile.

  Also, the tool depends on the symbol offsets from perf samples being accurate.
  As such, you should use the ":pp" suffix for events.

Examples:
  EVENT_TYPE=cycles:pp tools/run-perf.sh out/x64.release/d8
  tools/ignition/linux_perf_bytecode_annotate.py Add
"""


def bytecode_offset_generator(perf_stream, bytecode_name):
  skip_until_end_of_chain = False
  bytecode_symbol = "BytecodeHandler:" + bytecode_name;

  for line in perf_stream:
    # Lines starting with a "#" are comments, skip them.
    if line[0] == "#":
      continue
    line = line.strip()

    # Empty line signals the end of the callchain.
    if not line:
      skip_until_end_of_chain = False
      continue

    if skip_until_end_of_chain:
      continue

    symbol_and_offset = line.split(" ", 1)[1]

    if symbol_and_offset.startswith("BytecodeHandler:"):
      skip_until_end_of_chain = True

      if symbol_and_offset.startswith(bytecode_symbol):
        yield int(symbol_and_offset.split("+", 1)[1], 16)


def METHOD_NAME(bytecode_offsets):
  offset_counts = collections.defaultdict(int)
  for offset in bytecode_offsets:
    offset_counts[offset] += 1
  return offset_counts


def bytecode_disassembly_generator(ignition_codegen, bytecode_name):
  name_string = "name = " + bytecode_name
  for line in ignition_codegen:
    if line.startswith(name_string):
      break

  # Found the bytecode disassembly.
  for line in ignition_codegen:
    line = line.strip()
    # Blank line marks the end of the bytecode's disassembly.
    if not line:
      return

    # Only yield disassembly output.
    if not line.startswith("0x"):
      continue

    yield line


def print_disassembly_annotation(offset_counts, bytecode_disassembly):
  total = sum(offset_counts.values())
  offsets = sorted(offset_counts, reverse=True)
  def next_offset():
    return offsets.pop() if offsets else -1

  current_offset = next_offset()
  print(current_offset);

  for line in bytecode_disassembly:
    disassembly_offset = int(line.split()[1])
    if disassembly_offset == current_offset:
      count = offset_counts[current_offset]
      percentage = 100.0 * count / total
      print("{:>8d} ({:>5.1f}%) ".format(count, percentage), end=' ')
      current_offset = next_offset()
    else:
      print("                ", end=' ')
    print(line)

  if offsets:
    print ("WARNING: Offsets not empty. Output is most likely invalid due to "
           "a mismatch between perf output and debug d8 binary.")


def parse_command_line():
  command_line_parser = argparse.ArgumentParser(
      formatter_class=argparse.RawDescriptionHelpFormatter,
      description=__DESCRIPTION,
      epilog=__HELP_EPILOGUE)

  command_line_parser.add_argument(
      "--arch", "-a",
      help="The architecture (default: x64)",
      default="x64",
  )
  command_line_parser.add_argument(
      "--input", "-i",
      help="perf sample file to process (default: perf.data)",
      default="perf.data",
      metavar="<perf filename>",
      dest="perf_filename"
  )
  command_line_parser.add_argument(
      "--output", "-o",
      help="output file name (stdout if omitted)",
      type=argparse.FileType("wt"),
      default=sys.stdout,
      metavar="<output filename>",
      dest="output_stream"
  )
  command_line_parser.add_argument(
      "bytecode_name",
      metavar="<bytecode name>",
      nargs="?",
      help="The bytecode handler to annotate"
  )

  return command_line_parser.parse_args()


def main():
  program_options = parse_command_line()
  perf = subprocess.Popen(["perf", "script", "-f", "ip,sym,symoff",
                           "-i", program_options.perf_filename],
                          stdout=subprocess.PIPE)

  v8_root_path = os.path.dirname(__file__) + "/../../"
  d8_path = "{}/out/{}.debug/d8".format(v8_root_path, program_options.arch)
  d8_codegen = subprocess.Popen([d8_path, "--trace-ignition-codegen",
                                 "-e", "1"],
                                stdout=subprocess.PIPE)

  bytecode_offsets = bytecode_offset_generator(
      perf.stdout, program_options.bytecode_name)
  offset_counts = METHOD_NAME(bytecode_offsets)

  bytecode_disassembly = bytecode_disassembly_generator(
      d8_codegen.stdout, program_options.bytecode_name)

  print_disassembly_annotation(offset_counts, bytecode_disassembly)


if __name__ == "__main__":
  main()