2012-01-18

Zmiana systemu cachowania

Zadaniem cachowania jest przetrzymywanie w pamięci załadowanych zasobów tak długo jak są używane i przez pewien czas jak już nie są używane. Tak, że podczas ponownego żądania ich użycia nie musimy tracić czasu na ich załadowanie. Kiedy np. dwa materiały proszą o taką samą bitmapę z pliku to dostają zawsze taką samą z cachu. Dla bitmap ładowanych z pliku zakładamy, że nie można ich modyfikować.

Rozmiar pamięci cache jest określony sztywno z góry, jest to też rozmiar pamięci jaki możemy przeznaczyć na tekstury, mapy normalnych, mapowanie wypukłości itp. Kiedy kolejne ładowanie z pliku doprowadziło do przekroczenia rozmiaru cachu poszukujemy w nim nieużywanych wpisów i zwalniamy je. Jeśli mimo to przekroczyliśmy rozmiar cache raportowany jest błąd. Po zakończeniu renderowania obiekty zwalniają zasoby pobrane z cache i raportują CacheManager o zakończeniu ich używania. Jeśli od zasobu w cache odłączyli się wszyscy jego użytkownicy jest on wolny. Taki zasobów zwalniamy natychmiast kiedy brakuje nam pamięci, albo po określonym czasie nieużywania.

Próbowałem także zaimplementować rozwiązanie, które w takiej sytuacji wyładowało by zasób obecnie używany ale najdawniej użyty. Sprawa niby atrakcyjna okazała się kłopotliwa. Taki zasób trzeba synchronizować przy każdym użyciu. Każde pobranie koloru dla piksela. Nie da się tego przeskoczyć. Idealnie chcemy takiego efektu: wiele wątków renderujących uzyskuje dostęp równoległy do danych. Ale jeśli CacheManager oznaczył obiekt do zwolnienia to zakłada swoją blokadę i czeka aż wątki robocze go opuszczą. Wtedy go zwalnia. Jeśli w tym momencie wątek roboczy próbuje użyć zasobu musi on czekać na zwolnienie go przez CacheManager, zajmuje go swoją blokadą (dla wątków roboczych) i sprawdza (nie ma go, więc ponownie ładuje). Potrzebujemy dwóch obiektów do synchronizacji. Dałem sobie z tym spokój.

Wszystkie zasoby są ładowane w momencie startu renderowania.

Limit pamięci później będzie można rozbudować o bardziej mądre, agresywne zajmowanie wolnej pamięci.

Od razu odrzucam korzystanie z weak references, one się do tego nie nadają. Po przejściu obiekt w stan niewykorzystywania (tylko słaba referencja na niego wskazuje) jest on bardzo szybko zwalniany przez GC.

Nasz system cache musi mieć w osobnym wątku zaimplementowaną logikę zwalniającą pliki nieużywane przez dłuższy czas - niezależnie od tego czy pamięć jest, czy jej nie ma.

System cache ma mieć charakter globalny. Powinien być całkowicie oderwany od konkretnej sceny.

W przyszłości dodane zostaną mip-mapy. Warto to mieć na uwadze.

Obecny system cache zostanie przerobiony tak by spełniał powyższe wymagania.

Jeden obiekt może być związany tylko z jednym wpisem w cache tego samego typu. To ograniczenie wyszło podczas pisania i nie wydaje mi się, by była potrzeba jego rewidowania.

System cache w stosunku do poprzedniego stał się bardziej uniwersalny. Obiekt który chce być cachowany musi zaimplementować interfejs ICacheable.

Pewna dodatkowa logika była potrzebna ba zabezpieczenie się przed podwójnym ładowaniem tego samego zasobu. W takim wypadku jeden wątek ładuje zasób, drugi (reszta) czeka na zakończenie operacji. Sytuacja taka może się zdarzyć jeśli więcej wątków próbuje pobrać zasób jednocześnie.

Kod interfejsu ICacheable:

public interface ICacheable
{
    int MemoryUsage { get; }
    object Data { get; }
    string FileName { get; }
}

Kod menadżera pamięci tymczasowej CacheManager:

public class CacheManager
{
    #region CacheEntry
    private class CacheEntry
    {
        public object Data;
        public int MemoryUsage;
        public string FileName;
        public Type DataType;
        public DateTime ReleasedTime;
        public List<ICacheable> Users = new List<ICacheable>();

        public bool ContainsUser(ICacheable a_user)
        {
            for (int i = 0; i < Users.Count; i++)
            {
                if (Object.ReferenceEquals(Users[i], a_user))
                    return true;
            }

            return false;
        }
    }
    #endregion

    public static long CACHE_SIZE = 1024 * 1024 * 1024;
    public static TimeSpan NOT_USED_PERIOD = new TimeSpan(0, 0, seconds: 10);
    public static int SLEEP_PERIOD_MS = 1000;

    private static long s_memory_usage;

    private static List<CacheEntry> s_cache = new List<CacheEntry>();
    private static List<CacheEntry> s_loading = new List<CacheEntry>();

    private static Logger Logger = LogManager.GetLogger("Cache");

    static CacheManager()
    {
        Task.Factory.StartNew(() => 
        {
            for (; ; )
            {
                Thread.Sleep(SLEEP_PERIOD_MS);

                lock (s_cache)
                {
                    for (int i = s_cache.Count - 1; i >= 0; i--)
                    {
                        CacheEntry ce = s_cache[i];

                        if (ce.Users.Count != 0)
                            continue;

                        if (DateTime.Now - ce.ReleasedTime > NOT_USED_PERIOD)
                        {
                            s_cache.RemoveAt(i);
                            Interlocked.Add(ref s_memory_usage, -ce.MemoryUsage);

                            Logger.Info("Release not used data for: {0}", 
                                ce.FileName);

                            GC.Collect();
                        }
                    }
                }
            }
        });
    }

    public static long MemoryUsage
    {
        get
        {
            return s_memory_usage;
        }
    }

    private static CacheEntry FindEntry(ICacheable a_user)
    {
        lock (s_cache)
        {
            foreach (var ce in s_cache)
            {
                if (Object.ReferenceEquals(ce.Data, a_user.Data))
                {
                    Debug.Assert(ce.Data != null);
                    Debug.Assert(ce.ContainsUser(a_user));

                    return ce;
                }
            }

            return null;
        }
    }

    public static T GetOrLoad<T>(ICacheable a_user, Action a_loader) where T : class
    {
        CacheEntry nce = null;

        lock (s_cache)
        {
            foreach (var ce in s_cache)
            {
                if (ce.FileName != a_user.FileName)
                    continue;

                if (!(ce.Data is T))
                    continue;

                nce = ce;
                break;
            }

            if (nce == null)
            {
                lock (s_loading)
                {
                    foreach (var ce in s_loading)
                    {
                        if (ce.FileName != a_user.FileName)
                            continue;

                        if (!(ce.Data is T))
                            continue;

                        nce = ce;
                        break;
                    }


                    if (nce == null)
                    {
                        nce = new CacheEntry();

                        nce.DataType = typeof(T);
                        nce.FileName = a_user.FileName;

                        s_loading.Add(nce);
                    }
                }
            }
        }

        lock (nce)
        {
            if (nce.Data != null)
            {
                Logger.Info("Cached data finded: {0}", nce.FileName);
                Debug.Assert(!nce.ContainsUser(a_user));
                nce.Users.Add(a_user);

                return (T)nce.Data;
            }
            else
            {
                Logger.Info("Adding to cache: {0}", a_user.FileName);

                a_loader();

                nce.Data = a_user.Data;
                nce.MemoryUsage = a_user.MemoryUsage;

                Debug.Assert(!nce.ContainsUser(a_user));
                nce.Users.Add(a_user);

                Debug.Assert(nce.Data != null);
                Debug.Assert(!String.IsNullOrWhiteSpace(nce.FileName));
                Debug.Assert(nce.DataType == nce.Data.GetType());
                Debug.Assert(nce.MemoryUsage > 0);

                while (nce.MemoryUsage + MemoryUsage > CACHE_SIZE)
                {
                    Logger.Info("Memory usage exceeded: {0} > {1}", 
                        nce.MemoryUsage + MemoryUsage, CACHE_SIZE);

                    if (!RemoveEntry())
                        throw new OutOfMemoryException("Increase cache size");

                    if (MemoryUsage == 0)
                        break;
                }

                Interlocked.Add(ref s_memory_usage, nce.MemoryUsage);

                lock (s_cache)
                {
                    s_cache.Add(nce);
                }

                lock (s_loading)
                {
                    s_loading.Remove(nce);
                }

                return (T)nce.Data;
            }
        }
    }

    private static bool RemoveEntry()
    {
        CacheEntry ce = null;

        lock (s_cache)
        {
            DateTime last_usage_time = new DateTime(0);

            foreach (var c in s_cache)
            {
                if (c.Users.Count != 0)
                    continue;

                if (c.ReleasedTime >= last_usage_time)
                {
                    last_usage_time = c.ReleasedTime;
                    ce = c;
                }
            }
            
            if (ce == null)
                return false;

            Logger.Info("Releasing not used cache data: {0}", ce.FileName);

            s_cache.Remove(ce);
            Interlocked.Add(ref s_memory_usage, -ce.MemoryUsage);
            return true;
        }
    }

    public static void Unbind(ICacheable a_user)
    {
        Logger.Info("Unbinding: {0}", a_user.FileName);

        lock (s_cache)
        {
            CacheEntry ce = FindEntry(a_user);

            Debug.Assert(ce != null);
            Debug.Assert(ce.ContainsUser(a_user));

            for (int i = 0; i < ce.Users.Count; i++)
            {
                if (Object.ReferenceEquals(ce.Users[i], a_user))
                {
                    ce.Users.RemoveAt(i);
                    break;
                }
            }

            if (ce.Users.Count == 0)
                ce.ReleasedTime = DateTime.Now;
        }
    }
}

Wybrane fragmenty z obiektu implementującego interfejs ICacheable:

Brak komentarzy:

Prześlij komentarz