#!/usr/bin/python

import re
import sys

def ParseLine(line):
  m = re.search(r'^\[(\d*):(\d*):(\d*).*\] XXX (.*)$', line)
  if not m:
    return None

  parsed = {}
  parsed['t'] = int(m.group(3))

  msg = m.group(4)

  m = re.search(r'^Received request \[(.*)\] (.*)\s*$', msg)
  if m:
    parsed['type'] = 'request'
    parsed['phase'] = 'start'
    parsed['id'] = m.group(1)
    parsed['hostname'] = m.group(2)
    return parsed

  m = re.search(r'^Finished request \[(.*)\] (.*)\s*$', msg)
  if m:
    parsed['type'] = 'request'
    parsed['phase'] = 'end'
    parsed['id'] = m.group(1)
    parsed['hostname'] = m.group(2)
    return parsed

  m = re.search(r'^Canceled request \[(.*)\] (.*)\s*$', msg)
  if m:
    parsed['type'] = 'request'
    parsed['phase'] = 'end'
    parsed['cancelled'] = True
    parsed['id'] = m.group(1)
    parsed['hostname'] = m.group(2)
    return parsed

  m = re.search(r'^Running resolve for (.*) \[@(.*)\]\s*$', msg)
  if m:
    parsed['type'] = 'thread'
    parsed['phase'] = 'start'
    parsed['hostname'] = m.group(1)
    parsed['id'] = m.group(2)
    return parsed

  m = re.search(r'^Completed resolve for (.*) \[@(.*)\]\s*$', msg)
  if m:
    parsed['type'] = 'thread'
    parsed['phase'] = 'end'
    parsed['hostname'] = m.group(1)
    parsed['id'] = m.group(2)
    return parsed

  return None


def ParseFile(path):
  f = open(path, 'r')

  entries = []

  for line in f:
    parsed = ParseLine(line)
    if parsed:
      entries.append(parsed)

  f.close()
  return entries


def FindEndEntry(entries, start_index):
  start_entry = entries[start_index]

  for i in range(start_index + 1, len(entries)):
    e = entries[i]
    if (e['phase'] == 'end' and
        e['type'] == start_entry['type'] and
        e['id'] == start_entry['id'] and
        e['hostname'] == start_entry['hostname']):
      return e
  return None


def GetGroupEntries(entries):
  grouped = []

  for i in range(len(entries)):
    e1 = entries[i]

    if e1['phase'] == 'start':
      # Clone e1 (mostly).
      e = {}
      for prop in ['id', 'type', 'hostname', 't']:
        e[prop] = e1[prop]

      # Find the end entry, and update end information.
      e2 = FindEndEntry(entries, i)
      if e2:
        e['dt'] = e2['t'] - e1['t']
        e['cancelled'] = e2.get('cancelled', False)
        e2['phase'] = ''  # Make sure it isn't re-used later.

      grouped.append(e)

  return grouped

def FindMaxTime(entries):
  t_max = 0
  for e in entries:
    t_max = max(t_max, e['t'] + e.get('dt', 0))
  return t_max


row_height = 40
max_width = 800

def PrintEntryRow(entries, i, max_t):
  entry = entries[i]
  min_width = 2

  top_px = i * row_height
  left_px = int(entry['t'] * max_width / max_t)
  dt = entry.get('dt', max_t - entry['t'])
  width_px = max(min_width, int(dt * max_width / max_t))

  caption_offset_x = width_px + 15
  caption_offset_y = 4

  # Print a box on the timeline.
  bg = ''
  if entry['type'] == 'request':
    bg = 'blue'
  if entry['type'] == 'thread':
    bg = 'purple'
  if entry.get('cancelled', False):
    bg = 'red'
  

  print ('<div style="position: absolute; '
         'overflow: visible;'
         'top: %d px; '
         'left: %d px; '
         'width: %d px; '
         'height: %d px; '
         'background: %s; '
         'border: 1px solid white;'
         '">&nbsp;</div>') % (top_px,
                         left_px,
                         width_px,
                         row_height,
                         bg)

  # Print a caption besides it.
  msg = ''
  if entry['type'] == 'thread':
    msg = "Started thread for \"%s\" at t=%d (%d in progress), took %s ms" % (entry['hostname'], entry['t'], GetNumLiveThreads(entries, entry['t']), str(entry.get('dt', '???')))
  else:
    msg = "Issued request for \"%s\" at t=%d, took %s ms" % (entry['hostname'], entry['t'], str(entry.get('dt', '???')))
    if entry.get('cancelled', False):
      msg += " (was cancelled)"

  print ('<span style="position: absolute; '
         'top: %d px; '
         'left: %d px; '
         'border: 1px solid gray;'
         'white-space: nowrap; '
         'font-size: 80%%; '
         'background: white; '
         '">%s</span>') % (top_px + caption_offset_y,
                           left_px + caption_offset_x,
                           msg)


def GetNumLiveThreads(entries, t):
  total = 0
  for e in entries:
    if e['type'] == 'thread' and t >= e['t'] and (t <= (e['t'] + e.get('dt', t))):
      total += 1
  return total


def PrintEntries(entries):
  max_t = FindMaxTime(entries)

  print ('<body style="background: gray">')
  print ('<div style="'
         'position: absolute; '
         'top: 0; '
         'left: 0; '
         'width: %d px; '
         'height: %d px; '
         'background: white; ">' % (max_width, row_height * len(entries)))

  for i in range(len(entries)):
    PrintEntryRow(entries, i, max_t)

  print ('</div>')
  print ('</body')


def main():
  if len(sys.argv) != 2:
    print("Usage: <logfile_path>")
    sys.exit(1)

  raw_entries = ParseFile(sys.argv[1])
  entries = GetGroupEntries(raw_entries)

  # Normalize the timestamps so first entry starts at t=0.
  start_t = entries[0]['t']
  for e in entries:
    e['t'] -= start_t

  PrintEntries(entries)


def RunTests():
  print ParseLine('[4756:3868:687396750:INFO:host_resolver_impl.cc(513)] XXX Received request [c] top3.mail.ru')
  print ParseLine('[4756:2752:687396890:INFO:host_resolver_impl.cc(60)] XXX Completed resolve for counter.rambler.ru [@25498708]') 
  print ParseLine('[4756:5996:687396875:INFO:host_resolver_impl.cc(50)] XXX Running resolve for images.rambler.ru [@11157428]')
  print ParseLine('[4756:3868:687396968:INFO:host_resolver_impl.cc(536)] XXX Finished request [c] top3.mail.ru')
  print ParseLine('[4756:3868:687399171:INFO:host_resolver_impl.cc(560)] XXX Canceled request [2c] clients1.google.lv')


if __name__ == "__main__":
  main()


