increase performance and scalability by storing event coroutines linked to their event

This commit is contained in:
Ell 2021-03-20 15:13:39 +01:00
parent 548e07f19f
commit 501d744326
6 changed files with 63 additions and 69 deletions

View file

@ -13,6 +13,9 @@ namespace Coroutine {
private readonly Stopwatch stopwatch; private readonly Stopwatch stopwatch;
private Wait current; private Wait current;
internal Event Event => this.current.Event;
internal bool IsWaitingForEvent => this.Event != null;
/// <summary> /// <summary>
/// This property stores whether or not this active coroutine is finished. /// This property stores whether or not this active coroutine is finished.
/// A coroutine is finished if all of its waits have passed, or if it <see cref="WasCanceled"/>. /// A coroutine is finished if all of its waits have passed, or if it <see cref="WasCanceled"/>.
@ -79,18 +82,14 @@ namespace Coroutine {
} }
internal bool Tick(double deltaSeconds) { internal bool Tick(double deltaSeconds) {
if (!this.WasCanceled) { if (!this.WasCanceled && this.current.Tick(deltaSeconds))
if (this.current.Tick(deltaSeconds)) this.MoveNext();
this.MoveNext();
}
return this.IsFinished; return this.IsFinished;
} }
internal bool OnEvent(Event evt) { internal bool OnEvent(Event evt) {
if (!this.WasCanceled) { if (!this.WasCanceled && Equals(this.current.Event, evt))
if (this.current.OnEvent(evt)) this.MoveNext();
this.MoveNext();
}
return this.IsFinished; return this.IsFinished;
} }
@ -112,10 +111,6 @@ namespace Coroutine {
return true; return true;
} }
internal bool IsWaitingForEvent() {
return this.current.IsWaitingForEvent();
}
/// <summary> /// <summary>
/// A delegate method used by <see cref="ActiveCoroutine.OnFinished"/>. /// A delegate method used by <see cref="ActiveCoroutine.OnFinished"/>.
/// </summary> /// </summary>

View file

@ -11,9 +11,9 @@ namespace Coroutine {
public class CoroutineHandlerInstance { public class CoroutineHandlerInstance {
private readonly List<ActiveCoroutine> tickingCoroutines = new List<ActiveCoroutine>(); private readonly List<ActiveCoroutine> tickingCoroutines = new List<ActiveCoroutine>();
private readonly List<ActiveCoroutine> eventCoroutines = new List<ActiveCoroutine>(); private readonly Dictionary<Event, List<ActiveCoroutine>> eventCoroutines = new Dictionary<Event, List<ActiveCoroutine>>();
private readonly Queue<ActiveCoroutine> eventCoroutinesToRemove = new Queue<ActiveCoroutine>();
private readonly Queue<ActiveCoroutine> outstandingCoroutines = new Queue<ActiveCoroutine>(); private readonly Queue<ActiveCoroutine> outstandingCoroutines = new Queue<ActiveCoroutine>();
private readonly ISet<int> eventCoroutinesToRemove = new HashSet<int>();
private readonly Stopwatch stopwatch = new Stopwatch(); private readonly Stopwatch stopwatch = new Stopwatch();
/// <summary> /// <summary>
@ -23,7 +23,7 @@ namespace Coroutine {
/// <summary> /// <summary>
/// The amount of <see cref="ActiveCoroutine"/> instances that are currently waiting for an <see cref="Event"/> /// The amount of <see cref="ActiveCoroutine"/> instances that are currently waiting for an <see cref="Event"/>
/// </summary> /// </summary>
public int EventCount => this.eventCoroutines.Count; public int EventCount => this.eventCoroutines.Sum(c => c.Value.Count);
/// <summary> /// <summary>
/// Starts the given coroutine, returning a <see cref="ActiveCoroutine"/> object for management. /// Starts the given coroutine, returning a <see cref="ActiveCoroutine"/> object for management.
@ -66,7 +66,6 @@ namespace Coroutine {
/// <summary> /// <summary>
/// Ticks this coroutine handler, causing all time-based <see cref="Wait"/>s to be ticked. /// Ticks this coroutine handler, causing all time-based <see cref="Wait"/>s to be ticked.
/// Note that this method needs to be called even if only event-based coroutines are used.
/// </summary> /// </summary>
/// <param name="deltaSeconds">The amount of seconds that have passed since the last time this method was invoked</param> /// <param name="deltaSeconds">The amount of seconds that have passed since the last time this method was invoked</param>
public void Tick(double deltaSeconds) { public void Tick(double deltaSeconds) {
@ -74,7 +73,7 @@ namespace Coroutine {
this.tickingCoroutines.RemoveAll(c => { this.tickingCoroutines.RemoveAll(c => {
if (c.Tick(deltaSeconds)) { if (c.Tick(deltaSeconds)) {
return true; return true;
} else if (c.IsWaitingForEvent()) { } else if (c.IsWaitingForEvent) {
this.outstandingCoroutines.Enqueue(c); this.outstandingCoroutines.Enqueue(c);
return true; return true;
} }
@ -96,15 +95,19 @@ namespace Coroutine {
/// </summary> /// </summary>
/// <param name="evt">The event to raise</param> /// <param name="evt">The event to raise</param>
public void RaiseEvent(Event evt) { public void RaiseEvent(Event evt) {
for (var i = 0; i < this.eventCoroutines.Count; i++) { this.MoveOutstandingCoroutines();
if (this.eventCoroutinesToRemove.Contains(i)) var coroutines = this.GetEventCoroutines(evt, 0);
continue; if (coroutines != null) {
var c = this.eventCoroutines[i]; for (var i = 0; i < coroutines.Count; i++) {
if (c.OnEvent(evt)) { var c = coroutines[i];
this.eventCoroutinesToRemove.Add(i); if (this.eventCoroutinesToRemove.Contains(c))
} else if (!c.IsWaitingForEvent()) { continue;
this.outstandingCoroutines.Enqueue(c); if (c.OnEvent(evt)) {
this.eventCoroutinesToRemove.Add(i); this.eventCoroutinesToRemove.Enqueue(c);
} else if (!c.IsWaitingForEvent) {
this.outstandingCoroutines.Enqueue(c);
this.eventCoroutinesToRemove.Enqueue(c);
}
} }
} }
} }
@ -114,22 +117,32 @@ namespace Coroutine {
/// </summary> /// </summary>
/// <returns>All active coroutines</returns> /// <returns>All active coroutines</returns>
public IEnumerable<ActiveCoroutine> GetActiveCoroutines() { public IEnumerable<ActiveCoroutine> GetActiveCoroutines() {
return this.tickingCoroutines.Concat(this.eventCoroutines); return this.tickingCoroutines.Concat(this.eventCoroutines.Values.SelectMany(c => c));
} }
private void MoveOutstandingCoroutines() { private void MoveOutstandingCoroutines() {
var i = 0; while (this.eventCoroutinesToRemove.Count > 0) {
this.eventCoroutines.RemoveAll(c => this.eventCoroutinesToRemove.Contains(i++)); var c = this.eventCoroutinesToRemove.Peek();
this.eventCoroutinesToRemove.Clear(); this.GetEventCoroutines(c.Event, 0).Remove(c);
this.eventCoroutinesToRemove.Dequeue();
}
while (this.outstandingCoroutines.Count > 0) { while (this.outstandingCoroutines.Count > 0) {
var coroutine = this.outstandingCoroutines.Dequeue(); var coroutine = this.outstandingCoroutines.Peek();
var list = coroutine.IsWaitingForEvent() ? this.eventCoroutines : this.tickingCoroutines; var list = coroutine.IsWaitingForEvent ? this.GetEventCoroutines(coroutine.Event, 1) : this.tickingCoroutines;
var position = list.BinarySearch(coroutine); var position = list.BinarySearch(coroutine);
list.Insert(position < 0 ? ~position : position, coroutine); list.Insert(position < 0 ? ~position : position, coroutine);
this.outstandingCoroutines.Dequeue();
} }
} }
private List<ActiveCoroutine> GetEventCoroutines(Event evt, int capacity) {
if (!this.eventCoroutines.TryGetValue(evt, out var ret) && capacity > 0) {
ret = new List<ActiveCoroutine>(capacity);
this.eventCoroutines.Add(evt, ret);
}
return ret;
}
private static IEnumerator<Wait> InvokeLaterImpl(Wait wait, Action action) { private static IEnumerator<Wait> InvokeLaterImpl(Wait wait, Action action) {
yield return wait; yield return wait;
action(); action();

View file

@ -2,19 +2,19 @@ using System;
namespace Coroutine { namespace Coroutine {
/// <summary> /// <summary>
/// Represents either an amount of time, or an <see cref="Event"/> that is being waited for by an <see cref="ActiveCoroutine"/>. /// Represents either an amount of time, or an <see cref="Coroutine.Event"/> that is being waited for by an <see cref="ActiveCoroutine"/>.
/// </summary> /// </summary>
public struct Wait { public struct Wait {
private readonly Event evt; internal readonly Event Event;
private double seconds; private double seconds;
/// <summary> /// <summary>
/// Creates a new wait that waits for the given <see cref="Event"/>. /// Creates a new wait that waits for the given <see cref="Coroutine.Event"/>.
/// </summary> /// </summary>
/// <param name="evt">The event to wait for</param> /// <param name="evt">The event to wait for</param>
public Wait(Event evt) { public Wait(Event evt) {
this.evt = evt; this.Event = evt;
this.seconds = 0; this.seconds = 0;
} }
@ -24,7 +24,7 @@ namespace Coroutine {
/// <param name="seconds">The amount of seconds to wait for</param> /// <param name="seconds">The amount of seconds to wait for</param>
public Wait(double seconds) { public Wait(double seconds) {
this.seconds = seconds; this.seconds = seconds;
this.evt = null; this.Event = null;
} }
/// <summary> /// <summary>
@ -40,13 +40,5 @@ namespace Coroutine {
return this.seconds <= 0; return this.seconds <= 0;
} }
internal bool OnEvent(Event evt) {
return Equals(evt, this.evt);
}
internal bool IsWaitingForEvent() {
return this.evt != null;
}
} }
} }

View file

@ -14,7 +14,7 @@ namespace Example {
CoroutineHandler.Start(EmptyCoroutine()); CoroutineHandler.Start(EmptyCoroutine());
CoroutineHandler.InvokeLater(new Wait(10), () => { CoroutineHandler.InvokeLater(new Wait(5), () => {
Console.WriteLine("Raising test event"); Console.WriteLine("Raising test event");
CoroutineHandler.RaiseEvent(TestEvent); CoroutineHandler.RaiseEvent(TestEvent);
}); });

View file

@ -66,7 +66,5 @@ To actually cause the event to be raised, causing all currently waiting coroutin
CoroutineHandler.RaiseEvent(TestEvent); CoroutineHandler.RaiseEvent(TestEvent);
``` ```
Note that, since `Tick` is an important lifecycle method, it has to be [called continuously](#Setting-up-the-CoroutineHandler) even if only event-based coroutines are used.
## Additional Examples ## Additional Examples
For additional examples, take a look at the [Example class](https://github.com/Ellpeck/Coroutine/blob/master/Example/Example.cs). For additional examples, take a look at the [Example class](https://github.com/Ellpeck/Coroutine/blob/master/Example/Example.cs).

View file

@ -21,7 +21,7 @@ namespace Tests {
Assert.AreEqual(string.Empty, cr.Name, "Incorrect default name found"); Assert.AreEqual(string.Empty, cr.Name, "Incorrect default name found");
Assert.AreEqual(0, cr.Priority, "Default priority is not minimum"); Assert.AreEqual(0, cr.Priority, "Default priority is not minimum");
for (var i = 0; i < 5; i++) for (var i = 0; i < 5; i++)
SimulateTime(1); CoroutineHandler.Tick(1);
Assert.AreEqual(2, counter, "instruction after yield is not executed."); Assert.AreEqual(2, counter, "instruction after yield is not executed.");
Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value."); Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value.");
Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value."); Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value.");
@ -49,7 +49,7 @@ namespace Tests {
cr[0] = CoroutineHandler.Start(OnTimeTickNeverReturnYield()); cr[0] = CoroutineHandler.Start(OnTimeTickNeverReturnYield());
cr[1] = CoroutineHandler.Start(OnTimeTickYieldBreak()); cr[1] = CoroutineHandler.Start(OnTimeTickYieldBreak());
for (var i = 0; i < 5; i++) for (var i = 0; i < 5; i++)
SimulateTime(1); CoroutineHandler.Tick(1);
Assert.AreEqual(3, counter, "Incorrect counter value."); Assert.AreEqual(3, counter, "Incorrect counter value.");
for (var i = 0; i < cr.Length; i++) { for (var i = 0; i < cr.Length; i++) {
@ -71,7 +71,7 @@ namespace Tests {
var cr = CoroutineHandler.Start(OnTimeTickYieldDefault()); var cr = CoroutineHandler.Start(OnTimeTickYieldDefault());
for (var i = 0; i < 5; i++) for (var i = 0; i < 5; i++)
SimulateTime(1); CoroutineHandler.Tick(1);
Assert.AreEqual(2, counter, "Incorrect counter value."); Assert.AreEqual(2, counter, "Incorrect counter value.");
Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value."); Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value.");
@ -97,7 +97,7 @@ namespace Tests {
var cr = CoroutineHandler.Start(OnTimerTickInfinite()); var cr = CoroutineHandler.Start(OnTimerTickInfinite());
cr.OnFinished += SetCounterToUnreachableValue; cr.OnFinished += SetCounterToUnreachableValue;
for (var i = 0; i < 50; i++) for (var i = 0; i < 50; i++)
SimulateTime(1); CoroutineHandler.Tick(1);
Assert.AreEqual(51, counter, "Incorrect counter value."); Assert.AreEqual(51, counter, "Incorrect counter value.");
Assert.AreEqual(false, cr.IsFinished, "Incorrect IsFinished value."); Assert.AreEqual(false, cr.IsFinished, "Incorrect IsFinished value.");
@ -126,7 +126,7 @@ namespace Tests {
var cr = CoroutineHandler.Start(OnTimeTick()); var cr = CoroutineHandler.Start(OnTimeTick());
cr.OnFinished += SetCounterToUnreachableValue; cr.OnFinished += SetCounterToUnreachableValue;
SimulateTime(50); CoroutineHandler.Tick(50);
Assert.AreEqual(-100, counter, "Incorrect counter value."); Assert.AreEqual(-100, counter, "Incorrect counter value.");
} }
@ -173,22 +173,22 @@ namespace Tests {
Assert.AreEqual(0, counterGrandParent, "Grand Parent counter is invalid at time 0."); Assert.AreEqual(0, counterGrandParent, "Grand Parent counter is invalid at time 0.");
Assert.AreEqual(0, counterParent, "Parent counter is invalid at time 0."); Assert.AreEqual(0, counterParent, "Parent counter is invalid at time 0.");
Assert.AreEqual(0, counterChild, "Child counter is invalid at time 0."); Assert.AreEqual(0, counterChild, "Child counter is invalid at time 0.");
SimulateTime(1); CoroutineHandler.Tick(1);
Assert.AreEqual(1, counterAlwaysRunning, "Always running counter is invalid at time 1."); Assert.AreEqual(1, counterAlwaysRunning, "Always running counter is invalid at time 1.");
Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 1."); Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 1.");
Assert.AreEqual(0, counterParent, "Parent counter is invalid at time 1."); Assert.AreEqual(0, counterParent, "Parent counter is invalid at time 1.");
Assert.AreEqual(0, counterChild, "Child counter is invalid at time 1."); Assert.AreEqual(0, counterChild, "Child counter is invalid at time 1.");
SimulateTime(1); CoroutineHandler.Tick(1);
Assert.AreEqual(2, counterAlwaysRunning, "Always running counter is invalid at time 2."); Assert.AreEqual(2, counterAlwaysRunning, "Always running counter is invalid at time 2.");
Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 2."); Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 2.");
Assert.AreEqual(1, counterParent, "Parent counter is invalid at time 2."); Assert.AreEqual(1, counterParent, "Parent counter is invalid at time 2.");
Assert.AreEqual(0, counterChild, "Child counter is invalid at time 2."); Assert.AreEqual(0, counterChild, "Child counter is invalid at time 2.");
SimulateTime(1); CoroutineHandler.Tick(1);
Assert.AreEqual(3, counterAlwaysRunning, "Always running counter is invalid at time 3."); Assert.AreEqual(3, counterAlwaysRunning, "Always running counter is invalid at time 3.");
Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 3."); Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 3.");
Assert.AreEqual(1, counterParent, "Parent counter is invalid at time 3."); Assert.AreEqual(1, counterParent, "Parent counter is invalid at time 3.");
Assert.AreEqual(1, counterChild, "Child counter is invalid at time 3."); Assert.AreEqual(1, counterChild, "Child counter is invalid at time 3.");
SimulateTime(1); CoroutineHandler.Tick(1);
Assert.AreEqual(4, counterAlwaysRunning, "Always running counter is invalid at time 4."); Assert.AreEqual(4, counterAlwaysRunning, "Always running counter is invalid at time 4.");
Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 4."); Assert.AreEqual(1, counterGrandParent, "Grand Parent counter is invalid at time 4.");
Assert.AreEqual(1, counterParent, "Parent counter is invalid at time 4."); Assert.AreEqual(1, counterParent, "Parent counter is invalid at time 4.");
@ -243,7 +243,7 @@ namespace Tests {
CoroutineHandler.Start(ShouldExecuteAfter()); CoroutineHandler.Start(ShouldExecuteAfter());
CoroutineHandler.Start(ShouldExecuteBefore0(), priority: highPriority); CoroutineHandler.Start(ShouldExecuteBefore0(), priority: highPriority);
CoroutineHandler.Start(ShouldExecuteFinally(), priority: -1); CoroutineHandler.Start(ShouldExecuteFinally(), priority: -1);
SimulateTime(10); CoroutineHandler.Tick(10);
Assert.AreEqual(1, counterShouldExecuteAfter, $"ShouldExecuteAfter counter {counterShouldExecuteAfter} is invalid."); Assert.AreEqual(1, counterShouldExecuteAfter, $"ShouldExecuteAfter counter {counterShouldExecuteAfter} is invalid.");
Assert.AreEqual(1, counterShouldExecuteFinally, $"ShouldExecuteFinally counter {counterShouldExecuteFinally} is invalid."); Assert.AreEqual(1, counterShouldExecuteFinally, $"ShouldExecuteFinally counter {counterShouldExecuteFinally} is invalid.");
} }
@ -270,10 +270,10 @@ namespace Tests {
CoroutineHandler.Start(IncrementCounter0Ever10Seconds()); CoroutineHandler.Start(IncrementCounter0Ever10Seconds());
CoroutineHandler.Start(IncrementCounter1Every5Seconds()); CoroutineHandler.Start(IncrementCounter1Every5Seconds());
SimulateTime(3); CoroutineHandler.Tick(3);
Assert.AreEqual(0, counter0, "Incorrect counter0 value after 3 seconds."); Assert.AreEqual(0, counter0, "Incorrect counter0 value after 3 seconds.");
Assert.AreEqual(0, counter1, "Incorrect counter1 value after 3 seconds."); Assert.AreEqual(0, counter1, "Incorrect counter1 value after 3 seconds.");
SimulateTime(3); CoroutineHandler.Tick(3);
Assert.AreEqual(0, counter0, "Incorrect counter0 value after 6 seconds."); Assert.AreEqual(0, counter0, "Incorrect counter0 value after 6 seconds.");
Assert.AreEqual(1, counter1, "Incorrect counter1 value after 6 seconds."); Assert.AreEqual(1, counter1, "Incorrect counter1 value after 6 seconds.");
@ -281,7 +281,7 @@ namespace Tests {
// increments 5 seconds after last yield. not 5 seconds since start. // increments 5 seconds after last yield. not 5 seconds since start.
// So the when we send 3 seconds in the last SimulateTime, // So the when we send 3 seconds in the last SimulateTime,
// the 3rd second was technically ignored. // the 3rd second was technically ignored.
SimulateTime(5); CoroutineHandler.Tick(5);
Assert.AreEqual(1, counter0, "Incorrect counter0 value after 10 seconds."); Assert.AreEqual(1, counter0, "Incorrect counter0 value after 10 seconds.");
Assert.AreEqual(2, counter1, "Incorrect counter1 value after next 5 seconds."); Assert.AreEqual(2, counter1, "Incorrect counter1 value after next 5 seconds.");
} }
@ -293,9 +293,9 @@ namespace Tests {
counter++; counter++;
}, "Bird"); }, "Bird");
SimulateTime(5); CoroutineHandler.Tick(5);
Assert.AreEqual(0, counter, "Incorrect counter value after 5 seconds."); Assert.AreEqual(0, counter, "Incorrect counter value after 5 seconds.");
SimulateTime(5); CoroutineHandler.Tick(5);
Assert.AreEqual(1, counter, "Incorrect counter value after 10 seconds."); Assert.AreEqual(1, counter, "Incorrect counter value after 10 seconds.");
Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value."); Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value.");
Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value."); Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value.");
@ -313,7 +313,7 @@ namespace Tests {
var cr = CoroutineHandler.Start(CoroutineTakesMax500Ms()); var cr = CoroutineHandler.Start(CoroutineTakesMax500Ms());
for (var i = 0; i < 5; i++) for (var i = 0; i < 5; i++)
SimulateTime(50); CoroutineHandler.Tick(50);
const int expected1 = 350; const int expected1 = 350;
const float errorbar1 = 5 / 100f * expected1; const float errorbar1 = 5 / 100f * expected1;
@ -328,9 +328,5 @@ namespace Tests {
Assert.IsTrue(gTc && lTd, $"Maximum Move Next Time {cr.MaxMoveNextTime.Milliseconds} is invalid."); Assert.IsTrue(gTc && lTd, $"Maximum Move Next Time {cr.MaxMoveNextTime.Milliseconds} is invalid.");
} }
private static void SimulateTime(double totalSeconds) {
CoroutineHandler.Tick(totalSeconds);
}
} }
} }