diff --git a/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs b/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs index f0b97184..15ec96ea 100644 --- a/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs +++ b/IPA.Loader/Utilities/Async/UnityMainThreadTaskScheduler.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; using System.Threading; @@ -9,8 +10,15 @@ using System.Threading.Tasks; namespace IPA.Utilities.Async { + /// + /// A task scheduler that runs tasks on the Unity main thread via coroutines. + /// public class UnityMainThreadTaskScheduler : TaskScheduler, IDisposable { + /// + /// Gets the default main thread scheduler that is managed by BSIPA. + /// + /// a scheduler that is managed by BSIPA public static new TaskScheduler Default { get; } = new UnityMainThreadTaskScheduler(); private readonly ConcurrentDictionary tasks = new ConcurrentDictionary(); @@ -33,41 +41,113 @@ namespace IPA.Utilities.Async public bool Equals(QueueItem other) => other.Index == Index || other.Task == Task; } + /// + /// Gets whether or not this scheduler is currently executing tasks. + /// + /// if the scheduler is running, otherwise public bool IsRunning { get; private set; } = false; - public int YieldAfterTasks { get; set; } = 4; + /// + /// Gets whether or not this scheduler is in the process of shutting down. + /// + /// if the scheduler is shutting down, otherwise + public bool Cancelling { get; private set; } = false; + + private int yieldAfterTasks = 64; + /// + /// Gets or sets the number of tasks to execute before yielding back to Unity. + /// + /// the number of tasks to execute per resume + public int YieldAfterTasks + { + get => yieldAfterTasks; + set + { + ThrowIfDisposed(); + if (value < 1) + throw new ArgumentException("Value cannot be less than 1", nameof(value)); + yieldAfterTasks = value; + } + } + + private TimeSpan yieldAfterTime = TimeSpan.FromMilliseconds(.5); // auto-yield if more than half a millis has passed by default + /// + /// Gets or sets the amount of time to execute tasks for before yielding back to Unity. Default is 0.5ms. + /// + /// the amount of time to execute tasks for before yielding back to Unity + public TimeSpan YieldAfterTime + { + get => yieldAfterTime; + set + { + ThrowIfDisposed(); + if (value <= TimeSpan.Zero) + throw new ArgumentException("Value must be greater than zero", nameof(value)); + yieldAfterTime = value; + } + } + /// + /// When used as a Unity coroutine, runs the scheduler. Otherwise, this is an invalid call. + /// + /// a Unity coroutine + /// if this scheduler is disposed + /// if the scheduler is already running public IEnumerator Coroutine() { ThrowIfDisposed(); + if (IsRunning) + throw new InvalidOperationException("Scheduler already running"); + + Cancelling = false; IsRunning = true; yield return null; // yield immediately + var sw = new Stopwatch(); + try { - while (true) + while (!Cancelling) { if (queuePosition < queueEndPosition) { var yieldAfter = YieldAfterTasks; - for (int i = 0; i < yieldAfter && queuePosition < queueEndPosition; i++) + sw.Start(); + for (int i = 0; i < yieldAfter && queuePosition < queueEndPosition + && sw.Elapsed < YieldAfterTime; i++) { if (tasks.TryRemove(new QueueItem { Index = Interlocked.Increment(ref queuePosition) }, out var task)) TryExecuteTask(task); // we succesfully removed the task else i++; // we didn't } + sw.Reset(); } yield return null; } } finally { + sw.Reset(); IsRunning = false; } } + /// + /// Cancels the scheduler. If the scheduler is currently executing tasks, that batch will finish first. + /// All remaining tasks will be left in the queue. + /// + /// if this scheduler is disposed + /// if the scheduler is not running + public void Cancel() + { + ThrowIfDisposed(); + + if (!IsRunning) throw new InvalidOperationException("The scheduler is not running"); + Cancelling = true; + } + /// /// Throws a . /// @@ -79,6 +159,12 @@ namespace IPA.Utilities.Async throw new NotSupportedException(); } + /// + /// Queues a given to this scheduler. The must> be + /// scheduled for this by the runtime. + /// + /// the to queue + /// Thrown if this object has already been disposed. protected override void QueueTask(Task task) { ThrowIfDisposed(); @@ -86,6 +172,17 @@ namespace IPA.Utilities.Async tasks.TryAdd(new QueueItem(Interlocked.Increment(ref queueEndPosition), task), task); } + /// + /// Rejects any attempts to execute a task inline. + /// + /// + /// This task scheduler always runs its tasks on the thread that it manages, therefore it doesn't + /// make sense to run it inline. + /// + /// the task to attempt to execute + /// whether the task was previously queued to this scheduler + /// + /// Thrown if this object has already been disposed. protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) { ThrowIfDisposed(); @@ -114,7 +211,7 @@ namespace IPA.Utilities.Async { if (disposing) { - // TODO: dispose managed state (managed objects). + } disposedValue = true;