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))