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;