Commit ce902b95 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'ee-groups-controller-show-performance' into 'master'

Preload Group plans in EpicsFinder

Closes #6206

See merge request gitlab-org/gitlab-ee!5877
parents 05ef0599 348c89c4
...@@ -52,6 +52,8 @@ class EpicsFinder < IssuableFinder ...@@ -52,6 +52,8 @@ class EpicsFinder < IssuableFinder
private private
def groups_user_can_read_epics(groups) def groups_user_can_read_epics(groups)
groups = Gitlab::GroupPlansPreloader.new.preload(groups)
DeclarativePolicy.user_scope do DeclarativePolicy.user_scope do
groups.select { |g| Ability.allowed?(current_user, :read_epic, g) } groups.select { |g| Ability.allowed?(current_user, :read_epic, g) }
end end
......
...@@ -155,6 +155,19 @@ module EE ...@@ -155,6 +155,19 @@ module EE
actual_plan&.pipeline_size_limit.to_i actual_plan&.pipeline_size_limit.to_i
end end
def memoized_plans=(plans)
@plans = plans # rubocop: disable Gitlab/ModuleWithInstanceVariables
end
def plans
@plans ||=
if parent_id
Plan.where(id: self_and_ancestors.with_plan.reorder(nil).select(:plan_id))
else
Array(plan)
end
end
private private
def validate_plan_name def validate_plan_name
...@@ -180,14 +193,5 @@ module EE ...@@ -180,14 +193,5 @@ module EE
globally_available globally_available
end end
end end
def plans
@plans ||=
if parent_id
Plan.where(id: self_and_ancestors.with_plan.reorder(nil).select(:plan_id))
else
Array(plan)
end
end
end end
end end
---
title: Preload Group plans in EpicsFinder
merge_request:
author:
type: performance
# frozen_string_literal: true
module Gitlab
# Preloading of Plans for one or more groups.
#
# This class can be used to efficiently preload the plans of a given list of
# groups, including any plans the groups may have access to based on their
# parent groups.
class GroupPlansPreloader
# Preloads all the plans for the given Groups.
#
# groups - An ActiveRecord::Relation returning a set of Group instances.
#
# Returns an Array containing all the Groups, including their preloaded
# plans.
def preload(groups)
groups_and_ancestors = groups_and_ancestors_for(groups)
# A Hash mapping group IDs to their corresponding Group instances.
groups_map = groups_and_ancestors.each_with_object({}) do |group, hash|
hash[group.id] = group
end
all_plan_ids = Set.new
# A Hash that for every group ID maps _all_ the plan IDs this group has
# access to.
plans_map = groups_and_ancestors
.each_with_object(Hash.new { |h, k| h[k] = [] }) do |group, hash|
current = group
while current
if (plan_id = current.plan_id)
hash[group.id] << plan_id
all_plan_ids << plan_id
end
current = groups_map[current.parent_id]
end
end
# Grab all the plans for all the Groups, using only a single query.
plans = Plan
.where(id: all_plan_ids.to_a)
.each_with_object({}) do |plan, hash|
hash[plan.id] = plan
end
# Assign all the plans to the groups that have access to them.
groups.each do |group|
group.memoized_plans = plans_map[group.id].map { |id| plans[id] }
end
end
# Returns an ActiveRecord::Relation that includes the given groups, and all
# their (recursive) ancestors.
def groups_and_ancestors_for(groups)
Gitlab::GroupHierarchy
.new(groups)
.base_and_ancestors
.select(:id, :parent_id, :plan_id)
end
end
end
...@@ -53,6 +53,12 @@ describe EpicsFinder do ...@@ -53,6 +53,12 @@ describe EpicsFinder do
expect(epics).to contain_exactly(epic1, epic2, epic3) expect(epics).to contain_exactly(epic1, epic2, epic3)
end end
it 'does not execute more than 7 SQL queries' do
amount = ActiveRecord::QueryRecorder.new { epics.to_a }.count
expect(amount).to be <= 7
end
context 'by created_at' do context 'by created_at' do
it 'returns all epics created before the given date' do it 'returns all epics created before the given date' do
expect(epics(created_before: 2.days.ago)).to contain_exactly(epic1, epic2) expect(epics(created_before: 2.days.ago)).to contain_exactly(epic1, epic2)
...@@ -97,6 +103,24 @@ describe EpicsFinder do ...@@ -97,6 +103,24 @@ describe EpicsFinder do
it 'returns all epics that belong to the given group and its subgroups' do it 'returns all epics that belong to the given group and its subgroups' do
expect(epics).to contain_exactly(epic1, epic2, epic3, subepic1, subepic2) expect(epics).to contain_exactly(epic1, epic2, epic3, subepic1, subepic2)
end end
it 'does not execute more than 9 SQL queries' do
amount = ActiveRecord::QueryRecorder.new { epics.to_a }.count
expect(amount).to be <= 9
end
it 'does not execute more than 11 SQL queries when checking namespace plans' do
allow(Gitlab::CurrentSettings)
.to receive(:should_check_namespace_plan?)
.and_return(true)
group.update(plan: create(:gold_plan))
amount = ActiveRecord::QueryRecorder.new { epics.to_a }.count
expect(amount).to be <= 10
end
end end
context 'by timeframe' do context 'by timeframe' do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::GroupPlansPreloader do
describe '#preload' do
let!(:plan1) { create(:free_plan, name: 'plan-1') }
let!(:plan2) { create(:free_plan, name: 'plan-2') }
let(:preloaded_groups) do
# We don't use the factory objects here because they might have the plan
# loaded already (as we specify the plan when creating them).
described_class.new.preload(Group.order(id: :asc))
end
before do
group1 = create(:group, name: 'group-1', plan_id: plan1.id)
create(:group, name: 'group-2', plan_id: plan2.id)
create(:group, name: 'group-3', parent: group1)
end
it 'only executes three SQL queries to preload the data' do
amount = ActiveRecord::QueryRecorder
.new { preloaded_groups }
.count
# One query to get the groups and their ancestors, one query to get their
# plans, and one query to _just_ get the groups.
expect(amount).to eq(3)
end
it 'associates the correct plans with the correct groups' do
expect(preloaded_groups[0].plans).to eq([plan1])
expect(preloaded_groups[1].plans).to eq([plan2])
expect(preloaded_groups[2].plans).to eq([plan1])
end
it 'does not execute any queries for preloaded plans' do
preloaded_groups
amount = ActiveRecord::QueryRecorder
.new { preloaded_groups.each(&:plans) }
.count
expect(amount).to be_zero
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment