#!/usr/bin/env python3
# Copyright 2019 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.

"""
python %prog trace-file

Parses output generated by v8 with flag --trace-regexp-bytecodes and generates
a list of the most common sequences.
"""

from __future__ import print_function

import sys
import re
import collections

def parse(file, seqlen):
  # example:
  # pc = 00, sp = 0, curpos = 0, curchar = 0000000a ..., PushBacktrack, label: e4
  rx = re.compile(
      r'pc = (?P<pc>[0-9a-f]+), sp = (?P<sp>\d+), '
      r'curpos = (?P<curpos>\d+), curchar = (?P<char_hex>[0-9a-f]+) '
      r'(:?\.|\()(?P<char>\.|\w)(:?\.|\)), (?P<bc>\w+), .*')
  total = 0

  # stats[len][seq_tuple] = {'count': 0, 'loops': 0}
  # Using tuple as key for easier rotation
  stats = [{} for _ in range(seqlen)]

  # last[len] = deque of (bc, pc_int)
  last = [collections.deque(maxlen=i + 1) for i in range(seqlen)]

  with open(file) as f:
    l = f.readline()
    while l:
      l = l.strip()
      if l.startswith("Start bytecode interpreter"):
        for i in range(seqlen):
          last[i].clear()

      match = rx.search(l)
      if match:
        total += 1
        bc = match.group('bc')
        pc = int(match.group('pc'), 16)

        current_node = (bc, pc)

        for i in range(seqlen):
          deque = last[i]

          if len(deque) == i + 1:
            # Canonicalize based on PC: find the rotation that starts with the
            # lowest PC.  This aligns loops to their header.
            min_pc = float('inf')
            rotation_start = 0

            # Find the index with the minimum PC
            for idx, item in enumerate(deque):
              if item[1] < min_pc:
                min_pc = item[1]
                rotation_start = idx
              elif item[1] == min_pc:
                # Tie-break. If the same sequence appears twice (A->B->A->B),
                # we prefer the first.
                pass

            # Rotate to start at rotation_start
            canonical_seq = []
            for k in range(i + 1):
              idx = (rotation_start + k) % (i + 1)
              canonical_seq.append(deque[idx][0])

            seq_key = tuple(canonical_seq)

            if seq_key not in stats[i]:
              stats[i][seq_key] = {'count': 0, 'loops': 0}

            stats[i][seq_key]['count'] += 1

            # Check for loops. Note that current_node is the node *after* the
            # sequence we are currently looking at.
            first_node = deque[0]
            if current_node[1] == first_node[1]:
              stats[i][seq_key]['loops'] += 1

          deque.append(current_node)

      l = f.readline()
  return stats, total

def print_most_common(d, seqlen, total):
  # Sort by count
  sorted_items = sorted(d.items(), key=lambda kv: kv[1]['count'], reverse=True)

  for (k, val) in sorted_items:
    count = val['count']
    loops = val['loops']
    pct = count * 100.0 / total

    if pct < 1.0:
      return

    loop_pct = loops * 100.0 / count

    seq_str = ' --> '.join(k)
    print("{}: {} ({:.2f} %) - Loops: {} ({:.2f} %)".format(
        seq_str, count, pct, loops, loop_pct))

def main(argv):
  if len(argv) < 2:
    print("Usage: python {} <trace-file>".format(argv[0]))
    sys.exit(1)

  max_seq = 7
  stats, total = parse(argv[1], max_seq)
  for i in range(max_seq):
    print()
    print("Most common of length {}".format(i+1))
    print()
    print_most_common(stats[i], i, total)

if __name__ == '__main__':
  main(sys.argv)
