Skip to content
This repository has been archived by the owner on Dec 21, 2023. It is now read-only.

Commit

Permalink
Fix performance on instances list in admin UI (mastodon#15282)
Browse files Browse the repository at this point in the history
- Reduce duplicate queries
- Remove n+1 queries
- Add accounts count to detailed view
- Add separate action log entry for updating existing domain blocks
  • Loading branch information
Gargron authored Dec 14, 2020
1 parent a3b5675 commit 216b85b
Show file tree
Hide file tree
Showing 27 changed files with 326 additions and 166 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 1.1'
gem 'ruby-progressbar', '~> 1.10'
gem 'sanitize', '~> 5.2'
gem 'scenic', '~> 1.5'
gem 'sidekiq', '~> 6.1'
gem 'sidekiq-scheduler', '~> 3.0'
gem 'sidekiq-unique-jobs', '~> 6.0'
Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,9 @@ GEM
crass (~> 1.0.2)
nokogiri (>= 1.8.0)
nokogumbo (~> 2.0)
scenic (1.5.4)
activerecord (>= 4.0.0)
railties (>= 4.0.0)
securecompare (1.0.0)
semantic_range (2.3.0)
sidekiq (6.1.2)
Expand Down Expand Up @@ -782,6 +785,7 @@ DEPENDENCIES
rubocop-rails (~> 2.8)
ruby-progressbar (~> 1.10)
sanitize (~> 5.2)
scenic (~> 1.5)
sidekiq (~> 6.1)
sidekiq-bulk (~> 0.2.0)
sidekiq-scheduler (~> 3.0)
Expand Down
5 changes: 3 additions & 2 deletions app/controllers/admin/domain_blocks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ def create
@domain_block = existing_domain_block
@domain_block.update(resource_params)
end

if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id)
log_action :create, @domain_block
Expand All @@ -40,15 +41,15 @@ def create
end

def update
authorize :domain_block, :create?
authorize :domain_block, :update?

@domain_block.update(update_params)

severity_changed = @domain_block.severity_changed?

if @domain_block.save
DomainBlockWorker.perform_async(@domain_block.id, severity_changed)
log_action :create, @domain_block
log_action :update, @domain_block
redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg')
else
render :edit
Expand Down
44 changes: 5 additions & 39 deletions app/controllers/admin/instances_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,31 @@

module Admin
class InstancesController < BaseController
before_action :set_domain_block, only: :show
before_action :set_domain_allow, only: :show
before_action :set_instances, only: :index
before_action :set_instance, only: :show

def index
authorize :instance, :index?

@instances = ordered_instances
end

def show
authorize :instance, :show?

@following_count = Follow.where(account: Account.where(domain: params[:id])).count
@followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count
@reports_count = Report.where(target_account: Account.where(domain: params[:id])).count
@blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
@available = DeliveryFailureTracker.available?(params[:id])
@media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
@private_comment = @domain_block&.private_comment
@public_comment = @domain_block&.public_comment
end

private

def set_domain_block
@domain_block = DomainBlock.rule_for(params[:id])
end

def set_domain_allow
@domain_allow = DomainAllow.rule_for(params[:id])
end

def set_instance
resource = Account.by_domain_accounts.find_by(domain: params[:id])
resource ||= @domain_block
resource ||= @domain_allow
@instance = Instance.find(params[:id])
end

if resource
@instance = Instance.new(resource)
else
not_found
end
def set_instances
@instances = filtered_instances.page(params[:page])
end

def filtered_instances
InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results
end

def paginated_instances
filtered_instances.page(params[:page])
end

helper_method :paginated_instances

def ordered_instances
paginated_instances.map { |resource| Instance.new(resource) }
end

def filter_params
params.slice(*InstanceFilter::KEYS).permit(*InstanceFilter::KEYS)
end
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/v1/instances/peers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class Api::V1::Instances::PeersController < Api::BaseController

def index
expires_in 1.day, public: true
render_with_cache(expires_in: 1.day) { Account.remote.domains }
render_with_cache(expires_in: 1.day) { Instance.where.not(domain: DomainBlock.select(:domain)).pluck(:domain) }
end

private
Expand Down
6 changes: 1 addition & 5 deletions app/models/account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class Account < ApplicationRecord
include Paginable
include AccountCounters
include DomainNormalizable
include DomainMaterializable
include AccountMerging

TRUST_LEVELS = {
Expand Down Expand Up @@ -103,7 +104,6 @@ class Account < ApplicationRecord
scope :bots, -> { where(actor_type: %w(Application Service)) }
scope :groups, -> { where(actor_type: 'Group') }
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
Expand Down Expand Up @@ -438,10 +438,6 @@ def readonly_attributes
super - %w(statuses_count following_count followers_count)
end

def domains
reorder(nil).pluck(Arel.sql('distinct accounts.domain'))
end

def inboxes
urls = reorder(nil).where(protocol: :activitypub).group(:preferred_inbox_url).pluck(Arel.sql("coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url) AS preferred_inbox_url"))
DeliveryFailureTracker.without_unavailable(urls)
Expand Down
13 changes: 13 additions & 0 deletions app/models/concerns/domain_materializable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

module DomainMaterializable
extend ActiveSupport::Concern

included do
after_create_commit :refresh_instances_view
end

def refresh_instances_view
Instance.refresh unless domain.nil? || Instance.where(domain: domain).exists?
end
end
1 change: 1 addition & 0 deletions app/models/domain_allow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

class DomainAllow < ApplicationRecord
include DomainNormalizable
include DomainMaterializable

validates :domain, presence: true, uniqueness: true, domain: true

Expand Down
1 change: 1 addition & 0 deletions app/models/domain_block.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

class DomainBlock < ApplicationRecord
include DomainNormalizable
include DomainMaterializable

enum severity: [:silence, :suspend, :noop]

Expand Down
63 changes: 50 additions & 13 deletions app/models/instance.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,63 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: instances
#
# domain :string primary key
# accounts_count :bigint(8)
#

class Instance
include ActiveModel::Model
class Instance < ApplicationRecord
self.primary_key = :domain

attr_accessor :domain, :accounts_count, :domain_block
has_many :accounts, foreign_key: :domain, primary_key: :domain

def initialize(resource)
@domain = resource.domain
@accounts_count = resource.respond_to?(:accounts_count) ? resource.accounts_count : nil
@domain_block = resource.is_a?(DomainBlock) ? resource : DomainBlock.rule_for(domain)
@domain_allow = resource.is_a?(DomainAllow) ? resource : DomainAllow.rule_for(domain)
belongs_to :domain_block, foreign_key: :domain, primary_key: :domain
belongs_to :domain_allow, foreign_key: :domain, primary_key: :domain

scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }

def self.refresh
Scenic.database.refresh_materialized_view(table_name, concurrently: true, cascade: false)
end

def countable?
@accounts_count.present?
def readonly?
true
end

def to_param
domain
def delivery_failure_tracker
@delivery_failure_tracker ||= DeliveryFailureTracker.new(domain)
end

def following_count
@following_count ||= Follow.where(account: accounts).count
end

def followers_count
@followers_count ||= Follow.where(target_account: accounts).count
end

def reports_count
@reports_count ||= Report.where(target_account: accounts).count
end

def cache_key
def blocks_count
@blocks_count ||= Block.where(target_account: accounts).count
end

def public_comment
domain_block&.public_comment
end

def private_comment
domain_block&.private_comment
end

def media_storage
@media_storage ||= MediaAttachment.where(account: accounts).sum(:file_file_size)
end

def to_param
domain
end
end
31 changes: 20 additions & 11 deletions app/models/instance_filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,27 @@ def initialize(params)
end

def results
if params[:limited].present?
scope = DomainBlock
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?
scope.order(id: :desc)
elsif params[:allowed].present?
scope = DomainAllow
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?
scope.order(id: :desc)
scope = Instance.includes(:domain_block, :domain_allow).order(accounts_count: :desc)

params.each do |key, value|
scope.merge!(scope_for(key, value.to_s.strip)) if value.present?
end

scope
end

private

def scope_for(key, value)
case key.to_s
when 'limited'
Instance.joins(:domain_block).reorder(Arel.sql('domain_blocks.id desc'))
when 'allowed'
Instance.joins(:domain_allow).reorder(Arel.sql('domain_allows.id desc'))
when 'by_domain'
Instance.matches_domain(value)
else
scope = Account.remote
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?
scope.by_domain_accounts
raise "Unknown filter: #{key}"
end
end
end
2 changes: 2 additions & 0 deletions app/models/unavailable_domain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
class UnavailableDomain < ApplicationRecord
include DomainNormalizable

validates :domain, presence: true, uniqueness: true

after_commit :reset_cache!

private
Expand Down
4 changes: 4 additions & 0 deletions app/policies/domain_block_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ def create?
admin?
end

def update?
admin?
end

def destroy?
admin?
end
Expand Down
2 changes: 1 addition & 1 deletion app/presenters/instance_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def status_count
end

def domain_count
Rails.cache.fetch('distinct_domain_count') { Account.distinct.count(:domain) }
Rails.cache.fetch('distinct_domain_count') { Instance.count }
end

def sample_accounts
Expand Down
25 changes: 25 additions & 0 deletions app/views/admin/instances/_instance.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.directory__tag
= link_to admin_instance_path(instance) do
%h4
= instance.domain
%small
- if instance.domain_block
- first_item = true
- if !instance.domain_block.noop?
= t("admin.domain_blocks.severity.#{instance.domain_block.severity}")
- first_item = false
- unless instance.domain_block.suspend?
- if instance.domain_block.reject_media?
- unless first_item
&bull;
= t('admin.domain_blocks.rejecting_media')
- first_item = false
- if instance.domain_block.reject_reports?
- unless first_item
&bull;
= t('admin.domain_blocks.rejecting_reports')
- elsif whitelist_mode?
= t('admin.accounts.whitelisted')
- else
= t('admin.accounts.no_limits_imposed')
.trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= number_to_human instance.accounts_count, strip_insignificant_zeros: true
36 changes: 7 additions & 29 deletions app/views/admin/instances/index.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -32,32 +32,10 @@

%hr.spacer/

- @instances.each do |instance|
.directory__tag
= link_to admin_instance_path(instance) do
%h4
= instance.domain
%small
- if instance.domain_block
- first_item = true
- if !instance.domain_block.noop?
= t("admin.domain_blocks.severity.#{instance.domain_block.severity}")
- first_item = false
- unless instance.domain_block.suspend?
- if instance.domain_block.reject_media?
- unless first_item
&bull;
= t('admin.domain_blocks.rejecting_media')
- first_item = false
- if instance.domain_block.reject_reports?
- unless first_item
&bull;
= t('admin.domain_blocks.rejecting_reports')
- elsif whitelist_mode?
= t('admin.accounts.whitelisted')
- else
= t('admin.accounts.no_limits_imposed')
- if instance.countable?
.trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= number_to_human instance.accounts_count, strip_insignificant_zeros: true

= paginate paginated_instances
- if @instances.empty?
%div.muted-hint.center-text
= t 'admin.instances.empty'
- else
= render @instances

= paginate @instances
Loading

0 comments on commit 216b85b

Please sign in to comment.