Commit 01ca1a11 authored by Marc Shaw's avatar Marc Shaw Committed by Nick Thomas

Do not include stats when calling diff_batches/metadata

This call was making a gitaly call, which was causing signifcant
performance impact. We use these stats in the file, but they can
be replaced by iterating over the files instead.

Issue: gitlab.com/gitlab-org/gitlab/-/issues/209786
Merge Request: gitlab.com/gitlab-org/gitlab/-/merge_requests/33037
parent 8efcfd0a
---
title: Improve the performance for loading large diffs on a Merge request
merge_request: 33037
author:
type: performance
...@@ -230,11 +230,15 @@ module Gitlab ...@@ -230,11 +230,15 @@ module Gitlab
end end
def added_lines def added_lines
@stats&.additions || diff_lines.count(&:added?) strong_memoize(:added_lines) do
@stats&.additions || diff_lines.count(&:added?)
end
end end
def removed_lines def removed_lines
@stats&.deletions || diff_lines.count(&:removed?) strong_memoize(:removed_lines) do
@stats&.deletions || diff_lines.count(&:removed?)
end
end end
def file_identifier def file_identifier
......
...@@ -88,15 +88,18 @@ module Gitlab ...@@ -88,15 +88,18 @@ module Gitlab
def diff_stats_collection def diff_stats_collection
strong_memoize(:diff_stats) do strong_memoize(:diff_stats) do
# There are scenarios where we don't need to request Diff Stats, next unless fetch_diff_stats?
# when caching for instance.
next unless @include_stats
next unless diff_refs
@repository.diff_stats(diff_refs.base_sha, diff_refs.head_sha) @repository.diff_stats(diff_refs.base_sha, diff_refs.head_sha)
end end
end end
def fetch_diff_stats?
# There are scenarios where we don't need to request Diff Stats,
# when caching for instance.
@include_stats && diff_refs
end
def decorate_diff!(diff) def decorate_diff!(diff)
return diff if diff.is_a?(File) return diff if diff.is_a?(File)
......
...@@ -20,7 +20,7 @@ module Gitlab ...@@ -20,7 +20,7 @@ module Gitlab
strong_memoize(:diff_files) do strong_memoize(:diff_files) do
diff_files = super diff_files = super
diff_files.each { |diff_file| cache.decorate(diff_file) } diff_files.each { |diff_file| highlight_cache.decorate(diff_file) }
diff_files diff_files
end end
...@@ -28,16 +28,14 @@ module Gitlab ...@@ -28,16 +28,14 @@ module Gitlab
override :write_cache override :write_cache
def write_cache def write_cache
cache.write_if_empty highlight_cache.write_if_empty
diff_stats_cache&.write_if_empty(diff_stats_collection)
end end
override :clear_cache override :clear_cache
def clear_cache def clear_cache
cache.clear highlight_cache.clear
end diff_stats_cache&.clear
def cache_key
cache.key
end end
def real_size def real_size
...@@ -46,8 +44,27 @@ module Gitlab ...@@ -46,8 +44,27 @@ module Gitlab
private private
def cache def highlight_cache
@cache ||= Gitlab::Diff::HighlightCache.new(self) strong_memoize(:highlight_cache) do
Gitlab::Diff::HighlightCache.new(self)
end
end
def diff_stats_cache
strong_memoize(:diff_stats_cache) do
if Feature.enabled?(:cache_diff_stats_merge_request, project)
Gitlab::Diff::StatsCache.new(cachable_key: @merge_request_diff.cache_key)
end
end
end
override :diff_stats_collection
def diff_stats_collection
strong_memoize(:diff_stats) do
next unless fetch_diff_stats?
diff_stats_cache&.read || super
end
end end
end end
end end
......
# frozen_string_literal: true
#
module Gitlab
module Diff
class StatsCache
include Gitlab::Metrics::Methods
include Gitlab::Utils::StrongMemoize
EXPIRATION = 1.week
VERSION = 1
def initialize(cachable_key:)
@cachable_key = cachable_key
end
def read
strong_memoize(:cached_values) do
content = cache.fetch(key)
next unless content
stats = content.map { |stat| Gitaly::DiffStats.new(stat) }
Gitlab::Git::DiffStatsCollection.new(stats)
end
end
def write_if_empty(stats)
return if cache.exist?(key)
return unless stats
cache.write(key, stats.as_json, expires_in: EXPIRATION)
end
def clear
cache.delete(key)
end
private
attr_reader :cachable_key
def cache
Rails.cache
end
def key
strong_memoize(:redis_key) do
['diff_stats', cachable_key, VERSION].join(":")
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Diff::StatsCache, :use_clean_rails_memory_store_caching do
subject(:stats_cache) { described_class.new(cachable_key: cachable_key) }
let(:key) { ['diff_stats', cachable_key, described_class::VERSION].join(":") }
let(:cachable_key) { 'cachecachecache' }
let(:stat) { Gitaly::DiffStats.new(path: 'temp', additions: 10, deletions: 15) }
let(:stats) { Gitlab::Git::DiffStatsCollection.new([stat]) }
let(:cache) { Rails.cache }
describe '#read' do
before do
stats_cache.write_if_empty(stats)
end
it 'returns the expected stats' do
expect(stats_cache.read.to_json).to eq(stats.to_json)
end
end
describe '#write_if_empty' do
context 'when the cache already exists' do
before do
Rails.cache.write(key, true)
end
it 'does not write the stats' do
expect(cache).not_to receive(:write)
stats_cache.write_if_empty(stats)
end
end
context 'when the cache does not exist' do
it 'writes the stats' do
expect(cache)
.to receive(:write)
.with(key, stats.as_json, expires_in: described_class::EXPIRATION)
.and_call_original
stats_cache.write_if_empty(stats)
expect(stats_cache.read.to_a).to eq(stats.to_a)
end
context 'when given non utf-8 characters' do
let(:non_utf8_path) { '你好'.b }
let(:stat) { Gitaly::DiffStats.new(path: non_utf8_path, additions: 10, deletions: 15) }
it 'writes the stats' do
expect(cache)
.to receive(:write)
.with(key, stats.as_json, expires_in: described_class::EXPIRATION)
.and_call_original
stats_cache.write_if_empty(stats)
expect(stats_cache.read.to_a).to eq(stats.to_a)
end
end
context 'when given empty stats' do
let(:stats) { nil }
it 'does not write the stats' do
expect(cache).not_to receive(:write)
stats_cache.write_if_empty(stats)
end
end
end
end
describe '#clear' do
it 'clears cache' do
expect(cache).to receive(:delete).with(key)
stats_cache.clear
end
end
end
...@@ -4,11 +4,11 @@ require "spec_helper" ...@@ -4,11 +4,11 @@ require "spec_helper"
describe Gitlab::Git::DiffStatsCollection do describe Gitlab::Git::DiffStatsCollection do
let(:stats_a) do let(:stats_a) do
double(Gitaly::DiffStats, additions: 10, deletions: 15, path: 'foo') Gitaly::DiffStats.new(additions: 10, deletions: 15, path: 'foo')
end end
let(:stats_b) do let(:stats_b) do
double(Gitaly::DiffStats, additions: 5, deletions: 1, path: 'bar') Gitaly::DiffStats.new(additions: 5, deletions: 1, path: 'bar')
end end
let(:diff_stats) { [stats_a, stats_b] } let(:diff_stats) { [stats_a, stats_b] }
......
...@@ -34,10 +34,8 @@ describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_cachin ...@@ -34,10 +34,8 @@ describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_cachin
context 'cache clearing' do context 'cache clearing' do
it 'clears the cache for older diffs on the merge request' do it 'clears the cache for older diffs on the merge request' do
old_diff = merge_request.merge_request_diff expect_any_instance_of(Redis).to receive(:del).once.and_call_original
old_cache_key = old_diff.diffs_collection.cache_key expect(Rails.cache).to receive(:delete).once.and_call_original
expect_any_instance_of(Redis).to receive(:del).with(old_cache_key).and_call_original
subject.execute subject.execute
end end
......
...@@ -10,7 +10,7 @@ RSpec.shared_examples 'diff statistics' do |test_include_stats_flag: true| ...@@ -10,7 +10,7 @@ RSpec.shared_examples 'diff statistics' do |test_include_stats_flag: true|
end end
end end
context 'when should request diff stats' do context 'when include_stats is true' do
it 'Repository#diff_stats is called' do it 'Repository#diff_stats is called' do
expect(diffable.project.repository) expect(diffable.project.repository)
.to receive(:diff_stats) .to receive(:diff_stats)
...@@ -59,43 +59,134 @@ RSpec.shared_examples 'unfoldable diff' do ...@@ -59,43 +59,134 @@ RSpec.shared_examples 'unfoldable diff' do
end end
RSpec.shared_examples 'cacheable diff collection' do RSpec.shared_examples 'cacheable diff collection' do
let(:cache) { instance_double(Gitlab::Diff::HighlightCache) } let(:highlight_cache) { instance_double(Gitlab::Diff::HighlightCache, write_if_empty: true, clear: nil, decorate: nil) }
let(:stats_cache) { instance_double(Gitlab::Diff::StatsCache, read: nil, write_if_empty: true, clear: nil) }
before do before do
expect(Gitlab::Diff::HighlightCache).to receive(:new).with(subject) { cache } expect(Gitlab::Diff::HighlightCache).to receive(:new).with(subject) { highlight_cache }
end end
describe '#write_cache' do describe '#write_cache' do
it 'calls Gitlab::Diff::HighlightCache#write_if_empty' do it 'calls Gitlab::Diff::HighlightCache#write_if_empty' do
expect(cache).to receive(:write_if_empty).once expect(highlight_cache).to receive(:write_if_empty).once
subject.write_cache subject.write_cache
end end
context 'when the feature flag is enabled' do
before do
stub_feature_flags(cache_diff_stats_merge_request: true)
expect(Gitlab::Diff::StatsCache).to receive(:new).with(cachable_key: diffable.cache_key) { stats_cache }
end
it 'calls Gitlab::Diff::StatsCache#write_if_empty with diff stats' do
diff_stats = Gitlab::Git::DiffStatsCollection.new([])
expect(diffable.project.repository)
.to receive(:diff_stats).and_return(diff_stats)
expect(stats_cache).to receive(:write_if_empty).once.with(diff_stats)
subject.write_cache
end
end
context 'when the feature flag is disabled' do
before do
stub_feature_flags(cache_diff_stats_merge_request: false)
end
it 'does not call Gitlab::Diff::StatsCache#write_if_empty' do
expect(stats_cache).not_to receive(:write_if_empty)
subject.write_cache
end
end
end end
describe '#clear_cache' do describe '#clear_cache' do
it 'calls Gitlab::Diff::HighlightCache#clear' do it 'calls Gitlab::Diff::HighlightCache#clear' do
expect(cache).to receive(:clear).once expect(highlight_cache).to receive(:clear).once
subject.clear_cache subject.clear_cache
end end
end
describe '#cache_key' do context 'when the feature flag is enabled' do
it 'calls Gitlab::Diff::HighlightCache#key' do before do
expect(cache).to receive(:key).once stub_feature_flags(cache_diff_stats_merge_request: true)
expect(Gitlab::Diff::StatsCache).to receive(:new).with(cachable_key: diffable.cache_key) { stats_cache }
end
subject.cache_key it 'calls Gitlab::Diff::StatsCache#clear' do
expect(stats_cache).to receive(:clear).once
subject.clear_cache
end
end
context 'when the feature flag is disabled' do
before do
stub_feature_flags(cache_diff_stats_merge_request: false)
end
it 'does not calls Gitlab::Diff::StatsCache#clear' do
expect(stats_cache).not_to receive(:clear)
subject.clear_cache
end
end end
end end
describe '#diff_files' do describe '#diff_files' do
it 'calls Gitlab::Diff::HighlightCache#decorate' do it 'calls Gitlab::Diff::HighlightCache#decorate' do
expect(cache).to receive(:decorate) expect(highlight_cache).to receive(:decorate)
.with(instance_of(Gitlab::Diff::File)) .with(instance_of(Gitlab::Diff::File))
.exactly(cacheable_files_count).times .exactly(cacheable_files_count).times
subject.diff_files subject.diff_files
end end
context 'when the feature swtich is enabled' do
before do
stub_feature_flags(cache_diff_stats_merge_request: true)
expect(Gitlab::Diff::StatsCache).to receive(:new).with(cachable_key: diffable.cache_key) { stats_cache }
end
context 'when there are stats cached' do
before do
allow(stats_cache).to receive(:read).and_return(Gitlab::Git::DiffStatsCollection.new([]))
end
it 'does not make a diff stats rpc call' do
expect(diffable.project.repository).not_to receive(:diff_stats)
subject.diff_files
end
end
context 'when there are no stats cached' do
it 'makes a diff stats rpc call' do
expect(diffable.project.repository)
.to receive(:diff_stats)
.with(diffable.diff_refs.base_sha, diffable.diff_refs.head_sha)
subject.diff_files
end
end
end
context 'when the feature switch is disabled' do
before do
stub_feature_flags(cache_diff_stats_merge_request: false)
end
it 'makes a diff stats rpc call' do
expect(diffable.project.repository)
.to receive(:diff_stats)
.with(diffable.diff_refs.base_sha, diffable.diff_refs.head_sha)
subject.diff_files
end
end
end 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