aboutsummaryrefslogtreecommitdiff
path: root/tools/tools/prstats/prstats.pl
diff options
context:
space:
mode:
Diffstat (limited to 'tools/tools/prstats/prstats.pl')
-rw-r--r--tools/tools/prstats/prstats.pl357
1 files changed, 357 insertions, 0 deletions
diff --git a/tools/tools/prstats/prstats.pl b/tools/tools/prstats/prstats.pl
new file mode 100644
index 000000000000..c31241edb0b4
--- /dev/null
+++ b/tools/tools/prstats/prstats.pl
@@ -0,0 +1,357 @@
+#!/usr/bin/perl -w
+#-
+# Copyright (c) 2001 Dag-Erling Coïdan Smørgrav
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer
+# in this position and unchanged.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# 3. The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# $FreeBSD$
+#
+
+use strict;
+use Data::Dumper;
+use Fcntl;
+use POSIX qw(isatty mktime strftime tzset);
+use vars qw($TTY $NOW %MONTH %PR @EVENTS @COUNT @AGE);
+use vars qw(%STATE %CATEGORY %OWNER %CLOSER);
+
+%MONTH = (
+ 'Jan' => 1,
+ 'Feb' => 2,
+ 'Mar' => 3,
+ 'Apr' => 4,
+ 'May' => 5,
+ 'Jun' => 6,
+ 'Jul' => 7,
+ 'Aug' => 8,
+ 'Sep' => 9,
+ 'Oct' => 10,
+ 'Nov' => 11,
+ 'Dec' => 12,
+);
+
+@AGE = (
+ [ 0, 7, 0 ], # Less than one week
+ [ 7, 30, 0 ], # One week to one month
+ [ 30, 90, 0 ], # One to three months
+ [ 90, 365, 0 ], # Three months to a year
+ [ 365, 1095, 0 ], # One to three years
+ [ 1095, 999999, 0 ], # More than three years
+);
+
+sub GNATS_DIR { "/home/gnats" }
+sub GNATS_TZ { "America/Los_Angeles" }
+sub DATFILE { "/tmp/prstats.dat.$$" }
+sub GNUPLOT { "|/usr/local/bin/gnuplot /dev/stdin" }
+sub TIMEFMT { "%Y-%m-%d/%H:%M:%S" }
+
+sub parse_date($) {
+ my $date = shift; # Date to parse
+
+ my $year;
+ my $month;
+ my $day;
+ my $hour;
+ my $minute;
+ my $second;
+
+ $date =~ s/\s+/ /g;
+ $date =~ s/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\w*\s*//;
+ if ($date =~ m/^(\w{3}) (\d\d?) (\d\d):(\d\d):(\d\d) [A-Z ]*(\d{4})$/) {
+ ($month, $day, $hour, $minute, $second, $year) =
+ ($1, $2, $3, $4, $5, $6);
+ } else {
+ die("Unrecognized date format: $date\n");
+ }
+ defined($month = $MONTH{$month})
+ or die("Invalid month: $month\n");
+ return mktime($second, $minute, $hour, $day, $month - 1, $year - 1900);
+}
+
+sub scan_pr($) {
+ my $fn = shift; # File name
+
+ local *FILE; # File handle
+ my $pr = {}; # PR hash
+ my $age; # PR age
+
+ sysopen(FILE, $fn, O_RDONLY)
+ or die("$fn: open(): $!\n");
+ while (<FILE>) {
+ if (m/^>([A-Za-z-]+):\s+(.*?)\s*$/o ||
+ m/^(Category|Responsible|State-Changed-[A-Za-z-]+):\s+(.*?)\s*$/o) {
+ $pr->{lc($1)} = $2;
+ }
+ }
+
+ exists($PR{$pr->{'number'}})
+ and die("$fn: PR $pr->{'number'} already exists\n");
+
+ if ($TTY) {
+ print(" "x40, "\r", scalar(keys(%PR)),
+ " $pr->{'category'}/$pr->{'number'} ");
+ }
+
+ foreach ('arrival-date', 'closed-date', 'last-modified',
+ 'state-changed-when') {
+ if (defined($pr->{$_}) && length($pr->{$_})) {
+ $pr->{$_} = parse_date($pr->{$_});
+ }
+ }
+
+ $pr->{'_created'} = $pr->{'arrival-date'};
+ if ($pr->{'state'} eq 'closed') {
+ $pr->{'_closed'} = $pr->{'closed-date'} || $pr->{'state-changed-when'};
+ $pr->{'_closed_by'} = $pr->{'state-changed-by'};
+ if (!defined($pr->{'_closed_by'})) {
+ warn("PR $pr->{'category'}/$pr->{'number'} is incomplete\n");
+ return;
+ }
+ ++$CLOSER{$pr->{'_closed_by'}};
+ } else {
+ $age = $pr->{'arrival-date'} / 86400;
+ foreach (@AGE) {
+ if ($age >= $_->[0] && $age < $_->[1]) {
+ ++$_->[2];
+ last;
+ }
+ }
+ ++$CATEGORY{$pr->{'category'}};
+ ++$OWNER{$pr->{'responsible'}};
+ }
+ ++$STATE{$pr->{'state'}};
+
+ $PR{$pr->{'number'}} = {
+ 'category' => $pr->{'category'},
+ #'number' => $pr->{'number'},
+ 'responsible' => $pr->{'responsible'},
+ 'created' => $pr->{'created'},
+ 'closed' => $pr->{'closed'},
+ 'closer' => $pr->{'_closed_by'},
+ };
+ push(@EVENTS, [ $pr->{'_created'}, +1 ]);
+ push(@EVENTS, [ $pr->{'_closed'}, -1 ])
+ if defined($pr->{'_closed'});
+}
+
+sub scan_recurse($);
+sub scan_recurse($) {
+ my $dn = shift; # Directory name
+
+ local *DIR; # Directory handle
+ my $entry; # Entry
+
+ opendir(DIR, $dn)
+ or die("$dn: opendir(): $!\n");
+ while ($entry = readdir(DIR)) {
+ next if ($entry eq '.' || $entry eq '..');
+ if (-d "$dn/$entry") {
+ scan_recurse("$dn/$entry");
+ } elsif ($entry =~ m/^\d+$/) {
+ eval {
+ scan_pr("$dn/$entry");
+ };
+ }
+ }
+ closedir(DIR);
+}
+
+sub count_prs() {
+
+ my $pr; # Iterator
+ my @events; # Creations or closures
+ my $event; # Iterator
+ my $count; # PR count
+
+ if ($TTY) {
+ print(int(@EVENTS), " events\n");
+ }
+ @COUNT = ( [ 0, 0 ] );
+ foreach $event (sort({ $a->[0] <=> $b->[0] } @EVENTS)) {
+ if ($event->[0] == $COUNT[-1]->[0]) {
+ $COUNT[-1]->[1] += $event->[1];
+ } else {
+ push(@COUNT, [ $event->[0], $COUNT[-1]->[1] + $event->[1] ]);
+ }
+ }
+ if (@COUNT > 1) {
+ $COUNT[0]->[0] = $COUNT[1]->[0] - 1;
+ unshift(@COUNT, [ 0, 0 ]);
+ }
+}
+
+sub gnuplot(@) {
+ my @commands = @_; # Commands
+
+ my $pid; # Child PID
+ local *PIPE; # Pipe
+
+ open(PIPE, &GNUPLOT)
+ or die("fork(): $!\n");
+ print(PIPE join("\n", @commands, ""));
+ close(PIPE);
+ if ($? & 0x7f) {
+ die("gnuplot caught a signal " . ($? & 0x7f) . "\n");
+ } elsif ($?) {
+ die("gunplot returned exit code " . ($? >> 8) . "\n");
+ }
+}
+
+sub write_dat_file($) {
+ my $fn = shift; # File name
+
+ local *FILE; # File handle
+ my $datum; # Iterator
+
+ sysopen(FILE, $fn, O_RDWR|O_CREAT|O_TRUNC, 0640)
+ or die("$fn: open(): $!\n");
+ foreach $datum (@COUNT) {
+ print(FILE strftime(&TIMEFMT, localtime($datum->[0])),
+ " ", $datum->[1],
+ " ", $COUNT[-1]->[1],
+ "\n");
+ }
+ close(FILE);
+}
+
+sub graph_open_prs($$$$$) {
+ my $datfn = shift; # Data file name
+ my $fn = shift; # File name
+ my $start = shift; # Starting date
+ my $end = shift; # Ending date
+ my $title = shift; # Title
+
+ my $tickfmt; # Tick format
+ my $timefmt; # Time format
+
+ if ($end - $start > 86400 * 30) {
+ $tickfmt = "%Y-%m-%d";
+ } else {
+ $tickfmt = "%m-%d";
+ }
+ $start = strftime(&TIMEFMT, localtime($start));
+ $end = strftime(&TIMEFMT, localtime($end));
+ $timefmt = &TIMEFMT;
+ gnuplot("
+set term png small color
+set xdata time
+set timefmt '$timefmt'
+set data style line
+set grid
+set output '$fn'
+set format x '$tickfmt'
+set xrange ['$start':'$end']
+set yrange [0:*]
+set title '$title'
+plot '$datfn' using 1:2 title 'Open PRs'
+");
+}
+
+sub pr_stat_summary() {
+
+ my $n; # Loop counter
+
+ # Overall stats
+ printf("Total PRs in database: %d\n", scalar(keys(%PR)));
+ printf("Open PRs: %d\n", scalar(keys(%PR)) - $STATE{'closed'});
+ print("\n");
+
+ # Category ranking
+ print("Number of PRs in each category:\n");
+ foreach (sort({ $CATEGORY{$b} <=> $CATEGORY{$a} } keys(%CATEGORY))) {
+ printf("%12s: %d\n", $_, $CATEGORY{$_});
+ }
+ print("\n");
+
+ # State ranking
+ print("Number of PRs in each state:\n");
+ foreach (sort({ $STATE{$b} <=> $STATE{$a} } keys(%STATE))) {
+ printf("%12s: %d\n", $_, $STATE{$_});
+ }
+ print("\n");
+
+ # Closer ranking
+ print("Top ten PR busters:\n");
+ $n = 0;
+ foreach (sort({ $CLOSER{$b} <=> $CLOSER{$a} } keys(%CLOSER))) {
+ printf(" %2d. %s (%d)\n", ++$n, $_, $CLOSER{$_});
+ last if ($n == 10);
+ }
+ print("\n");
+
+ # Owner ranking
+ print("Top ten owners of open PRs:\n");
+ $n = 0;
+ foreach (sort({ $OWNER{$b} <=> $OWNER{$a} } keys(%OWNER))) {
+ next if (m/^freebsd-(bugs|doc|ports)$/);
+ printf(" %2d. %s (%d)\n", ++$n, $_, $OWNER{$_});
+ last if ($n == 10);
+ }
+ print("\n");
+
+}
+
+MAIN:{
+ $| = 1;
+ $TTY = isatty(*STDOUT);
+
+ # Perl lacks strptime(), and its mktime() doesn't accept a
+ # timezone argument, so we set our local timezone to that of the
+ # FreeBSD cluster and use localtime() instead.
+ $ENV{'TZ'} = &GNATS_TZ;
+ tzset();
+ $NOW = time();
+
+ # Read and count PRs
+ if (@ARGV) {
+ foreach (@ARGV) {
+ scan_recurse(join('/', &GNATS_DIR, $_));
+ }
+ } else {
+ scan_recurse(&GNATS_DIR);
+ }
+ if ($TTY) {
+ print("\r", scalar(keys(%PR)), " problem reports scanned\n");
+ }
+
+ # Generate graphs
+ if (0) {
+ count_prs();
+ write_dat_file(&DATFILE);
+ graph_open_prs(&DATFILE, "week.png", $NOW - (86400 * 7) + 1, $NOW,
+ "Open FreeBSD problem reports (week view)");
+ graph_open_prs(&DATFILE, "month.png", $NOW - (86400 * 30) + 1, $NOW,
+ "Open FreeBSD problem reports (month view)");
+ graph_open_prs(&DATFILE, "year.png", $NOW - (86400 * 365) + 1, $NOW,
+ "Open FreeBSD problem reports (year view)");
+ graph_open_prs(&DATFILE, "ever.png", $COUNT[1]->[0], $NOW,
+ "Open FreeBSD problem reports (project history)");
+ graph_open_prs(&DATFILE, "drive.png", mktime(0, 0, 0, 29, 4, 101), $NOW,
+ "Open FreeBSD problem reports (drive progress)");
+ unlink(&DATFILE);
+ }
+
+ # Print summary
+ pr_stat_summary();
+}