Skip to content

Commit d5c8091

Browse files
GustedEarl Warren
authored andcommitted
perf: optimize converting releases to feed items (go-gitea#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 <[email protected]> Co-authored-by: Gusted <[email protected]> Co-committed-by: Gusted <[email protected]>
1 parent ccd8700 commit d5c8091

File tree

4 files changed

+192
-8
lines changed

4 files changed

+192
-8
lines changed

models/repo/release_list.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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+
}

models/repo/release_list_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
}

routers/web/feed/convert.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -298,14 +298,14 @@ func GetFeedType(name string, req *http.Request) (bool, string, string) {
298298
return false, name, ""
299299
}
300300

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+
}
308306

307+
composeCache := make(map[int64]map[string]string)
308+
for _, rel := range releases {
309309
var title string
310310
var content template.HTML
311311

@@ -315,13 +315,19 @@ func releasesToFeedItems(ctx *context.Context, releases []*repo_model.Release) (
315315
title = rel.Title
316316
}
317317

318+
metas, ok := composeCache[rel.RepoID]
319+
if !ok {
320+
metas = rel.Repo.ComposeMetas(ctx)
321+
composeCache[rel.RepoID] = metas
322+
}
323+
318324
link := &feeds.Link{Href: rel.HTMLURL()}
319325
content, err = markdown.RenderString(&markup.RenderContext{
320326
Ctx: ctx,
321327
Links: markup.Links{
322328
Base: rel.Repo.Link(),
323329
},
324-
Metas: rel.Repo.ComposeMetas(ctx),
330+
Metas: metas,
325331
}, rel.Note)
326332
if err != nil {
327333
return nil, err
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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>[email protected]</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>[email protected]</email>
87+
</author>
88+
</entry>
89+
</feed>`, normalize(resp.Body.String()))
90+
})
91+
}

0 commit comments

Comments
 (0)