#!/usr/bin/perl -w # A basic perl port of the bootchart renderer, SVG only # (c) 2005 Vincent Caron # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # BUGS # - ignore number of CPUs (disk util.) # 2005-06-29 - fixes # - process name is latest sample argv value # - sibling processes are properly PID-sorted from top to bottom # - process start is now parsed from proc_ps # - hiding bootchartd-related processes # # 2005-06-28 # - initial release use strict; my %headers; my @disk_samples; my @cpu_samples; my %ps_info; my %ps_by_parent; my $chart_svg_template = `cat svg/bootchart.svg.template` || die; my $process_svg_template = `cat svg/process.svg.template` || die; my $svg_css_file = `cat svg/style.css` || die; $svg_css_file =~ s/^/\t\t\t/mg; $svg_css_file =~ s/^\s+//g; my $HZ = 100; my $min_img_w = 800; my $sec_w = 25; my $proc_h = 16; my $header_h = 300; my $img_w; my $img_h; my $t_begin; my $t_end; my $t_dur; sub parse_logs { my $logfile = shift; my $tmp = "/tmp/bootchart-$$"; mkdir $tmp or die; system "cd $tmp && tar xzf $logfile"; parse_log_header($tmp); parse_log_cpu($tmp); parse_log_disk($tmp); parse_log_ps($tmp); system('rm', '-rf', $tmp); print STDERR "Disk samples: ".scalar(@disk_samples)."\n"; print STDERR "CPU samples : ".scalar(@cpu_samples)."\n"; print STDERR "Processes : ".scalar(keys %ps_info)."\n"; } sub parse_log_header { my $tmp = shift; open(HEADER, "<$tmp/header"); while (
) { chomp; my ($key, $val) = split /\s*=\s*/, $_, 2; $headers{$key} = $val; } close HEADER; } sub parse_log_cpu { my $tmp = shift; # Reads in: time, user, system, iowait my ($time, $ltime); my (@timings, @ltimings); open(PROC_STAT, "<$tmp/proc_stat.log"); while () { chomp; $time = $1, next if /^(\d+)$/; if (/^$/) { if (defined $ltime) { my $dtime = ($time - $ltime) / $HZ; # in seconds my $user = ($timings[0] + $timings[1]) - ($ltimings[0] + $ltimings[1]); my $system = ($timings[2] + $timings[5] + $timings[6]) - ($ltimings[2] + $ltimings[5] + $ltimings[6]); my $idle = $timings[3] - $ltimings[3]; my $iowait = $timings[4] - $ltimings[4]; my $sum = $user + $system + $idle + $iowait; my %sample; $sample{time} = $time; $sample{user} = $user / $sum; $sample{system} = $system / $sum; $sample{iowait} = $iowait / $sum; push @cpu_samples, \%sample; } $ltime = $time; @ltimings = @timings; next; } @timings = split /\s+/ if s/^cpu\s+//; } close(PROC_STAT); } sub parse_log_disk { my $tmp = shift; # Reads in: time, read, write, use my ($time, $ltime); my ($read, $write, $util) = (0, 0, 0); my ($lread, $lwrite, $lutil); open(DISK_STAT, "<$tmp/proc_diskstats.log"); while () { chomp; $time = $1, next if /^(\d+)$/; if (/^$/) { if (defined $ltime) { my $dtime = ($time - $ltime) / $HZ; # in seconds my $dutil = ($util - $lutil) / (1000 * $dtime); my %sample; $sample{time} = $time; $sample{read} = ($read - $lread) / 2 / $dtime; # in KB/s $sample{write} = ($write - $lwrite) / 2 / $dtime; # in KB/s $sample{util} = ($dutil > 1 ? 1 : $dutil); push @disk_samples, \%sample; } $ltime = $time; $lread = $read, $read = 0; $lwrite = $write, $write = 0; $lutil = $util, $util = 0; next; } s/\s+//; my @tokens = split /\s+/; next if scalar(@tokens) != 14 || not $tokens[2] =~ /hd|sd/; $read += $tokens[5]; $write += $tokens[9]; $util += $tokens[12]; } close(DISK_STAT); } sub parse_log_ps { my $tmp = shift; my $time; open(PS_STAT, "<$tmp/proc_ps.log"); while () { chomp; next if /^$/; $time = $1, next if /^(\d+)$/; my @tokens = split /\s+/; my $pid = $tokens[0]; if (!defined $ps_info{$pid}) { my %info; my @empty; my $ppid = $tokens[3]; my $start = $tokens[21]; $t_begin = $start if !defined $t_begin || $t_begin > $start; $info{ppid} = $ppid; $info{start} = $start; $info{samples} = \@empty; $ps_info{$pid} = \%info; if (!defined $ps_by_parent{$ppid}) { my @pidlist = ($pid); $ps_by_parent{$ppid} = \@pidlist; } else { push @{$ps_by_parent{$ppid}}, $pid; } } # argv may change, we'll store here the last sampled value my $comm = $tokens[1]; $comm =~ s/[()]//g; $ps_info{$pid}->{comm} = $comm; my %sample; $sample{time} = $time; $sample{state} = $tokens[2]; $sample{sys} = $tokens[13]; $sample{user} = $tokens[14]; push @{$ps_info{$pid}->{samples}}, \%sample; } close PS_STAT; } sub unxml { my $x = shift; $x =~ s//>/g; return $x; } sub render_svg { my $t_init = $cpu_samples[0]->{time}; $t_end = $cpu_samples[-1]->{time}; $t_dur = $t_end - $t_begin; $img_w = $t_dur / $HZ * $sec_w; my @subst; $subst[1] = $svg_css_file; # # Headers # my $cpu = $headers{'system.cpu'}; $cpu =~ s/model name\s*:\s*//; $subst[2] = unxml($headers{title}); $subst[3] = "uname: ".unxml($headers{'system.uname'}); $subst[4] = "release: ".unxml($headers{'system.release'}); $subst[5] = "CPU: ".unxml($cpu); $subst[6] = "kernel options: ".unxml($headers{'system.kernel.options'}); $subst[7] = "boot time: ".int($t_end / $HZ)." seconds"; # # CPU I/O chart # my $bar_h = 50; my $cpu_ticks = ''; for (my $i = 0; $i < $t_dur / $HZ; $i++) { my $x = $i * $sec_w; $cpu_ticks .= "\n"; } $cpu_ticks =~ s/^/\t\t\t/mg; $cpu_ticks =~ s/^\s+//g; my $io_points = ''; my $cpu_points = ''; if (@cpu_samples) { my $last_x = 0; for (@cpu_samples) { my $time = $_->{time}; my $iowait = $_->{iowait}; my $usersys = $_->{user} + $_->{system}; my $pos_x = int(($time - $t_begin) * $img_w / $t_dur); my $pos_y = int($bar_h - ($usersys + $iowait) * $bar_h); $io_points .= "$pos_x,$bar_h" if $io_points eq ''; $io_points .= " $pos_x,$pos_y"; $pos_y = int($bar_h - $usersys * $bar_h); $cpu_points .= "$pos_x,$bar_h" if $cpu_points eq ''; $cpu_points .= " $pos_x,$pos_y"; $last_x = $pos_x; } $io_points .= " $last_x,$bar_h"; $cpu_points .= " $last_x,$bar_h"; } $subst[8] = $cpu_ticks; $subst[9] = $io_points; $subst[10] = $cpu_points; # # Disk usage chart # my $util_points = ''; my $read_points = ''; if (@disk_samples) { my $max_tput = 0; my $max_tput_label = ''; for (@disk_samples) { my $put = $_->{read} + $_->{write}; $max_tput = $put if $put > $max_tput; } my $last_x = 0; my $last_y = 0; for (@disk_samples) { my $time = $_->{time}; my $read = $_->{read}; my $write = $_->{write}; my $util = $_->{util}; my $pos_x = int(($time - $t_begin) * $img_w / $t_dur); my $pos_y = int($bar_h - $util * $bar_h); $util_points .= "$pos_x,$bar_h" if $util_points eq ''; $util_points .= " $pos_x,$pos_y"; $pos_y = int($bar_h - ($read + $write) / $max_tput * $bar_h); if ($read_points ne '') { $read_points .= "\t\t\t"; } $read_points .= "\n"; if ($max_tput_label eq '' && $read + $write == $max_tput) { my $label = int($max_tput / 1024)." MB/s"; $max_tput_label = "\t\t\t$label\n"; } $last_x = $pos_x; $last_y = $pos_y; } $util_points .= " $last_x,$bar_h"; $read_points .= $max_tput_label; } $subst[11] = $cpu_ticks; $subst[12] = $util_points; $subst[13] = $read_points; $subst[14] = ''; # open_points, no openfile parser implemented # # Process tree # my $tree_h = scalar(@cpu_samples) * $proc_h; my $axis = ''; my $proc_ticks = ''; for (my $i = 0; $i < $t_dur / $HZ; $i++) { my $x = $i * $sec_w; $proc_ticks .= "\n"; if ($i > 0 && $i % 5 == 0) { my $label = $i.'s'; $axis .= "$label\n"; } } $proc_ticks =~ s/^/\t\t\t/mg; $proc_ticks =~ s/^\s+//g; $axis =~ s/^/\t\t\t/mg; $axis =~ s/^\s+//g; my $proc_tree = ''; my @init = (1); my $ps_count = 0; $proc_tree = render_proc_tree(\@init, \$ps_count); $subst[15] = $axis; $subst[16] = $proc_ticks; $subst[17] = $proc_tree; $img_h = $proc_h * $ps_count + $header_h; $subst[0] = 'width="'.($img_w > $min_img_w ? $img_w : $min_img_w).'px" height="'.($img_h + 1).'px"'; print template_subst($chart_svg_template, @subst); } sub render_proc_tree { my $ps_list = shift; my $ps_count = shift; my $level = shift || 0; my $proc_tree = ''; for (sort {$a <=> $b} @{$ps_list}) { # Hide bootchartd and its children processes next if $ps_info{$_}->{comm} eq 'bootchartd'; my $children = $ps_by_parent{$_}; $proc_tree .= render_proc($_, $ps_count, $level); $proc_tree .= render_proc_tree($children, $ps_count, $level + 1) if defined $children; } return $proc_tree; } sub render_proc { my $pid = shift; my $ps_count = shift; my $level = shift; my $info = $ps_info{$pid}; return '' if defined $info->{done}; # Draw once my @samples = @{$info->{samples}}; return '' if scalar(@samples) < 2; # Skip processes with only 1 sample my $p_begin = ($pid == 1) ? $samples[0]->{time} : $info->{start}; my $p_period = $samples[1]->{time} - $p_begin; my $p_end = $samples[-1]->{time}; my $p_dur = $p_end - $p_begin + $p_period; my $x = ($p_begin - $t_begin) * $sec_w / $HZ; my $y = ${$ps_count} * $proc_h; my $w = $p_dur * $sec_w / $HZ; my $position = "$x,$y"; my $border = "width=\"$w\" height=\"$proc_h\""; my $timeline = "\n"; my ($last_tx, $last_sample); for my $sample (@samples) { my $time = $sample->{time}; my $state = $sample->{state}; my $tx = ($time - $p_begin) * $sec_w / $HZ; $tx = 0 if $tx < 0; if (defined $last_sample) { my $tw = $tx - $last_tx; if ($state ne 'S') { my $sys = ($sample->{sys} - $last_sample->{sys}) / $HZ; my $user = ($sample->{user} - $last_sample->{user}) / $HZ; my %class = ('D' => 'UnintSleep', 'R' => 'Running', 'T' => 'Traced', 'Z' => 'Zombie'); my $opacity = ($state eq 'R') ? ' fill-opacity="'.($sys + $user).'"' : ''; $timeline .= "\n"; } } $last_tx = $tx; $last_sample = $sample; } $timeline =~ s/^/\t\t/mg; $timeline =~ s/^\s+//g; my $label = $info->{comm}; my $label_pos = ($w < 200 && ($x + $w + 200) < $img_w) ? "dx=\"2\" dy=\"".($proc_h - 1)."\" x=\"".($w + 1)."\" y=\"0\"" : "dx=\"".($w / 2)."\" dy=\"".($proc_h - 1)."\" x=\"0\" y=\"0\""; my @subst = ($position, $timeline, $border, '', $label_pos, $label, ''); $info->{done} = 1; ${$ps_count}++; return template_subst($process_svg_template, @subst); } sub template_subst { my $template = shift; my $i = 0; for(@_) { $template =~ s/\Q{$i}\E/$_[$i]/g; $i++; } return $template; } parse_logs('/var/log/bootchart.tgz'); render_svg();