#!/usr/bin/env python
#
# based on plot-timeline.py by Federico Mena-Quintero <federico at ximian dotcom>
# example:
#   GST_DEBUG_COLOR_MODE=off GST_DEBUG="*:3" gst-launch-1.0 2>debug.log audiotestsrc num-buffers=10 ! audioconvert ! alsasink
#   gst-plot-timeline.py debug.log --output=debug.png

import math
import optparse
import os
import re
import sys

import cairo

FONT_NAME = "Bitstream Vera Sans"
FONT_SIZE = 8
# how many pixels for a second on the timeline
PIXELS_PER_SECOND = 300
# how many pixels for one line of log
PIXELS_PER_LINE = 10
PLOT_WIDTH = 1400
TIME_SCALE_WIDTH = 20
SYSCALL_MARKER_WIDTH = 20
LOG_TEXT_XPOS = 400
LOG_MARKER_WIDTH = 20
BACKGROUND_COLOR = (0, 0, 0)

# assumes GST_DEBUG_LOG_COLOR=1
#                             timestamp          pid  thread        level   category,file,line,msg
mark_regex = re.compile (r'^(\d+:\d+:\d+\.\d+) +\d+ +0?x?[0-9a-f]+ [A-Z]+ +([-a-zA-Z0-9_]+ )(.*)')
mark_timestamp_group = 1
mark_program_group = 2
mark_log_group = 3

success_result = "0"

skip_lines = 0
max_lines = 500
filter_regex = re.compile ('')
skip_regex = re.compile('')

class BaseMark:
    colors = 0, 0, 0
    def __init__(self, timestamp, log):
        self.timestamp = timestamp
        self.log = log
        self.timestamp_ypos = 0
        self.log_ypos = 0

class AccessMark(BaseMark):
    pass

class LastMark(BaseMark):
    colors = 1.0, 0, 0

class FirstMark(BaseMark):
    colors = 1.0, 0, 0

class ExecMark(BaseMark):
#    colors = 0.75, 0.33, 0.33
    colors = (1.0, 0.0, 0.0)
    def __init__(self, timestamp, log):
        BaseMark.__init__(self, timestamp,
                          'execve: ' + os.path.basename(log))

class Metrics:
    def __init__(self):
        self.width = 0
        self.height = 0

# don't use black or red
palette = [
    (0.12, 0.29, 0.49),
    (0.36, 0.51, 0.71),
    (0.75, 0.31, 0.30),
    (0.62, 0.73, 0.38),
    (0.50, 0.40, 0.63),
    (0.29, 0.67, 0.78),
    (0.96, 0.62, 0.34)
    ]

class SyscallParser:
    def __init__ (self):
        self.syscalls = []

    def add_line (self, str):
        m = mark_regex.search (str)
        if m:
            timestr = m.group (mark_timestamp_group).split(':')
            timestamp = float (timestr[2]) + (float (timestr[1]) * 60.0) + (float (timestr[0]) * 3600.0)
            program = m.group (mark_program_group)
            text = program + m.group (mark_log_group)
            if text == 'last':
                self.syscalls.append (LastMark (timestamp, text))
            elif text == 'first':
                self.syscalls.append (FirstMark (timestamp, text))
            else:
                s = AccessMark (timestamp, text)
                program_hash = program.__hash__ ()
                s.colors = palette[program_hash % len (palette)]
                self.syscalls.append (s)
        else:
            print 'No log in %s' % str
            return

def parse_strace(filename):
    parser = SyscallParser ()

    global skip_lines
    global max_lines
    global skip_regex

    skip_found = False

    for line in file(filename, "r").readlines():
        if line == "":
            break

        if not skip_found:
            if skip_regex.search(line):
                skip_found = True
            else:
                continue

        if skip_lines > 0:
            skip_lines -= 1
            continue

        if len(parser.syscalls) >= max_lines:
            break

        if filter_regex.search(line):
            parser.add_line (line)

    return parser.syscalls

def normalize_timestamps(syscalls):

    first_timestamp = syscalls[0].timestamp

    for syscall in syscalls:
        syscall.timestamp -= first_timestamp

def compute_syscall_metrics(syscalls):
    global PIXELS_PER_SECOND
    global PIXELS_PER_LINE
    
    num_syscalls = len(syscalls)

    metrics = Metrics()
    metrics.width = PLOT_WIDTH

    last_timestamp = syscalls[num_syscalls - 1].timestamp

    time_height = int(math.ceil(last_timestamp * PIXELS_PER_SECOND))
    line_height = num_syscalls * PIXELS_PER_LINE

    if time_height > line_height:
        metrics.height = time_height
        print "Adjusting PIXELS_PER_LINE = %d" % PIXELS_PER_LINE
        PIXELS_PER_LINE = metrics.height / num_syscalls
        print "          PIXELS_PER_LINE = %d" % PIXELS_PER_LINE
    else:
        metrics.height = line_height
        print "Adjusting PIXELS_PER_SECOND %d" % PIXELS_PER_SECOND
        PIXELS_PER_SECOND = int(math.ceil(metrics.height / last_timestamp))
        print "          PIXELS_PER_SECOND %d" % PIXELS_PER_SECOND

    text_ypos = 0

    for syscall in syscalls:
        syscall.timestamp_ypos = syscall.timestamp * PIXELS_PER_SECOND
        syscall.log_ypos = text_ypos + FONT_SIZE

        text_ypos += PIXELS_PER_LINE

    return metrics

def plot_time_scale(surface, ctx, metrics):
    num_seconds = (metrics.height + PIXELS_PER_SECOND - 1) / PIXELS_PER_SECOND

    ctx.set_source_rgb(0.5, 0.5, 0.5)
    ctx.set_line_width(1.0)

    for i in range(num_seconds):
        ypos = i * PIXELS_PER_SECOND

        ctx.move_to(0, ypos + 0.5)
        ctx.line_to(TIME_SCALE_WIDTH, ypos + 0.5)
        ctx.stroke()

        ctx.move_to(0, ypos + 2 + FONT_SIZE)
        ctx.show_text("%d s" % i)

def plot_syscall(surface, ctx, syscall):
    ctx.set_source_rgb(*syscall.colors)

    # Line

    ctx.move_to(TIME_SCALE_WIDTH, syscall.timestamp_ypos)
    ctx.line_to(TIME_SCALE_WIDTH + SYSCALL_MARKER_WIDTH, syscall.timestamp_ypos)
    ctx.line_to(LOG_TEXT_XPOS - LOG_MARKER_WIDTH, syscall.log_ypos - FONT_SIZE / 2 + 0.5)
    ctx.line_to(LOG_TEXT_XPOS, syscall.log_ypos - FONT_SIZE / 2 + 0.5)
    ctx.stroke()

    # Log text

    ctx.move_to(LOG_TEXT_XPOS, syscall.log_ypos)
    ctx.show_text("%8.5f: %s" % (syscall.timestamp, syscall.log))

def plot_syscalls_to_surface(syscalls, metrics):
    num_syscalls = len(syscalls)

    print 'picture size: %d x %d' % (metrics.width, metrics.height);

    surface = cairo.ImageSurface(cairo.FORMAT_RGB24,
                                 metrics.width, metrics.height)

    ctx = cairo.Context(surface)
    ctx.select_font_face(FONT_NAME)
    ctx.set_font_size(FONT_SIZE)

    # Background

    ctx.set_source_rgb (*BACKGROUND_COLOR)
    ctx.rectangle(0, 0, metrics.width, metrics.height)
    ctx.fill()

    # Time scale

    plot_time_scale(surface, ctx, metrics)

    # Contents

    ctx.set_line_width(1.0)

    for syscall in syscalls:
        plot_syscall(surface, ctx, syscall)

    return surface

def main(args):

    global skip_lines
    global max_lines
    global filter_regex
    global skip_regex

    option_parser = optparse.OptionParser(
        usage="usage: %prog -o output.png <debug.log>")
    option_parser.add_option("-o",
                             "--output", dest="output",
                             metavar="FILE",
                             help="Name of output file (output is a PNG file)")
    option_parser.add_option("-s",
                             "--skip", dest="skip",
                             metavar="LINES",
                             help="Skip a number of loglines at the beginning of the file or wait till a regular expression happens")
    option_parser.add_option("-m",
                             "--max-lines", dest="max",
                             help="max lines that need to be plotted")
    option_parser.add_option("-f",
                             "--filter", dest="filter",
                             help="filter the log lines on a regular expression")

    options, args = option_parser.parse_args()

    if not options.output:
        print 'Please specify an output filename with "-o file.png" or "--output=file.png".'
        return 1

    if len(args) != 1:
        print 'Please specify only one input filename, which is an debug log taken with "GST_DEBUG_COLOR_MODE=off GST_DEBUG=XXX <application>"'
        return 1

    in_filename = args[0]
    out_filename = options.output

    if options.skip:
        try:
            skip_lines = int(options.skip)
        except:
            skip_regex = re.compile(options.skip)
            skip_lines = 0

    if options.max:
        max_lines = int(options.max)

    if options.filter:
        filter_regex = re.compile(options.filter)

    syscalls = []
    for syscall in parse_strace(in_filename):
        syscalls.append(syscall)
        if isinstance(syscall, FirstMark):
            syscalls = []
        elif isinstance(syscall, LastMark):
            break

    if not syscalls:
        print 'No logs in %s' % in_filename
        return 1

    normalize_timestamps(syscalls)
    metrics = compute_syscall_metrics(syscalls)

    surface = plot_syscalls_to_surface(syscalls, metrics)
    surface.write_to_png(out_filename)

    return 0

if __name__ == "__main__":
    sys.exit(main(sys.argv))
