Skip to content

Sort contributed projects on profile page by last_activity_at

What does this MR do and why?

The list of contributed projects (on personal profile page) is now ordered by the project id field. This MR will change that to last_activity_at, so it is consistent with project lists on group pages.

The specs are now expecting the returned project list to be sorted.

Changelog: changed

Related issue: #432410 (closed)

MR acceptance checklist

Please evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Screenshots or screen recordings

http://localhost:3000/users/root/contributed

Before After
Order looks random Order by most recent updated
before after

How to set up and validate locally

For user_id 1, we need to populate the list of Contributed Projects. Create some Events and update the related Project.last_activity_at value:
current_user = User.find(1)

Issue.all.shuffle.each do |i|
  Event.create(project: i.project, target: i, action: 'commented', author: current_user)
  # Project update time: random hours ago
  last_activity = rand(1..50)
  i.project.touch(:last_activity_at, time: last_activity.hours.ago)
end

Visit Contributed Projects

  • On master branch, the list order seems random (it is ordered by project id).
  • On this branch, the list is ordered by last_activity_at

Database-related changes

The ContributedProjectsFinder was using an hard-coded ORDER BY projects.id DESC. This order can now be configured but will still default to ORDER BY projects.id DESC.

The only code path that is now using a different order is the contributed projects list. There, we have the very same query but we use ORDER BY project.last_activity_at DESC.

Query plan for current master query:

 Sort  (cost=3161.02..3161.30 rows=113 width=824) (actual time=1113.838..1113.849 rows=32 loops=1)
   Sort Key: projects.id DESC
   Sort Method: quicksort  Memory: 58kB
   Buffers: shared hit=2401 read=449 dirtied=15
   I/O Timings: read=1084.873 write=0.000
   ->  Nested Loop  (cost=1.14..3157.17 rows=113 width=824) (actual time=6.182..1110.633 rows=32 loops=1)
         Buffers: shared hit=2398 read=449 dirtied=15
         I/O Timings: read=1084.873 write=0.000
         ->  Unique  (cost=0.70..120.78 rows=883 width=4) (actual time=6.082..369.851 rows=103 loops=1)
               Buffers: shared hit=2212 read=291 dirtied=7
               I/O Timings: read=352.617 write=0.000
               ->  Index Only Scan using index_events_author_id_project_id_action_target_type_created_at on public.events  (cost=0.70..118.57 rows=884 width=4) (actual time=6.079..369.094 rows=3123 loops=1)
                     Index Cond: ((events.author_id = 116) AND (events.created_at >= '2023-02-14 09:40:46.939399+00'::timestamp with time zone))
                     Heap Fetches: 14
                     Filter: ((events.action = ANY ('{5,6}'::integer[])) OR (((events.target_type)::text = ANY ('{MergeRequest,Issue,WorkItem}'::text[])) AND (events.action = ANY ('{1,3,7,12}'::integer[]))))
                     Rows Removed by Filter: 72
                     Buffers: shared hit=2212 read=291 dirtied=7
                     I/O Timings: read=352.617 write=0.000
         ->  Index Scan using index_projects_on_id_partial_for_visibility on public.projects  (cost=0.43..3.43 rows=1 width=824) (actual time=7.183..7.183 rows=0 loops=103)
               Index Cond: (projects.id = events.project_id)
               Filter: ((projects.marked_for_deletion_at IS NULL) AND (NOT projects.pending_delete) AND (projects.visibility_level = 20))
               Rows Removed by Filter: 0
               Buffers: shared hit=182 read=158 dirtied=4
               I/O Timings: read=732.257 write=0.000

Query plan for this branch (with different order by clause):

 Sort  (cost=3161.02..3161.30 rows=113 width=824) (actual time=9.323..9.328 rows=32 loops=1)
   Sort Key: projects.last_activity_at DESC
   Sort Method: quicksort  Memory: 58kB
   Buffers: shared hit=2844
   I/O Timings: read=0.000 write=0.000
   ->  Nested Loop  (cost=1.14..3157.17 rows=113 width=824) (actual time=0.199..9.127 rows=32 loops=1)
         Buffers: shared hit=2841
         I/O Timings: read=0.000 write=0.000
         ->  Unique  (cost=0.70..120.78 rows=883 width=4) (actual time=0.124..8.023 rows=103 loops=1)
               Buffers: shared hit=2503
               I/O Timings: read=0.000 write=0.000
               ->  Index Only Scan using index_events_author_id_project_id_action_target_type_created_at on public.events  (cost=0.70..118.57 rows=884 width=4) (actual time=0.123..7.739 rows=3123 loops=1)
                     Index Cond: ((events.author_id = 116) AND (events.created_at >= '2023-02-14 09:40:46.939399+00'::timestamp with time zone))
                     Heap Fetches: 14
                     Filter: ((events.action = ANY ('{5,6}'::integer[])) OR (((events.target_type)::text = ANY ('{MergeRequest,Issue,WorkItem}'::text[])) AND (events.action = ANY ('{1,3,7,12}'::integer[]))))
                     Rows Removed by Filter: 72
                     Buffers: shared hit=2503
                     I/O Timings: read=0.000 write=0.000
         ->  Index Scan using index_projects_on_id_partial_for_visibility on public.projects  (cost=0.43..3.43 rows=1 width=824) (actual time=0.010..0.010 rows=0 loops=103)
               Index Cond: (projects.id = events.project_id)
               Filter: ((projects.marked_for_deletion_at IS NULL) AND (NOT projects.pending_delete) AND (projects.visibility_level = 20))
               Rows Removed by Filter: 0
               Buffers: shared hit=338
               I/O Timings: read=0.000 write=0.000
Edited by Rutger Wessels

Merge request reports