Fix #227: add limit to on-disk image cache size, periodic pruning

This commit is contained in:
Drew Weymouth
2023-07-28 18:53:45 -07:00
parent 09fcc6dcb8
commit f63fcc14ff
3 changed files with 85 additions and 17 deletions
+2
View File
@@ -103,6 +103,8 @@ func StartupApp(appName, displayAppName, appVersionTag, configFile, latestReleas
a.ServerManager = NewServerManager(appName, a.Config)
a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.Player, &a.Config.Scrobbling)
a.ImageManager = NewImageManager(a.bgrndCtx, a.ServerManager, configdir.LocalCache(a.appName))
a.Config.Application.MaxImageCacheSizeMB = clamp(a.Config.Application.MaxImageCacheSizeMB, 1, 500)
a.ImageManager.SetMaxOnDiskCacheSizeBytes(int64(a.Config.Application.MaxImageCacheSizeMB) * 1_048_576)
a.ServerManager.SetPrefetchAlbumCoverCallback(func(coverID string) {
_, _ = a.ImageManager.GetCoverThumbnail(coverID)
})
+18 -16
View File
@@ -22,14 +22,15 @@ type ServerConfig struct {
}
type AppConfig struct {
WindowWidth int
WindowHeight int
LastCheckedVersion string
EnableSystemTray bool
CloseToSystemTray bool
StartupPage string
SettingsTab string
AllowMultiInstance bool
WindowWidth int
WindowHeight int
LastCheckedVersion string
EnableSystemTray bool
CloseToSystemTray bool
StartupPage string
SettingsTab string
AllowMultiInstance bool
MaxImageCacheSizeMB int
// Experimental - may be removed in future
FontNormalTTF string
@@ -119,14 +120,15 @@ var SupportedStartupPages = []string{"Albums", "Favorites", "Playlists"}
func DefaultConfig(appVersionTag string) *Config {
return &Config{
Application: AppConfig{
WindowWidth: 1000,
WindowHeight: 800,
LastCheckedVersion: appVersionTag,
EnableSystemTray: true,
CloseToSystemTray: false,
StartupPage: "Albums",
SettingsTab: "General",
AllowMultiInstance: false,
WindowWidth: 1000,
WindowHeight: 800,
LastCheckedVersion: appVersionTag,
EnableSystemTray: true,
CloseToSystemTray: false,
StartupPage: "Albums",
SettingsTab: "General",
AllowMultiInstance: false,
MaxImageCacheSizeMB: 50,
},
AlbumPage: AlbumPageConfig{
TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite", "Rating"},
+65 -1
View File
@@ -7,10 +7,13 @@ import (
"fmt"
"image"
"image/jpeg"
"io/fs"
"log"
"os"
"path"
"path/filepath"
"sort"
"strings"
"time"
"fyne.io/fyne/v2"
@@ -23,8 +26,13 @@ const CachedImageValidTime = 24 * time.Hour
const (
coverArtThumbnailSize = 300
fullSizeCoverExpires = 5 * time.Minute
defaultDiskCacheSizeBytes = 50 * 1_048_576
)
// The ImageManager is responsible for retrieving and serving images to the UI layer.
// It maintains an in-memory cache of recently used images for immediate future access,
// and a larger on-disc cache of images that is periodically re-requested from the server.
type ImageManager struct {
s *ServerManager
baseCacheDir string
@@ -33,6 +41,9 @@ type ImageManager struct {
cachedFullSizeCover image.Image
cachedFullSizeCoverID string
cachedFullSizeCoverAccessedAt int64 // unixMillis
maxOnDiskCacheSizeBytes int64
filesWrittenSinceLastPrune bool
}
func NewImageManager(ctx context.Context, s *ServerManager, baseCacheDir string) *ImageManager {
@@ -48,16 +59,24 @@ func NewImageManager(ctx context.Context, s *ServerManager, baseCacheDir string)
MaxSize: 150,
DefaultTTL: 1 * time.Minute,
},
maxOnDiskCacheSizeBytes: defaultDiskCacheSizeBytes,
}
s.OnLogout(func() {
i.thumbnailCache.Clear()
i.clearFullSizeCover()
})
i.thumbnailCache.OnEvictTaskRan = i.clearFullSizeCoverIfExpired
i.thumbnailCache.OnEvictTaskRan = func() {
i.clearFullSizeCoverIfExpired()
i.pruneOnDiskCache()
}
i.thumbnailCache.Init(ctx, 2*time.Minute)
return i
}
func (i *ImageManager) SetMaxOnDiskCacheSizeBytes(size int64) {
i.maxOnDiskCacheSizeBytes = size
}
func (i *ImageManager) GetCoverThumbnailFromCache(coverID string) (image.Image, bool) {
img, err := i.thumbnailCache.GetExtendTTL(coverID, i.thumbnailCache.DefaultTTL)
if err == nil && img != nil {
@@ -214,6 +233,7 @@ func (i *ImageManager) writeJpeg(img image.Image, path string) error {
return err
}
}
i.filesWrittenSinceLastPrune = true
return err
}
@@ -238,3 +258,47 @@ func (i *ImageManager) clearFullSizeCover() {
i.cachedFullSizeCoverID = ""
i.cachedFullSizeCover = nil
}
func (im *ImageManager) pruneOnDiskCache() {
if !im.filesWrittenSinceLastPrune {
return // no new covers cached since last run, no need to walk dir
}
// collect list of all cached covers (across servers)
// we use modTime as a proxy for last accessed time
// since covers are refreshed from the server after a fixed interval,
// modTime is roughly equivalent to last access
type fileInfo struct {
path string
size int64
modTime int64
}
var allCovers []fileInfo
var totalSize int64
filepath.WalkDir(im.baseCacheDir, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() || !strings.HasSuffix(path, "jpg") {
return nil
}
if info, err := d.Info(); err == nil {
s := info.Size()
allCovers = append(allCovers,
fileInfo{path: path, size: s, modTime: info.ModTime().UnixMilli()})
totalSize += s
}
return nil
})
if totalSize > im.maxOnDiskCacheSizeBytes {
// sort and then delete from least recently modified until size is under threshold
sort.Slice(allCovers, func(i, j int) bool {
return allCovers[i].modTime < allCovers[j].modTime
})
for i := 0; i < len(allCovers) && totalSize > im.maxOnDiskCacheSizeBytes; i++ {
if err := os.Remove(allCovers[i].path); err == nil {
totalSize -= allCovers[i].size
}
}
}
im.filesWrittenSinceLastPrune = false
}