< Summary

Class:GDX.Threading.TaskDirector
Assembly:GDX
File(s):./Packages/com.dotbunny.gdx/GDX/Threading/TaskDirector.cs
Covered lines:213
Uncovered lines:0
Coverable lines:213
Total lines:469
Line coverage:100% (213 of 213)
Covered branches:0
Total branches:0
Covered methods:15
Total methods:15
Method coverage:100% (15 of 15)

Coverage History

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
TaskDirector()0%110100%
GetBusyCount()0%110100%
GetQueueCount()0%110100%
GetStatus()0%4.074083.33%
HasTasks()0%220100%
IsBlockingBit(...)0%110100%
Log(...)0%220100%
QueueTask(...)0%4.064084.62%
Tick()0%21.7521088.06%
UpdateTask(...)0%330100%
Wait()0%220100%
WaitAsync()0%440100%
AddBusyTask(...)0%10.389074.29%
IsBlockedByBits(...)0%440100%
RemoveBusyTask(...)0%14.2711070%

File(s)

./Packages/com.dotbunny.gdx/GDX/Threading/TaskDirector.cs

#LineLine coverage
 1// Copyright (c) 2020-2024 dotBunny Inc.
 2// dotBunny licenses this file to you under the BSL-1.0 license.
 3// See the LICENSE file in the project root for more information.
 4
 5using System;
 6using System.Collections.Generic;
 7using System.Threading;
 8using System.Threading.Tasks;
 9using GDX.Collections;
 10
 11namespace GDX.Threading
 12{
 13    /// <summary>
 14    ///     A simple control mechanism for distributed <see cref="TaskBase" /> work across the
 15    ///     thread pool. Tasks should be short-lived and can queue up additional work.
 16    /// </summary>
 17    public static class TaskDirector
 18    {
 19        /// <summary>
 20        ///     An event invoked when a <see cref="TaskBase" /> throws an exception.
 21        /// </summary>
 22        public static Action<Exception> exceptionOccured;
 23
 24        /// <summary>
 25        ///     An event invoked during <see cref="Tick" /> when user input should be blocked.
 26        /// </summary>
 27        public static Action<bool> inputBlocked;
 28
 29        /// <summary>
 30        ///     An event invoked during <see cref="Tick" /> with new log content.
 31        /// </summary>
 32        public static Action<string[]> logAdded;
 33
 34        /// <summary>
 35        ///     A running tally of bits that are blocked by the currently executing tasks.
 36        /// </summary>
 237        static readonly int[] k_BlockedBits = new int[16];
 38
 39        /// <summary>
 40        ///     A collection of task names which are currently blocked from beginning to executed based
 41        ///     on the currently executing tasks.
 42        /// </summary>
 243        static readonly List<string> k_BlockedNames = new List<string>();
 44
 45        /// <summary>
 46        ///     An accumulating collection of log content which will be passed to <see cref="logAdded" />
 47        ///     subscribed methods during <see cref="Tick" />.
 48        /// </summary>
 249        static readonly Queue<string> k_Log = new Queue<string>(10);
 50
 51        /// <summary>
 52        ///     A locking mechanism used for log entries ensuring thread safety.
 53        /// </summary>
 254        static readonly object k_LogLock = new object();
 55
 56        /// <summary>
 57        ///     A locking mechanism used for changes to task lists ensuring thread safety.
 58        /// </summary>
 259        static readonly object k_StatusChangeLock = new object();
 60
 61        /// <summary>
 62        ///     A list of tasks currently being executed by the thread pool.
 63        /// </summary>
 264        static readonly List<TaskBase> k_TasksBusy = new List<TaskBase>();
 65
 66        /// <summary>
 67        ///     A working list of tasks that recently finished, used in <see cref="Tick" /> to ensure
 68        ///     callbacks occur on the main thread.
 69        /// </summary>
 270        static readonly List<TaskBase> k_TasksFinished = new List<TaskBase>();
 71
 72        /// <summary>
 73        ///     A list of tasks that were moved from waiting state to a working/busy state during
 74        ///     <see cref="Tick" />.
 75        /// </summary>
 276        static readonly List<TaskBase> k_TasksProcessed = new List<TaskBase>();
 77
 78        /// <summary>
 79        ///     A list of tasks currently waiting to start work.
 80        /// </summary>
 281        static readonly List<TaskBase> k_TasksQueue = new List<TaskBase>();
 82
 83        /// <summary>
 84        ///     The number of tasks that are busy executing which block all other tasks from executing.
 85        /// </summary>
 86        /// <remarks>
 87        ///     This number can be higher then one, when tasks are forcibly started and then added to the
 88        ///     <see cref="TaskDirector" />.
 89        /// </remarks>
 90        static int s_BlockAllTasksCount;
 91
 92        /// <summary>
 93        ///     Is user input blocked?
 94        /// </summary>
 95        static bool s_BlockInput;
 96
 97        /// <summary>
 98        ///     The number of tasks that are busy executing which block user input.
 99        /// </summary>
 100        static int s_BlockInputCount;
 101
 102        /// <summary>
 103        ///     A cached count of <see cref="k_TasksBusy" />.
 104        /// </summary>
 105        static int s_TasksBusyCount;
 106
 107        /// <summary>
 108        ///     A cached count of <see cref="k_TasksQueue" />.
 109        /// </summary>
 110        static int s_TasksQueueCount;
 111
 112        /// <summary>
 113        ///     The number of tasks currently in process or awaiting execution by the thread pool.
 114        /// </summary>
 115        /// <returns>The number of tasks sitting in <see cref="k_TasksBusy" />.</returns>
 116        public static int GetBusyCount()
 101117        {
 101118            return s_TasksBusyCount;
 101119        }
 120
 121        /// <summary>
 122        ///     The number of tasks waiting in the queue.
 123        /// </summary>
 124        /// <returns>The number of tasks sitting in <see cref="k_TasksQueue" />.</returns>
 125        public static int GetQueueCount()
 11126        {
 11127            return s_TasksQueueCount;
 11128        }
 129
 130        /// <summary>
 131        ///     Get the status message for the <see cref="TaskDirector" />.
 132        /// </summary>
 133        /// <returns>A pre-formatted status message.</returns>
 134        public static string GetStatus()
 3135        {
 3136            if (s_TasksBusyCount > 0)
 1137            {
 1138                return $"{s_TasksBusyCount.ToString()} Busy / {s_TasksQueueCount.ToString()} Queued";
 139            }
 140
 2141            return s_TasksQueueCount > 0 ? $"{s_TasksQueueCount.ToString()} Queued" : null;
 3142        }
 143
 144        /// <summary>
 145        ///     Does the <see cref="TaskDirector" /> have any known busy or queued tasks?
 146        /// </summary>
 147        /// <remarks>
 148        ///     It's not performant to poll this.
 149        /// </remarks>
 150        /// <returns>A true/false value indicating tasks.</returns>
 151        public static bool HasTasks()
 282152        {
 282153            return s_TasksBusyCount > 0 || s_TasksQueueCount > 0;
 282154        }
 155
 156        /// <summary>
 157        ///     Is the <see cref="TaskDirector" /> blocking tasks with a specific bit?
 158        /// </summary>
 159        /// <remarks>
 160        ///     It isn't ideal to constantly poll this method, ideally this could be used to block things outside of
 161        ///     the <see cref="TaskDirector" />'s control based on tasks running.
 162        /// </remarks>
 163        /// <returns>A true/false value indicating if a <see cref="BitArray16" /> index is being blocked.</returns>
 164        public static bool IsBlockingBit(int index)
 1165        {
 1166            return k_BlockedBits[index] > 0;
 1167        }
 168
 169        /// <summary>
 170        ///     Adds a thread-safe log entry to a queue which will be dispatched to <see cref="logAdded" /> on
 171        ///     the <see cref="Tick" /> invoking thread.
 172        /// </summary>
 173        /// <param name="message">The log content.</param>
 174        public static void Log(string message)
 13175        {
 13176            lock (k_LogLock)
 13177            {
 13178                k_Log.Enqueue(message);
 13179            }
 13180        }
 181
 182        /// <summary>
 183        ///     Add a task to the queue, to be later started when possible.
 184        /// </summary>
 185        /// <remarks>
 186        ///     If the <paramref name="task" /> is already executing it will be added to the known busy list.
 187        /// </remarks>
 188        /// <param name="task">An established task.</param>
 189        public static void QueueTask(TaskBase task)
 22190        {
 22191            if (task.IsExecuting())
 1192            {
 193                // Already running tasks self subscribe
 1194                return;
 195            }
 196
 21197            lock (k_StatusChangeLock)
 21198            {
 21199                if (k_TasksQueue.Contains(task))
 2200                {
 2201                    return;
 202                }
 203
 19204                k_TasksQueue.Add(task);
 19205                s_TasksQueueCount++;
 19206            }
 22207        }
 208
 209        /// <summary>
 210        ///     Update the <see cref="TaskDirector" />, evaluating known tasks for work eligibility and execution.
 211        /// </summary>
 212        /// <remarks>
 213        ///     This should occur on the main thread. If the <see cref="TaskDirector" /> is used during play mode,
 214        ///     something needs to call this every global tick. While in edit mode the EditorTaskDirector triggers this
 215        ///     method.
 216        /// </remarks>
 217        public static void Tick()
 351218        {
 219            // We are blocked by a running task from adding anything else.
 351220            lock (k_StatusChangeLock)
 351221            {
 351222                int finishedWorkersCount = k_TasksFinished.Count;
 351223                if (finishedWorkersCount > 0)
 20224                {
 80225                    for (int i = 0; i < finishedWorkersCount; i++)
 20226                    {
 20227                        TaskBase taskBase = k_TasksFinished[i];
 20228                        taskBase.completedMainThread?.Invoke(taskBase);
 20229                    }
 230
 20231                    k_TasksFinished.Clear();
 20232                }
 233
 351234                if (s_BlockAllTasksCount == 0)
 324235                {
 236                    // Spin up workers needed to process
 324237                    int count = k_TasksQueue.Count;
 238
 324239                    if (count > 0)
 47240                    {
 212241                        for (int i = 0; i < count; i++)
 59242                        {
 59243                            TaskBase task = k_TasksQueue[i];
 244
 245                            // Check if task has a blocked name
 59246                            if (k_BlockedNames.Contains(task.GetName()))
 33247                            {
 33248                                continue;
 249                            }
 250
 26251                            BitArray16 bits = task.GetBits();
 26252                            if (IsBlockedByBits(ref bits))
 7253                            {
 7254                                continue;
 255                            }
 256
 19257                            AddBusyTask(task);
 76258                            ThreadPool.QueueUserWorkItem(delegate { task.Run(); });
 19259                            k_TasksProcessed.Add(task);
 19260                        }
 261
 47262                        int processedCount = k_TasksProcessed.Count;
 132263                        for (int i = 0; i < processedCount; i++)
 19264                        {
 19265                            k_TasksQueue.Remove(k_TasksProcessed[i]);
 19266                        }
 267
 47268                        s_TasksQueueCount = k_TasksQueue.Count;
 47269                        k_TasksProcessed.Clear();
 47270                    }
 324271                }
 351272            }
 273
 274            // Dispatch logging
 351275            lock (k_LogLock)
 351276            {
 351277                if (k_Log.Count > 0)
 3278                {
 3279                    logAdded?.Invoke(k_Log.ToArray());
 3280                    k_Log.Clear();
 3281                }
 351282            }
 283
 284            // Invoke notification to anything subscribed to block input
 351285            if (s_BlockInputCount > 0 && !s_BlockInput)
 3286            {
 3287                inputBlocked?.Invoke(true);
 3288                s_BlockInput = true;
 3289            }
 348290            else if (s_BlockInputCount <= 0 && s_BlockInput)
 3291            {
 3292                inputBlocked?.Invoke(false);
 3293                s_BlockInput = false;
 3294            }
 351295        }
 296
 297        /// <summary>
 298        ///     Evaluate the provided task and update its state inside of the <see cref="TaskDirector" />.
 299        /// </summary>
 300        /// <remarks>
 301        ///     This will add a task to the <see cref="TaskDirector" /> if it does not already know about it, regardless
 302        ///     of the current blocking mode status. Do not use this method to add non executing tasks, they will not
 303        ///     be added to the <see cref="TaskDirector" /> in this way.
 304        /// </remarks>
 305        /// <param name="task">An established task.</param>
 306        public static void UpdateTask(TaskBase task)
 40307        {
 40308            if (task.IsDone())
 20309            {
 20310                RemoveBusyTask(task);
 20311            }
 20312            else if (task.IsExecuting())
 20313            {
 20314                AddBusyTask(task);
 20315            }
 40316        }
 317
 318        /// <summary>
 319        ///     Wait on the completion of all known tasks, blocking the invoking thread.
 320        /// </summary>
 321        /// <remarks>
 322        ///     Useful to force the main thread to wait for completion of tasks.
 323        /// </remarks>
 324        public static void Wait()
 23325        {
 223326            while (HasTasks())
 200327            {
 200328                Thread.Sleep(1);
 200329                Tick();
 200330            }
 331
 23332            Tick();
 23333        }
 334
 335        /// <summary>
 336        ///     Asynchronously wait on the completion of all known tasks.
 337        /// </summary>
 338        public static async Task WaitAsync()
 18339        {
 54340            while (HasTasks())
 36341            {
 108342                await Task.Delay(1);
 36343                Tick();
 36344            }
 345
 18346            Tick();
 18347        }
 348
 349
 350        /// <summary>
 351        ///     Add a <see cref="TaskBase" /> to the known list of working tasks.
 352        /// </summary>
 353        /// <remarks>
 354        ///     This will add the blocking mode settings to the current settings.
 355        /// </remarks>
 356        /// <param name="task">An established task.</param>
 357        static void AddBusyTask(TaskBase task)
 39358        {
 39359            lock (k_StatusChangeLock)
 39360            {
 39361                if (!k_TasksBusy.Contains(task))
 20362                {
 20363                    if (task.IsBlockingAllTasks())
 5364                    {
 5365                        s_BlockAllTasksCount++;
 5366                    }
 367
 368                    // Add to the count of tasks that block input so we can update based off it
 20369                    if (task.IsBlockingUserInterface())
 3370                    {
 3371                        s_BlockInputCount++;
 3372                    }
 373
 20374                    if (task.IsBlockingSameName())
 7375                    {
 7376                        k_BlockedNames.Add(task.GetName());
 7377                    }
 378
 20379                    if (task.IsBlockingBits())
 2380                    {
 2381                        BitArray16 blockedBits = task.GetBlockedBits();
 68382                        for (int i = 0; i < 16; i++)
 32383                        {
 32384                            if (blockedBits[(byte)i])
 2385                            {
 2386                                k_BlockedBits[i]++;
 2387                            }
 32388                        }
 2389                    }
 390
 20391                    k_TasksBusy.Add(task);
 20392                    s_TasksBusyCount++;
 20393                }
 39394            }
 39395        }
 396
 397        /// <summary>
 398        ///     Is the provided bit array blocked by the current blocking settings.
 399        /// </summary>
 400        /// <param name="bits">A <see cref="TaskBase" />'s bits.</param>
 401        /// <returns>true/false if the task should be blocked from executing.</returns>
 402        static bool IsBlockedByBits(ref BitArray16 bits)
 26403        {
 674404            for (int i = 0; i < 16; i++)
 318405            {
 318406                if (bits[(byte)i] && k_BlockedBits[i] > 0)
 7407                {
 7408                    return true;
 409                }
 311410            }
 411
 19412            return false;
 26413        }
 414
 415        /// <summary>
 416        ///     Remove a <see cref="TaskBase" /> from the known list of working tasks.
 417        /// </summary>
 418        /// <remarks>
 419        ///     This will remove the blocking mode settings to the current settings.
 420        /// </remarks>
 421        /// <param name="task">An established task.</param>
 422        static void RemoveBusyTask(TaskBase task)
 20423        {
 20424            lock (k_StatusChangeLock)
 20425            {
 20426                if (k_TasksBusy.Contains(task))
 20427                {
 20428                    k_TasksBusy.Remove(task);
 20429                    s_TasksBusyCount--;
 430
 431                    // Add to list of tasks so that the next tick the main thread will call their completion callbacks.
 20432                    k_TasksFinished.Add(task);
 433
 20434                    if (task.IsBlockingAllTasks())
 5435                    {
 5436                        s_BlockAllTasksCount--;
 5437                    }
 438
 20439                    if (task.IsBlockingUserInterface())
 3440                    {
 3441                        s_BlockInputCount--;
 3442                    }
 443
 20444                    if (task.IsBlockingSameName())
 7445                    {
 7446                        k_BlockedNames.Remove(task.GetName());
 7447                    }
 448
 20449                    if (task.IsBlockingBits())
 2450                    {
 2451                        BitArray16 blockedBits = task.GetBlockedBits();
 68452                        for (int i = 0; i < 16; i++)
 32453                        {
 32454                            if (blockedBits[(byte)i])
 2455                            {
 2456                                k_BlockedBits[i]--;
 2457                            }
 32458                        }
 2459                    }
 20460                }
 461
 20462                if (task.IsFaulted())
 2463                {
 2464                    exceptionOccured?.Invoke(task.GetException());
 2465                }
 20466            }
 20467        }
 468    }
 469}

Coverage by test methods








































Methods/Properties

TaskDirector()
GetBusyCount()
GetQueueCount()
GetStatus()
HasTasks()
IsBlockingBit(System.Int32)
Log(System.String)
QueueTask(GDX.Threading.TaskBase)
Tick()
UpdateTask(GDX.Threading.TaskBase)
Wait()
WaitAsync()
AddBusyTask(GDX.Threading.TaskBase)
IsBlockedByBits(GDX.Collections.BitArray16&)
RemoveBusyTask(GDX.Threading.TaskBase)