From 57bde0ce65caf2cbbb6a57f21435639cdaa06225 Mon Sep 17 00:00:00 2001
From: Yorick Peterse <yorickpeterse@gmail.com>
Date: Thu, 24 Mar 2016 15:53:38 +0100
Subject: [PATCH] Cache Banzai projects/objects using RequestStore

This was originally suggested by @ayufan and modified to be a bit
cleaner and use RequestStore instead of a regular Hash.

By caching the output of the two methods involved the number of queries
is reduced significantly. For example, for an issue with 200 notes (of
which 100 reference a number of merge requests) this cuts down the
amount of queries from around 6300 to around 3300.
---
 Gemfile                                       |  2 +-
 Gemfile.lock                                  |  4 +-
 .../filter/abstract_reference_filter.rb       | 71 +++++++++++++++++--
 3 files changed, 70 insertions(+), 7 deletions(-)

diff --git a/Gemfile b/Gemfile
index 006e53e0c100c..6327227282ae0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -214,7 +214,7 @@ gem 'jquery-rails',       '~> 4.0.0'
 gem 'jquery-scrollto-rails', '~> 1.4.3'
 gem 'jquery-ui-rails',    '~> 5.0.0'
 gem 'raphael-rails',      '~> 2.1.2'
-gem 'request_store',      '~> 1.2.0'
+gem 'request_store',      '~> 1.3.0'
 gem 'select2-rails',      '~> 3.5.9'
 gem 'virtus',             '~> 1.0.1'
 gem 'net-ssh',            '~> 3.0.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index bd41cc84198e1..229089f431d13 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -652,7 +652,7 @@ GEM
       redis-store (~> 1.1.0)
     redis-store (1.1.7)
       redis (>= 2.2)
-    request_store (1.2.1)
+    request_store (1.3.0)
     rerun (0.11.0)
       listen (~> 3.0)
     responders (2.1.1)
@@ -1011,7 +1011,7 @@ DEPENDENCIES
   redcarpet (~> 3.3.3)
   redis-namespace
   redis-rails (~> 4.0.0)
-  request_store (~> 1.2.0)
+  request_store (~> 1.3.0)
   rerun (~> 0.11.0)
   responders (~> 2.0)
   rouge (~> 1.10.1)
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 34c38913474d1..41fd4be76acf9 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -62,11 +62,53 @@ def find_object(project, id)
         # Example: project.merge_requests.find
       end
 
+      def find_object_cached(project, id)
+        if RequestStore.active?
+          cache = find_objects_cache[object_class][project.id]
+
+          if cache.key?(id)
+            cache[id]
+          else
+            cache[id] = find_object(project, id)
+          end
+        else
+          find_object(project, id)
+        end
+      end
+
+      def project_from_ref_cache(ref)
+        if RequestStore.active?
+          cache = project_refs_cache
+
+          if cache.key?(ref)
+            cache[ref]
+          else
+            cache[ref] = project_from_ref(ref)
+          end
+        else
+          project_from_ref(ref)
+        end
+      end
+
       def url_for_object(object, project)
         # Implement in child class
         # Example: project_merge_request_url
       end
 
+      def url_for_object_cached(object, project)
+        if RequestStore.active?
+          cache = url_for_object_cache[object_class][project.id]
+
+          if cache.key?(object)
+            cache[object]
+          else
+            cache[object] = url_for_object(object, project)
+          end
+        else
+          url_for_object(object, project)
+        end
+      end
+
       def call
         if object_class.reference_pattern
           # `#123`
@@ -109,9 +151,9 @@ def call
       # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
       def object_link_filter(text, pattern, link_text: nil)
         references_in(text, pattern) do |match, id, project_ref, matches|
-          project = project_from_ref(project_ref)
+          project = project_from_ref_cache(project_ref)
 
-          if project && object = find_object(project, id)
+          if project && object = find_object_cached(project, id)
             title = object_link_title(object)
             klass = reference_class(object_sym)
 
@@ -121,8 +163,11 @@ def object_link_filter(text, pattern, link_text: nil)
               object_sym => object.id
             )
 
-            url = matches[:url] if matches.names.include?("url")
-            url ||= url_for_object(object, project)
+            if matches.names.include?("url") && matches[:url]
+              url = matches[:url]
+            else
+              url = url_for_object_cached(object, project)
+            end
 
             text = link_text || object_link_text(object, matches)
 
@@ -157,6 +202,24 @@ def object_link_text(object, matches)
 
         text
       end
+
+      private
+
+      def project_refs_cache
+        RequestStore[:banzai_project_refs] ||= {}
+      end
+
+      def find_objects_cache
+        RequestStore[:banzai_find_objects_cache] ||= Hash.new do |hash, key|
+          hash[key] = Hash.new { |h, k| h[k] = {} }
+        end
+      end
+
+      def url_for_object_cache
+        RequestStore[:banzai_url_for_object] ||= Hash.new do |hash, key|
+          hash[key] = Hash.new { |h, k| h[k] = {} }
+        end
+      end
     end
   end
 end
-- 
GitLab