#!/usr/bin/perl # irqtop # # by Robert Elliott, HP # contributed to the sysstat project # ######################################################################### # 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 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 ######################################################################### # # Monitor differences in /proc/interrupts and /proc/softirqs # per CPU, along with CPU statistics # # Usage: irqtop [interval] # # Displays interrupts that have occurred since this program # began, filtering those that have always been zeros. # # TODO features: # * increase column widths automatically # * add an all-CPU column # * add option to choose between # - online CPUs # - just the all-CPU column # - select CPUs (e.g., ranges like in smp_affinity_list) # * automatically determine interrupts to total (e.g. # all mpt3sas or hpsaN interrupts) based on them having # common prefixes in their names or common names # use strict; use Getopt::Long; my $myname = "irqtop"; my $myversion = "0.2"; sub print_usage { print "Usage: $myname [interval] [-V | --version] [-h | --help]\n\n"; print " [internal] time in seconds between updates (default: 1 second)\n"; print " [-V | --version] display version number\n"; print " [-h | --help] display usage\n\n"; print "Use Control-C to exit\n"; } #my $mpstat = "../sysstat/mpstat"; my $mpstat = "mpstat"; # calculate total interrupt stats for this list #my @track_list = (); my @track_list = ("eth", "hpsa1", "hpsa2", "mpt3sas"); # exclude these from per-vector display (e.g., eth while studying storage) # still included in totals if also in track_list #my @exclude_list = (); my @exclude_list = ("eth"); my $interval = 1; # default interval in seconds # hashes of arrays # key is the interrupt number or name (e.g., 95 or TIMER) # array is the interrupt counts and the description on the right my %current; my %delta; # hold the deltas between collection and printing # hash of values my %ever_changed; # indicates if this row ever changed my %track_interrupts; # number of each type my $online_cpus; # as seen by process_hardirq, used to filter columns in softirq # return 1 to exclude, 0 to not sub is_excluded { my ($line) = @_; foreach (@exclude_list) { return 1 if ($line =~ /$_/); } return 0; } # convert affinity_list bitmask (64-bit binary) into numbered list # Example: 5800a000f -> "0-3,17,19,31-32,34" # used for parsing /proc/irq/NN/affinity_hint sub bitmask_to_list { my ($bitmask) = @_; my $inrange; my $rangelist; my $rangenew; my $lowcpu; my $highcpu; for (my $i = 0; $i < 63; $i++) { if (!$inrange && $bitmask & 1) { $inrange = 1; $lowcpu = $i; $highcpu = $i; } elsif ($inrange && $bitmask & 1) { $highcpu = $i; } elsif ($inrange && !($bitmask & 1)) { $inrange = 0; if ($lowcpu == $highcpu) { $rangenew = "$lowcpu"; } else { $rangenew = "$lowcpu-$highcpu"; } if ($rangelist) { $rangelist = "$rangelist,$rangenew"; } else { $rangelist = "$rangenew"; } } $bitmask = $bitmask >> 1; } if ($inrange) { $rangelist = "$rangelist,$lowcpu-$highcpu"; } if (!$rangelist) { $rangelist = "none"; } return $rangelist; } # process /proc/interrupts # argument: # 0 do not display - use the first time to not display values since reboot # 1 display - use on all subsequent calls sub collect_hardirqs { my ($firstpass) = @_; open HARDIRQFILE, "/proc/interrupts" or die "Cannot open $_"; foreach (@track_list) { $track_interrupts{$_} = 0; } # /proc/interrupts lists only online cpus # first line contains CPUnn headers for each column # CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7 CPU8 CPU9 CPU10 CPU11 $_ = ; my @cpulist = split; $online_cpus = $#cpulist + 1; # hardirqs are a source of online_cpus # remaining lines contain vector number: per-cpu counts, interrupt type, device # or vector name, per-CPU counts, interrupt type, description while () { my @values = split; my $irqname = $values[0]; my $cpu; my $line = $_; for ($cpu = 0; $cpu < $online_cpus; $cpu++) { $delta{$irqname}[$cpu] = $values[$cpu + 1] - $current{$irqname}[$cpu]; $current{$irqname}[$cpu] = $values[$cpu + 1]; # if this is not the first pass, # keep track of whether the values have changed if ($delta{$irqname}[$cpu] && !$firstpass && !is_excluded($line)) { $ever_changed{$irqname} = 1; } foreach (@track_list) { if ($line =~ /$_/) { $track_interrupts{$_} += $delta{$irqname}[$cpu]; } } } # capture the rest of the line (interrupt type, handlers) # these are not really per-cpu values, but continue storing # each word in %current since it's convenient for (; $cpu < $#values + 1; $cpu++ ) { $current{$irqname}[$cpu] = $values[$cpu + 1]; } } close HARDIRQFILE; } # process /proc/softirqs # argument: # 0 do not display - use the first time to not display values since reboot # 1 display - use on all subsequent calls sub collect_softirqs { my ($firstpass) = @_; open SOFTIRQFILE, "/proc/softirqs" or die "Cannot open $_"; # /proc/softirqs includes all possible cpus, not all online cpus # this function ignores all those extra cpus # first line contains CPUnn headers for each column # CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7 CPU8 CPU9 CPU10 CPU11 $_ = ; my @cpulist = split; # discard the first line # remaining lines contain # vector number: per-cpu counts, interrupt type, device # or vector name: per-cpu counts, interrupt type, description while () { my @values = split; my $irqname = $values[0]; my $line = $_; # just remember the values for online cpus, not offline cpus for (my $cpu = 0; $cpu < $online_cpus; $cpu++) { $delta{$irqname}[$cpu] = $values[$cpu + 1] - $current{$irqname}[$cpu]; $current{$irqname}[$cpu] = $values[$cpu + 1]; if ($delta{$irqname}[$cpu] && !$firstpass && !is_excluded($line)) { $ever_changed{$irqname} = 1; } } } close SOFTIRQFILE; } # print the CPU0 CPU1 CPU2... header line for the online cpus sub print_cpulist { for (my $cpu = 0; $cpu < $online_cpus; $cpu++) { my $cpustring; $cpustring = sprintf("CPU%d", $cpu); printf("%6s ", $cpustring); } print "\n"; } # print hardirqs and softirqs from %delta that have ever %changed sub print_irqs { # header line printf("%13s ", "--- IRQs ---"); # print_cpulist(); # uncomment if print_mpstat is not used, since that prints the header line print "\n"; foreach (sort keys %delta) { my $irqname = $_; # if any values have ever changed for an interrupt, print the delta values if ($ever_changed{$irqname}) { # match the width of softirq names 12 characters plus : printf("%13s ", $irqname); my $cpu; for ($cpu = 0; $cpu < $online_cpus; $cpu++) { printf("%6d ", $delta{$irqname}[$cpu]); } # print the rest of the line (interrupt type, handlers) for (; $cpu < @{$current{$irqname}} + 1; $cpu++ ) { print "$current{$irqname}[$cpu] "; } # print the irq smp affinity list, if any my $irqname_nocolon = $irqname; $irqname_nocolon =~ s/://; if (!($irqname =~ /[A-Z]/)) { my $affinity_hint_file = "/proc/irq/$irqname_nocolon/affinity_hint"; if (-e $affinity_hint_file) { open IRQAFF, "<$affinity_hint_file"; my $affhint = ; close IRQAFF; chomp $affhint; my ($affhigh,$afflow) = $affhint =~ /(.*),(.*)/; my $aff = hex $affhigh << 32 | hex $afflow; printf("hint=%s,", bitmask_to_list($aff)); } else { print "hint=none,"; } my $smp_affinity_list_file = "/proc/irq/$irqname_nocolon/smp_affinity_list"; if (-e $smp_affinity_list_file) { open IRQAFF, "<$smp_affinity_list_file"; my $afflist = ; close IRQAFF; chomp $afflist; print "aff=$afflist"; } else { print "aff=none"; } } print "\n"; } } # print summary of selected sets of interrupts foreach (@track_list) { printf("%12s: total=%-7d, average=%-7d; total/s=%-7d, average/s=%-7d\n", $_, $track_interrupts{$_}, $track_interrupts{$_} / $online_cpus, $track_interrupts{$_} / $interval, $track_interrupts{$_} / $interval / $online_cpus); } } # collect interesting CPU statistics from mpstat # %usr, %sys, %iowait, %idle, %irq, %soft # mpstat displays: # 06:14:48 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle # plain "mpstat" shows averages since boot, which is not useful # so, use the interval option based on the first argument to this function, # which also causes this function to incur the delay. # per-cpu arrays of value strings my @usr; my @sys; my @irq; my @soft; my @iowait; my @idle; sub collect_mpstat { my ($delay) = @_; open MPSTATFILE, "$mpstat -P ON -u $delay 1 |" or sleep($delay); while () { if (/CPU/ || /all/ || /^$/ || /Average/) { next; } my ($time, $ampm, $cpu, $usr, $nice, $sys, $iowait, $irq, $soft, $steal, $guest, $idle) = split; $usr[$cpu] = $usr; $sys[$cpu] = $sys; $irq[$cpu] = $irq; $soft[$cpu] = $soft; $iowait[$cpu] = $iowait; $idle[$cpu] = $idle; $online_cpus = $cpu + 1; # mpstat is a source for # online CPUs } close MPSTATFILE; } # print the interesting CPU statistics from mpstat # collected in collect_mpstat() sub print_mpstat { # header line printf("%13s ", "CPU usage:"); print_cpulist(); printf("%13s ", "\%usr:"); for (my $cpu = 0; $cpu < $online_cpus; $cpu++) { printf("%6s ", $usr[$cpu]); } print "\n"; printf("%13s ", "\%sys:"); for (my $cpu = 0; $cpu < $online_cpus; $cpu++) { printf("%6s ", $sys[$cpu]); } print "\n"; printf("%13s ", "\%irq:"); for (my $cpu = 0; $cpu < $online_cpus; $cpu++) { printf("%6s ", $irq[$cpu]); } print "\n"; printf("%13s ", "\%soft:"); for (my $cpu = 0; $cpu < $online_cpus; $cpu++) { printf("%6s ", $soft[$cpu]); } print "\n"; printf("%13s ", "\%iowait idle:"); # clarify that iowait is really idle time for (my $cpu = 0; $cpu < $online_cpus; $cpu++) { printf("%6s ", $iowait[$cpu]); } print "\n"; printf("%13s ", "\%idle:"); for (my $cpu = 0; $cpu < $online_cpus; $cpu++) { printf("%6s ", $idle[$cpu]); } print "\n"; } # # start of program # foreach (@ARGV) { if (/--version/ || /-V/) { print "$myname version $myversion\n"; exit; } elsif (/--help/ || /-h/) { print_usage(); exit; } elsif (!/[a-z]/) { # assume an all-numeric argument is the interval $interval = $_; } print "Collecting CPU and interrupt activity for $interval seconds between updates\n"; } # remember the clear screen characters to avoid system() during run time my $clearscreen = `clear`; collect_hardirqs(1); collect_softirqs(1); while (1) { collect_hardirqs(0); collect_softirqs(0); collect_mpstat($interval); my $datestring = localtime(); print "${clearscreen}$myname $datestring interval: $interval s\n"; print_mpstat(); print_irqs(); # if collect_mpstat(), which delays, is commented out, sleep here # sleep($interval); }