< Summary

Class:DCL.AssetPromiseKeeper
Assembly:AssetPromiseKeeper
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/AssetManager/Common/AssetPromiseKeeper.cs
Covered lines:1
Uncovered lines:0
Coverable lines:1
Total lines:370
Line coverage:100% (1 of 1)
Covered branches:0
Total branches:0
Covered methods:1
Total methods:1
Method coverage:100% (1 of 1)

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
AssetPromiseKeeper()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    {
 111        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.
 31        HashSet<AssetPromiseType> waitingPromises = new HashSet<AssetPromiseType>();
 32        public int waitingPromisesCount => waitingPromises.Count;
 33
 34        //NOTE(Brian): List of promises waiting for assets not in library.
 35        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.
 38        HashSet<AssetPromiseType> blockedPromises = new HashSet<AssetPromiseType>();
 39
 40        //NOTE(Brian): Master promise id -> blocked promises HashSet
 41        protected Dictionary<object, HashSet<AssetPromiseType>> masterToBlockedPromises = new Dictionary<object, HashSet
 42
 43        public bool useTimeBudget => CommonScriptableObjects.rendererState.Get();
 44
 45        float startTime;
 46
 47        private CancellationTokenSource blockedPromiseSolverCancellationTokenSource;
 48
 49        public bool IsBlocked(AssetPromiseType promise)
 50        {
 51            return blockedPromises.Contains(promise);
 52        }
 53
 54        public string GetMasterState(AssetPromiseType promise)
 55        {
 56            object promiseId = promise.GetId();
 57
 58            if (!masterToBlockedPromises.ContainsKey(promiseId))
 59                return "Master not found";
 60
 61            if (!masterToBlockedPromises[promiseId].Contains(promise))
 62                return "Promise is not blocked???";
 63
 64            if (!masterPromiseById.ContainsKey(promiseId))
 65                return "not registered as master?";
 66
 67            return $"master state = {masterPromiseById[promiseId].state}";
 68        }
 69
 70        public AssetPromiseKeeper(AssetLibraryType library)
 71        {
 72            this.library = library;
 73            EnsureProcessBlockerPromiseQueueTask();
 74        }
 75
 76        private void EnsureProcessBlockerPromiseQueueTask()
 77        {
 78            if (blockedPromiseSolverCancellationTokenSource == null)
 79            {
 80                blockedPromiseSolverCancellationTokenSource = new CancellationTokenSource();
 81                ProcessBlockedPromisesQueueAsync(blockedPromiseSolverCancellationTokenSource.Token).Forget();
 82            }
 83        }
 84
 85        public AssetPromiseType Keep(AssetPromiseType promise)
 86        {
 87            if (promise == null || promise.state != AssetPromiseState.IDLE_AND_EMPTY || waitingPromises.Contains(promise
 88                return promise;
 89
 90            object id = promise.GetId();
 91
 92            if (id == null)
 93            {
 94                Debug.LogError("ERROR: ID == null. Promise is not set up correctly.");
 95                return promise;
 96            }
 97
 98            promise.isDirty = true;
 99
 100            //NOTE(Brian): We already have a master promise for this id, add to blocked list.
 101            if (masterPromiseById.ContainsKey(id))
 102            {
 103                // TODO(Brian): Remove once this class has a proper lifecycle and is on service locator.
 104                EnsureProcessBlockerPromiseQueueTask();
 105                waitingPromises.Add(promise);
 106
 107                if (!masterToBlockedPromises.ContainsKey(id))
 108                    masterToBlockedPromises.Add(id, new HashSet<AssetPromiseType>());
 109
 110                masterToBlockedPromises[id].Add(promise);
 111
 112                blockedPromises.Add(promise);
 113                promise.SetWaitingState();
 114                return promise;
 115            }
 116
 117            // NOTE(Brian): Not in library, add to corresponding lists...
 118            if (!library.Contains(id))
 119            {
 120                waitingPromises.Add(promise);
 121                masterPromiseById.Add(id, promise);
 122            }
 123
 124            promise.library = library;
 125            promise.OnPreFinishEvent += OnRequestCompleted;
 126            promise.Load();
 127
 128            return promise;
 129        }
 130
 131        public virtual AssetPromiseType Forget(AssetPromiseType promise)
 132        {
 133            if (promise == null)
 134                return null;
 135
 136            if (promise.state == AssetPromiseState.IDLE_AND_EMPTY || promise.state == AssetPromiseState.WAITING)
 137            {
 138                CleanPromise(promise);
 139                promise.OnForget();
 140                return promise;
 141            }
 142
 143            object id = promise.GetId();
 144
 145            bool isMasterPromise = masterPromiseById.ContainsKey(id) && masterPromiseById[id] == promise;
 146            bool hasBlockedPromises = masterToBlockedPromises.ContainsKey(id) && masterToBlockedPromises[id].Count > 0;
 147
 148            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.
 152                OnSilentForget(promise);
 153                promise.OnForget();
 154                return promise;
 155            }
 156
 157            promise.Unload();
 158            CleanPromise(promise);
 159            promise.OnForget();
 160
 161            return promise;
 162        }
 163
 164        Queue<AssetPromiseType> toResolveBlockedPromisesQueue = new Queue<AssetPromiseType>();
 165
 166        private void OnRequestCompleted(AssetPromise<AssetType> loadedPromise)
 167        {
 168            object id = loadedPromise.GetId();
 169
 170            if (!masterToBlockedPromises.ContainsKey(id) || !masterPromiseById.ContainsKey(id))
 171            {
 172                CleanPromise(loadedPromise);
 173                return;
 174            }
 175
 176            toResolveBlockedPromisesQueue.Enqueue(loadedPromise as AssetPromiseType);
 177        }
 178
 179        private bool IsToResolveQueueEmpty()
 180        {
 181            return toResolveBlockedPromisesQueue.Count <= 0;
 182        }
 183
 184        private async UniTask ProcessBlockedPromisesQueueAsync(CancellationToken cancellationToken)
 185        {
 186            startTime = Time.unscaledTime;
 187
 188            while (true)
 189            {
 190                await UniTask.WaitWhile(IsToResolveQueueEmpty, PlayerLoopTiming.Update, cancellationToken);
 191
 192                cancellationToken.ThrowIfCancellationRequested();
 193                AssetPromiseType promise = toResolveBlockedPromisesQueue.Dequeue();
 194                await ProcessBlockedPromisesDeferredAsync(promise, cancellationToken);
 195
 196                cancellationToken.ThrowIfCancellationRequested();
 197                CleanPromise(promise);
 198
 199                if (promise.isForgotten) { promise.Unload(); }
 200
 201                await SkipFrameIfOverBudgetAsync();
 202            }
 203        }
 204
 205        private async UniTask ProcessBlockedPromisesDeferredAsync(AssetPromiseType loadedPromise, CancellationToken canc
 206        {
 207            object loadedPromiseId = loadedPromise.GetId();
 208
 209            if (!masterToBlockedPromises.ContainsKey(loadedPromiseId))
 210                return;
 211
 212            if (!masterPromiseById.ContainsKey(loadedPromiseId))
 213                return;
 214
 215            if (masterPromiseById[loadedPromiseId] != loadedPromise)
 216            {
 217                Debug.LogWarning($"Unexpected issue: masterPromiseById promise isn't the same as loaded promise? id: {lo
 218                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.
 224            while (masterToBlockedPromises.ContainsKey(loadedPromiseId) &&
 225                   masterToBlockedPromises[loadedPromiseId].Count > 0)
 226            {
 227                cancellationToken.ThrowIfCancellationRequested();
 228
 229                List<AssetPromiseType> promisesToLoadForId = GetBlockedPromisesToLoadForId(loadedPromiseId);
 230
 231                await SkipFrameIfOverBudgetAsync();
 232
 233                cancellationToken.ThrowIfCancellationRequested();
 234
 235                if (loadedPromise.state != AssetPromiseState.FINISHED)
 236                    await ForceFailPromiseListAsync(promisesToLoadForId);
 237                else
 238                    await LoadPromisesListAsync(promisesToLoadForId);
 239
 240                await SkipFrameIfOverBudgetAsync();
 241            }
 242
 243            if (masterToBlockedPromises.ContainsKey(loadedPromiseId))
 244                masterToBlockedPromises.Remove(loadedPromiseId);
 245        }
 246
 247        private async UniTask SkipFrameIfOverBudgetAsync()
 248        {
 249            if (useTimeBudget && Time.realtimeSinceStartup - startTime >= AssetPromiseKeeper.PROCESS_PROMISES_TIME_BUDGE
 250            {
 251                await UniTask.Yield();
 252                startTime = Time.unscaledTime;
 253            }
 254        }
 255
 256        private List<AssetPromiseType> GetBlockedPromisesToLoadForId(object masterPromiseId)
 257        {
 258            var blockedPromisesToLoadAux = new List<AssetPromiseType>();
 259
 260            using (var iterator = masterToBlockedPromises[masterPromiseId].GetEnumerator())
 261            {
 262                while (iterator.MoveNext())
 263                {
 264                    var blockedPromise = iterator.Current;
 265
 266                    blockedPromises.Remove(blockedPromise);
 267
 268                    if (blockedPromise != null && !blockedPromise.isForgotten) { blockedPromisesToLoadAux.Add(blockedPro
 269                }
 270            }
 271
 272            return blockedPromisesToLoadAux;
 273        }
 274
 275        private async UniTask ForceFailPromiseListAsync(List<AssetPromiseType> promises)
 276        {
 277            int promisesCount = promises.Count;
 278
 279            for (int i = 0; i < promisesCount; i++)
 280            {
 281                var promise = promises[i];
 282
 283                if (promise.isForgotten)
 284                    continue;
 285
 286                promise.ForceFail(new PromiseForgottenException("Promise is forgotten"));
 287                Forget(promise);
 288                CleanPromise(promise);
 289
 290                await SkipFrameIfOverBudgetAsync();
 291            }
 292        }
 293
 294        private async UniTask LoadPromisesListAsync(List<AssetPromiseType> promises)
 295        {
 296            int promisesCount = promises.Count;
 297
 298            for (int i = 0; i < promisesCount; i++)
 299            {
 300                AssetPromiseType promise = promises[i];
 301
 302                if (promise.isForgotten)
 303                    continue;
 304
 305                promise.library = library;
 306                CleanPromise(promise);
 307                promise.Load();
 308
 309                await SkipFrameIfOverBudgetAsync();
 310            }
 311        }
 312
 313        void CleanPromise(AssetPromise<AssetType> promise)
 314        {
 315            if (!promise.isDirty)
 316                return;
 317
 318            promise.isDirty = false;
 319
 320            AssetPromiseType finalPromise = promise as AssetPromiseType;
 321
 322            object id = promise.GetId();
 323
 324            if (masterToBlockedPromises.ContainsKey(id))
 325            {
 326                if (masterToBlockedPromises[id].Contains(finalPromise)) { masterToBlockedPromises[id].Remove(finalPromis
 327            }
 328
 329            if (masterPromiseById.ContainsKey(id) && masterPromiseById[id] == promise) { masterPromiseById.Remove(id); }
 330
 331            if (blockedPromises.Contains(finalPromise))
 332                blockedPromises.Remove(finalPromise);
 333
 334            if (waitingPromises.Contains(finalPromise))
 335                waitingPromises.Remove(finalPromise);
 336        }
 337
 338        public void Cleanup()
 339        {
 340            if (blockedPromiseSolverCancellationTokenSource != null)
 341            {
 342                blockedPromiseSolverCancellationTokenSource.Cancel();
 343                blockedPromiseSolverCancellationTokenSource.Dispose();
 344                blockedPromiseSolverCancellationTokenSource = null;
 345            }
 346
 347            blockedPromises = new HashSet<AssetPromiseType>();
 348            masterToBlockedPromises = new Dictionary<object, HashSet<AssetPromiseType>>();
 349
 350            using (var e = waitingPromises.GetEnumerator())
 351            {
 352                while (e.MoveNext())
 353                    e.Current?.Cleanup();
 354            }
 355
 356            foreach (var kvp in masterPromiseById) { kvp.Value.Cleanup(); }
 357
 358            masterPromiseById = new Dictionary<object, AssetPromiseType>();
 359            waitingPromises = new HashSet<AssetPromiseType>();
 360            library.Cleanup();
 361        }
 362
 363        protected virtual void OnSilentForget(AssetPromiseType promise) { }
 364    }
 365
 366    public class PromiseForgottenException : Exception
 367    {
 368        public PromiseForgottenException(string message) : base(message) { }
 369    }
 370}

Methods/Properties

AssetPromiseKeeper()