From 3eed8ffc449bc9044d986bf69af05777dd2e242a Mon Sep 17 00:00:00 2001 From: nmilojevic1 <nmilojevic@gitlab.com> Date: Wed, 23 Mar 2022 18:38:49 +0100 Subject: [PATCH] Fix discussions N+1 queries - Add specs for html and text node - Fix N plus one queries for new review email Changelog: performance --- app/mailers/emails/reviews.rb | 8 ++++++ app/models/note.rb | 1 + app/views/notify/_note_email.html.haml | 4 +-- app/views/notify/_note_email.text.erb | 4 +-- app/views/notify/new_review_email.html.haml | 8 +++++- app/views/notify/new_review_email.text.erb | 8 +++++- spec/mailers/notify_spec.rb | 31 ++++++++++++++++++++- 7 files changed, 57 insertions(+), 7 deletions(-) diff --git a/app/mailers/emails/reviews.rb b/app/mailers/emails/reviews.rb index ddb9e161a80b8..5bbea1a5fe0c1 100644 --- a/app/mailers/emails/reviews.rb +++ b/app/mailers/emails/reviews.rb @@ -22,6 +22,8 @@ def setup_review_email(review_id, recipient_id) review = Review.find_by_id(review_id) @notes = review.notes + discussion_ids = @notes.pluck(:discussion_id) + @discussions = discussions(discussion_ids) @author = review.author @author_name = review.author_name @project = review.project @@ -29,5 +31,11 @@ def setup_review_email(review_id, recipient_id) @target_url = project_merge_request_url(@project, @merge_request) @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key) end + + def discussions(discussion_ids) + notes = Note.where(discussion_id: discussion_ids).inc_note_diff_file.fresh + grouped_notes = notes.group_by { |n| n.discussion_id } + grouped_notes.transform_values { |notes| Discussion.build(notes) } + end end end diff --git a/app/models/note.rb b/app/models/note.rb index 4f2e7ebe2c52a..8f51873544266 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -121,6 +121,7 @@ class Note < ApplicationRecord scope :with_discussion_ids, ->(discussion_ids) { where(discussion_id: discussion_ids) } scope :with_suggestions, -> { joins(:suggestions) } scope :inc_author, -> { includes(:author) } + scope :inc_note_diff_file, -> { includes(:note_diff_file) } scope :with_api_entity_associations, -> { preload(:note_diff_file, :author) } scope :inc_relations_for_view, -> do includes({ project: :group }, { author: :status }, :updated_by, :resolved_by, :award_emoji, diff --git a/app/views/notify/_note_email.html.haml b/app/views/notify/_note_email.html.haml index d5398337320c9..7b7a7d5b995cc 100644 --- a/app/views/notify/_note_email.html.haml +++ b/app/views/notify/_note_email.html.haml @@ -4,7 +4,7 @@ - note_style = local_assigns.fetch(:note_style, "") - skip_stylesheet_link = local_assigns.fetch(:skip_stylesheet_link, false) -- discussion = note.discussion if note.part_of_discussion? +- discussion = local_assigns.fetch(:discussion){ note.discussion } if note.part_of_discussion? %p{ style: "color: #777777;" } = succeed ':' do @@ -12,7 +12,7 @@ - if discussion.nil? = link_to 'commented', target_url - else - - if note.start_of_discussion? + - if discussion.first_note == note started a new - else commented on a diff --git a/app/views/notify/_note_email.text.erb b/app/views/notify/_note_email.text.erb index 8e2f7e6f76ef4..8b8b75cb40dca 100644 --- a/app/views/notify/_note_email.text.erb +++ b/app/views/notify/_note_email.text.erb @@ -1,13 +1,13 @@ <% note = local_assigns.fetch(:note, @note) -%> <% diff_limit = local_assigns.fetch(:diff_limit, nil) -%> <% target_url = local_assigns.fetch(:target_url, @target_url) -%> -<% discussion = note.discussion if note.part_of_discussion? -%> +<% discussion = local_assigns.fetch(:discussion){ note.discussion } if note.part_of_discussion? -%> <%= sanitize_name(note.author_name) -%> <% if discussion.nil? -%> <%= 'commented' -%>: <% else -%> -<% if note.start_of_discussion? -%> +<% if discussion.first_note == note -%> <%= 'started a new discussion' -%> <% else -%> <%= 'commented on a discussion' -%> diff --git a/app/views/notify/new_review_email.html.haml b/app/views/notify/new_review_email.html.haml index d5399c872d5cb..c0ad773caafd3 100644 --- a/app/views/notify/new_review_email.html.haml +++ b/app/views/notify/new_review_email.html.haml @@ -15,5 +15,11 @@ %tr %td{ style: "overflow:hidden;font-size:14px;line-height:1.4;display:grid;" } - @notes.each do |note| + -# Preload author, to prevent N+1 queries + - note.author = @author + -# Get preloaded note discussion + - discussion = @discussions.try(:[], note.discussion_id) if note.part_of_discussion? + -# Preload project for discussions first note + - discussion.first_note.project = @project if discussion&.first_note - target_url = project_merge_request_url(@project, @merge_request, anchor: "note_#{note.id}") - = render 'note_email', note: note, diff_limit: 3, target_url: target_url, note_style: "border-bottom:1px solid #ededed;", skip_stylesheet_link: true + = render 'note_email', note: note, diff_limit: 3, target_url: target_url, note_style: "border-bottom:1px solid #ededed;", skip_stylesheet_link: true, discussion: discussion diff --git a/app/views/notify/new_review_email.text.erb b/app/views/notify/new_review_email.text.erb index 164735abad0f8..a6899a65f2bbd 100644 --- a/app/views/notify/new_review_email.text.erb +++ b/app/views/notify/new_review_email.text.erb @@ -4,8 +4,14 @@ -- <% @notes.each_with_index do |note, index| %> + <!-- Preload author, to prevent N+1 queries--> + <% note.author = @author %> + <!-- Get preloaded note discussion--> + <% discussion = @discussions.try(:[], note.discussion_id) if note.part_of_discussion?%> + <!-- Preload project for discussions first note--> + <% discussion.first_note.project = @project if discussion&.first_note %> <% target_url = project_merge_request_url(@project, @merge_request, anchor: "note_#{note.id}") %> - <%= render 'note_email', note: note, diff_limit: 3, target_url: target_url %> + <%= render 'note_email', note: note, diff_limit: 3, target_url: target_url, discussion: discussion %> <% if index != @notes.length-1 %> -- diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index e2ee63078bbc0..8aefaadae2e78 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -2181,18 +2181,47 @@ def invite_to_group(group, inviter:, user: nil, tasks_to_be_done: []) context 'when diff note' do let!(:notes) { create_list(:diff_note_on_merge_request, 3, review: review, project: project, author: review.author, noteable: merge_request) } - it 'links to notes' do + it 'links to notes and discussions', :aggregate_failures do + + reply_note = create(:diff_note_on_merge_request, review: review, project: project, author: review.author, noteable: merge_request, in_reply_to: notes.first) + review.notes.each do |note| # Text part expect(subject.text_part.body.raw_source).to include( project_merge_request_url(project, merge_request, anchor: "note_#{note.id}") ) + + if note == reply_note + expect(subject.text_part.body.raw_source).to include("commented on a discussion on #{note.discussion.file_path}") + else + expect(subject.text_part.body.raw_source).to include("started a new discussion on #{note.discussion.file_path}") + end end end it 'includes only one link to the highlighted_diff_email' do expect(subject.html_part.body.raw_source).to include('assets/mailers/highlighted_diff_email').once end + + it 'avoids N+1 cached queries when rendering html', :use_sql_query_cache, :request_store do + control_count = ActiveRecord::QueryRecorder.new(query_recorder_debug: true, skip_cached: false) do + subject.html_part + end + + create_list(:diff_note_on_merge_request, 3, review: review, project: project, author: review.author, noteable: merge_request) + + expect { described_class.new_review_email(recipient.id, review.id).html_part }.not_to exceed_all_query_limit(control_count) + end + + it 'avoids N+1 cached queries when rendering text', :use_sql_query_cache, :request_store do + control_count = ActiveRecord::QueryRecorder.new(query_recorder_debug: true, skip_cached: false) do + subject.text_part + end + + create_list(:diff_note_on_merge_request, 3, review: review, project: project, author: review.author, noteable: merge_request) + + expect { described_class.new_review_email(recipient.id, review.id).text_part }.not_to exceed_all_query_limit(control_count) + end end it 'contains review author name' do -- GitLab