loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Performance improvements for pull request list page (#29900)

This PR will avoid load pullrequest.Issue twice in pull request list
page. It will reduce x times database queries for those WIP pull
requests.

Partially fix #29585

---------

Co-authored-by: Giteabot <teabot@gitea.io>
(cherry picked from commit 62f8174aa2fae1481c7e17a6afcb731a5b178cd0)

Conflicts:
models/activities/notification_list.go
moved to models/activities/notification.go

authored by

Lunny Xiao
Giteabot
and committed by
Earl Warren
d92c2048 0f461400

+86 -50
+29
models/activities/notification.go
··· 20 20 "code.gitea.io/gitea/modules/log" 21 21 "code.gitea.io/gitea/modules/setting" 22 22 "code.gitea.io/gitea/modules/timeutil" 23 + "code.gitea.io/gitea/modules/util" 23 24 24 25 "xorm.io/builder" 25 26 ) ··· 841 842 Update(n) 842 843 return err 843 844 } 845 + 846 + // LoadIssuePullRequests loads all issues' pull requests if possible 847 + func (nl NotificationList) LoadIssuePullRequests(ctx context.Context) error { 848 + issues := make(map[int64]*issues_model.Issue, len(nl)) 849 + for _, notification := range nl { 850 + if notification.Issue != nil && notification.Issue.IsPull && notification.Issue.PullRequest == nil { 851 + issues[notification.Issue.ID] = notification.Issue 852 + } 853 + } 854 + 855 + if len(issues) == 0 { 856 + return nil 857 + } 858 + 859 + pulls, err := issues_model.GetPullRequestByIssueIDs(ctx, util.KeysOfMap(issues)) 860 + if err != nil { 861 + return err 862 + } 863 + 864 + for _, pull := range pulls { 865 + if issue := issues[pull.IssueID]; issue != nil { 866 + issue.PullRequest = pull 867 + issue.PullRequest.Issue = issue 868 + } 869 + } 870 + 871 + return nil 872 + }
-14
models/issues/issue.go
··· 194 194 return issue.Repo.IsTimetrackerEnabled(ctx) 195 195 } 196 196 197 - // GetPullRequest returns the issue pull request 198 - func (issue *Issue) GetPullRequest(ctx context.Context) (pr *PullRequest, err error) { 199 - if !issue.IsPull { 200 - return nil, fmt.Errorf("Issue is not a pull request") 201 - } 202 - 203 - pr, err = GetPullRequestByIssueID(ctx, issue.ID) 204 - if err != nil { 205 - return nil, err 206 - } 207 - pr.Issue = issue 208 - return pr, err 209 - } 210 - 211 197 // LoadPoster loads poster 212 198 func (issue *Issue) LoadPoster(ctx context.Context) (err error) { 213 199 if issue.Poster == nil && issue.PosterID != 0 {
+3
models/issues/issue_list.go
··· 370 370 371 371 for _, issue := range issues { 372 372 issue.PullRequest = pullRequestMaps[issue.ID] 373 + if issue.PullRequest != nil { 374 + issue.PullRequest.Issue = issue 375 + } 373 376 } 374 377 return nil 375 378 }
+9
models/issues/pull_list.go
··· 220 220 Limit(1). 221 221 Get(new(Issue)) 222 222 } 223 + 224 + // GetPullRequestByIssueIDs returns all pull requests by issue ids 225 + func GetPullRequestByIssueIDs(ctx context.Context, issueIDs []int64) (PullRequestList, error) { 226 + prs := make([]*PullRequest, 0, len(issueIDs)) 227 + return prs, db.GetEngine(ctx). 228 + Where("issue_id > 0"). 229 + In("issue_id", issueIDs). 230 + Find(&prs) 231 + }
+4 -5
models/issues/review.go
··· 239 239 240 240 // IsOfficialReviewer check if at least one of the provided reviewers can make official reviews in issue (counts towards required approvals) 241 241 func IsOfficialReviewer(ctx context.Context, issue *Issue, reviewer *user_model.User) (bool, error) { 242 - pr, err := GetPullRequestByIssueID(ctx, issue.ID) 243 - if err != nil { 242 + if err := issue.LoadPullRequest(ctx); err != nil { 244 243 return false, err 245 244 } 246 245 246 + pr := issue.PullRequest 247 247 rule, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) 248 248 if err != nil { 249 249 return false, err ··· 271 271 272 272 // IsOfficialReviewerTeam check if reviewer in this team can make official reviews in issue (counts towards required approvals) 273 273 func IsOfficialReviewerTeam(ctx context.Context, issue *Issue, team *organization.Team) (bool, error) { 274 - pr, err := GetPullRequestByIssueID(ctx, issue.ID) 275 - if err != nil { 274 + if err := issue.LoadPullRequest(ctx); err != nil { 276 275 return false, err 277 276 } 278 - pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch) 277 + pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, issue.PullRequest.BaseRepoID, issue.PullRequest.BaseBranch) 279 278 if err != nil { 280 279 return false, err 281 280 }
+10 -1
modules/util/slice.go
··· 54 54 return values 55 55 } 56 56 57 - // TODO: Replace with "maps.Values" once available 57 + // TODO: Replace with "maps.Values" once available, current it only in golang.org/x/exp/maps but not in standard library 58 58 func ValuesOfMap[K comparable, V any](m map[K]V) []V { 59 59 values := make([]V, 0, len(m)) 60 60 for _, v := range m { ··· 62 62 } 63 63 return values 64 64 } 65 + 66 + // TODO: Replace with "maps.Keys" once available, current it only in golang.org/x/exp/maps but not in standard library 67 + func KeysOfMap[K comparable, V any](m map[K]V) []K { 68 + keys := make([]K, 0, len(m)) 69 + for k := range m { 70 + keys = append(keys, k) 71 + } 72 + return keys 73 + }
+3 -2
routers/api/v1/repo/issue.go
··· 874 874 } 875 875 if form.State != nil { 876 876 if issue.IsPull { 877 - if pr, err := issue.GetPullRequest(ctx); err != nil { 877 + if err := issue.LoadPullRequest(ctx); err != nil { 878 878 ctx.Error(http.StatusInternalServerError, "GetPullRequest", err) 879 879 return 880 - } else if pr.HasMerged { 880 + } 881 + if issue.PullRequest.HasMerged { 881 882 ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged") 882 883 return 883 884 }
+5 -11
routers/api/v1/repo/issue_pin.go
··· 240 240 } 241 241 242 242 apiPrs := make([]*api.PullRequest, len(issues)) 243 + if err := issues.LoadPullRequests(ctx); err != nil { 244 + ctx.Error(http.StatusInternalServerError, "LoadPullRequests", err) 245 + return 246 + } 243 247 for i, currentIssue := range issues { 244 - pr, err := currentIssue.GetPullRequest(ctx) 245 - if err != nil { 246 - ctx.Error(http.StatusInternalServerError, "GetPullRequest", err) 247 - return 248 - } 249 - 250 - if err = pr.LoadIssue(ctx); err != nil { 251 - ctx.Error(http.StatusInternalServerError, "LoadIssue", err) 252 - return 253 - } 254 - 248 + pr := currentIssue.PullRequest 255 249 if err = pr.LoadAttributes(ctx); err != nil { 256 250 ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) 257 251 return
+6
routers/web/user/notification.go
··· 144 144 ctx.ServerError("LoadIssues", err) 145 145 return 146 146 } 147 + 148 + if err = notifications.LoadIssuePullRequests(ctx); err != nil { 149 + ctx.ServerError("LoadIssuePullRequests", err) 150 + return 151 + } 152 + 147 153 notifications = notifications.Without(failures) 148 154 failCount += len(failures) 149 155
+3 -2
services/convert/notification.go
··· 61 61 result.Subject.LatestCommentHTMLURL = comment.HTMLURL(ctx) 62 62 } 63 63 64 - pr, _ := n.Issue.GetPullRequest(ctx) 65 - if pr != nil && pr.HasMerged { 64 + if err := n.Issue.LoadPullRequest(ctx); err == nil && 65 + n.Issue.PullRequest != nil && 66 + n.Issue.PullRequest.HasMerged { 66 67 result.Subject.State = "merged" 67 68 } 68 69 }
+2 -2
services/pull/review.go
··· 268 268 269 269 // SubmitReview creates a review out of the existing pending review or creates a new one if no pending review exist 270 270 func SubmitReview(ctx context.Context, doer *user_model.User, gitRepo *git.Repository, issue *issues_model.Issue, reviewType issues_model.ReviewType, content, commitID string, attachmentUUIDs []string) (*issues_model.Review, *issues_model.Comment, error) { 271 - pr, err := issue.GetPullRequest(ctx) 272 - if err != nil { 271 + if err := issue.LoadPullRequest(ctx); err != nil { 273 272 return nil, nil, err 274 273 } 275 274 275 + pr := issue.PullRequest 276 276 var stale bool 277 277 if reviewType != issues_model.ReviewTypeApprove && reviewType != issues_model.ReviewTypeReject { 278 278 stale = false
+8 -8
templates/shared/issueicon.tmpl
··· 1 1 {{if .IsPull}} 2 - {{if and .PullRequest .PullRequest.HasMerged}} 3 - {{svg "octicon-git-merge" 16 "text purple"}} 4 - {{else if and (.GetPullRequest ctx) (.GetPullRequest ctx).HasMerged}} 5 - {{svg "octicon-git-merge" 16 "text purple"}} 2 + {{if not .PullRequest}} 3 + No PullRequest 6 4 {{else}} 7 5 {{if .IsClosed}} 8 - {{svg "octicon-git-pull-request" 16 "text red"}} 6 + {{if .PullRequest.HasMerged}} 7 + {{svg "octicon-git-merge" 16 "text purple"}} 8 + {{else}} 9 + {{svg "octicon-git-pull-request" 16 "text red"}} 10 + {{end}} 9 11 {{else}} 10 - {{if and .PullRequest (.PullRequest.IsWorkInProgress ctx)}} 11 - {{svg "octicon-git-pull-request-draft" 16 "text grey"}} 12 - {{else if and (.GetPullRequest ctx) ((.GetPullRequest ctx).IsWorkInProgress ctx)}} 12 + {{if .PullRequest.IsWorkInProgress ctx}} 13 13 {{svg "octicon-git-pull-request-draft" 16 "text grey"}} 14 14 {{else}} 15 15 {{svg "octicon-git-pull-request" 16 "text green"}}
+2 -2
tests/integration/pull_merge_test.go
··· 513 513 assert.NoError(t, err) 514 514 515 515 issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "PR with conflict!"}) 516 - conflictingPR, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, issue.ID) 517 - assert.NoError(t, err) 516 + assert.NoError(t, issue.LoadPullRequest(db.DefaultContext)) 517 + conflictingPR := issue.PullRequest 518 518 519 519 // Ensure conflictedFiles is populated. 520 520 assert.Len(t, conflictingPR.ConflictedFiles, 1)
+2 -3
tests/integration/pull_update_test.go
··· 167 167 assert.NoError(t, err) 168 168 169 169 issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{Title: "Test Pull -to-update-"}) 170 - pr, err := issues_model.GetPullRequestByIssueID(db.DefaultContext, issue.ID) 171 - assert.NoError(t, err) 170 + assert.NoError(t, issue.LoadPullRequest(db.DefaultContext)) 172 171 173 - return pr 172 + return issue.PullRequest 174 173 }