With the migration from GNATS to Bugzilla, we no longer need a tool to
graph GNATS PR statistics. It would be nice to have a similar tool for the new bug database, but that would likely be a from-scratch rewrite.
This commit is contained in:
parent
8b04766b28
commit
872442932b
@ -55,7 +55,6 @@ pciid Generate src/share/misc/pci_vendors.
|
||||
pciroms A tool for dumping PCI ROM images. WARNING: alpha quality.
|
||||
pirtool A tool for dumping the $PIR table on i386 machines at runtime.
|
||||
portsinfo Generate list of new ports for last two weeks.
|
||||
prstats Generate statistics about the PR database.
|
||||
recoverdisk Copy as much data as possible from a defective disk.
|
||||
scsi-defects Get at the primary or grown defect list of a SCSI disk.
|
||||
sysdoc Build a manual page with available sysctls for a specific
|
||||
|
@ -1,357 +0,0 @@
|
||||
#!/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();
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user