PERF: optimize lookup of reviewable info in post stream

PERF: optimize lookup of reviewable info in post stream

This previously was a hot path in topic view. Avoids an expensive active record operation and instead perform SQL directly which is far more targeted and efficient

diff --git a/lib/topic_view.rb b/lib/topic_view.rb
index 57b7585..ab1d3e6 100644
--- a/lib/topic_view.rb
+++ b/lib/topic_view.rb
@@ -421,17 +421,32 @@ class TopicView
   def reviewable_counts
     if @reviewable_counts.blank?
 
-      # Create a hash with counts by post so we can quickly look up whether there is reviewable content.
+      post_ids = @posts.map(&:id)
+
+      sql = <<~SQL
+        SELECT target_id,
+          MAX(r.id) reviewable_id,
+          COUNT(*) total,
+          SUM(CASE WHEN s.status = :pending THEN 1 ELSE 0 END) pending
+        FROM reviewables r
+        JOIN reviewable_scores s ON reviewable_id = r.id
+        WHERE r.target_id IN (:post_ids) AND
+          r.target_type = 'Post'
+        GROUP BY target_id
+      SQL
+
       @reviewable_counts = {}
-      Reviewable.
-        where(target_type: 'Post', target_id: @posts.map(&:id)).
-        includes(:reviewable_scores).each do |r|
-
-        for_post = (@reviewable_counts[r.target_id] ||= { total: 0, pending: 0, reviewable_id: r.id })
-        r.reviewable_scores.each do |s|
-          for_post[:total] += 1
-          for_post[:pending] += 1 if s.pending?
-        end
+
+      DB.query(
+        sql,
+        pending: ReviewableScore.statuses[:pending],
+        post_ids: post_ids
+      ).each do |row|
+        @reviewable_counts[row.target_id] = {
+          total: row.total,
+          pending: row.pending,
+          reviewable_id: row.reviewable_id
+        }
       end
     end
 
diff --git a/spec/serializers/topic_view_posts_serializer_spec.rb b/spec/serializers/topic_view_posts_serializer_spec.rb
index 2c14b08..55be89f 100644
--- a/spec/serializers/topic_view_posts_serializer_spec.rb
+++ b/spec/serializers/topic_view_posts_serializer_spec.rb
@@ -2,28 +2,51 @@
 
 require 'rails_helper'
 
-RSpec.describe TopicViewPostsSerializer do
-  fab!(:user) { Fabricate(:user) }
-  fab!(:post) { Fabricate(:post) }
-  let(:topic) { post.topic }
-  let(:topic_view) { TopicView.new(topic, user, post_ids: [post.id]) }
-
-  subject do
-    described_class.new(topic_view,
+describe TopicViewPostsSerializer do
+
+  it 'should return the right attributes' do
+
+    user = Fabricate(:user)
+    post = Fabricate(:post)
+    topic = post.topic
+
+    reviewable = Fabricate(:reviewable_flagged_post, created_by: user, target: post, topic: topic)
+
+    ReviewableScore.create!(
+      reviewable_id: reviewable.id,
+      user_id: user.id,
+      reviewable_score_type: 0,
+      status: ReviewableScore.statuses[:pending]
+    )
+
+    ReviewableScore.create!(
+      reviewable_id: reviewable.id,
+      user_id: user.id,
+      reviewable_score_type: 0,
+      status: ReviewableScore.statuses[:ignored]
+    )
+
+    topic_view = TopicView.new(topic, user, post_ids: [post.id])
+
+    serializer = TopicViewPostsSerializer.new(
+      topic_view,
       scope: Guardian.new(Fabricate(:admin)),
       root: false
     )
-  end
 
-  it 'should return the right attributes' do
-    body = JSON.parse(subject.to_json)
+    body = JSON.parse(serializer.to_json)
 
     posts = body["post_stream"]["posts"]
 
     expect(posts.count).to eq(1)
     expect(posts.first["id"]).to eq(post.id)
+
+    expect(posts.first["reviewable_score_count"]).to eq(2)
+    expect(posts.first["reviewable_score_pending_count"]).to eq(1)
+
     expect(body["post_stream"]["stream"]).to eq(nil)
     expect(body["post_stream"]["timeline_lookup"]).to eq(nil)
     expect(body["post_stream"]["gaps"]).to eq(nil)
+
   end
 end

GitHub sha: f88dced0