#!/usr/bin/perl -w # keine0.pl - Don Yang (uguu.org) # # Simple time tracking tool. # # 08/25/07 use strict; use Time::Local; use constant LOG_START => "BEGIN LOG"; use constant LOG_END => "END LOG"; my (@Log, $LogFile, %Dict); # Convert UTC seconds to string time sub EncodeTime($) { my ($s, $m, $h, $day, $month, $year) = localtime $_[0]; return sprintf '%04d-%02d-%02d %02d:%02d:%02d', $year + 1900, $month + 1, $day, $h, $m, $s; } # Convert string time to UTC seconds sub DecodeTime($) { return undef unless $_[0] =~ /^(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/; return timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900); } # Format a single time duration as a string sub FormatTimeLength($) { my ($t) = @_; my $s = $t % 60; my $m = int($t /= 60) % 60; my $h = int($t /= 60) % 24; my $d = int($t / 24); return sprintf('%d (%dd %02d:%02d:%02d)', $_[0], $d, $h, $m, $s); } # Add command to log sub LogCommand($$) { my ($time, $cmd) = @_; # Buffer to memory my $event = EncodeTime($time) . ' ' . $cmd; push @Log, $event; # Flush to file immediately my $file; open $file, ">> $LogFile" or die $!; if ( $cmd eq "> " . LOG_END ) { $event .= "\n"; } print $file $event, "\n"; close $file; } # Show report for all events loaded in log sub ShowReport() { # Get stats my (%tasks, %int, $switch, $current_task, $start_time, $total_time); $switch = $total_time = 0; $current_task = undef; foreach my $event (@Log) { # Parse event, ignoring comments next unless $event =~ /^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d) ([<>]) (.*)/; my ($time, $prefix, $event) = (DecodeTime($1), $2, $3); if( $prefix eq '<' ) { $int{$event}++; } else { if( $event ne LOG_START && $event ne LOG_END ) { $switch++; } } # Update event times if( defined $current_task ) { # Note: no check for negative times here $tasks{$current_task} += $time - $start_time; $total_time += $time - $start_time; } if( $event ne LOG_START && $event ne LOG_END ) { $current_task = $event; $start_time = $time; } else { $current_task = undef; } } # Add last unterminated event if( defined $current_task ) { $tasks{$current_task} += time - $start_time; } # Output task time stats if( $total_time > 0 ) { print "Total time = ", FormatTimeLength($total_time), "\n"; foreach my $event (sort {$tasks{$b} <=> $tasks{$a}} keys %tasks) { print " $event = ", FormatTimeLength($tasks{$event}), "\n"; } } # Output interrupt stats my ($total_int, $interrupt_time); $total_int = $interrupt_time = 0; foreach my $event (keys %int) { $total_int += $int{$event}; $interrupt_time += $tasks{$event}; } if( $total_int > 0 ) { print "Interrupts = $total_int / ", FormatTimeLength($interrupt_time), "\n"; foreach my $event (sort {$tasks{$b} <=> $tasks{$a}} keys %int) { print " $event = $int{$event} / ", FormatTimeLength($tasks{$event}), "\n"; } } if( $switch > 0 ) { print "Context switch = $switch\n"; } # Blank line at end of nonempty report print "\n" if $total_time > 0; } # Show list of available commands and abbreviations sub ShowHelp() { print <<'EOT'; Commands: task Record context switch to "task" > task Record context switch to "task" < task Record interrupt to "task" - task Record interrupt to "task" . prefix Record context switch to previous task matching "prefix" , prefix Record interrupt to previous task matching "prefix" / substr Record context switch to previous task containing "substr" \ substr Record interrupt to previous task containing "substr" key=task Define abbreviation and record task # key=task Define abbreviation without recording task ? Show commands and abbreviations ! Reload events and abbreviations from file (empty string) Show stats (EOF) Exit EOT if( scalar keys %Dict ) { print "Abbreviations:\n"; foreach my $key (sort keys %Dict) { print " $key = $Dict{$key}\n"; } } } # Find last command in log that matches expected prefix. If nothing # matched, return prefix as is. sub FindLastCommand($$) { my ($key, $prefix) = @_; for(my $i = $#Log; $i >= 0; $i--) { my $event = substr($Log[$i], length('####-##-## ##:##:## ')); next if $event =~ /^#/; my $index = index(substr($event, 2), $key); if( $index >= 0 ) { next if $prefix && $index != 0; return substr($event, 2); } } return $key; } # Run a single command sub RunCommand($$) { my ($time, $cmd) = @_; # Check if dictionary need to be updated if( $cmd =~ s/(#?)\s*(\S+)\s*=\s*(\S.*)$/$1$3/ ) { $Dict{$2} = $3; LogCommand($time, "# $2=$3"); return if $1 eq '#'; } elsif( $cmd =~ /^(\S+)$/ ) { # Expand command if( exists $Dict{$1} ) { $cmd = $Dict{$1}; } } # Get command prefix my $prefix = ">"; if( $cmd =~ s/^([,.<>\/\\#-])\s*(.*?)/$2/ ) { $prefix = $1; if( exists $Dict{$cmd} ) { # Expand command after removing prefix, but don't use # the prefix defined in the abbreviation. ($cmd = $Dict{$cmd}) =~ s/^[<>]\s*(.*?)/$1/; } if( $prefix eq "-" ) { $prefix = "<"; } } # Check for special commands if( $cmd eq "" ) { ShowReport(); return; } elsif( $cmd eq "?" ) { ShowHelp(); return; } elsif( $cmd eq "!" ) { OpenLog(); return; } if( $prefix =~ /[,.]/ ) { # Find previous command matching prefix $cmd = FindLastCommand($cmd, 1); $prefix = $prefix eq '.' ? '>' : '<'; } elsif( $prefix =~ /[\/\\]/ ) { # Find previous command containing substring $cmd = FindLastCommand($cmd, 0); $prefix = $prefix eq '/' ? '>' : '<'; } # Record command to memory and disk LogCommand($time, "$prefix $cmd"); # Echo event that just got recorded print $Log[$#Log], "\n"; } # Load existing log file and abbreviations sub OpenLog() { my $file; if( open $file, "< $LogFile" ) { @Log = (); %Dict = (); while(my $line = <$file>) { chomp $line; if( $line =~ /\d{4}-\d\d-\d\d \d\d:\d\d:\d\d ([#<>] .*)/ ) { # Load event my $command = $1; push @Log, $line; # Add abbreviation if( $command =~ /# (\S+)\s*=\s*(\S.*)/ ) { $Dict{$1} = $2; } } } close $file; print "Loaded ", $#Log + 1, " events from $LogFile\n\n"; ShowReport(); } else { print "Starting empty log $LogFile\n", "Enter ? for help\n\n"; } } sub CloseLog() { RunCommand(time, LOG_END); print "Stopped logging to $LogFile\n"; exit 0; } # Main program loop if( $#ARGV == 0 ) { # Interactive loop $LogFile = $ARGV[0]; OpenLog(); RunCommand(time, LOG_START); $SIG{'INT'} = "CloseLog"; while() { chomp; RunCommand(time, $_); } CloseLog(); } elsif( $#ARGV == -1 && !(-t STDIN) ) { # Summarize report while(my $line = ) { if( $line =~ /\d{4}-\d\d-\d\d \d\d:\d\d:\d\d ([<>] .*)/ ) { push @Log, $line; } } ShowReport(); } else { # Command line help print <<"EOT"; $0 log.txt Start logging commands to log.txt $0 < log.txt Summarize log.txt and exit EOT }