Files
allstarr/MEMORY_OPTIMIZATION_RECOMMENDATIONS.md

5.8 KiB

Memory Optimization Recommendations for Allstarr

Current Implementation Status

COMPLETED: Mark-for-deletion system with 24-hour delay COMPLETED: Persistent favorites tracking using JSON files COMPLETED: Cache-first copying for favorites (avoids re-downloads) COMPLETED: Dependency injection for CacheCleanupService to process pending deletions

Memory Optimization Strategies

1. Collection Optimizations

Current Issues:

  • Multiple List<Song>, List<Album>, List<Artist> collections created during searches
  • Large Dictionary<string, object?> objects for Jellyfin metadata
  • Concurrent collections like ConcurrentDictionary<string, SessionInfo> for sessions

Recommendations:

// Use ArrayPool for temporary collections
private static readonly ArrayPool<Song> SongArrayPool = ArrayPool<Song>.Shared;

// Use Span<T> for temporary operations
ReadOnlySpan<Song> ProcessSongs(ReadOnlySpan<Song> songs) { ... }

// Use IAsyncEnumerable for streaming large results
IAsyncEnumerable<Song> SearchSongsStreamAsync(string query);

2. JSON Serialization Optimizations

Current Issues:

  • Heavy use of JsonSerializer.Deserialize<Dictionary<string, object?>>()
  • Multiple serialization/deserialization cycles for caching

Recommendations:

// Use System.Text.Json source generators for better performance
[JsonSerializable(typeof(List<Song>))]
[JsonSerializable(typeof(Dictionary<string, FavoriteTrackInfo>))]
public partial class AllstarrJsonContext : JsonSerializerContext { }

// Use JsonDocument for read-only scenarios instead of Dictionary
JsonDocument.Parse(json).RootElement.GetProperty("Items")

3. Caching Strategy Improvements

Current Issues:

  • File-based caching creates multiple copies of data
  • Redis and file caches can contain duplicate data

Recommendations:

// Implement cache eviction policies
public class LRUCache<TKey, TValue> where TKey : notnull
{
    private readonly int _maxSize;
    private readonly Dictionary<TKey, LinkedListNode<CacheItem>> _cache;
    private readonly LinkedList<CacheItem> _lruList;
}

// Use weak references for large objects
private readonly WeakReference<List<Song>> _cachedSongs = new(null);

4. String Interning and Optimization

Current Issues:

  • Many duplicate strings (artist names, album titles) across collections
  • Path strings created repeatedly

Recommendations:

// Use string interning for common values
private static readonly ConcurrentDictionary<string, string> InternedStrings = new();
public static string Intern(string value) => InternedStrings.GetOrAdd(value, v => v);

// Use StringBuilder for path construction
private static readonly ThreadLocal<StringBuilder> PathBuilder = 
    new(() => new StringBuilder(256));

5. Background Service Optimizations

Current Issues:

  • Multiple background services running simultaneously
  • Potential memory leaks in long-running services

Recommendations:

// Implement proper disposal patterns
public class CacheCleanupService : BackgroundService, IDisposable
{
    private readonly SemaphoreSlim _semaphore = new(1, 1);
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var _ = await _semaphore.WaitAsync(stoppingToken);
        // ... cleanup logic
    }
    
    public override void Dispose()
    {
        _semaphore?.Dispose();
        base.Dispose();
    }
}

6. HTTP Client Optimizations

Current Issues:

  • Multiple HTTP clients for different services
  • Large response buffers

Recommendations:

// Use HttpClientFactory with proper configuration
builder.Services.AddHttpClient<DeezerMetadataService>(client =>
{
    client.DefaultRequestHeaders.Add("User-Agent", "Allstarr/1.0");
    client.Timeout = TimeSpan.FromSeconds(30);
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
    MaxConnectionsPerServer = 10,
    PooledConnectionLifetime = TimeSpan.FromMinutes(15)
});

// Stream large responses instead of loading into memory
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);

7. Container Memory Limits

Docker Configuration:

# Set memory limits in docker-compose.yml
services:
  allstarr:
    deploy:
      resources:
        limits:
          memory: 512M
        reservations:
          memory: 256M

Runtime Configuration:

// Configure GC for container environments
GCSettings.LatencyMode = GCLatencyMode.Batch;
GC.Collect(2, GCCollectionMode.Optimized);

Immediate Actions (Priority Order)

  1. Enable GC monitoring - Add memory usage logging to identify hotspots
  2. Implement cache size limits - Prevent unbounded growth of in-memory caches
  3. Use object pooling - For frequently allocated objects like Song/Album/Artist
  4. Stream large responses - Instead of loading entire JSON responses into memory
  5. Optimize JSON serialization - Use source generators and reduce Dictionary usage

Monitoring Recommendations

// Add memory monitoring to Program.cs
builder.Services.AddHostedService<MemoryMonitoringService>();

public class MemoryMonitoringService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var memoryUsage = GC.GetTotalMemory(false);
            var gen0 = GC.CollectionCount(0);
            var gen1 = GC.CollectionCount(1);
            var gen2 = GC.CollectionCount(2);
            
            _logger.LogInformation("Memory: {Memory:N0} bytes, GC: Gen0={Gen0}, Gen1={Gen1}, Gen2={Gen2}",
                memoryUsage, gen0, gen1, gen2);
                
            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}