File indexing completed on 2024-04-28 04:48:05

0001 #!/usr/bin/env python
0002 #
0003 # plot-timeline.py - A simple program to plot timelines based on a Linux strace(1) log.
0004 # Copyright (C) 2007 Federico Mena-Quintero, Johan Dahlin
0005 #
0006 # This program is free software; you can redistribute it and/or modify
0007 # it under the terms of the GNU General Public License as published by
0008 # the Free Software Foundation; either version 2 of the License, or
0009 # (at your option) any later version.
0010 #
0011 # This program is distributed in the hope that it will be useful,
0012 # but WITHOUT ANY WARRANTY; without even the implied warranty of
0013 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0014 # GNU General Public License for more details.
0015 #
0016 # You should have received a copy of the GNU General Public License
0017 # along with this program; if not, write to the Free Software
0018 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
0019 #
0020 # Authors: Federico Mena-Quintero <federico@gnu.org>
0021 #          Johan Dahlin <johan@gnome.org>
0022 
0023 import math
0024 import optparse
0025 import os
0026 import re
0027 import sys
0028 
0029 import cairo
0030 
0031 ### CUSTOMIZATION BEGINS HERE
0032 
0033 FONT_NAME = "Bitstream Vera Sans"
0034 FONT_SIZE = 10
0035 PIXELS_PER_SECOND = 300
0036 PIXELS_PER_LINE = 12
0037 PLOT_WIDTH = 1400
0038 TIME_SCALE_WIDTH = 20
0039 SYSCALL_MARKER_WIDTH = 20
0040 LOG_TEXT_XPOS = 300
0041 LOG_MARKER_WIDTH = 20
0042 BACKGROUND_COLOR = (0, 0, 0)
0043 
0044 # list of strings to ignore in the plot
0045 ignore_strings = [
0046 #    "nautilus_directory_async_state_changed"
0047     ]
0048 
0049 # list of pairs ("string", (r, g, b)) to give a special color to some strings
0050 special_colors = [
0051 #    ("nautilus_window_size_allocate", (1, 1, 1)),
0052 #    ("STARTING MAIN LOOP", (1, 0, 0)),
0053     ]
0054 
0055 ### CUSTOMIZATION ENDS HERE
0056 
0057 def get_special_color (string):
0058     for sc in special_colors:
0059         if string.find (sc[0]) >= 0:
0060             return sc[1]
0061 
0062     return None
0063 
0064 def string_has_substrings (string, substrings):
0065     for i in substrings:
0066         if string.find (i) >= 0:
0067             return True
0068 
0069     return False
0070 
0071 # assumes "strace -ttt -f"
0072 mark_regex = re.compile (r'^\d+ +(\d+\.\d+) +access\("MARK: ([^:]*: )(.*)", F_OK.*')
0073 mark_timestamp_group = 1
0074 mark_program_group = 2
0075 mark_log_group = 3
0076 
0077 # 3273  1141862703.998196 execve("/usr/bin/dbus-launch", ["/usr/bin/dbus-launch", "--sh-syntax", "--exit-with-session", "/usr/X11R6/bin/gnome"], [/* 61 vars */]) = 0
0078 # 3275  1141862704.003623 execve("/home/devel/bin/dbus-daemon", ["dbus-daemon", "--fork", "--print-pid", "8", "--print-address", "6", "--session"], [/* 61 vars */]) = -1 ENOENT (No such file or directory)
0079 complete_exec_regex = re.compile (r'^(\d+) +(\d+\.\d+) +execve\("(.*)", \[".*= (0|-1 ENOENT \(No such file or directory\))$')
0080 complete_exec_pid_group = 1
0081 complete_exec_timestamp_group = 2
0082 complete_exec_command_group = 3
0083 complete_exec_result_group = 4
0084 
0085 # 3283  1141862704.598008 execve("/opt/gnome/lib/GConf/2/gconf-sanity-check-2", ["/opt/gnome/lib/GConf/2/gconf-san"...], [/* 66 vars */] <unfinished ...>
0086 unfinished_exec_regex = re.compile (r'^(\d+) +(\d+\.\d+) +execve\("(.*)", \[".*<unfinished \.\.\.>$')
0087 unfinished_exec_pid_group = 1
0088 unfinished_exec_timestamp_group = 2
0089 unfinished_exec_command_group = 3
0090 
0091 # 3283  1141862704.598704 <... execve resumed> ) = 0
0092 # 3309  1141862707.027481 <... execve resumed> ) = -1 ENOENT (No such file or directory)
0093 resumed_exec_regex = re.compile (r'^(\d+) +(\d+\.\d+) +<\.\.\. execve resumed.*= (0|-1 ENOENT \(No such file or directory\))$')
0094 resumed_exec_pid_group = 1
0095 resumed_exec_timestamp_group = 2
0096 resumed_exec_result_group = 3
0097 
0098 success_result = "0"
0099 
0100 class BaseMark:
0101     colors = 0, 0, 0
0102     def __init__(self, timestamp, log):
0103         self.timestamp = timestamp
0104         self.log = log
0105         self.timestamp_ypos = 0
0106         self.log_ypos = 0
0107 
0108 class AccessMark(BaseMark):
0109     pass
0110 
0111 class LastMark(BaseMark):
0112     colors = 1.0, 0, 0
0113 
0114 class FirstMark(BaseMark):
0115     colors = 1.0, 0, 0
0116 
0117 class ExecMark(BaseMark):
0118 #    colors = 0.75, 0.33, 0.33
0119     colors = (1.0, 0.0, 0.0)
0120     def __init__(self, timestamp, log, is_complete, is_resumed):
0121 #        if is_complete:
0122     text = 'execve: '
0123 #        elif is_resumed:
0124 #            text = 'execve resumed: '
0125 #        else:
0126 #            text = 'execve started: '
0127 
0128         text = text + os.path.basename(log)
0129         BaseMark.__init__(self, timestamp, text)
0130 
0131 class Metrics:
0132     def __init__(self):
0133         self.width = 0
0134         self.height = 0
0135 
0136 # don't use black or red
0137 palette = [
0138     (0.12, 0.29, 0.49),
0139     (0.36, 0.51, 0.71),
0140     (0.75, 0.31, 0.30),
0141     (0.62, 0.73, 0.38),
0142     (0.50, 0.40, 0.63),
0143     (0.29, 0.67, 0.78),
0144     (0.96, 0.62, 0.34),
0145     (1.0 - 0.12, 1.0 - 0.29, 1.0 - 0.49),
0146     (1.0 - 0.36, 1.0 - 0.51, 1.0 - 0.71),
0147     (1.0 - 0.75, 1.0 - 0.31, 1.0 - 0.30),
0148     (1.0 - 0.62, 1.0 - 0.73, 1.0 - 0.38),
0149     (1.0 - 0.50, 1.0 - 0.40, 1.0 - 0.63),
0150     (1.0 - 0.29, 1.0 - 0.67, 1.0 - 0.78),
0151     (1.0 - 0.96, 1.0 - 0.62, 1.0 - 0.34)
0152     ]
0153 
0154 class SyscallParser:
0155     def __init__ (self):
0156         self.pending_execs = []
0157         self.syscalls = []
0158 
0159     def search_pending_execs (self, search_pid):
0160         n = len (self.pending_execs)
0161         for i in range (n):
0162             (pid, timestamp, command) = self.pending_execs[i]
0163             if pid == search_pid:
0164                 return (i, timestamp, command)
0165 
0166         return (None, None, None)
0167 
0168     def add_line (self, str):
0169         m = mark_regex.search (str)
0170         if m:
0171             timestamp = float (m.group (mark_timestamp_group))
0172             program = m.group (mark_program_group)
0173             text = program + m.group (mark_log_group)
0174             if text == 'last':
0175                 self.syscalls.append (LastMark (timestamp, text))
0176             elif text == 'first':
0177                 self.syscalls.append (FirstMark (timestamp, text))
0178             else:
0179                 if not string_has_substrings (text, ignore_strings):
0180                     s = AccessMark (timestamp, text)
0181                     c = get_special_color (text)
0182                     if c:
0183                         s.colors = c
0184                     else:
0185                         program_hash = program.__hash__ ()
0186                         s.colors = palette[program_hash % len (palette)]
0187 
0188                     self.syscalls.append (s)
0189 
0190             return
0191 
0192         m = complete_exec_regex.search (str)
0193         if m:
0194             result = m.group (complete_exec_result_group)
0195             if result == success_result:
0196                 pid = m.group (complete_exec_pid_group)
0197                 timestamp = float (m.group (complete_exec_timestamp_group))
0198                 command = m.group (complete_exec_command_group)
0199                 self.syscalls.append (ExecMark (timestamp, command, True, False))
0200 
0201             return
0202 
0203         m = unfinished_exec_regex.search (str)
0204         if m:
0205             pid = m.group (unfinished_exec_pid_group)
0206             timestamp = float (m.group (unfinished_exec_timestamp_group))
0207             command = m.group (unfinished_exec_command_group)
0208             self.pending_execs.append ((pid, timestamp, command))
0209 #            self.syscalls.append (ExecMark (timestamp, command, False, False))
0210             return
0211 
0212         m = resumed_exec_regex.search (str)
0213         if m:
0214             pid = m.group (resumed_exec_pid_group)
0215             timestamp = float (m.group (resumed_exec_timestamp_group))
0216             result = m.group (resumed_exec_result_group)
0217 
0218             (index, old_timestamp, command) = self.search_pending_execs (pid)
0219             if index == None:
0220                 print "Didn't find pid %s in pending_execs!" % pid
0221                 sys.exit (1)
0222 
0223             del self.pending_execs[index]
0224 
0225             if result == success_result:
0226                 self.syscalls.append (ExecMark (timestamp, command, False, True))
0227 
0228 def parse_strace(filename):
0229     parser = SyscallParser ()
0230 
0231     for line in file(filename, "r"):
0232         if line == "":
0233             break
0234 
0235         parser.add_line (line)
0236 
0237     return parser.syscalls
0238 
0239 def normalize_timestamps(syscalls):
0240 
0241     first_timestamp = syscalls[0].timestamp
0242 
0243     for syscall in syscalls:
0244         syscall.timestamp -= first_timestamp
0245 
0246 def compute_syscall_metrics(syscalls):
0247     num_syscalls = len(syscalls)
0248 
0249     metrics = Metrics()
0250     metrics.width = PLOT_WIDTH
0251 
0252     last_timestamp = syscalls[num_syscalls - 1].timestamp
0253     num_seconds = int(math.ceil(last_timestamp))
0254     metrics.height = max(num_seconds * PIXELS_PER_SECOND,
0255                          num_syscalls * PIXELS_PER_LINE)
0256 
0257     text_ypos = 0
0258 
0259     for syscall in syscalls:
0260         syscall.timestamp_ypos = syscall.timestamp * PIXELS_PER_SECOND
0261         syscall.log_ypos = text_ypos + FONT_SIZE
0262 
0263         text_ypos += PIXELS_PER_LINE
0264 
0265     return metrics
0266 
0267 def plot_time_scale(surface, ctx, metrics):
0268     num_seconds = (metrics.height + PIXELS_PER_SECOND - 1) / PIXELS_PER_SECOND
0269 
0270     ctx.set_source_rgb(0.5, 0.5, 0.5)
0271     ctx.set_line_width(1.0)
0272 
0273     for i in range(num_seconds):
0274         ypos = i * PIXELS_PER_SECOND
0275 
0276         ctx.move_to(0, ypos + 0.5)
0277         ctx.line_to(TIME_SCALE_WIDTH, ypos + 0.5)
0278         ctx.stroke()
0279 
0280         ctx.move_to(0, ypos + 2 + FONT_SIZE)
0281         ctx.show_text("%d s" % i)
0282 
0283 def plot_syscall(surface, ctx, syscall):
0284     ctx.set_source_rgb(*syscall.colors)
0285 
0286     # Line
0287 
0288     ctx.move_to(TIME_SCALE_WIDTH, syscall.timestamp_ypos)
0289     ctx.line_to(TIME_SCALE_WIDTH + SYSCALL_MARKER_WIDTH, syscall.timestamp_ypos)
0290     ctx.line_to(LOG_TEXT_XPOS - LOG_MARKER_WIDTH, syscall.log_ypos - FONT_SIZE / 2 + 0.5)
0291     ctx.line_to(LOG_TEXT_XPOS, syscall.log_ypos - FONT_SIZE / 2 + 0.5)
0292     ctx.stroke()
0293 
0294     # Log text
0295 
0296     ctx.move_to(LOG_TEXT_XPOS, syscall.log_ypos)
0297     ctx.show_text("%8.5f: %s" % (syscall.timestamp, syscall.log))
0298 
0299 def plot_syscalls_to_surface(syscalls, metrics):
0300     num_syscalls = len(syscalls)
0301 
0302     surface = cairo.ImageSurface(cairo.FORMAT_RGB24,
0303                                  metrics.width, metrics.height)
0304 
0305     ctx = cairo.Context(surface)
0306     ctx.select_font_face(FONT_NAME)
0307     ctx.set_font_size(FONT_SIZE)
0308 
0309     # Background
0310 
0311     ctx.set_source_rgb (*BACKGROUND_COLOR)
0312     ctx.rectangle(0, 0, metrics.width, metrics.height)
0313     ctx.fill()
0314 
0315     # Time scale
0316 
0317     plot_time_scale(surface, ctx, metrics)
0318 
0319     # Contents
0320 
0321     ctx.set_line_width(1.0)
0322 
0323     for syscall in syscalls:
0324         plot_syscall(surface, ctx, syscall)
0325 
0326     return surface
0327 
0328 def main(args):
0329     option_parser = optparse.OptionParser(
0330         usage="usage: %prog -o output.png <strace.txt>")
0331     option_parser.add_option("-o",
0332                              "--output", dest="output",
0333                              metavar="FILE",
0334                              help="Name of output file (output is a PNG file)")
0335 
0336     options, args = option_parser.parse_args()
0337 
0338     if not options.output:
0339         print 'Please specify an output filename with "-o file.png" or "--output=file.png".'
0340         return 1
0341 
0342     if len(args) != 1:
0343         print 'Please specify only one input filename, which is an strace log taken with "strace -ttt -f"'
0344         return 1
0345 
0346     in_filename = args[0]
0347     out_filename = options.output
0348 
0349     syscalls = []
0350     for syscall in parse_strace(in_filename):
0351         syscalls.append(syscall)
0352         if isinstance(syscall, FirstMark):
0353             syscalls = []
0354         elif isinstance(syscall, LastMark):
0355             break
0356 
0357     if not syscalls:
0358         print 'No marks in %s, add access("MARK: ...", F_OK)' % in_filename
0359         return 1
0360 
0361     normalize_timestamps(syscalls)
0362     metrics = compute_syscall_metrics(syscalls)
0363 
0364     surface = plot_syscalls_to_surface(syscalls, metrics)
0365     surface.write_to_png(out_filename)
0366 
0367     return 0
0368 
0369 if __name__ == "__main__":
0370     sys.exit(main(sys.argv))