mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-16 21:36:35 +00:00
954776ab38
So it's not in PATH in an uninstalled setup (thwarting gst-play autocompletion).
314 lines
8.6 KiB
Python
Executable file
314 lines
8.6 KiB
Python
Executable file
#!/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))
|