#!/usr/bin/env python3
# Copyright (c) 2018 Arista Networks, Inc.  All rights reserved.
# Arista Networks, Inc. Confidential and Proprietary.

#pylint: disable-msg=W0621,W0702,E1133,E1135,E1136

import os
import json
import optparse # pylint: disable=deprecated-module
import sys

parser = optparse.OptionParser(
   usage="%prog [options] <file1> <file2>",
   description="diff qtprof output"
)

parser.add_option('-p', '--parseable',
                  dest='parsable', action='store_true',
                  help=optparse.SUPPRESS_HELP)
parser.add_option('-l', '--long',
                  dest='long', action='store_true',
                  help=optparse.SUPPRESS_HELP)
parser.add_option('-s', '--seconds',
                  dest='seconds', action='store_true',
                  help='diff total times (seconds change; default option)')
parser.add_option('-t', '--total',
                  dest='metric', action='append_const', const='total',
                  help='diff total times (percent change)')
parser.add_option('-a', '--average',
                  dest='metric', action='append_const', const='average',
                  help='diff avg.  times (percent change)')
(options, args) = parser.parse_args()

if not options.metric and not options.seconds:
   metric = 'total'
   options.seconds = True
else:
   # we support only either -t or -a; not both
   metric = options.metric[0] if options.metric else 'total'
   if options.seconds:
      metric = 'total'

width = 100
try:
   _, width = os.popen('stty size', 'r').read().split()
   width = int(width)
except:
   pass

if len(args) != 2:
   parser.print_help()
   sys.exit()

data = [0, 0]

# get the input data
for index, arg in enumerate(args):
   if options.parsable:
      with open( arg ) as f:
         value = f.read()
      data[index] = json.loads(f.read())
   elif arg.startswith('http://'):
      value = os.popen('curl -sSL %s | qtparse -b' % arg).read()
      if value:
         data[index] = json.loads(value)
   elif arg.endswith('.qt'):
      value = os.popen('qtcat -j %s' % arg).read()
      if value:
         data[index] = json.loads(value)
   else:
      value = os.popen('qtparse -b %s' % arg).read()
      if value:
         data[index] = json.loads(value)

if not data[0] or not data[1]:
   print( 'failed to process input' )
   sys.exit()


KEY_QT = 0
KEY_FILE = 1
KEY_LINE = 2
KEY_MSG = 3 # i.e. function name

def reindex(qtprof):
   temp = {}
   for row in qtprof:
      key = (
         row['qt'],   # KEY_QT
         row['file'], # KEY_FILE
         row['line'], # KEY_LINE
         row['msg']   # KEY_MSG
      )
      value = { k: row[k] for k in ['line', 'count', 'average', 'total'] }
      if key not in temp:
         temp[key] = [value]
      else:
         temp[key] += [value]
   return temp

data[0] = reindex(data[0])
data[1] = reindex(data[1])

#
# compute the "diff" of two qtprof entries according to the metric that was chosen
# by the user. do some basic filtering here.
#
def diff( r1, r2 ):

   if not (r1['average'] and r2['average']):
      return 0

   # do not consider if total time < 2 seconds
   if float(r1['total']) < 2:
      return 0

   if options.seconds:
      change = float(r2[metric]) - float(r1[metric])
      # do not consider if change is within 2s of original
      if abs(change) < 2:
         return 0
   else:
      change = float(r2[metric]) / float(r1[metric])
      # do not consider if change is within 5% of original
      if 0.95 < change < 1.05:
         return 0

   return change

def display(key, change):
   CEND = '\33[0m'
   CRED = '\33[31m'
   CGRN = '\33[32m'
   if options.long:
      print( "%-90s" % ( key[ KEY_MSG ][ :90 ] ), end=' ' )
   else:
      print( "%-50s" % ( key[ KEY_MSG ][ :50 ] ), end=' ' )
   color = CRED if change > 1 else CGRN
   cend  = CEND
   sign  = '+'  if change > 1 else '-'
   if options.seconds:
      change = abs(int(change))
      label = 's'
   else:
      change = abs(int((change-1) * 100))
      label = '%'
   bars = sign * change if not options.long else ''
   print( "%s%s%3d %s %s %s" % ( color, sign, change, label, bars[ :width-60 ],
                                 cend ) )

####################################################################################

changes = {}

#
# find the closest matching key in the given data
# match everything except for line-number because for qt collected over time, the
# line-numbers of the qt can change slightly over time
#
def closest(d, key):
   match = []
   for k in d:
      if all(k[i] == key[i] for i in [KEY_QT, KEY_FILE, KEY_MSG]):
         match.append(k)
   return match[0] if len(match) == 1 else 0

#
# main calculation loop
#
for key0 in data[0]:

   val = [0, 0]

   if key0 not in data[1]:
      key1 = closest(data[1], key0)
   else:
      key1 = key0

   if not key1:
      continue

   if len(data[0][key0]) > 1 or len(data[1][key1]) > 1:
      continue

   val[0] = data[0][key0][0]
   val[1] = data[1][key1][0]

   change = diff(val[0], val[1])

   if change:
      changes[key0] = change

#
# display the results
#
for key in sorted(changes, key=changes.get, reverse=True):
   display(key, changes[key])

if not changes:
   print( "no significant changes in qt profiling snapshots" )
