perf: Various performance regression fixes (#38078)

Fixes five N+1 / O(n) query patterns found across common user paths.
Each uses a bulk query that already existed elsewhere in the codebase.

| Location | Problem | Introduced in |
| -------------------------------- |
-------------------------------------------------------------------------------------------------------------------------------
| ------------- |
| `IssueList.LoadIsRead` | `.In("issue_id")` missing its arg — xorm
generates `WHERE 0=1`, so `IsRead` is **never** set; every issue always
appears unread | #29515 |
| `ParseCommitsWithStatus` | `GetLatestCommitStatus` called once per
commit (O(n) queries on commit list / PR commits tab) | #33605 |
| `getReleaseInfos` (release list) | `GetLatestCommitStatus` called once
per release for CI badges | #29149 |
| User milestone dashboard | O(n×m) nested loop matching milestones to
repos | #26300 |
| `findCodeComments` (PR diff) | `LoadResolveDoer` + `LoadReactions`
called per inline comment — up to ~150 queries on a PR with 50 comments
| #20821 |

---------

Co-authored-by: Lauris B <lauris@nix.lv>
This commit is contained in:
bircni
2026-06-21 14:34:07 +00:00
committed by GitHub
co-authored by Lauris B
parent ceec230fc0
commit 180af33f86
7 changed files with 135 additions and 32 deletions
+68
View File
@@ -11,6 +11,7 @@ import (
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
)
// CommentList defines a list of comments
@@ -444,6 +445,73 @@ func (comments CommentList) loadReviews(ctx context.Context) error {
return nil
}
// loadResolveDoers bulk-loads the resolve doer for all code comments that have one.
func (comments CommentList) loadResolveDoers(ctx context.Context) error {
resolveDoerIDs := container.FilterSlice(comments, func(c *Comment) (int64, bool) {
return c.ResolveDoerID, c.ResolveDoerID != 0 && c.Type == CommentTypeCode
})
if len(resolveDoerIDs) == 0 {
return nil
}
userMaps, err := user_model.GetUsersMapByIDs(ctx, resolveDoerIDs)
if err != nil {
return err
}
for _, comment := range comments {
if comment.ResolveDoerID == 0 || comment.Type != CommentTypeCode {
continue
}
if u, ok := userMaps[comment.ResolveDoerID]; ok {
comment.ResolveDoer = u
} else {
comment.ResolveDoer = user_model.NewGhostUser()
}
}
return nil
}
// loadReactions bulk-loads reactions for all comments in the list.
func (comments CommentList) loadReactions(ctx context.Context, repo *repo_model.Repository) error {
if len(comments) == 0 {
return nil
}
commentIDs := container.FilterSlice(comments, func(c *Comment) (int64, bool) {
return c.ID, c.Reactions == nil
})
if len(commentIDs) == 0 {
return nil
}
var allReactions ReactionList
if err := db.GetEngine(ctx).
Where("`comment_id` > 0").
In("comment_id", commentIDs).
In("`type`", setting.UI.Reactions).
Asc("issue_id", "comment_id", "created_unix", "id").
Find(&allReactions); err != nil {
return err
}
if _, err := allReactions.LoadUsers(ctx, repo); err != nil {
return err
}
reactByComment := make(map[int64]ReactionList, len(commentIDs))
for _, r := range allReactions {
reactByComment[r.CommentID] = append(reactByComment[r.CommentID], r)
}
for _, comment := range comments {
if comment.Reactions == nil {
comment.Reactions = reactByComment[comment.ID]
}
}
return nil
}
// LoadAttributes loads attributes of the comments, except for attachments and
// comments
func (comments CommentList) LoadAttributes(ctx context.Context) (err error) {