Commit 470e3c9b authored by Dmitry Gruzd's avatar Dmitry Gruzd

Fix N+1 for searching commits

The Ci::Pipeline data (latest_pipeline) loaded in the CommitPresenter
causes N+1. This change fixes that by using BatchLoader
for latest_pipeline
parent 78deb796
...@@ -23,6 +23,7 @@ module RendersCommits ...@@ -23,6 +23,7 @@ module RendersCommits
def prepare_commits_for_rendering(commits) def prepare_commits_for_rendering(commits)
commits.each(&:lazy_author) # preload commits' authors commits.each(&:lazy_author) # preload commits' authors
commits.each(&:lazy_latest_pipeline)
Banzai::CommitRenderer.render(commits, @project, current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables Banzai::CommitRenderer.render(commits, @project, current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
......
...@@ -18,9 +18,25 @@ class Ci::CommitWithPipeline < SimpleDelegator ...@@ -18,9 +18,25 @@ class Ci::CommitWithPipeline < SimpleDelegator
end end
end end
def lazy_latest_pipeline
BatchLoader.for(sha).batch do |shas, loader|
preload_pipelines = project.ci_pipelines.latest_pipeline_per_commit(shas.compact)
shas.each do |sha|
pipeline = preload_pipelines[sha]
loader.call(sha, pipeline)
end
end
end
def latest_pipeline(ref = nil) def latest_pipeline(ref = nil)
@latest_pipelines.fetch(ref) do |ref| @latest_pipelines.fetch(ref) do |ref|
@latest_pipelines[ref] = latest_pipeline_for_project(ref, project) @latest_pipelines[ref] = if ref
latest_pipeline_for_project(ref, project)
else
lazy_latest_pipeline&.itself
end
end end
end end
......
...@@ -142,6 +142,7 @@ class Commit ...@@ -142,6 +142,7 @@ class Commit
delegate \ delegate \
:pipelines, :pipelines,
:last_pipeline, :last_pipeline,
:lazy_latest_pipeline,
:latest_pipeline, :latest_pipeline,
:latest_pipeline_for_project, :latest_pipeline_for_project,
:set_latest_pipeline_for_ref, :set_latest_pipeline_for_ref,
......
---
title: Fix N+1 for commits with pipelines
merge_request: 59234
author:
type: performance
...@@ -102,6 +102,23 @@ RSpec.describe 'Global elastic search', :elastic, :sidekiq_inline do ...@@ -102,6 +102,23 @@ RSpec.describe 'Global elastic search', :elastic, :sidekiq_inline do
expect(page).to have_css('.search-results') # Confirm there are search results to prevent false positives expect(page).to have_css('.search-results') # Confirm there are search results to prevent false positives
end end
end end
context 'searching commits' do
let(:path_for_one) { search_path(search: '*', scope: 'commits', per_page: 1) }
let(:path_for_multiple) { search_path(search: '*', scope: 'commits', per_page: 5) }
it 'avoids N+1 database queries' do
project.repository.index_commits_and_blobs
ensure_elasticsearch_index!
control = ActiveRecord::QueryRecorder.new { visit path_for_one }
expect(page).to have_css('.results') # Confirm there are search results to prevent false positives
expect { visit path_for_multiple }.not_to exceed_query_limit(control.count).with_threshold(2) # We still have users N+1 here
expect(page).to have_css('.results') # Confirm there are search results to prevent false positives
end
end
end end
describe 'I search through the issues and I see pagination' do describe 'I search through the issues and I see pagination' do
......
...@@ -66,6 +66,7 @@ RSpec.describe RendersCommits do ...@@ -66,6 +66,7 @@ RSpec.describe RendersCommits do
expect do expect do
subject.prepare_commits_for_rendering(merge_request.commits) subject.prepare_commits_for_rendering(merge_request.commits)
merge_request.commits.each(&:latest_pipeline)
end.not_to exceed_all_query_limit(control.count) end.not_to exceed_all_query_limit(control.count)
end end
end end
......
...@@ -26,15 +26,47 @@ RSpec.describe Ci::CommitWithPipeline do ...@@ -26,15 +26,47 @@ RSpec.describe Ci::CommitWithPipeline do
end end
end end
describe '#lazy_latest_pipeline' do
let(:commit_1) do
described_class.new(Commit.new(RepoHelpers.sample_commit, project))
end
let(:commit_2) do
described_class.new(Commit.new(RepoHelpers.another_sample_commit, project))
end
let!(:commits) { [commit_1, commit_2] }
it 'executes only 1 SQL query' do
recorder = ActiveRecord::QueryRecorder.new do
# Running this first ensures we don't run one query for every
# commit.
commits.each(&:lazy_latest_pipeline)
# This forces the execution of the SQL queries necessary to load the
# data.
commits.each { |c| c.latest_pipeline.try(:id) }
end
expect(recorder.count).to eq(1)
end
end
describe '#latest_pipeline' do describe '#latest_pipeline' do
let(:pipeline) { double } let(:pipeline) { double }
shared_examples_for 'fetching latest pipeline' do |ref| shared_examples_for 'fetching latest pipeline' do |ref|
it 'returns the latest pipeline for the project' do it 'returns the latest pipeline for the project' do
expect(commit) if ref
.to receive(:latest_pipeline_for_project) expect(commit)
.with(ref, project) .to receive(:latest_pipeline_for_project)
.and_return(pipeline) .with(ref, project)
.and_return(pipeline)
else
expect(commit)
.to receive(:lazy_latest_pipeline)
.and_return(pipeline)
end
expect(result).to eq(pipeline) expect(result).to eq(pipeline)
end end
......
...@@ -24,6 +24,9 @@ RSpec.describe ::Ci::DestroyPipelineService do ...@@ -24,6 +24,9 @@ RSpec.describe ::Ci::DestroyPipelineService do
subject subject
# We need to reset lazy_latest_pipeline cache to simulate a new request
BatchLoader::Executor.clear_current
# Need to use find to avoid memoization # Need to use find to avoid memoization
expect(Project.find(project.id).pipeline_status.has_status?).to be_falsey expect(Project.find(project.id).pipeline_status.has_status?).to be_falsey
end end
......
...@@ -60,6 +60,9 @@ RSpec.describe Ci::ExpirePipelineCacheService do ...@@ -60,6 +60,9 @@ RSpec.describe Ci::ExpirePipelineCacheService do
pipeline_with_commit.destroy! pipeline_with_commit.destroy!
# We need to reset lazy_latest_pipeline cache to simulate a new request
BatchLoader::Executor.clear_current
# Need to use find to avoid memoization # Need to use find to avoid memoization
expect(Project.find(project_with_repo.id).pipeline_status.has_status?).to be_falsey expect(Project.find(project_with_repo.id).pipeline_status.has_status?).to be_falsey
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