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

Commit

Permalink
Fix performance of account timelines (mastodon#17709)
Browse files Browse the repository at this point in the history
* Fix performance of account timelines

* Various fixes and improvements

* Fix duplicate results being returned

Co-authored-by: Claire <[email protected]>

* Fix grouping for pinned statuses scope

Co-authored-by: Claire <[email protected]>
  • Loading branch information
Gargron and ClearlyClaire authored Mar 8, 2022
1 parent 61ae6b3 commit 8f6c67b
Show file tree
Hide file tree
Showing 6 changed files with 366 additions and 115 deletions.
2 changes: 1 addition & 1 deletion app/controllers/activitypub/outboxes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def set_statuses
return unless page_requested?

@statuses = cache_collection_paginated_by_id(
@account.statuses.permitted_for(@account, signed_request_account),
AccountStatusesFilter.new(@account, signed_request_account).results,
Status,
LIMIT,
params_slice(:max_id, :min_id, :since_id)
Expand Down
41 changes: 2 additions & 39 deletions app/controllers/api/v1/accounts/statuses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,53 +22,16 @@ def load_statuses
end

def cached_account_statuses
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses

statuses.merge!(only_media_scope) if truthy_param?(:only_media)
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
statuses.merge!(hashtag_scope) if params[:tagged].present?

cache_collection_paginated_by_id(
statuses,
AccountStatusesFilter.new(@account, current_account, params).results,
Status,
limit_param(DEFAULT_STATUSES_LIMIT),
params_slice(:max_id, :since_id, :min_id)
)
end

def permitted_account_statuses
@account.statuses.permitted_for(@account, current_account)
end

def only_media_scope
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)
end

def pinned_scope
@account.pinned_statuses.permitted_for(@account, current_account)
end

def no_replies_scope
Status.without_replies
end

def no_reblogs_scope
Status.without_reblogs
end

def hashtag_scope
tag = Tag.find_normalized(params[:tagged])

if tag
Status.tagged_with(tag.id)
else
Status.none
end
end

def pagination_params(core_params)
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
params.slice(:limit, *AccountStatusesFilter::KEYS).permit(:limit, *AccountStatusesFilter::KEYS).merge(core_params)
end

def insert_pagination_headers
Expand Down
134 changes: 134 additions & 0 deletions app/models/account_statuses_filter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# frozen_string_literal: true

class AccountStatusesFilter
KEYS = %i(
pinned
tagged
only_media
exclude_replies
exclude_reblogs
).freeze

attr_reader :params, :account, :current_account

def initialize(account, current_account, params = {})
@account = account
@current_account = current_account
@params = params
end

def results
scope = initial_scope

scope.merge!(pinned_scope) if pinned?
scope.merge!(only_media_scope) if only_media?
scope.merge!(no_replies_scope) if exclude_replies?
scope.merge!(no_reblogs_scope) if exclude_reblogs?
scope.merge!(hashtag_scope) if tagged?

scope
end

private

def initial_scope
if suspended?
Status.none
elsif anonymous?
account.statuses.where(visibility: %i(public unlisted))
elsif author?
account.statuses.all # NOTE: #merge! does not work without the #all
elsif blocked?
Status.none
else
filtered_scope
end
end

def filtered_scope
scope = account.statuses.left_outer_joins(:mentions)

scope.merge!(scope.where(visibility: follower? ? %i(public unlisted private) : %i(public unlisted)).or(scope.where(mentions: { account_id: current_account.id })).group(Status.arel_table[:id]))
scope.merge!(filtered_reblogs_scope) if reblogs_may_occur?

scope
end

def filtered_reblogs_scope
Status.left_outer_joins(:reblog).where(reblog_of_id: nil).or(Status.where.not(reblogs_statuses: { account_id: current_account.excluded_from_timeline_account_ids }))
end

def only_media_scope
Status.joins(:media_attachments).merge(account.media_attachments.reorder(nil)).group(Status.arel_table[:id])
end

def no_replies_scope
Status.without_replies
end

def no_reblogs_scope
Status.without_reblogs
end

def pinned_scope
account.pinned_statuses.group(Status.arel_table[:id], StatusPin.arel_table[:created_at])
end

def hashtag_scope
tag = Tag.find_normalized(params[:tagged])

if tag
Status.tagged_with(tag.id)
else
Status.none
end
end

def suspended?
account.suspended?
end

def anonymous?
current_account.nil?
end

def author?
current_account.id == account.id
end

def blocked?
account.blocking?(current_account) || (current_account.domain.present? && account.domain_blocking?(current_account.domain))
end

def follower?
current_account.following?(account)
end

def reblogs_may_occur?
!exclude_reblogs? && !only_media? && !tagged?
end

def pinned?
truthy_param?(:pinned)
end

def only_media?
truthy_param?(:only_media)
end

def exclude_replies?
truthy_param?(:exclude_replies)
end

def exclude_reblogs?
truthy_param?(:exclude_reblogs)
end

def tagged?
params[:tagged].present?
end

def truthy_param?(key)
ActiveModel::Type::Boolean.new.cast(params[key])
end
end
22 changes: 0 additions & 22 deletions app/models/status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -345,28 +345,6 @@ def reload_stale_associations!(cached_items)
end
end

def permitted_for(target_account, account)
visibility = [:public, :unlisted]

if account.nil?
where(visibility: visibility)
elsif target_account.blocking?(account) || (account.domain.present? && target_account.domain_blocking?(account.domain)) # get rid of blocked peeps
none
elsif account.id == target_account.id # author can see own stuff
all
else
# followers can see followers-only stuff, but also things they are mentioned in.
# non-followers can see everything that isn't private/direct, but can see stuff they are mentioned in.
visibility.push(:private) if account.following?(target_account)

scope = left_outer_joins(:reblog)

scope.where(visibility: visibility)
.or(scope.where(id: account.mentions.select(:status_id)))
.merge(scope.where(reblog_of_id: nil).or(scope.where.not(reblogs_statuses: { account_id: account.excluded_from_timeline_account_ids })))
end
end

def from_text(text)
return [] if text.blank?

Expand Down
Loading

0 comments on commit 8f6c67b

Please sign in to comment.