]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 176002: Move duplicate statistics into the db
authormkanat%bugzilla.org <>
Sun, 6 Sep 2009 22:45:51 +0000 (22:45 +0000)
committermkanat%bugzilla.org <>
Sun, 6 Sep 2009 22:45:51 +0000 (22:45 +0000)
Patch by Max Kanat-Alexander <mkanat@bugzilla.org> r=LpSolit, a=LpSolit

Bugzilla/DB/Schema.pm
Bugzilla/Install/DB.pm
Bugzilla/Install/Filesystem.pm
collectstats.pl
duplicates.cgi
template/en/default/global/user-error.html.tmpl
template/en/default/reports/duplicates-table.html.tmpl

index 583b49352f5c1c5c16bd61b8a1232f2f5e0019a0..5e8c49ccf514df7f54db3bc3ccac752f5390697e 100644 (file)
@@ -352,7 +352,7 @@ use constant ABSTRACT_SCHEMA => {
             fieldid   => {TYPE => 'INT3', NOTNULL => 1,
                           REFERENCES    =>  {TABLE  =>  'fielddefs',
                                              COLUMN =>  'id'}},
-            added     => {TYPE => 'TINYTEXT'},
+            added     => {TYPE => 'varchar(255)'},
             removed   => {TYPE => 'TINYTEXT'},
         ],
         INDEXES => [
@@ -360,6 +360,7 @@ use constant ABSTRACT_SCHEMA => {
             bugs_activity_who_idx     => ['who'],
             bugs_activity_bug_when_idx => ['bug_when'],
             bugs_activity_fieldid_idx => ['fieldid'],
+            bugs_activity_added_idx   => ['added'],
         ],
     },
 
index dd4b2fe1d47a531384e16bffc1397c70794bb429..d39d1a770e1430403c41ee2cdf1a0599ce89ab4e 100644 (file)
@@ -574,6 +574,10 @@ sub update_table_definitions {
         
     _convert_disallownew_to_isactive();
 
+    $dbh->bz_alter_column('bugs_activity', 'added', 
+        { TYPE => 'varchar(255)' });
+    $dbh->bz_add_index('bugs_activity', 'bugs_activity_added_idx', ['added']);
+
     ################################################################
     # New --TABLE-- changes should go *** A B O V E *** this point #
     ################################################################
index fe9783805ddb72f1d7e8a9ecfa20f24145c8c329..763fd85bfeb3534eeccedcff2cec6f9b590d0d73 100644 (file)
@@ -157,8 +157,6 @@ sub FILESYSTEM {
          # Readable directories
          "$datadir/mining"     => { files => $ws_readable,
                                      dirs => $ws_dir_readable },
-         "$datadir/duplicates" => { files => $ws_readable,
-                                     dirs => $ws_dir_readable },
          "$libdir/Bugzilla"    => { files => $ws_readable,
                                      dirs => $ws_dir_readable },
          $extlib               => { files => $ws_readable,
@@ -379,6 +377,11 @@ EOT
         unlink "$datadir/duplicates.rdf";
         unlink "$datadir/duplicates-old.rdf";
     }
+
+    if (-e "$datadir/duplicates") {
+        print "Removing duplicates directory...\n";
+        rmtree("$datadir/duplicates");
+    }
 }
 
 # A simple helper for creating "empty" CSS files.
index e550c16134e6fecdee721310300ce8dc2a9ad78f..5f9b4eef337d6b8b0ee2918126077b2b50bb5b5a 100755 (executable)
 use strict;
 use lib qw(. lib);
 
-use AnyDBM_File;
-use IO::Handle;
 use List::Util qw(first);
 use Cwd;
 
-
 use Bugzilla;
 use Bugzilla::Constants;
 use Bugzilla::Error;
@@ -160,8 +157,6 @@ my $tend = time;
 # Uncomment the following line for performance testing.
 #print "Total time taken " . delta_time($tstart, $tend) . "\n";
 
-&calculate_dupes();
-
 CollectSeriesData();
 
 sub check_data_dir {
@@ -299,81 +294,6 @@ sub get_old_data {
     return @data;
 }
 
-sub calculate_dupes {
-    my $dbh = Bugzilla->dbh;
-    my $rows = $dbh->selectall_arrayref("SELECT dupe_of, dupe FROM duplicates");
-
-    my %dupes;
-    my %count;
-    my $key;
-    my $changed = 1;
-
-    my $today = &today_dash;
-
-    # Save % count here in a date-named file
-    # so we can read it back in to do changed counters
-    # First, delete it if it exists, so we don't add to the contents of an old file
-    my $datadir = bz_locations()->{'datadir'};
-
-    if (my @files = <$datadir/duplicates/dupes$today*>) {
-        map { trick_taint($_) } @files;
-        unlink @files;
-    }
-   
-    dbmopen(%count, "$datadir/duplicates/dupes$today", 0644) || die "Can't open DBM dupes file: $!";
-
-    # Create a hash with key "a bug number", value "bug which that bug is a
-    # direct dupe of" - straight from the duplicates table.
-    foreach my $row (@$rows) {
-        my ($dupe_of, $dupe) = @$row;
-        $dupes{$dupe} = $dupe_of;
-    }
-
-    # Total up the number of bugs which are dupes of a given bug
-    # count will then have key = "bug number", 
-    # value = "number of immediate dupes of that bug".
-    foreach $key (keys(%dupes)) 
-    {
-        my $dupe_of = $dupes{$key};
-
-        if (!defined($count{$dupe_of})) {
-            $count{$dupe_of} = 0;
-        }
-
-        $count{$dupe_of}++;
-    }
-
-    # Now we collapse the dupe tree by iterating over %count until
-    # there is no further change.
-    while ($changed == 1)
-    {
-        $changed = 0;
-        foreach $key (keys(%count)) {
-            # if this bug is actually itself a dupe, and has a count...
-            if (defined($dupes{$key}) && $count{$key} > 0) {
-                # add that count onto the bug it is a dupe of,
-                # and zero the count; the check is to avoid
-                # loops
-                if ($count{$dupes{$key}} != 0) {
-                    $count{$dupes{$key}} += $count{$key};
-                    $count{$key} = 0;
-                    $changed = 1;
-                }
-            }
-        }
-    }
-
-    # Remove the values for which the count is zero
-    foreach $key (keys(%count))
-    {
-        if ($count{$key} == 0) {
-            delete $count{$key};
-        }
-    }
-   
-    dbmclose(%count);
-}
-
 # This regenerates all statistics from the database.
 sub regenerate_stats {
     my ($dir, $product, $bug_resolution, $bug_status, $removed) = @_;
index af239d6323e96d52858697e865e76c0d4e8fc75a..4c050986470723c38dc1c9a8260810f636f0e86f 100755 (executable)
 # Copyright (C) 1998 Netscape Communications Corporation. All
 # Rights Reserved.
 #
-# Contributor(s): Gervase Markham <gerv@gerv.net>
-#
-# Generates mostfreq list from data collected by collectstats.pl.
-
+# Contributor(s): 
+#   Gervase Markham <gerv@gerv.net>
+#   Max Kanat-Alexander <mkanat@bugzilla.org>
 
 use strict;
-
-use AnyDBM_File;
-
 use lib qw(. lib);
 
 use Bugzilla;
@@ -34,28 +30,52 @@ use Bugzilla::Constants;
 use Bugzilla::Util;
 use Bugzilla::Error;
 use Bugzilla::Search;
+use Bugzilla::Field;
 use Bugzilla::Product;
 
+###############
+# Subroutines #
+###############
+
+# $counts is a count of exactly how many direct duplicates there are for
+# each bug we're considering. $dups is a map of duplicates, from one
+# bug_id to another. We go through the duplicates map ($dups) and if one bug
+# in $count is a duplicate of another bug in $count, we add their counts
+# together under the target bug.
+sub add_indirect_dups {
+    my ($counts, $dups) = @_;
+
+    foreach my $add_from (keys %$dups) {
+        my $add_to     = walk_dup_chain($dups, $add_from);
+        my $add_amount = delete $counts->{$add_from} || 0;
+        $counts->{$add_to} += $add_amount;
+    }
+}
+
+sub walk_dup_chain {
+    my ($dups, $from_id) = @_;
+    my $to_id = $dups->{$from_id};
+    while (my $bug_id = $dups->{$to_id}) {
+        last if $bug_id == $from_id; # avoid duplicate loops
+        $to_id = $bug_id;
+    }
+    # Optimize for future calls to add_indirect_dups.
+    $dups->{$from_id} = $to_id;
+    return $to_id;
+}
+
+###############
+# Main Script #
+###############
+
 my $cgi = Bugzilla->cgi;
 my $template = Bugzilla->template;
 my $vars = {};
 
-# collectstats.pl uses duplicates.cgi to generate the RDF duplicates stats.
-# However, this conflicts with requirelogin if it's enabled; so we make
-# logging-in optional if we are running from the command line.
-if ($::ENV{'GATEWAY_INTERFACE'} eq "cmdline") {
-    Bugzilla->login(LOGIN_OPTIONAL);
-}
-else {
-    Bugzilla->login();
-}
+Bugzilla->login();
 
 my $dbh = Bugzilla->switch_to_shadow_db();
 
-my %dbmcount;
-my %count;
-my %before;
-
 # Get params from URL
 sub formvalue {
     my ($name, $default) = (@_);
@@ -70,6 +90,10 @@ my $reverse = formvalue("reverse") ? 1 : 0;
 my @query_products = $cgi->param('product');
 my $sortvisible = formvalue("sortvisible");
 my @buglist = (split(/[:,]/, formvalue("bug_id")));
+detaint_natural($_) foreach @buglist;
+# If we got any non-numeric items, they will now be undef. Remove them from
+# the list.
+@buglist = grep($_, @buglist);
 
 # Make sure all products are valid.
 foreach my $p (@query_products) {
@@ -79,54 +103,6 @@ foreach my $p (@query_products) {
 # Small backwards-compatibility hack, dated 2002-04-10.
 $sortby = "count" if $sortby eq "dup_count";
 
-# Open today's record of dupes
-my $today = days_ago(0);
-my $yesterday = days_ago(1);
-
-# We don't know the exact file name, because the extension depends on the
-# underlying dbm library, which could be anything. We can't glob, because
-# perl < 5.6 considers if (<*>) { ... } to be tainted
-# Instead, just check the return value for today's data and yesterday's,
-# and ignore file not found errors
-
-use Errno;
-use Fcntl;
-
-my $datadir = bz_locations()->{'datadir'};
-
-if (!tie(%dbmcount, 'AnyDBM_File', "$datadir/duplicates/dupes$today",
-         O_RDONLY, 0644)) {
-    if ($!{ENOENT}) {
-        if (!tie(%dbmcount, 'AnyDBM_File', "$datadir/duplicates/dupes$yesterday",
-                 O_RDONLY, 0644)) {
-            my $vars = { today => $today };
-            if ($!{ENOENT}) {
-                ThrowUserError("no_dupe_stats", $vars);
-            } else {
-                $vars->{'error_msg'} = $!;
-                ThrowUserError("no_dupe_stats_error_yesterday", $vars);
-            }
-        }
-    } else {
-        ThrowUserError("no_dupe_stats_error_today",
-                       { error_msg => $! });
-    }
-}
-
-# Copy hash (so we don't mess up the on-disk file when we remove entries)
-%count = %dbmcount;
-
-# Remove all those dupes under the threshold parameter. 
-# We do this, before the sorting, for performance reasons.
-my $threshold = Bugzilla->params->{"mostfreqthreshold"};
-
-while (my ($key, $value) = each %count) {
-    delete $count{$key} if ($value < $threshold);
-    
-    # If there's a buglist, restrict the bugs to that list.
-    delete $count{$key} if $sortvisible && (lsearch(\@buglist, $key) == -1);
-}
-
 my $origmaxrows = $maxrows;
 detaint_natural($maxrows)
   || ThrowUserError("invalid_maxrows", { maxrows => $origmaxrows});
@@ -136,34 +112,45 @@ detaint_natural($changedsince)
   || ThrowUserError("invalid_changedsince", 
                     { changedsince => $origchangedsince });
 
-# Try and open the database from "changedsince" days ago
-my $dobefore = 0;
-my %delta;
-my $whenever = days_ago($changedsince);    
-
-if (!tie(%before, 'AnyDBM_File', "$datadir/duplicates/dupes$whenever",
-         O_RDONLY, 0644)) {
-    # Ignore file not found errors
-    if (!$!{ENOENT}) {
-        ThrowUserError("no_dupe_stats_error_whenever",
-                       { error_msg => $!,
-                         changedsince => $changedsince,
-                         whenever => $whenever,
-                       });
-    }
-} else {
-    # Calculate the deltas
-    ($delta{$_} = $count{$_} - ($before{$_} || 0)) foreach (keys(%count));
 
-    $dobefore = 1;
+my %total_dups = @{$dbh->selectcol_arrayref(
+    "SELECT dupe_of, COUNT(dupe)
+       FROM duplicates
+   GROUP BY dupe_of", {Columns => [1,2]})};
+
+my %dupe_relation = @{$dbh->selectcol_arrayref(
+    "SELECT dupe, dupe_of FROM duplicates
+      WHERE dupe IN (SELECT dupe_of FROM duplicates)",
+    {Columns => [1,2]})};
+add_indirect_dups(\%total_dups, \%dupe_relation);
+
+my $reso_field_id = get_field_id('resolution');
+my %since_dups = @{$dbh->selectcol_arrayref(
+    "SELECT dupe_of, COUNT(dupe)
+       FROM duplicates INNER JOIN bugs_activity 
+                       ON bugs_activity.bug_id = duplicates.dupe 
+      WHERE added = 'DUPLICATE' AND fieldid = ? AND " 
+            . $dbh->sql_to_days('bug_when') . " >= (" 
+            . $dbh->sql_to_days('NOW()') . " - ?)
+   GROUP BY dupe_of", {Columns=>[1,2]},
+    $reso_field_id, $changedsince)};
+add_indirect_dups(\%since_dups, \%dupe_relation);
+
+my (@bugs, @bug_ids);
+
+foreach my $id (keys %total_dups) {
+    if ($total_dups{$id} < Bugzilla->params->{'mostfreqthreshold'}) {
+        delete $total_dups{$id};
+        next;
+    }
+    if ($sortvisible and @buglist and !grep($_ == $id, @buglist)) {
+        delete $total_dups{$id};
+    }
 }
 
-my @bugs;
-my @bug_ids; 
-
-if (scalar(%count)) {
+if (scalar %total_dups) {
     # use Bugzilla::Search so that we get the security checking
-    my $params = new Bugzilla::CGI({ 'bug_id' => [keys %count] });
+    my $params = new Bugzilla::CGI({ 'bug_id' => [keys %total_dups] });
 
     if ($openonly) {
         $params->param('resolution', '---');
@@ -221,8 +208,8 @@ if (scalar(%count)) {
             $short_desc, $bug_status, $resolution) = @$result;
 
         push (@bugs, { id => $id,
-                       count => $count{$id},
-                       delta => $delta{$id}
+                       count => $total_dups{$id},
+                       delta => $since_dups{$id} || 0
                        component => $component,
                        bug_severity => $bug_severity,
                        op_sys => $op_sys,
@@ -237,7 +224,6 @@ if (scalar(%count)) {
 $vars->{'bugs'} = \@bugs;
 $vars->{'bug_ids'} = \@bug_ids;
 
-$vars->{'dobefore'} = $dobefore;
 $vars->{'sortby'} = $sortby;
 $vars->{'sortvisible'} = $sortvisible;
 $vars->{'changedsince'} = $changedsince;
@@ -264,9 +250,3 @@ print $cgi->header(
 # Generate and return the UI (HTML page) from the appropriate template.
 $template->process($format->{'template'}, $vars)
   || ThrowTemplateError($template->error());
-
-
-sub days_ago {
-    my ($dom, $mon, $year) = (localtime(time - ($_[0]*24*60*60)))[3, 4, 5];
-    return sprintf "%04d-%02d-%02d", 1900 + $year, ++$mon, $dom;
-}
index c4eefb4aa0df771e42ae8e2c8e945f6d9efb68e4..3783e523b5812eacac9531d08ca36113fbacc249 100644 (file)
     [% title = "No Tag Selected" %]
     You didn't select a tag from which to remove [% terms.bugs %].
 
-  [% ELSIF error == "no_dupe_stats" %]
-    [% title = "Cannot Find Duplicate Statistics" %]
-    [% admindocslinks = {'extraconfig.html' => 'Setting up the collecstats.pl job'} %]
-    There are no duplicate statistics for today ([% today FILTER html %]) 
-    or yesterday.
-
-  [% ELSIF error == "no_dupe_stats_error_today" %]
-    [% title = "Error Reading Today's Dupes File" %]
-    [% admindocslinks = {'extraconfig.html' => 'Setting up the collecstats.pl job'} %]
-    An error occurred opening today's dupes file: [% error_msg FILTER html %].
-
-  [% ELSIF error == "no_dupe_stats_error_whenever" %]
-    [% title = "Error Reading Previous Dupes File" %]
-    [% admindocslinks = {'extraconfig.html' => 'Setting up the collecstats.pl job'} %]
-    An error occurred opening [% changedsince FILTER html %] days ago
-    ([% whenever FILTER html %])'s dupes file:
-    [% error_msg FILTER html %].
-
-  [% ELSIF error == "no_dupe_stats_error_yesterday" %]
-    [% title = "Error Reading Yesterday's Dupes File" %]
-    [% admindocslinks = {'extraconfig.html' => 'Setting up the collecstats.pl job'} %]
-    There are no duplicate statistics for today ([% today FILTER html %]), 
-    and an error
-    occurred opening yesterday's dupes file: [% error_msg FILTER html %].
-
   [% ELSIF error == "no_initial_bug_status" %]
     [% title = "No Initial $terms.Bug Status" %]
     No [% terms.bug %] status is available on [% terms.bug %] creation.
index 5dbef214494f695019ff654a8c41cc6152b38d97..f651a7fd43cc4c5f3241f9e3622508e2b79d6e2d 100644 (file)
@@ -61,9 +61,6 @@
                               { name => "short_desc", description => "Summary" } ]
          %]
 
-          [%# Small hack to keep delta column out if we don't need it %]
-          [% NEXT IF column.name == "delta" AND NOT dobefore %]
-
           <th>
             [% bug_ids_string = bug_ids.join(',') %]
             <a href="duplicates.cgi?sortby=[% column.name %]
             </center>
           </td>
 
-          [% IF dobefore %]
-            <td><center>[% bug.delta %]</center></td>
-          [% END %]
+          <td><center>[% bug.delta %]</center></td>
 
           <td>[% bug.component FILTER html %]</td>
           <td><center>[% bug.bug_severity FILTER html %]</center></td>