#! /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.
#

import argparse
import heapq
import json
from matplotlib import colors
from matplotlib import pyplot
import numpy
import struct
import sys


__DESCRIPTION = """
Process v8.ignition_dispatches_counters.json and list top counters,
or plot a dispatch heatmap.

Please note that those handlers that may not or will never dispatch
(e.g. Return or Throw) do not show up in the results.
"""


__HELP_EPILOGUE = """
examples:
  # Print the hottest bytecodes in descending order, reading from
  # default filename v8.ignition_dispatches_counters.json (default mode)
  $ tools/ignition/bytecode_dispatches_report.py

  # Print the hottest 15 bytecode dispatch pairs reading from data.json
  $ tools/ignition/bytecode_dispatches_report.py -t -n 15 data.json

  # Save heatmap to default filename v8.ignition_dispatches_counters.svg
  $ tools/ignition/bytecode_dispatches_report.py -p

  # Save heatmap to filename data.svg
  $ tools/ignition/bytecode_dispatches_report.py -p -o data.svg

  # Open the heatmap in an interactive viewer
  $ tools/ignition/bytecode_dispatches_report.py -p -i

  # Display the top 5 sources and destinations of dispatches to/from LdaZero
  $ tools/ignition/bytecode_dispatches_report.py -f LdaZero -n 5
"""

__COUNTER_BITS = struct.calcsize("P") * 8  # Size in bits of a pointer
__COUNTER_MAX = 2**__COUNTER_BITS - 1


def warn_if_counter_may_have_saturated(dispatches_table):
  for source, counters_from_source in iteritems(dispatches_table):
    for destination, counter in iteritems(counters_from_source):
      if counter == __COUNTER_MAX:
        print "WARNING: {} -> {} may have saturated.".format(source,
                                                             destination)


def find_top_bytecode_dispatch_pairs(dispatches_table, top_count):
  def flattened_counters_generator():
    for source, counters_from_source in iteritems(dispatches_table):
      for destination, counter in iteritems(counters_from_source):
        yield source, destination, counter

  return heapq.nlargest(top_count, flattened_counters_generator(),
                        key=lambda x: x[2])


def print_top_bytecode_dispatch_pairs(dispatches_table, top_count):
  top_bytecode_dispatch_pairs = (
    find_top_bytecode_dispatch_pairs(dispatches_table, top_count))
  print "Top {} bytecode dispatch pairs:".format(top_count)
  for source, destination, counter in top_bytecode_dispatch_pairs:
    print "{:>12d}\t{} -> {}".format(counter, source, destination)


def find_top_bytecodes(dispatches_table):
  top_bytecodes = []
  for bytecode, counters_from_bytecode in iteritems(dispatches_table):
    top_bytecodes.append((bytecode, sum(itervalues(counters_from_bytecode))))

  top_bytecodes.sort(key=lambda x: x[1], reverse=True)
  return top_bytecodes


def print_top_bytecodes(dispatches_table):
  top_bytecodes = find_top_bytecodes(dispatches_table)
  print "Top bytecodes:"
  for bytecode, counter in top_bytecodes:
    print "{:>12d}\t{}".format(counter, bytecode)


def find_top_dispatch_sources_and_destinations(
    dispatches_table, bytecode, top_count, sort_source_relative):
  sources = []
  for source, destinations in iteritems(dispatches_table):
    total = float(sum(itervalues(destinations)))
    if bytecode in destinations:
      count = destinations[bytecode]
      sources.append((source, count, count / total))

  destinations = []
  bytecode_destinations = dispatches_table[bytecode]
  bytecode_total = float(sum(itervalues(bytecode_destinations)))
  for destination, count in iteritems(bytecode_destinations):
    destinations.append((destination, count, count / bytecode_total))

  return (heapq.nlargest(top_count, sources,
                         key=lambda x: x[2 if sort_source_relative else 1]),
          heapq.nlargest(top_count, destinations, key=lambda x: x[1]))


def print_top_dispatch_sources_and_destinations(dispatches_table, bytecode,
                                                top_count, sort_relative):
  top_sources, top_destinations = find_top_dispatch_sources_and_destinations(
      dispatches_table, bytecode, top_count, sort_relative)
  print "Top sources of dispatches to {}:".format(bytecode)
  for source_name, counter, ratio in top_sources:
    print "{:>12d}\t{:>5.1f}%\t{}".format(counter, ratio * 100, source_name)

  print "\nTop destinations of dispatches from {}:".format(bytecode)
  for destination_name, counter, ratio in top_destinations:
    print "{:>12d}\t{:>5.1f}%\t{}".format(counter, ratio * 100, destination_name)


def build_counters_matrix(dispatches_table):
  labels = sorted(dispatches_table.keys())

  counters_matrix = numpy.empty([len(labels), len(labels)], dtype=int)
  for from_index, from_name in enumerate(labels):
    current_row = dispatches_table[from_name];
    for to_index, to_name in enumerate(labels):
      counters_matrix[from_index, to_index] = current_row.get(to_name, 0)

  # Reverse y axis for a nicer appearance
  xlabels = labels
  ylabels = list(reversed(xlabels))
  counters_matrix = numpy.flipud(counters_matrix)

  return counters_matrix, xlabels, ylabels


def plot_dispatches_table(dispatches_table, figure, axis):
  counters_matrix, xlabels, ylabels = build_counters_matrix(dispatches_table)

  image = axis.pcolor(
    counters_matrix,
    cmap="jet",
    norm=colors.LogNorm(),
    edgecolor="grey",
    linestyle="dotted",
    linewidth=0.5
  )

  axis.xaxis.set(
    ticks=numpy.arange(0.5, len(xlabels)),
    label="From bytecode handler"
  )
  axis.xaxis.tick_top()
  axis.set_xlim(0, len(xlabels))
  axis.set_xticklabels(xlabels, rotation="vertical")

  axis.yaxis.set(
    ticks=numpy.arange(0.5, len(ylabels)),
    label="To bytecode handler",
    ticklabels=ylabels
  )
  axis.set_ylim(0, len(ylabels))

  figure.colorbar(
    image,
    ax=axis,
    fraction=0.01,
    pad=0.01
  )


def parse_command_line():
  command_line_parser = argparse.ArgumentParser(
    formatter_class=argparse.RawDescriptionHelpFormatter,
    description=__DESCRIPTION,
    epilog=__HELP_EPILOGUE
  )
  command_line_parser.add_argument(
    "--plot-size", "-s",
    metavar="N",
    default=30,
    help="shorter side in inches of the output plot (default 30)"
  )
  command_line_parser.add_argument(
    "--plot", "-p",
    action="store_true",
    help="plot dispatch pairs heatmap"
  )
  command_line_parser.add_argument(
    "--interactive", "-i",
    action="store_true",
    help="open the heatmap in an interactive viewer, instead of writing to file"
  )
  command_line_parser.add_argument(
    "--top-bytecode-dispatch-pairs", "-t",
    action="store_true",
    help="print the top bytecode dispatch pairs"
  )
  command_line_parser.add_argument(
    "--top-entries-count", "-n",
    metavar="N",
    type=int,
    default=10,
    help="print N top entries when running with -t or -f (default 10)"
  )
  command_line_parser.add_argument(
    "--top-dispatches-for-bytecode", "-f",
    metavar="<bytecode name>",
    help="print top dispatch sources and destinations to the specified bytecode"
  )
  command_line_parser.add_argument(
    "--output-filename", "-o",
    metavar="<output filename>",
    default="v8.ignition_dispatches_table.svg",
    help=("file to save the plot file to. File type is deduced from the "
          "extension. PDF, SVG, PNG supported")
  )
  command_line_parser.add_argument(
    "--sort-sources-relative", "-r",
    action="store_true",
    help=("print top sources in order to how often they dispatch to the "
          "specified bytecode, only applied when using -f")
  )
  command_line_parser.add_argument(
    "input_filename",
    metavar="<input filename>",
    default="v8.ignition_dispatches_table.json",
    nargs='?',
    help="Ignition counters JSON file"
  )

  return command_line_parser.parse_args()


def itervalues(d):
  return d.values() if sys.version_info[0] > 2 else d.itervalues()


def iteritems(d):
  return d.items() if sys.version_info[0] > 2 else d.iteritems()


def main():
  program_options = parse_command_line()

  with open(program_options.input_filename) as stream:
    dispatches_table = json.load(stream)

  warn_if_counter_may_have_saturated(dispatches_table)

  if program_options.plot:
    figure, axis = pyplot.subplots()
    plot_dispatches_table(dispatches_table, figure, axis)

    if program_options.interactive:
      pyplot.show()
    else:
      figure.set_size_inches(program_options.plot_size,
                             program_options.plot_size)
      pyplot.savefig(program_options.output_filename)
  elif program_options.top_bytecode_dispatch_pairs:
    print_top_bytecode_dispatch_pairs(
      dispatches_table, program_options.top_entries_count)
  elif program_options.top_dispatches_for_bytecode:
    print_top_dispatch_sources_and_destinations(
      dispatches_table, program_options.top_dispatches_for_bytecode,
      program_options.top_entries_count, program_options.sort_sources_relative)
  else:
    print_top_bytecodes(dispatches_table)


if __name__ == "__main__":
  main()
