< Summary

Class:DCL.AssetPromiseKeeper[AssetType,AssetLibraryType,AssetPromiseType]
Assembly:AssetPromiseKeeper
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/AssetManager/Common/AssetPromiseKeeper.cs
Covered lines:147
Uncovered lines:15
Coverable lines:162
Total lines:370
Line coverage:90.7% (147 of 162)
Covered branches:0
Total branches:0
Covered methods:17
Total methods:19
Method coverage:89.4% (17 of 19)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
AssetPromiseKeeper(...)0%110100%
IsBlocked(...)0%2100%
GetMasterState(...)0%20400%
EnsureProcessBlockerPromiseQueueTask()0%220100%
Keep(...)0%8.048091.3%
Forget(...)0%770100%
OnRequestCompleted(...)0%330100%
IsToResolveQueueEmpty()0%110100%
ProcessBlockedPromisesQueueAsync()0%880100%
ProcessBlockedPromisesDeferredAsync()0%17.7716080.95%
SkipFrameIfOverBudgetAsync()0%550100%
GetBlockedPromisesToLoadForId(...)0%440100%
ForceFailPromiseListAsync()0%550100%
LoadPromisesListAsync()0%550100%
CleanPromise(...)0%880100%
Cleanup()0%550100%
OnSilentForget(...)0%110100%

File(s)

/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/AssetManager/Common/AssetPromiseKeeper.cs

#LineLine coverage
 1using Cysharp.Threading.Tasks;
 2using System;
 3using System.Collections.Generic;
 4using System.Threading;
 5using UnityEngine;
 6
 7namespace DCL
 8{
 9    public class AssetPromiseKeeper
 10    {
 11        public static float PROCESS_PROMISES_TIME_BUDGET = 0.006f;
 12    }
 13
 14    /// <summary>
 15    /// The AssetPromiseKeeper is the user entry point interface of the asset management system.
 16    /// It manages stuff like requesting something that's already being loaded, etc.
 17    ///
 18    /// It also handles the special cases of promises that depend on another to be completed (blocked promise)
 19    /// </summary>
 20    /// <typeparam name="AssetType">Asset type to be handled</typeparam>
 21    /// <typeparam name="AssetLibraryType">Asset library type. It must handle the same type as AssetType.</typeparam>
 22    /// <typeparam name="AssetPromiseType">Asset promise type. It must handle the same type as AssetType.</typeparam>
 23    public class AssetPromiseKeeper<AssetType, AssetLibraryType, AssetPromiseType>
 24        where AssetType: Asset, new()
 25        where AssetLibraryType: AssetLibrary<AssetType>, new()
 26        where AssetPromiseType: AssetPromise<AssetType>
 27    {
 28        public AssetLibraryType library;
 29
 30        //NOTE(Brian): All waiting promises. Only used for cleanup and to keep count.
 14431        HashSet<AssetPromiseType> waitingPromises = new HashSet<AssetPromiseType>();
 2332        public int waitingPromisesCount => waitingPromises.Count;
 33
 34        //NOTE(Brian): List of promises waiting for assets not in library.
 14435        protected Dictionary<object, AssetPromiseType> masterPromiseById = new Dictionary<object, AssetPromiseType>(100)
 36
 37        //NOTE(Brian): List of promises waiting for assets that are currently being loaded by another promise.
 14438        HashSet<AssetPromiseType> blockedPromises = new HashSet<AssetPromiseType>();
 39
 40        //NOTE(Brian): Master promise id -> blocked promises HashSet
 14441        protected Dictionary<object, HashSet<AssetPromiseType>> masterToBlockedPromises = new Dictionary<object, HashSet
 42
 14743        public bool useTimeBudget => CommonScriptableObjects.rendererState.Get();
 44
 45        float startTime;
 46
 47        private CancellationTokenSource blockedPromiseSolverCancellationTokenSource;
 48
 49        public bool IsBlocked(AssetPromiseType promise)
 50        {
 051            return blockedPromises.Contains(promise);
 52        }
 53
 54        public string GetMasterState(AssetPromiseType promise)
 55        {
 056            object promiseId = promise.GetId();
 57
 058            if (!masterToBlockedPromises.ContainsKey(promiseId))
 059                return "Master not found";
 60
 061            if (!masterToBlockedPromises[promiseId].Contains(promise))
 062                return "Promise is not blocked???";
 63
 064            if (!masterPromiseById.ContainsKey(promiseId))
 065                return "not registered as master?";
 66
 067            return $"master state = {masterPromiseById[promiseId].state}";
 68        }
 69
 14470        public AssetPromiseKeeper(AssetLibraryType library)
 71        {
 14472            this.library = library;
 14473            EnsureProcessBlockerPromiseQueueTask();
 14474        }
 75
 76        private void EnsureProcessBlockerPromiseQueueTask()
 77        {
 25778            if (blockedPromiseSolverCancellationTokenSource == null)
 79            {
 14880                blockedPromiseSolverCancellationTokenSource = new CancellationTokenSource();
 14881                ProcessBlockedPromisesQueueAsync(blockedPromiseSolverCancellationTokenSource.Token).Forget();
 82            }
 25783        }
 84
 85        public AssetPromiseType Keep(AssetPromiseType promise)
 86        {
 72687            if (promise == null || promise.state != AssetPromiseState.IDLE_AND_EMPTY || waitingPromises.Contains(promise
 188                return promise;
 89
 72590            object id = promise.GetId();
 91
 72592            if (id == null)
 93            {
 094                Debug.LogError("ERROR: ID == null. Promise is not set up correctly.");
 095                return promise;
 96            }
 97
 72598            promise.isDirty = true;
 99
 100            //NOTE(Brian): We already have a master promise for this id, add to blocked list.
 725101            if (masterPromiseById.ContainsKey(id))
 102            {
 103                // TODO(Brian): Remove once this class has a proper lifecycle and is on service locator.
 113104                EnsureProcessBlockerPromiseQueueTask();
 113105                waitingPromises.Add(promise);
 106
 113107                if (!masterToBlockedPromises.ContainsKey(id))
 24108                    masterToBlockedPromises.Add(id, new HashSet<AssetPromiseType>());
 109
 113110                masterToBlockedPromises[id].Add(promise);
 111
 113112                blockedPromises.Add(promise);
 113113                promise.SetWaitingState();
 113114                return promise;
 115            }
 116
 117            // NOTE(Brian): Not in library, add to corresponding lists...
 612118            if (!library.Contains(id))
 119            {
 535120                waitingPromises.Add(promise);
 535121                masterPromiseById.Add(id, promise);
 122            }
 123
 612124            promise.library = library;
 612125            promise.OnPreFinishEvent += OnRequestCompleted;
 612126            promise.Load();
 127
 612128            return promise;
 129        }
 130
 131        public virtual AssetPromiseType Forget(AssetPromiseType promise)
 132        {
 3686133            if (promise == null)
 3142134                return null;
 135
 544136            if (promise.state == AssetPromiseState.IDLE_AND_EMPTY || promise.state == AssetPromiseState.WAITING)
 137            {
 112138                CleanPromise(promise);
 112139                promise.OnForget();
 112140                return promise;
 141            }
 142
 432143            object id = promise.GetId();
 144
 432145            bool isMasterPromise = masterPromiseById.ContainsKey(id) && masterPromiseById[id] == promise;
 432146            bool hasBlockedPromises = masterToBlockedPromises.ContainsKey(id) && masterToBlockedPromises[id].Count > 0;
 147
 432148            if (isMasterPromise && hasBlockedPromises)
 149            {
 150                //NOTE(Brian): Pending promises are waiting for this one.
 151                //             We clear the events because we shouldn't call them, as this promise is forgotten.
 6152                OnSilentForget(promise);
 6153                promise.OnForget();
 6154                return promise;
 155            }
 156
 426157            promise.Unload();
 426158            CleanPromise(promise);
 426159            promise.OnForget();
 160
 426161            return promise;
 162        }
 163
 144164        Queue<AssetPromiseType> toResolveBlockedPromisesQueue = new Queue<AssetPromiseType>();
 165
 166        private void OnRequestCompleted(AssetPromise<AssetType> loadedPromise)
 167        {
 568168            object id = loadedPromise.GetId();
 169
 568170            if (!masterToBlockedPromises.ContainsKey(id) || !masterPromiseById.ContainsKey(id))
 171            {
 548172                CleanPromise(loadedPromise);
 548173                return;
 174            }
 175
 20176            toResolveBlockedPromisesQueue.Enqueue(loadedPromise as AssetPromiseType);
 20177        }
 178
 179        private bool IsToResolveQueueEmpty()
 180        {
 2048772181            return toResolveBlockedPromisesQueue.Count <= 0;
 182        }
 183
 184        private async UniTask ProcessBlockedPromisesQueueAsync(CancellationToken cancellationToken)
 185        {
 148186            startTime = Time.unscaledTime;
 187
 20188            while (true)
 189            {
 168190                await UniTask.WaitWhile(IsToResolveQueueEmpty, PlayerLoopTiming.Update, cancellationToken);
 191
 20192                cancellationToken.ThrowIfCancellationRequested();
 20193                AssetPromiseType promise = toResolveBlockedPromisesQueue.Dequeue();
 20194                await ProcessBlockedPromisesDeferredAsync(promise, cancellationToken);
 195
 20196                cancellationToken.ThrowIfCancellationRequested();
 20197                CleanPromise(promise);
 198
 25199                if (promise.isForgotten) { promise.Unload(); }
 200
 20201                await SkipFrameIfOverBudgetAsync();
 20202            }
 203        }
 204
 205        private async UniTask ProcessBlockedPromisesDeferredAsync(AssetPromiseType loadedPromise, CancellationToken canc
 206        {
 20207            object loadedPromiseId = loadedPromise.GetId();
 208
 20209            if (!masterToBlockedPromises.ContainsKey(loadedPromiseId))
 0210                return;
 211
 20212            if (!masterPromiseById.ContainsKey(loadedPromiseId))
 0213                return;
 214
 20215            if (masterPromiseById[loadedPromiseId] != loadedPromise)
 216            {
 0217                Debug.LogWarning($"Unexpected issue: masterPromiseById promise isn't the same as loaded promise? id: {lo
 0218                return;
 219            }
 220
 221            //NOTE(Brian): We have to keep checking to support the case in which
 222            //             new promises are enqueued while this promise ID is being
 223            //             resolved.
 40224            while (masterToBlockedPromises.ContainsKey(loadedPromiseId) &&
 225                   masterToBlockedPromises[loadedPromiseId].Count > 0)
 226            {
 20227                cancellationToken.ThrowIfCancellationRequested();
 228
 20229                List<AssetPromiseType> promisesToLoadForId = GetBlockedPromisesToLoadForId(loadedPromiseId);
 230
 20231                await SkipFrameIfOverBudgetAsync();
 232
 20233                cancellationToken.ThrowIfCancellationRequested();
 234
 20235                if (loadedPromise.state != AssetPromiseState.FINISHED)
 3236                    await ForceFailPromiseListAsync(promisesToLoadForId);
 237                else
 17238                    await LoadPromisesListAsync(promisesToLoadForId);
 239
 20240                await SkipFrameIfOverBudgetAsync();
 20241            }
 242
 20243            if (masterToBlockedPromises.ContainsKey(loadedPromiseId))
 20244                masterToBlockedPromises.Remove(loadedPromiseId);
 20245        }
 246
 247        private async UniTask SkipFrameIfOverBudgetAsync()
 248        {
 147249            if (useTimeBudget && Time.realtimeSinceStartup - startTime >= AssetPromiseKeeper.PROCESS_PROMISES_TIME_BUDGE
 250            {
 1251                await UniTask.Yield();
 1252                startTime = Time.unscaledTime;
 253            }
 147254        }
 255
 256        private List<AssetPromiseType> GetBlockedPromisesToLoadForId(object masterPromiseId)
 257        {
 20258            var blockedPromisesToLoadAux = new List<AssetPromiseType>();
 259
 20260            using (var iterator = masterToBlockedPromises[masterPromiseId].GetEnumerator())
 261            {
 107262                while (iterator.MoveNext())
 263                {
 87264                    var blockedPromise = iterator.Current;
 265
 87266                    blockedPromises.Remove(blockedPromise);
 267
 174268                    if (blockedPromise != null && !blockedPromise.isForgotten) { blockedPromisesToLoadAux.Add(blockedPro
 269                }
 20270            }
 271
 20272            return blockedPromisesToLoadAux;
 273        }
 274
 275        private async UniTask ForceFailPromiseListAsync(List<AssetPromiseType> promises)
 276        {
 3277            int promisesCount = promises.Count;
 278
 18279            for (int i = 0; i < promisesCount; i++)
 280            {
 6281                var promise = promises[i];
 282
 6283                if (promise.isForgotten)
 284                    continue;
 285
 6286                promise.ForceFail(new PromiseForgottenException("Promise is forgotten"));
 6287                Forget(promise);
 6288                CleanPromise(promise);
 289
 6290                await SkipFrameIfOverBudgetAsync();
 291            }
 3292        }
 293
 294        private async UniTask LoadPromisesListAsync(List<AssetPromiseType> promises)
 295        {
 17296            int promisesCount = promises.Count;
 297
 196298            for (int i = 0; i < promisesCount; i++)
 299            {
 81300                AssetPromiseType promise = promises[i];
 301
 81302                if (promise.isForgotten)
 303                    continue;
 304
 81305                promise.library = library;
 81306                CleanPromise(promise);
 81307                promise.Load();
 308
 81309                await SkipFrameIfOverBudgetAsync();
 310            }
 17311        }
 312
 313        void CleanPromise(AssetPromise<AssetType> promise)
 314        {
 1193315            if (!promise.isDirty)
 474316                return;
 317
 719318            promise.isDirty = false;
 319
 719320            AssetPromiseType finalPromise = promise as AssetPromiseType;
 321
 719322            object id = promise.GetId();
 323
 719324            if (masterToBlockedPromises.ContainsKey(id))
 325            {
 182326                if (masterToBlockedPromises[id].Contains(finalPromise)) { masterToBlockedPromises[id].Remove(finalPromis
 327            }
 328
 1236329            if (masterPromiseById.ContainsKey(id) && masterPromiseById[id] == promise) { masterPromiseById.Remove(id); }
 330
 719331            if (blockedPromises.Contains(finalPromise))
 4332                blockedPromises.Remove(finalPromise);
 333
 719334            if (waitingPromises.Contains(finalPromise))
 608335                waitingPromises.Remove(finalPromise);
 719336        }
 337
 338        public void Cleanup()
 339        {
 2963340            if (blockedPromiseSolverCancellationTokenSource != null)
 341            {
 87342                blockedPromiseSolverCancellationTokenSource.Cancel();
 87343                blockedPromiseSolverCancellationTokenSource.Dispose();
 87344                blockedPromiseSolverCancellationTokenSource = null;
 345            }
 346
 2963347            blockedPromises = new HashSet<AssetPromiseType>();
 2963348            masterToBlockedPromises = new Dictionary<object, HashSet<AssetPromiseType>>();
 349
 2963350            using (var e = waitingPromises.GetEnumerator())
 351            {
 3003352                while (e.MoveNext())
 40353                    e.Current?.Cleanup();
 2963354            }
 355
 5980356            foreach (var kvp in masterPromiseById) { kvp.Value.Cleanup(); }
 357
 2963358            masterPromiseById = new Dictionary<object, AssetPromiseType>();
 2963359            waitingPromises = new HashSet<AssetPromiseType>();
 2963360            library.Cleanup();
 2963361        }
 362
 5363        protected virtual void OnSilentForget(AssetPromiseType promise) { }
 364    }
 365
 366    public class PromiseForgottenException : Exception
 367    {
 368        public PromiseForgottenException(string message) : base(message) { }
 369    }
 370}