Skip to content

Commit 8385d2f

Browse files
committed
perf: cache the format of feeds
Detecting the format of a feed accounts for up to 30% of the time spent in `parser.ParseFeed`. Cache the value once detected, as it'll never change.
1 parent 502db64 commit 8385d2f

File tree

12 files changed

+52
-9
lines changed

12 files changed

+52
-9
lines changed

internal/database/migrations.go

+8
Original file line numberDiff line numberDiff line change
@@ -1008,4 +1008,12 @@ var migrations = []func(tx *sql.Tx, driver string) error{
10081008
_, err = tx.Exec(sql)
10091009
return err
10101010
},
1011+
func(tx *sql.Tx, _ string) (err error) {
1012+
sql := `
1013+
ALTER TABLE feeds ADD COLUMN format text default '';
1014+
ALTER TABLE feeds ADD COLUMN format_version text default '';
1015+
`
1016+
_, err = tx.Exec(sql)
1017+
return err
1018+
},
10111019
}

internal/model/feed.go

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ type Feed struct {
5858
NtfyPriority int `json:"ntfy_priority"`
5959
PushoverEnabled bool `json:"pushover_enabled,omitempty"`
6060
PushoverPriority int `json:"pushover_priority,omitempty"`
61+
Format string `json:"format"`
62+
FormatVersion string `json:"format_version"`
6163

6264
// Non-persisted attributes
6365
Category *Category `json:"category,omitempty"`

internal/reader/atom/atom_03_adapter.go

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ func NewAtom03Adapter(atomFeed *Atom03Feed) *Atom03Adapter {
2525
func (a *Atom03Adapter) BuildFeed(baseURL string) *model.Feed {
2626
feed := new(model.Feed)
2727

28+
feed.Format = "atom"
29+
feed.FormatVersion = "0.3"
30+
2831
// Populate the feed URL.
2932
feedURL := a.atomFeed.Links.firstLinkWithRelation("self")
3033
if feedURL != "" {

internal/reader/atom/atom_10_adapter.go

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ func NewAtom10Adapter(atomFeed *Atom10Feed) *Atom10Adapter {
2929
func (a *Atom10Adapter) BuildFeed(baseURL string) *model.Feed {
3030
feed := new(model.Feed)
3131

32+
feed.Format = "atom"
33+
feed.FormatVersion = "10"
34+
3235
// Populate the feed URL.
3336
feedURL := a.atomFeed.Links.firstLinkWithRelation("self")
3437
if feedURL != "" {

internal/reader/handler/handler.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,18 @@ func RefreshFeed(store *storage.Storage, userID, feedID int64, forceRefresh bool
275275
return localizedError
276276
}
277277

278-
updatedFeed, parseErr := parser.ParseFeed(responseHandler.EffectiveURL(), bytes.NewReader(responseBody))
278+
var updatedFeed *model.Feed
279+
var parseErr error
280+
if originalFeed.Format != "" {
281+
format, version := originalFeed.Format, originalFeed.FormatVersion
282+
updatedFeed, parseErr = parser.ParseFeedWithFormat(responseHandler.EffectiveURL(), bytes.NewReader(responseBody), format, version)
283+
if parseErr != nil { // Maybe the feed changed its format.
284+
slog.Warn("Unable to parse feed with the given format", slog.String("feed_url", originalFeed.FeedURL), slog.String("format", format), slog.Any("error", parseErr))
285+
updatedFeed, parseErr = parser.ParseFeed(responseHandler.EffectiveURL(), bytes.NewReader(responseBody))
286+
}
287+
} else {
288+
updatedFeed, parseErr = parser.ParseFeed(responseHandler.EffectiveURL(), bytes.NewReader(responseBody))
289+
}
279290
if parseErr != nil {
280291
localizedError := locale.NewLocalizedErrorWrapper(parseErr, "error.unable_to_parse_feed", parseErr)
281292

internal/reader/json/adapter.go

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func (j *JSONAdapter) BuildFeed(baseURL string) *model.Feed {
2929
Title: strings.TrimSpace(j.jsonFeed.Title),
3030
FeedURL: strings.TrimSpace(j.jsonFeed.FeedURL),
3131
SiteURL: strings.TrimSpace(j.jsonFeed.HomePageURL),
32+
Format: "json",
3233
}
3334

3435
if feed.FeedURL == "" {

internal/reader/parser/parser.go

+9-4
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,8 @@ import (
1616

1717
var ErrFeedFormatNotDetected = errors.New("parser: unable to detect feed format")
1818

19-
// ParseFeed analyzes the input data and returns a normalized feed object.
20-
func ParseFeed(baseURL string, r io.ReadSeeker) (*model.Feed, error) {
21-
r.Seek(0, io.SeekStart)
22-
format, version := DetectFeedFormat(r)
19+
// ParseFeedWithFormat returns a normalized feed object.
20+
func ParseFeedWithFormat(baseURL string, r io.ReadSeeker, format, version string) (*model.Feed, error) {
2321
switch format {
2422
case FormatAtom:
2523
r.Seek(0, io.SeekStart)
@@ -37,3 +35,10 @@ func ParseFeed(baseURL string, r io.ReadSeeker) (*model.Feed, error) {
3735
return nil, ErrFeedFormatNotDetected
3836
}
3937
}
38+
39+
// ParseFeed analyzes the input data and returns a normalized feed object.
40+
func ParseFeed(baseURL string, r io.ReadSeeker) (*model.Feed, error) {
41+
r.Seek(0, io.SeekStart)
42+
format, version := DetectFeedFormat(r)
43+
return ParseFeedWithFormat(baseURL, r, format, version)
44+
}

internal/reader/rdf/adapter.go

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func (r *RDFAdapter) BuildFeed(baseURL string) *model.Feed {
2929
Title: stripTags(r.rdf.Channel.Title),
3030
FeedURL: strings.TrimSpace(baseURL),
3131
SiteURL: strings.TrimSpace(r.rdf.Channel.Link),
32+
Format: "rdf",
3233
}
3334

3435
if feed.Title == "" {

internal/reader/rss/adapter.go

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func (r *RSSAdapter) BuildFeed(baseURL string) *model.Feed {
3131
Title: html.UnescapeString(strings.TrimSpace(r.rss.Channel.Title)),
3232
FeedURL: strings.TrimSpace(baseURL),
3333
SiteURL: strings.TrimSpace(r.rss.Channel.Link),
34+
Format: "rss",
3435
}
3536

3637
// Ensure the Site URL is absolute.

internal/storage/feed.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -248,10 +248,12 @@ func (s *Storage) CreateFeed(feed *model.Feed) error {
248248
apprise_service_urls,
249249
webhook_url,
250250
disable_http2,
251-
description
251+
description,
252+
format,
253+
format_version
252254
)
253255
VALUES
254-
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27)
256+
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29)
255257
RETURNING
256258
id
257259
`
@@ -284,6 +286,8 @@ func (s *Storage) CreateFeed(feed *model.Feed) error {
284286
feed.WebhookURL,
285287
feed.DisableHTTP2,
286288
feed.Description,
289+
feed.Format,
290+
feed.FormatVersion,
287291
).Scan(&feed.ID)
288292
if err != nil {
289293
return fmt.Errorf(`store: unable to create feed %q: %v`, feed.FeedURL, err)

internal/storage/feed_query_builder.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,9 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
170170
f.ntfy_enabled,
171171
f.ntfy_priority,
172172
f.pushover_enabled,
173-
f.pushover_priority
173+
f.pushover_priority,
174+
f.format,
175+
f.format_version
174176
FROM
175177
feeds f
176178
LEFT JOIN
@@ -244,6 +246,8 @@ func (f *FeedQueryBuilder) GetFeeds() (model.Feeds, error) {
244246
&feed.NtfyPriority,
245247
&feed.PushoverEnabled,
246248
&feed.PushoverPriority,
249+
&feed.Format,
250+
&feed.FormatVersion,
247251
)
248252

249253
if err != nil {

internal/ui/static/css/common.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ a:hover {
7575
padding: var(--padding-size);
7676
position: absolute;
7777
transition: translate 0.3s;
78-
translate: -50% calc(-100% - calc(var(--padding-size) * 2) - calc(var(--border-size) * 2));
78+
translate: -50% calc(-100% - var(--padding-size) * 2 - var(--border-size) * 2);
7979
}
8080

8181
.skip-to-content-link:focus {

0 commit comments

Comments
 (0)