navidrome/core/artwork/benchmark_e2e_test.go
Deluan 2a43c4683e chore: go fix
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-28 22:13:05 -03:00

190 lines
4.8 KiB
Go

package artwork
import (
"context"
"fmt"
"image/jpeg"
"io"
"os"
"path/filepath"
"runtime"
"sync"
"testing"
"time"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/tests"
"github.com/navidrome/navidrome/utils/cache"
)
// setupE2EBenchmark creates an artwork instance with a real album cover image on disk,
// backed by either a real file cache or disabled cache depending on cacheSize.
// Note: This benchmarks artwork.Get() directly (not the full HTTP handler), which covers
// the critical path (source selection, decode, resize, encode, cache). This is a deliberate
// spec deviation — the full HTTP round-trip benchmark requires significant infrastructure
// (DB, scanner, fake filesystem) and can be added later if HTTP overhead proves significant.
//
// Depends on fakeFolderRepo defined in reader_artist_test.go (same package, compiled together).
func setupE2EBenchmark(b *testing.B, cacheSize string) (Artwork, model.ArtworkID, func()) {
b.Helper()
cleanup := configtest.SetupConfig()
b.Cleanup(cleanup)
tmpDir, err := os.MkdirTemp("", "artwork-bench-*")
if err != nil {
b.Fatal(err)
}
// Create a realistic cover image on disk
coverPath := filepath.Join(tmpDir, "cover.jpg")
coverImg := generateGradientImage(1000, 1000)
f, err := os.Create(coverPath)
if err != nil {
b.Fatal(err)
}
if err := jpeg.Encode(f, coverImg, &jpeg.Options{Quality: 90}); err != nil {
f.Close()
b.Fatal(err)
}
f.Close()
// Configure cache
conf.Server.ImageCacheSize = cacheSize
conf.Server.CacheFolder = conf.NewDir(tmpDir)
conf.Server.CoverArtQuality = 75
conf.Server.CoverArtPriority = "cover.*"
// Set up mock data store with album pointing to our cover.
// Set UpdatedAt so CoverArtID().LastUpdate is consistent across calls.
album := model.Album{
ID: "bench-album-1",
Name: "Benchmark Album",
FolderIDs: []string{"f1"},
UpdatedAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
}
folderRepo := &fakeFolderRepo{
result: []model.Folder{{
Path: tmpDir,
ImageFiles: []string{"cover.jpg"},
}},
}
ds := &tests.MockDataStore{
MockedTranscoding: &tests.MockTranscodingRepo{},
MockedFolder: folderRepo,
}
ds.Album(context.Background()).(*tests.MockAlbumRepo).SetData(model.Albums{album})
artID := album.CoverArtID()
imgCache := cache.NewFileCache("BenchImage", cacheSize, "bench-images", 0,
func(ctx context.Context, arg cache.Item) (io.Reader, error) {
r, _, err := arg.(artworkReader).Reader(ctx)
return r, err
})
// Wait for cache init if enabled
if cacheSize != "0" {
for !imgCache.Available(context.Background()) && !imgCache.Disabled(context.Background()) {
runtime.Gosched() // Yield to allow background init goroutine to run
}
}
ffmpeg := tests.NewMockFFmpeg("fallback content")
aw := NewArtwork(ds, imgCache, ffmpeg, nil)
cleanupAll := func() {
os.RemoveAll(tmpDir)
}
return aw, artID, cleanupAll
}
func BenchmarkArtworkGetE2E(b *testing.B) {
cacheConfigs := []struct {
name string
cacheSize string
}{
{"no_cache", "0"},
{"with_cache", "100MB"},
}
sizes := []int{0, 300}
for _, cc := range cacheConfigs {
for _, size := range sizes {
b.Run(fmt.Sprintf("%s/size_%d", cc.name, size), func(b *testing.B) {
aw, artID, cleanup := setupE2EBenchmark(b, cc.cacheSize)
defer cleanup()
// Warm the cache on first call if cache is enabled
if cc.cacheSize != "0" {
r, _, err := aw.Get(context.Background(), artID, size, size > 0)
if err != nil {
b.Fatal(err)
}
_, _ = io.ReadAll(r)
r.Close()
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
r, _, err := aw.Get(context.Background(), artID, size, size > 0)
if err != nil {
b.Fatal(err)
}
_, _ = io.ReadAll(r)
r.Close()
}
})
}
}
}
func BenchmarkArtworkGetE2EConcurrent(b *testing.B) {
cacheConfigs := []struct {
name string
cacheSize string
}{
{"no_cache", "0"},
{"with_cache", "100MB"},
}
concurrencyLevels := []int{10, 50}
for _, cc := range cacheConfigs {
for _, n := range concurrencyLevels {
b.Run(fmt.Sprintf("%s/goroutines_%d", cc.name, n), func(b *testing.B) {
aw, artID, cleanup := setupE2EBenchmark(b, cc.cacheSize)
defer cleanup()
// Warm cache
if cc.cacheSize != "0" {
r, _, _ := aw.Get(context.Background(), artID, 300, true)
if r != nil {
_, _ = io.ReadAll(r)
r.Close()
}
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
var wg sync.WaitGroup
wg.Add(n)
for range n {
go func() {
defer wg.Done()
r, _, err := aw.Get(context.Background(), artID, 300, true)
if err != nil {
b.Error(err)
return
}
_, _ = io.ReadAll(r)
r.Close()
}()
}
wg.Wait()
}
})
}
}
}