#!/usr/bin/perl -wi
use strict;
## Analyses a ps awxl listing from linux. Script is assumed to run this
## every N seconds and output has a "COUNT: N" line for which run it is.
## andyw - VISTORM 2006. Licensed under the GNU GPL V2.0

# user defines: {{{1
## (these are now defaults only)
my %config;

$config{CPUS}		= 1;	## only important if --percentage requested
$config{SLEEP}		= 15;	## time we slept (needed to work out start time)
$config{CPU_MIN}	= 10;	## ignore processes using fewer than this many
							## seconds of cpu over the whole test
$config{RECORD_MOD}	= 8;	## number of records to skip. This is so we can see
							## big changed in values, rather than trickly ones.
							## This will illustrate the real cpu hogs. Default
							## is 8, and we run every 15 seconds.
							## use '1' to do every record.
$config{fname} = 'pslist';	## filename to read

# variables: {{{1
my (%cur, %prev, $k, $l, $m, $s, $z, $pid, $vsz, $res, $stat, $time, $com);
my ($uid, $i, $rss);
my ($out, $c) = (0,0);

# init: {{{1
get_flags();
my ($coms, $scpu, $tcpu, $rc) = get_pids($config{fname}); # pre-filter the pids
my $initial_time = get_start($config{fname},$rc);

map { $cur{$_}{time}=0; } keys %$coms;

open(A,$config{fname});

# main: {{{1
while (<A>) {
	if (m/COUNT\: (\d+)/o) {
		next unless $1 % $config{RECORD_MOD} == 0;
		if ($1 == 0) {
			print 'START,' . join ',',map { $_ = "$_ $$coms{$_}" }
				reverse sort { $$tcpu{$a} <=> $$tcpu{$b} } keys %$coms;
			print "\n";
		}

		print STDERR;

		if ($1 > 1) {
			print sprintf("%02d:%02d,",
				(localtime($initial_time + $1 * $config{SLEEP}))[2,1]);
			print join',',map {
				$z = $cur{$_}{'time'} - ($prev{$_}{'time'}||$$scpu{$_});
				$z = sprintf("%3d",$z/$config{TSCPU}*100) if $config{PERCENTAGE}||0;
				#print "PID $_ cur $cur{$_}{'time'} prev $prev{$_}{'time'}\n";
				$_ = $z > 0 ? $z : 0;
			} reverse sort { $$tcpu{$a} <=> $$tcpu{$b} } keys %$coms;
	
			print "\n";
		}

		%prev = %cur;
		#%cur = ();
		next;

	} else {
		# COUNT: 0
		#F	UID	PID	PPID	PRI	NI	VSZ\
		#RSS	WCHAN	STAT	TTY	TIME	COMMAND
		#
		# caputre: pid, vsz, rss, stat, time, command
		next if /^F/o;
		if ( ($uid, $pid, $vsz, $rss, $stat, $time, $com) =
			(m/^\S+\s+(\S+)\s+(\S+)\s+\S+\s+\S+\s+\S+\s+(\d+)\s+(\d+)\s+(\S+)\s+\S+\s+\S+\s+(\S+)\s+(\S*)/o)) {
			next if ! exists $$coms{$pid};

			($m, $s) = ($time=~m/(\d+)\:(\d+)/o);
			$time = $m*60+$s||0;
			$com =~ s/,/_/og; # just in case, lose the commas

			$cur{$pid} = {
				'uid', $uid, 'vsz', $vsz, 'rss', $rss, 'stat', $stat,
				'time', $time, 'com', $com
			};

			$prev{$pid}||=$cur{$pid}; # BODGE
			$prev{$pid}{time}||=0; # BODGE to start time off

		} else {
			print STDERR "IRREGULAR LINE: $_";
			print STDERR "$uid\n";
		}
	}
}

# get_pids: {{{1
sub get_pids {
	my (%coms, %t, $rss, $pid, $uid, %scpu, %ecpu, $m, $s, $start, $end);
	my ($fr, $c) = ('',0);
	open(A,shift @_);

	print STDERR "Working out which processes I can throw away: " .
		"(Min utilisation = $config{CPU_MIN} seconds)\n";

	while(<A>) {
		if (/COUNT\: (\d+)/) { $fr = $1 unless length($fr); $c = $1; }
		next if /^COUNT|^F/o;
		if ( ($uid, $pid, $vsz, $rss, $stat, $time, $com) =
			(m/^\S+\s+(\S+)\s+(\S+)\s+\S+\s+\S+\s+\S+\s+(\d+)\s+(\d+)\s+(\S+)\s+\S+\s+\S+\s+(\S+)\s+(.*)/o)) {
			next if $com =~/sshd\:|\/sshd|bash$/o;
			#next if $uid != 500;
			if (!exists $scpu{$pid}) { $scpu{$pid} = $time; }
			$coms{$pid} = $com;
			$ecpu{$pid} = $time;
		} else {
			print STDERR "Irregular Line: $_";
		}
	}

	close(A);

	for $pid (keys %coms) {
		($m, $s)	= ($scpu{$pid}=~m/(\d+)\:(\d+)/o);
		$start		= $m*60+$s||0;
		($m, $s)	= ($ecpu{$pid}=~m/(\d+)\:(\d+)/o);
		$end		= $m*60+$s||0;

		if ($end - $start < $config{CPU_MIN}) {
			delete $coms{$pid};
		} else {
			$t{$pid}	= $end - $start;
			$scpu{$pid}	= $start;
		}

		print STDERR (exists($coms{$pid}) ? 'Removing' : 'Keeping') .
				" pid $pid since it used " . ($end - $start) . 
				"(S: $scpu{$pid} " .  "[$start] E: $ecpu{$pid} [$end]) " .
				" second(s) CPU time\n";
	}
	
	return \%coms, \%scpu, \%t, ($c - $fr);
}

# get_start {{{1
sub get_start {
	my ($fname, $c) = @_;
	return (stat($fname))[9] - $c * $config{SLEEP};
}

# usage {{{1
sub usage {
	print "Usage: $0 --cpu-min=N --sleep=S --recordmerge=R --cpus=N --percentage\n";
	print "          Default is --sleep=15 --recordmerge=8\n";
	print "          percentage needs to know the number of CPU(s)\n";

	exit 1;
}

# get_flags {{{1
sub get_flags {
	for (@ARGV) {
		if (/\-\-sleep=(\d+)/) {
			$config{SLEEP} = $1; next;
		}
		if (/\-\-recordmerge=(\d+)/) {
			$config{RECORD_MOD} = $1; next;
		}
		if (/\-\-cpu\-min=(\d+)/) {
			$config{CPU_MIN} = $1; next;
		}
		if (/\-\-cpus=(\d+)/) {
			$config{CPUS}=$1; next;
		}
		if (/\-\-percentage/) {
			$config{PERCENTAGE}=1; next;
		}
		else { print "$0 @ARGV not understood '$_'\n"; usage() }
	}

	if ($config{PERCENTAGE} && ! int($config{CPUS})) { usage(); }
	$config{TSCPU} = $config{SLEEP} * $config{RECORD_MOD} * $config{CPUS};
}

# vim: ts=4 fdm=marker
