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