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.

perf: optimize converting releases to feed items (#7221)

- `releasesToFeedItems` is called to convert release structs to feed items, which is then used to render RSS or Atom feeds.
- Optimize the loading of attributes for the releases, introduce `ReleaseList` type which uses caching to load repository and publishers. It also no longer loads release attachments and downloads counts as that is not used in feed items.
- Optimize the composing of meta by introducing caching, this operation is especially slow when the owner is an organization.
- Add unit test (ensures new `LoadAttributes` works correctly).
- Add integration test (ensures that feed output is still as expected).

Loading https://codeberg.org/forgejo/forgejo/releases.rss reduced from ~15s to ~1s. (It is currently is deployed on codeberg.org)

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/7221
Reviewed-by: Otto <otto@codeberg.org>
Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-committed-by: Gusted <postmaster@gusted.xyz>

authored by

Gusted
Gusted
and committed by
Earl Warren
d5c8091e ccd87001

+192 -8
+45
models/repo/release_list.go
··· 1 + // Copyright 2025 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + package repo 5 + 6 + import ( 7 + "context" 8 + 9 + user_model "code.gitea.io/gitea/models/user" 10 + ) 11 + 12 + type ReleaseList []*Release 13 + 14 + // LoadAttributes loads the repository and publisher for the releases. 15 + func (r ReleaseList) LoadAttributes(ctx context.Context) error { 16 + repoCache := make(map[int64]*Repository) 17 + userCache := make(map[int64]*user_model.User) 18 + 19 + for _, release := range r { 20 + var err error 21 + repo, ok := repoCache[release.RepoID] 22 + if !ok { 23 + repo, err = GetRepositoryByID(ctx, release.RepoID) 24 + if err != nil { 25 + return err 26 + } 27 + repoCache[release.RepoID] = repo 28 + } 29 + release.Repo = repo 30 + 31 + publisher, ok := userCache[release.PublisherID] 32 + if !ok { 33 + publisher, err = user_model.GetUserByID(ctx, release.PublisherID) 34 + if err != nil { 35 + if !user_model.IsErrUserNotExist(err) { 36 + return err 37 + } 38 + publisher = user_model.NewGhostUser() 39 + } 40 + userCache[release.PublisherID] = publisher 41 + } 42 + release.Publisher = publisher 43 + } 44 + return nil 45 + }
+42
models/repo/release_list_test.go
··· 1 + // Copyright 2025 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + package repo 5 + 6 + import ( 7 + "testing" 8 + 9 + "code.gitea.io/gitea/models/unittest" 10 + 11 + "github.com/stretchr/testify/assert" 12 + "github.com/stretchr/testify/require" 13 + ) 14 + 15 + func TestReleaseListLoadAttributes(t *testing.T) { 16 + require.NoError(t, unittest.PrepareTestDatabase()) 17 + 18 + releases := ReleaseList{&Release{ 19 + RepoID: 1, 20 + PublisherID: 1, 21 + }, &Release{ 22 + RepoID: 2, 23 + PublisherID: 2, 24 + }, &Release{ 25 + RepoID: 1, 26 + PublisherID: 2, 27 + }, &Release{ 28 + RepoID: 2, 29 + PublisherID: 1, 30 + }} 31 + 32 + require.NoError(t, releases.LoadAttributes(t.Context())) 33 + 34 + assert.EqualValues(t, 1, releases[0].Repo.ID) 35 + assert.EqualValues(t, 1, releases[0].Publisher.ID) 36 + assert.EqualValues(t, 2, releases[1].Repo.ID) 37 + assert.EqualValues(t, 2, releases[1].Publisher.ID) 38 + assert.EqualValues(t, 1, releases[2].Repo.ID) 39 + assert.EqualValues(t, 2, releases[2].Publisher.ID) 40 + assert.EqualValues(t, 2, releases[3].Repo.ID) 41 + assert.EqualValues(t, 1, releases[3].Publisher.ID) 42 + }
+14 -8
routers/web/feed/convert.go
··· 298 298 return false, name, "" 299 299 } 300 300 301 - // feedActionsToFeedItems convert gitea's Repo's Releases to feeds Item 302 - func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release) (items []*feeds.Item, err error) { 303 - for _, rel := range releases { 304 - err := rel.LoadAttributes(ctx) 305 - if err != nil { 306 - return nil, err 307 - } 301 + // feedActionsToFeedItems convert repository releases into feed items. 302 + func releasesToFeedItems(ctx *context.Context, releases repo_model.ReleaseList) (items []*feeds.Item, err error) { 303 + if err := releases.LoadAttributes(ctx); err != nil { 304 + return nil, err 305 + } 308 306 307 + composeCache := make(map[int64]map[string]string) 308 + for _, rel := range releases { 309 309 var title string 310 310 var content template.HTML 311 311 ··· 315 315 title = rel.Title 316 316 } 317 317 318 + metas, ok := composeCache[rel.RepoID] 319 + if !ok { 320 + metas = rel.Repo.ComposeMetas(ctx) 321 + composeCache[rel.RepoID] = metas 322 + } 323 + 318 324 link := &feeds.Link{Href: rel.HTMLURL()} 319 325 content, err = markdown.RenderString(&markup.RenderContext{ 320 326 Ctx: ctx, 321 327 Links: markup.Links{ 322 328 Base: rel.Repo.Link(), 323 329 }, 324 - Metas: rel.Repo.ComposeMetas(ctx), 330 + Metas: metas, 325 331 }, rel.Note) 326 332 if err != nil { 327 333 return nil, err
+91
tests/integration/release_feed_test.go
··· 1 + // Copyright 2025 The Forgejo Authors. All rights reserved. 2 + // SPDX-License-Identifier: GPL-3.0-or-later 3 + 4 + package integration 5 + 6 + import ( 7 + "net/http" 8 + "regexp" 9 + "testing" 10 + 11 + "code.gitea.io/gitea/tests" 12 + 13 + "github.com/stretchr/testify/assert" 14 + ) 15 + 16 + func TestReleaseFeed(t *testing.T) { 17 + defer tests.PrepareTestEnv(t)() 18 + 19 + normalize := func(body string) string { 20 + // Remove port. 21 + body = regexp.MustCompile(`localhost:\d+`).ReplaceAllString(body, "localhost") 22 + // date is timezone dependent. 23 + body = regexp.MustCompile(`<pubDate>.*</pubDate>`).ReplaceAllString(body, "<pubDate></pubDate>") 24 + body = regexp.MustCompile(`<updated>.*</updated>`).ReplaceAllString(body, "<updated></updated>") 25 + return body 26 + } 27 + t.Run("RSS feed", func(t *testing.T) { 28 + defer tests.PrintCurrentTest(t)() 29 + 30 + resp := MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/releases.rss"), http.StatusOK) 31 + assert.EqualValues(t, `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"> 32 + <channel> 33 + <title>Releases for user2/repo1</title> 34 + <link>http://localhost/user2/repo1/release</link> 35 + <description></description> 36 + <pubDate></pubDate> 37 + <item> 38 + <title>pre-release</title> 39 + <link>http://localhost/user2/repo1/releases/tag/v1.0</link> 40 + <description></description> 41 + <content:encoded><![CDATA[<p dir="auto">some text for a pre release</p> 42 + ]]></content:encoded> 43 + <author>user2</author> 44 + <guid>5: http://localhost/user2/repo1/releases/tag/v1.0</guid> 45 + <pubDate></pubDate> 46 + </item> 47 + <item> 48 + <title>testing-release</title> 49 + <link>http://localhost/user2/repo1/releases/tag/v1.1</link> 50 + <description></description> 51 + <author>user2</author> 52 + <guid>1: http://localhost/user2/repo1/releases/tag/v1.1</guid> 53 + <pubDate></pubDate> 54 + </item> 55 + </channel> 56 + </rss>`, normalize(resp.Body.String())) 57 + }) 58 + 59 + t.Run("Atom feed", func(t *testing.T) { 60 + defer tests.PrintCurrentTest(t)() 61 + 62 + resp := MakeRequest(t, NewRequest(t, "GET", "/user2/repo1/releases.atom"), http.StatusOK) 63 + assert.EqualValues(t, `<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom"> 64 + <title>Releases for user2/repo1</title> 65 + <id>http://localhost/user2/repo1/release</id> 66 + <updated></updated> 67 + <link href="http://localhost/user2/repo1/release"></link> 68 + <entry> 69 + <title>pre-release</title> 70 + <updated></updated> 71 + <id>5: http://localhost/user2/repo1/releases/tag/v1.0</id> 72 + <content type="html">&lt;p dir=&#34;auto&#34;&gt;some text for a pre release&lt;/p&gt;&#xA;</content> 73 + <link href="http://localhost/user2/repo1/releases/tag/v1.0" rel="alternate"></link> 74 + <author> 75 + <name>user2</name> 76 + <email>user2@noreply.example.org</email> 77 + </author> 78 + </entry> 79 + <entry> 80 + <title>testing-release</title> 81 + <updated></updated> 82 + <id>1: http://localhost/user2/repo1/releases/tag/v1.1</id> 83 + <link href="http://localhost/user2/repo1/releases/tag/v1.1" rel="alternate"></link> 84 + <author> 85 + <name>user2</name> 86 + <email>user2@noreply.example.org</email> 87 + </author> 88 + </entry> 89 + </feed>`, normalize(resp.Body.String())) 90 + }) 91 + }