]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 576670: Optimize Search.pm's "init" method for being called many times
authorMax Kanat-Alexander <mkanat@bugzilla.org>
Fri, 9 Jul 2010 02:00:31 +0000 (19:00 -0700)
committerMax Kanat-Alexander <mkanat@bugzilla.org>
Fri, 9 Jul 2010 02:00:31 +0000 (19:00 -0700)
in a loop
r=glob, a=mkanat

Bugzilla.pm
Bugzilla/Constants.pm
Bugzilla/Error.pm
Bugzilla/Field.pm
Bugzilla/Search.pm
Bugzilla/Template.pm
Bugzilla/User.pm
xt/lib/Bugzilla/Test/Search.pm

index 33df05efb792fdeaddc2c5f092615c02ce5e1062..6ecbc27db43d29933ed4c4a16f0babebb7b540f4 100644 (file)
@@ -523,6 +523,45 @@ sub switch_to_main_db {
     return $class->dbh_main;
 }
 
+sub fields {
+    my ($class, $criteria) = @_;
+    $criteria ||= {};
+    my $cache = $class->request_cache;
+
+    # We create an advanced cache for fields by type, so that we
+    # can avoid going back to the database for every fields() call.
+    # (And most of our fields() calls are for getting fields by type.)
+    #
+    # We also cache fields by name, because calling $field->name a few
+    # million times can be slow in calling code, but if we just do it
+    # once here, that makes things a lot faster for callers.
+    if (!defined $cache->{fields}) {
+        my @all_fields = Bugzilla::Field->get_all;
+        my (%by_name, %by_type);
+        foreach my $field (@all_fields) {
+            my $name = $field->name;
+            $by_type{$field->type}->{$name} = $field;
+            $by_name{$name} = $field;
+        }
+        $cache->{fields} = { by_type => \%by_type, by_name => \%by_name };
+    }
+
+    my $fields = $cache->{fields};
+    my %requested;
+    if (my $types = $criteria->{type}) {
+        $types = ref($types) ? $types : [$types];
+        %requested = map { %{ $fields->{by_type}->{$_} || {} } } @$types;
+    }
+    else {
+        %requested = %{ $fields->{by_name} };
+    }
+    
+    my $do_by_name = $criteria->{by_name};
+
+    return $do_by_name ? \%requested : [values %requested];
+}
+
+# DEPRECATED. Use fields() instead.
 sub get_fields {
     my $class = shift;
     my $criteria = shift;
@@ -768,6 +807,30 @@ Essentially, causes calls to C<Bugzilla-E<gt>user> to return C<undef>. This has
 effect of logging out a user for the current request only; cookies and
 database sessions are left intact.
 
+=item C<fields>
+
+This is the standard way to get arrays or hashes of L<Bugzilla::Field>
+objects when you need them. It takes the following named arguments
+in a hashref:
+
+=over
+
+=item C<by_name>
+
+If false (or not specified), this method will return an arrayref of
+the requested fields. The order of the returned fields is random.
+
+If true, this method will return a hashref of fields, where the keys
+are field names and the valules are L<Bugzilla::Field> objects.
+
+=item C<type>
+
+Either a single C<FIELD_TYPE_*> constant or an arrayref of them. If specified,
+the returned fields will be limited to the types in the list. If you don't
+specify this argument, all fields will be returned.
+
+=back
+
 =item C<error_mode>
 
 Call either C<Bugzilla->error_mode(Bugzilla::Constants::ERROR_MODE_DIE)>
index d58fe9388af7dc4ee81abeaf77387e69bbfc9ca0..2285f89b0976852724cdbfe501853b8215d21d78 100644 (file)
@@ -35,6 +35,7 @@ use base qw(Exporter);
 
 # For bz_locations
 use File::Basename;
+use Memoize;
 
 @Bugzilla::Constants::EXPORT = qw(
     BUGZILLA_VERSION
@@ -404,11 +405,11 @@ use constant EMPTY_DATETIME_REGEX => qr/^[0\-:\sA-Za-z]+$/;
 
 # See the POD for Bugzilla::Field/is_abnormal to see why these are listed
 # here.
-use constant ABNORMAL_SELECTS => qw(
-    classification
-    product
-    component
-);
+use constant ABNORMAL_SELECTS => {
+    classification => 1,
+    component      => 1,
+    product        => 1,
+};
 
 # The fields from fielddefs that are blocked from non-timetracking users.
 # work_time is sometimes called actual_time.
@@ -619,4 +620,8 @@ sub bz_locations {
     };
 }
 
+# This makes us not re-compute all the bz_locations data every time it's
+# called.
+BEGIN { memoize('bz_locations') };
+
 1;
index 649fdd48666413c8efcfa06caff88fa68386faf5..395cc0dc9d7d3c057683a17d97bc31dbcef07903 100644 (file)
@@ -53,12 +53,6 @@ sub _throw_error {
     $vars ||= {};
 
     $vars->{error} = $error;
-    # Don't show function arguments, in case they contain confidential data.
-    local $Carp::MaxArgNums = -1;
-    # Don't show the error as coming from Bugzilla::Error, show it as coming
-    # from the caller.
-    local $Carp::CarpInternal{'Bugzilla::Error'} = 1; 
-    $vars->{traceback} = Carp::longmess();
 
     # Make sure any transaction is rolled back (if supported).
     # If we are within an eval(), do not roll back transactions as we are
@@ -159,6 +153,16 @@ sub ThrowUserError {
 }
 
 sub ThrowCodeError {
+    my (undef, $vars) = @_;
+
+    # Don't show function arguments, in case they contain
+    # confidential data.
+    local $Carp::MaxArgNums = -1;
+    # Don't show the error as coming from Bugzilla::Error, show it
+    # as coming from the caller.
+    local $Carp::CarpInternal{'Bugzilla::Error'} = 1;
+    $vars->{traceback} = Carp::longmess();
+
     _throw_error("global/code-error.html.tmpl", @_);
 }
 
index 26025015ab34a739ecfe19ba49f750b4b077a985..c0e88fc37b9db0a0da1390ccac68d8d825b4519c 100644 (file)
@@ -544,7 +544,7 @@ This method returns C<1> if the field is "abnormal", C<0> otherwise.
 
 sub is_abnormal {
     my $self = shift;
-    return grep($_ eq $self->name, ABNORMAL_SELECTS) ? 1 : 0;
+    return ABNORMAL_SELECTS->{$self->name} ? 1 : 0;
 }
 
 sub legal_values {
index 3653cff678f2a1f370179e9e85030ece3454891b..63e4586553f1210f8a09b2caab0eb92489c45e6f 100644 (file)
@@ -461,13 +461,11 @@ sub init {
     my %special_order      = %{SPECIAL_ORDER()};
     my %special_order_join = %{SPECIAL_ORDER_JOIN()};
 
-    my @select_fields = 
-        Bugzilla->get_fields({ type => FIELD_TYPE_SINGLE_SELECT });
+    my $select_fields = Bugzilla->fields({ type => FIELD_TYPE_SINGLE_SELECT });
     
-    my @multi_select_fields = Bugzilla->get_fields({
-        type     => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS],
-        obsolete => 0 });
-    foreach my $field (@select_fields) {
+    my $multi_select_fields = Bugzilla->fields({
+        type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS]});
+    foreach my $field (@$select_fields) {
         next if $field->is_abnormal;
         my $name = $field->name;
         $special_order{$name} = [ "$name.sortkey", "$name.value" ],
@@ -522,7 +520,7 @@ sub init {
         push(@supptables, "LEFT JOIN longdescs AS ldtime " .
                           "ON ldtime.bug_id = bugs.bug_id");
     }
-    foreach my $field (@multi_select_fields) {
+    foreach my $field (@$multi_select_fields) {
         my $field_name = $field->name;
         next if !grep($_ eq $field_name, @fields);
         push(@supptables, "LEFT JOIN bug_$field_name AS map_bug_$field_name"
@@ -594,15 +592,18 @@ sub init {
     
     # All fields that don't have a . in their name should be specifyable
     # in the URL directly.
-    my @legal_fields = grep { $_->name !~ /\./ } Bugzilla->get_fields;
+    my $legal_fields = Bugzilla->fields({ by_name => 1 });
     if (!$user->is_timetracker) {
-        foreach my $field (TIMETRACKING_FIELDS) {
-            @legal_fields = grep { $_->name ne $field } @legal_fields;
+        foreach my $name (TIMETRACKING_FIELDS) {
+            delete $legal_fields->{$name};
         }
     }
+    foreach my $name (keys %$legal_fields) {
+        delete $legal_fields->{$name} if $name =~ /\./;
+    }
 
     foreach my $field ($params->param()) {
-        if (grep { $_->name eq $field } @legal_fields) {
+        if ($legal_fields->{$field}) {
             my $type = $params->param("${field}_type");
             if (!$type) {
                 if ($field eq 'keywords') {
@@ -940,12 +941,11 @@ sub init {
 # $suppstring = String which is pasted into query containing all table names
 
     # get a list of field names to verify the user-submitted chart fields against
-    my %chartfields = @{$dbh->selectcol_arrayref(
-        q{SELECT name, id FROM fielddefs}, { Columns=>[1,2] })};
+    my $chart_fields = Bugzilla->fields({ by_name => 1 });
 
     if (!$user->is_timetracker) {
         foreach my $tt_field (TIMETRACKING_FIELDS) {
-            delete $chartfields{$tt_field};
+            delete $chart_fields->{$tt_field};
         }
     }
 
@@ -976,12 +976,12 @@ sub init {
 
                 # chart -1 is generated by other code above, not from the user-
                 # submitted form, so we'll blindly accept any values in chart -1
-                if (!$chartfields{$field} and $chart != -1) {
+                if (!$chart_fields->{$field} and $chart != -1) {
                     ThrowCodeError("invalid_field_name", { field => $field });
                 }
 
                 # This is either from the internal chart (in which case we
-                # already know about it), or it was in %chartfields, so it is
+                # already know about it), or it was in $chart_fields, so it is
                 # a valid field name, which means that it's ok.
                 trick_taint($field);
                 my $quoted = $dbh->quote($value);
@@ -996,13 +996,13 @@ sub init {
                     operator   => $operator,
                     value      => $value,
                     quoted     => $quoted,
-                    multi_fields => \@multi_select_fields,
+                    multi_fields => $multi_select_fields,
                     joins        => \@supptables,
                     where        => \@wherepart,
                     having       => \@having,
                     group_by     => \@groupby,
                     fields       => \@fields,
-                    chart_fields => \%chartfields,
+                    chart_fields => $chart_fields,
                 );
                 # This should add a "term" selement to %search_args.
                 $self->do_search_function(\%search_args);
@@ -2358,13 +2358,16 @@ sub _nowords {
 
 sub _changedbefore_changedafter {
     my ($self, $args) = @_;
-    my ($chart_id, $joins, $field, $operator, $value) =
-        @$args{qw(chart_id joins field operator value)};
+    my ($chart_id, $joins, $field, $operator, $value, $chart_fields) =
+        @$args{qw(chart_id joins field operator value chart_fields)};
     my $dbh = Bugzilla->dbh;
     
     my $sql_operator = ($operator =~ /before/) ? '<' : '>';
     my $table = "act_$chart_id";
-    my $field_id = get_field_id($field);
+    my $field_object = $chart_fields->{$field}
+        || ThrowCodeError("invalid_field_name", { field => $field });
+    my $field_id = $field_object->id;
+    
     my $sql_date = $dbh->quote(SqlifyDate($value));
     push(@$joins,
         "LEFT JOIN bugs_activity AS $table"
@@ -2376,12 +2379,14 @@ sub _changedbefore_changedafter {
 
 sub _changedfrom_changedto {
     my ($self, $args) = @_;
-    my ($chart_id, $joins, $field, $operator, $quoted) =
-        @$args{qw(chart_id joins field operator quoted)};
+    my ($chart_id, $joins, $field, $operator, $quoted, $chart_fields) =
+        @$args{qw(chart_id joins field operator quoted chart_fields)};
     
     my $column = ($operator =~ /from/) ? 'removed' : 'added';
     my $table = "act_$chart_id";
-    my $field_id = get_field_id($field);
+    my $field_object = $chart_fields->{$field}
+        || ThrowCodeError("invalid_field_name", { field => $field });
+    my $field_id = $field_object->id;
     push(@$joins,
         "LEFT JOIN bugs_activity AS $table"
         .     " ON $table.bug_id = bugs.bug_id"
@@ -2392,11 +2397,13 @@ sub _changedfrom_changedto {
 
 sub _changedby {
     my ($self, $args) = @_;
-    my ($chart_id, $joins, $field, $operator, $value) =
-        @$args{qw(chart_id joins field operator value)};
+    my ($chart_id, $joins, $field, $operator, $value, $chart_fields) =
+        @$args{qw(chart_id joins field operator value chart_fields)};
     
     my $table = "act_$chart_id";
-    my $field_id = get_field_id($field);
+    my $field_object = $chart_fields->{$field}
+        || ThrowCodeError("invalid_field_name", { field => $field });
+    my $field_id = $field_object->id;
     my $user_id  = login_to_id($value, THROW_ERROR);
     push(@$joins,
         "LEFT JOIN bugs_activity AS $table"
index 4bd3d2d7750e9059d5cd3f4a35470a0e80b80a57..2c2d402a3943d896c0e8c545570b94d43a54786e 100644 (file)
@@ -765,8 +765,8 @@ sub create {
             # A way for all templates to get at Field data, cached.
             'bug_fields' => sub {
                 my $cache = Bugzilla->request_cache;
-                $cache->{template_bug_fields} ||= 
-                    { map { $_->name => $_ } Bugzilla->get_fields() };
+                $cache->{template_bug_fields} ||=
+                    Bugzilla->fields({ by_name => 1 });
                 return $cache->{template_bug_fields};
             },
 
index 78e92eca01f7d8aef047df06f818538b8308d920..09af233ecf2ba069f821480c8f8154701da6e2bd 100644 (file)
@@ -603,18 +603,28 @@ sub groups {
     return $self->{groups};
 }
 
+# It turns out that calling ->id on objects a few hundred thousand
+# times is pretty slow. (It showed up as a significant time contributor
+# when profiling xt/search.t.) So we cache the group ids separately from
+# groups for functions that need the group ids.
+sub _group_ids {
+    my ($self) = @_;
+    $self->{group_ids} ||= [map { $_->id } @{ $self->groups }];
+    return $self->{group_ids};
+}
+
 sub groups_as_string {
     my $self = shift;
-    my @ids = map { $_->id } @{ $self->groups };
-    return scalar(@ids) ? join(',', @ids) : '-1';
+    my $ids = $self->_group_ids;
+    return scalar(@$ids) ? join(',', @$ids) : '-1';
 }
 
 sub groups_in_sql {
     my ($self, $field) = @_;
     $field ||= 'group_id';
-    my @ids = map { $_->id } @{ $self->groups };
-    @ids = (-1) if !scalar @ids;
-    return Bugzilla->dbh->sql_in($field, \@ids);
+    my $ids = $self->_group_ids;
+    $ids = [-1] if !scalar @$ids;
+    return Bugzilla->dbh->sql_in($field, $ids);
 }
 
 sub bless_groups {
@@ -1096,7 +1106,7 @@ sub queryshare_groups {
             }
         }
         else {
-            @queryshare_groups = map { $_->id } @{ $self->groups };
+            @queryshare_groups = @{ $self->_group_ids };
         }
     }
 
@@ -1848,15 +1858,31 @@ sub is_available_username {
     return 1;
 }
 
+# This is used in a few performance-critical areas where we don't want to
+# do check() and pull all the user data from the database.
 sub login_to_id {
     my ($login, $throw_error) = @_;
     my $dbh = Bugzilla->dbh;
-    # No need to validate $login -- it will be used by the following SELECT
-    # statement only, so it's safe to simply trick_taint.
-    trick_taint($login);
-    my $user_id = $dbh->selectrow_array("SELECT userid FROM profiles WHERE " .
-                                        $dbh->sql_istrcmp('login_name', '?'),
-                                        undef, $login);
+    my $cache = Bugzilla->request_cache->{user_login_to_id} ||= {};
+
+    # We cache lookups because this function showed up as taking up a 
+    # significant amount of time in profiles of xt/search.t. However,
+    # for users that don't exist, we re-do the check every time, because
+    # otherwise we break is_available_username.
+    my $user_id;
+    if (defined $cache->{$login}) {
+        $user_id = $cache->{$login};
+    }
+    else {
+        # No need to validate $login -- it will be used by the following SELECT
+        # statement only, so it's safe to simply trick_taint.
+        trick_taint($login);
+        $user_id = $dbh->selectrow_array(
+            "SELECT userid FROM profiles 
+              WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login);
+        $cache->{$login} = $user_id;
+    }
+
     if ($user_id) {
         return $user_id;
     } elsif ($throw_error) {
index af595e373c4b10bacd6e34b547f2ff622a9bd2d3..87df927ca078e23e1810ada964cdbc41e3418657 100644 (file)
@@ -147,7 +147,7 @@ sub all_fields {
     my $self = shift;
     if (not $self->{all_fields}) {
         $self->_create_custom_fields();
-        my @fields = Bugzilla->get_fields;
+        my @fields = @{ Bugzilla->fields };
         @fields = sort { $a->name cmp $b->name } @fields;
         $self->{all_fields} = \@fields;
     }