🏡 index : ironmagma/quintodrome.git

package cache

import (
	"context"
	"fmt"
	"io"
	"os"
	"runtime"
	"strings"
	"sync"
	"testing"

	"github.com/navidrome/navidrome/conf"
	"github.com/navidrome/navidrome/conf/configtest"
)

type benchItem struct {
	key string
}

func (b *benchItem) Key() string { return b.key }

// setupBenchCache creates a file cache in a temp directory. Returns the cache and cleanup function.
func setupBenchCache(b *testing.B, cacheSize string, getReader ReadFunc) (*fileCache, func()) {
	b.Helper()
	tmpDir, err := os.MkdirTemp("", "bench-cache-*")
	if err != nil {
		b.Fatal(err)
	}
	b.Cleanup(configtest.SetupConfig())
	conf.Server.CacheFolder = conf.NewDir(tmpDir)

	fc := NewFileCache("bench", cacheSize, "bench", 0, getReader).(*fileCache)

	// Wait for cache to be ready
	for !fc.ready.Load() {
		runtime.Gosched() // Yield to allow background init goroutine to run
	}

	teardown := func() {
		os.RemoveAll(tmpDir)
	}
	return fc, teardown
}

func BenchmarkCacheWrite(b *testing.B) {
	// Simulate writing 50KB images (typical 300px JPEG)
	imageData := strings.Repeat("x", 50*1024)

	fc, cleanup := setupBenchCache(b, "100MB", func(ctx context.Context, item Item) (io.Reader, error) {
		return strings.NewReader(imageData), nil
	})
	defer cleanup()

	b.SetBytes(int64(len(imageData)))
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		key := fmt.Sprintf("write-bench-%d", i)
		s, err := fc.Get(context.Background(), &benchItem{key: key})
		if err != nil {
			b.Fatal(err)
		}
		_, _ = io.ReadAll(s)
		s.Close()
	}
}

func BenchmarkCacheRead(b *testing.B) {
	imageData := strings.Repeat("x", 50*1024)

	fc, cleanup := setupBenchCache(b, "100MB", func(ctx context.Context, item Item) (io.Reader, error) {
		return strings.NewReader(imageData), nil
	})
	defer cleanup()

	// Pre-populate cache
	item := &benchItem{key: "read-bench"}
	s, err := fc.Get(context.Background(), item)
	if err != nil {
		b.Fatal(err)
	}
	_, _ = io.ReadAll(s)
	s.Close()

	b.SetBytes(int64(len(imageData)))
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		s, err := fc.Get(context.Background(), item)
		if err != nil {
			b.Fatal(err)
		}
		_, _ = io.ReadAll(s)
		s.Close()
	}
}

func BenchmarkConcurrentCacheRead(b *testing.B) {
	imageData := strings.Repeat("x", 50*1024)

	fc, cleanup := setupBenchCache(b, "100MB", func(ctx context.Context, item Item) (io.Reader, error) {
		return strings.NewReader(imageData), nil
	})
	defer cleanup()

	// Pre-populate cache
	item := &benchItem{key: "concurrent-read"}
	s, _ := fc.Get(context.Background(), item)
	_, _ = io.ReadAll(s)
	s.Close()

	concurrencyLevels := []int{1, 10, 50}
	for _, n := range concurrencyLevels {
		b.Run(fmt.Sprintf("goroutines_%d", n), func(b *testing.B) {
			b.SetBytes(int64(len(imageData)))
			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				var wg sync.WaitGroup
				wg.Add(n)
				for range n {
					go func() {
						defer wg.Done()
						s, err := fc.Get(context.Background(), item)
						if err != nil {
							b.Error(err)
							return
						}
						_, _ = io.ReadAll(s)
						s.Close()
					}()
				}
				wg.Wait()
			}
		})
	}
}

func BenchmarkConcurrentCacheMiss(b *testing.B) {
	imageData := strings.Repeat("x", 50*1024)

	concurrencyLevels := []int{1, 10, 50}
	for _, n := range concurrencyLevels {
		b.Run(fmt.Sprintf("goroutines_%d", n), func(b *testing.B) {
			fc, cleanup := setupBenchCache(b, "100MB", func(ctx context.Context, item Item) (io.Reader, error) {
				return strings.NewReader(imageData), nil
			})
			defer cleanup()

			b.SetBytes(int64(len(imageData)))
			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				var wg sync.WaitGroup
				wg.Add(n)
				// All goroutines request the SAME key (not yet cached)
				item := &benchItem{key: fmt.Sprintf("miss-%d", i)}
				for range n {
					go func() {
						defer wg.Done()
						s, err := fc.Get(context.Background(), item)
						if err != nil {
							b.Error(err)
							return
						}
						_, _ = io.ReadAll(s)
						s.Close()
					}()
				}
				wg.Wait()
			}
		})
	}
}