< 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:465
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-2023 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()
 99117        {
 99118            return s_TasksBusyCount;
 99119        }
 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            }
 2140            return s_TasksQueueCount > 0 ? $"{s_TasksQueueCount.ToString()} Queued" : null;
 3141        }
 142
 143        /// <summary>
 144        ///     Does the <see cref="TaskDirector"/> have any known busy or queued tasks?
 145        /// </summary>
 146        /// <remarks>
 147        ///     It's not performant to poll this.
 148        /// </remarks>
 149        /// <returns>A true/false value indicating tasks.</returns>
 150        public static bool HasTasks()
 289151        {
 289152            return s_TasksBusyCount > 0 || s_TasksQueueCount > 0;
 289153        }
 154
 155        /// <summary>
 156        ///     Is the <see cref="TaskDirector"/> blocking tasks with a specific bit?
 157        /// </summary>
 158        /// <remarks>
 159        ///     It isn't ideal to constantly poll this method, ideally this could be used to block things outside of
 160        ///     the <see cref="TaskDirector"/>'s control based on tasks running.
 161        /// </remarks>
 162        /// <returns>A true/false value indicating if a <see cref="BitArray16"/> index is being blocked.</returns>
 163        public static bool IsBlockingBit(int index)
 1164        {
 1165            return k_BlockedBits[index] > 0;
 1166        }
 167
 168        /// <summary>
 169        ///     Adds a thread-safe log entry to a queue which will be dispatched to <see cref="logAdded"/> on
 170        ///     the <see cref="Tick"/> invoking thread.
 171        /// </summary>
 172        /// <param name="message">The log content.</param>
 173        public static void Log(string message)
 13174        {
 13175            lock (k_LogLock)
 13176            {
 13177                k_Log.Enqueue(message);
 13178            }
 13179        }
 180
 181        /// <summary>
 182        ///     Add a task to the queue, to be later started when possible.
 183        /// </summary>
 184        /// <remarks>
 185        ///     If the <paramref name="task"/> is already executing it will be added to the known busy list.
 186        /// </remarks>
 187        /// <param name="task">An established task.</param>
 188        public static void QueueTask(TaskBase task)
 22189        {
 22190            if (task.IsExecuting())
 1191            {
 192                // Already running tasks self subscribe
 1193                return;
 194            }
 195
 21196            lock (k_StatusChangeLock)
 21197            {
 21198                if (k_TasksQueue.Contains(task))
 2199                {
 2200                    return;
 201                }
 202
 19203                k_TasksQueue.Add(task);
 19204                s_TasksQueueCount++;
 19205            }
 22206        }
 207
 208        /// <summary>
 209        ///     Update the <see cref="TaskDirector"/>, evaluating known tasks for work eligibility and execution.
 210        /// </summary>
 211        /// <remarks>
 212        ///     This should occur on the main thread. If the <see cref="TaskDirector"/> is used during play mode,
 213        ///     something needs to call this every global tick. While in edit mode the EditorTaskDirector triggers this
 214        ///     method.
 215        /// </remarks>
 216        public static void Tick()
 357217        {
 218            // We are blocked by a running task from adding anything else.
 357219            lock (k_StatusChangeLock)
 357220            {
 357221                int finishedWorkersCount = k_TasksFinished.Count;
 357222                if (finishedWorkersCount > 0)
 20223                {
 80224                    for (int i = 0; i < finishedWorkersCount; i++)
 20225                    {
 20226                        TaskBase taskBase = k_TasksFinished[i];
 20227                        taskBase.completedMainThread?.Invoke(taskBase);
 20228                    }
 229
 20230                    k_TasksFinished.Clear();
 20231                }
 232
 357233                if (s_BlockAllTasksCount == 0)
 327234                {
 235                    // Spin up workers needed to process
 327236                    int count = k_TasksQueue.Count;
 237
 327238                    if (count > 0)
 47239                    {
 210240                        for (int i = 0; i < count; i++)
 58241                        {
 58242                            TaskBase task = k_TasksQueue[i];
 243
 244                            // Check if task has a blocked name
 58245                            if (k_BlockedNames.Contains(task.GetName()))
 32246                            {
 32247                                continue;
 248                            }
 249
 26250                            BitArray16 bits = task.GetBits();
 26251                            if (IsBlockedByBits(ref bits))
 7252                            {
 7253                                continue;
 254                            }
 255
 19256                            AddBusyTask(task);
 76257                            ThreadPool.QueueUserWorkItem(delegate { task.Run(); });
 19258                            k_TasksProcessed.Add(task);
 19259                        }
 260
 47261                        int processedCount = k_TasksProcessed.Count;
 132262                        for (int i = 0; i < processedCount; i++)
 19263                        {
 19264                            k_TasksQueue.Remove(k_TasksProcessed[i]);
 19265                        }
 266
 47267                        s_TasksQueueCount = k_TasksQueue.Count;
 47268                        k_TasksProcessed.Clear();
 47269                    }
 327270                }
 357271            }
 272
 273            // Dispatch logging
 357274            lock (k_LogLock)
 357275            {
 357276                if (k_Log.Count > 0)
 3277                {
 3278                    logAdded?.Invoke(k_Log.ToArray());
 3279                    k_Log.Clear();
 3280                }
 357281            }
 282
 283            // Invoke notification to anything subscribed to block input
 357284            if (s_BlockInputCount > 0 && !s_BlockInput)
 3285            {
 3286                inputBlocked?.Invoke(true);
 3287                s_BlockInput = true;
 3288            }
 354289            else if (s_BlockInputCount <= 0 && s_BlockInput)
 3290            {
 3291                inputBlocked?.Invoke(false);
 3292                s_BlockInput = false;
 3293            }
 357294        }
 295
 296        /// <summary>
 297        ///     Evaluate the provided task and update its state inside of the <see cref="TaskDirector"/>.
 298        /// </summary>
 299        /// <remarks>
 300        ///     This will add a task to the <see cref="TaskDirector"/> if it does not already know about it, regardless
 301        ///     of the current blocking mode status. Do not use this method to add non executing tasks, they will not
 302        ///     be added to the <see cref="TaskDirector"/> in this way.
 303        /// </remarks>
 304        /// <param name="task">An established task.</param>
 305        public static void UpdateTask(TaskBase task)
 40306        {
 40307            if (task.IsDone())
 20308            {
 20309                RemoveBusyTask(task);
 20310            }
 20311            else if (task.IsExecuting())
 20312            {
 20313                AddBusyTask(task);
 20314            }
 40315        }
 316
 317        /// <summary>
 318        ///     Wait on the completion of all known tasks, blocking the invoking thread.
 319        /// </summary>
 320        /// <remarks>
 321        ///     Useful to force the main thread to wait for completion of tasks.
 322        /// </remarks>
 323        public static void Wait()
 23324        {
 223325            while (HasTasks())
 200326            {
 200327                Thread.Sleep(1);
 200328                Tick();
 200329            }
 23330            Tick();
 23331        }
 332
 333        /// <summary>
 334        ///     Asynchronously wait on the completion of all known tasks.
 335        /// </summary>
 336        public static async Task WaitAsync()
 18337        {
 61338            while (HasTasks())
 43339            {
 129340                await Task.Delay(1);
 43341                Tick();
 43342            }
 18343            Tick();
 18344        }
 345
 346
 347        /// <summary>
 348        ///     Add a <see cref="TaskBase"/> to the known list of working tasks.
 349        /// </summary>
 350        /// <remarks>
 351        ///     This will add the blocking mode settings to the current settings.
 352        /// </remarks>
 353        /// <param name="task">An established task.</param>
 354        static void AddBusyTask(TaskBase task)
 39355        {
 39356            lock (k_StatusChangeLock)
 39357            {
 39358                if (!k_TasksBusy.Contains(task))
 20359                {
 20360                    if (task.IsBlockingAllTasks())
 5361                    {
 5362                        s_BlockAllTasksCount++;
 5363                    }
 364
 365                    // Add to the count of tasks that block input so we can update based off it
 20366                    if (task.IsBlockingUserInterface())
 3367                    {
 3368                        s_BlockInputCount++;
 3369                    }
 370
 20371                    if (task.IsBlockingSameName())
 7372                    {
 7373                        k_BlockedNames.Add(task.GetName());
 7374                    }
 375
 20376                    if (task.IsBlockingBits())
 2377                    {
 2378                        BitArray16 blockedBits = task.GetBlockedBits();
 68379                        for (int i = 0; i < 16; i++)
 32380                        {
 32381                            if (blockedBits[(byte)i])
 2382                            {
 2383                                k_BlockedBits[i]++;
 2384                            }
 32385                        }
 2386                    }
 387
 20388                    k_TasksBusy.Add(task);
 20389                    s_TasksBusyCount++;
 20390                }
 39391            }
 39392        }
 393
 394        /// <summary>
 395        ///     Is the provided bit array blocked by the current blocking settings.
 396        /// </summary>
 397        /// <param name="bits">A <see cref="TaskBase"/>'s bits.</param>
 398        /// <returns>true/false if the task should be blocked from executing.</returns>
 399        static bool IsBlockedByBits(ref BitArray16 bits)
 26400        {
 674401            for (int i = 0; i < 16; i++)
 318402            {
 318403                if (bits[(byte)i] && k_BlockedBits[i] > 0)
 7404                {
 7405                    return true;
 406                }
 311407            }
 19408            return false;
 26409        }
 410
 411        /// <summary>
 412        ///     Remove a <see cref="TaskBase"/> from the known list of working tasks.
 413        /// </summary>
 414        /// <remarks>
 415        ///     This will remove the blocking mode settings to the current settings.
 416        /// </remarks>
 417        /// <param name="task">An established task.</param>
 418        static void RemoveBusyTask(TaskBase task)
 20419        {
 20420            lock (k_StatusChangeLock)
 20421            {
 20422                if (k_TasksBusy.Contains(task))
 20423                {
 20424                    k_TasksBusy.Remove(task);
 20425                    s_TasksBusyCount--;
 426
 427                    // Add to list of tasks so that the next tick the main thread will call their completion callbacks.
 20428                    k_TasksFinished.Add(task);
 429
 20430                    if (task.IsBlockingAllTasks())
 5431                    {
 5432                        s_BlockAllTasksCount--;
 5433                    }
 434
 20435                    if (task.IsBlockingUserInterface())
 3436                    {
 3437                        s_BlockInputCount--;
 3438                    }
 439
 20440                    if (task.IsBlockingSameName())
 7441                    {
 7442                        k_BlockedNames.Remove(task.GetName());
 7443                    }
 444
 20445                    if (task.IsBlockingBits())
 2446                    {
 2447                        BitArray16 blockedBits = task.GetBlockedBits();
 68448                        for (int i = 0; i < 16; i++)
 32449                        {
 32450                            if (blockedBits[(byte)i])
 2451                            {
 2452                                k_BlockedBits[i]--;
 2453                            }
 32454                        }
 2455                    }
 20456                }
 457
 20458                if (task.IsFaulted())
 2459                {
 2460                    exceptionOccured?.Invoke(task.GetException());
 2461                }
 20462            }
 20463        }
 464    }
 465}

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)