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.
This commit is contained in:
Harvey Tindall
2025-12-10 16:44:21 +00:00
parent 420a22970d
commit a08a0fd3e6
4 changed files with 36 additions and 5 deletions
+24 -3
View File
@@ -902,9 +902,23 @@ func (app *appContext) AdminPasswordReset(gc *gin.Context) {
respondBool(204, true, gc) 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. // 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. // 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) adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
allowAll := app.config.Section("ui").Key("allow_all").MustBool(false) allowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
referralsEnabled := app.config.Section("user_page").Key("referrals").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 // FIXME: Send referral data
referrerInv := Invite{} referrerInv := Invite{}
// FIXME: This is veeery slow when running an arm64 binary through qemu
if referralsEnabled { if referralsEnabled {
// 1. Directly attached invite. // 1. Directly attached invite.
err := app.storage.db.FindOne(&referrerInv, badgerhold.Where("ReferrerJellyfinID").Eq(jfUser.ID)) found := false
if err == nil { 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 user.ReferralsEnabled = true
// 2. Referrals via profile template. Shallow check, doesn't look for the thing in the database. // 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 != "" { } else if email, ok := app.storage.GetEmailsKey(jfUser.ID); ok && email.ReferralTemplateKey != "" {
+3
View File
@@ -328,6 +328,9 @@ const (
// webhooks.go // webhooks.go
WebhookRequest = "Webhook request send to \"%s\" (%d): %v" WebhookRequest = "Webhook request send to \"%s\" (%d): %v"
// usercache.go
CacheRefreshCompleted = "Usercache refreshed, %d in %.2fs (%f.2u/sec)"
) )
const ( const (
+8 -1
View File
@@ -8,6 +8,8 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
lm "github.com/hrfee/jfa-go/logmessages"
) )
const ( const (
@@ -85,10 +87,12 @@ func (c *UserCache) MaybeSync(app *appContext) error {
status <- err status <- err
return return
} }
startTime := time.Now()
cache := make([]respUser, len(users)) cache := make([]respUser, len(users))
labels := map[string]bool{} labels := map[string]bool{}
referralCache := app.getActiveReferrals()
for i, jfUser := range users { for i, jfUser := range users {
cache[i] = app.userSummary(jfUser) cache[i] = app.userSummary(jfUser, &referralCache)
if cache[i].Label != "" { if cache[i].Label != "" {
labels[cache[i].Label] = true labels[cache[i].Label] = true
} }
@@ -101,6 +105,9 @@ func (c *UserCache) MaybeSync(app *appContext) error {
for label, _ := range labels { for label, _ := range labels {
labelSlice = append(labelSlice, label) 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.Cache = cache
c.Ref = ref c.Ref = ref
c.Sorted = false c.Sorted = false
+1 -1
View File
@@ -141,7 +141,7 @@ func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData
webhookURIs := app.config.Section("webhooks").Key("created").StringsWithShadows("|") webhookURIs := app.config.Section("webhooks").Key("created").StringsWithShadows("|")
if len(webhookURIs) != 0 { if len(webhookURIs) != 0 {
summary := app.userSummary(out.User) summary := app.userSummary(out.User, nil)
for _, uri := range webhookURIs { for _, uri := range webhookURIs {
pendingTasks.Add(1) pendingTasks.Add(1)
go func() { go func() {