From a08a0fd3e617dff15d08f7bf399c3e278691a6b0 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 10 Dec 2025 16:44:21 +0000 Subject: [PATCH] accounts: reduce initial load time further When generating the cache and calling userSummary per user, previously the DB was queried for an invite with the ReferrerJellyfinID set to the user's ID. This was fast enough on my test system (~5000 users in ~1.5s), while testing cross-compilation, I found it ran extremely slow on an arm64 build (running through QEMU admittedly), doing ~5000 in ~18s. Instead, a map of IDs to invites/referrals is generated once and queried instead. Initial load now takes ~80ms on my system, and 0.95s through QEMU, and 0.68s on a rockpro64 SBC. --- api-users.go | 27 ++++++++++++++++++++++++--- logmessages/logmessages.go | 3 +++ usercache.go | 9 ++++++++- users.go | 2 +- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/api-users.go b/api-users.go index cf77781..adaad24 100644 --- a/api-users.go +++ b/api-users.go @@ -902,9 +902,23 @@ func (app *appContext) AdminPasswordReset(gc *gin.Context) { respondBool(204, true, gc) } +// getActiveReferrals returns a map of jellyfin user IDs to their active referral "invite" code, if they have one. +// It does not check if the user still exists, simply finding invites with the ReferrerJellyfinID field set. +func (app *appContext) getActiveReferrals() map[string]string { + out := map[string]string{} + for _, inv := range app.storage.GetInvites() { + if inv.ReferrerJellyfinID == "" { + continue + } + out[inv.ReferrerJellyfinID] = inv.Code + } + return out +} + // userSummary generates a respUser for to be displayed to the user, or sorted/filtered. // also, consider it a source of which data fields/struct modifications need to trigger a cache invalidation. -func (app *appContext) userSummary(jfUser mediabrowser.User) respUser { +// referralCache can be passed to avoid querying the db each time this is called. It can be generated with app.getActiveReferrals(). +func (app *appContext) userSummary(jfUser mediabrowser.User, referralCache *map[string]string) respUser { adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true) allowAll := app.config.Section("ui").Key("allow_all").MustBool(false) referralsEnabled := app.config.Section("user_page").Key("referrals").MustBool(false) @@ -944,10 +958,17 @@ func (app *appContext) userSummary(jfUser mediabrowser.User) respUser { } // FIXME: Send referral data referrerInv := Invite{} + // FIXME: This is veeery slow when running an arm64 binary through qemu if referralsEnabled { // 1. Directly attached invite. - err := app.storage.db.FindOne(&referrerInv, badgerhold.Where("ReferrerJellyfinID").Eq(jfUser.ID)) - if err == nil { + found := false + if referralCache != nil { + _, found = (*referralCache)[jfUser.ID] + } else { + err := app.storage.db.FindOne(&referrerInv, badgerhold.Where("IsReferral").Eq(true).And("ReferrerJellyfinID").Eq(jfUser.ID)) + found = err == nil + } + if found { user.ReferralsEnabled = true // 2. Referrals via profile template. Shallow check, doesn't look for the thing in the database. } else if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok && email.ReferralTemplateKey != "" { diff --git a/logmessages/logmessages.go b/logmessages/logmessages.go index 333ab5b..4d537e2 100644 --- a/logmessages/logmessages.go +++ b/logmessages/logmessages.go @@ -328,6 +328,9 @@ const ( // webhooks.go WebhookRequest = "Webhook request send to \"%s\" (%d): %v" + + // usercache.go + CacheRefreshCompleted = "Usercache refreshed, %d in %.2fs (%f.2u/sec)" ) const ( diff --git a/usercache.go b/usercache.go index 35b13e2..8b9cd33 100644 --- a/usercache.go +++ b/usercache.go @@ -8,6 +8,8 @@ import ( "strings" "sync" "time" + + lm "github.com/hrfee/jfa-go/logmessages" ) const ( @@ -85,10 +87,12 @@ func (c *UserCache) MaybeSync(app *appContext) error { status <- err return } + startTime := time.Now() cache := make([]respUser, len(users)) labels := map[string]bool{} + referralCache := app.getActiveReferrals() for i, jfUser := range users { - cache[i] = app.userSummary(jfUser) + cache[i] = app.userSummary(jfUser, &referralCache) if cache[i].Label != "" { labels[cache[i].Label] = true } @@ -101,6 +105,9 @@ func (c *UserCache) MaybeSync(app *appContext) error { for label, _ := range labels { labelSlice = append(labelSlice, label) } + elapsed := time.Since(startTime).Seconds() + usersPerSec := float64(len(users)) / elapsed + app.debug.Printf(lm.CacheRefreshCompleted, len(users), elapsed, usersPerSec) c.Cache = cache c.Ref = ref c.Sorted = false diff --git a/users.go b/users.go index e641ba4..f7b8f13 100644 --- a/users.go +++ b/users.go @@ -141,7 +141,7 @@ func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData webhookURIs := app.config.Section("webhooks").Key("created").StringsWithShadows("|") if len(webhookURIs) != 0 { - summary := app.userSummary(out.User) + summary := app.userSummary(out.User, nil) for _, uri := range webhookURIs { pendingTasks.Add(1) go func() {