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 Wait current;
internal Event Event => this.current.Event;
internal bool IsWaitingForEvent => this.Event != null;
/// <summary>
/// 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"/>.
@ -79,18 +82,14 @@ namespace Coroutine {
}
internal bool Tick(double deltaSeconds) {
if (!this.WasCanceled) {
if (this.current.Tick(deltaSeconds))
if (!this.WasCanceled && this.current.Tick(deltaSeconds))
this.MoveNext();
}
return this.IsFinished;
}
internal bool OnEvent(Event evt) {
if (!this.WasCanceled) {
if (this.current.OnEvent(evt))
if (!this.WasCanceled && Equals(this.current.Event, evt))
this.MoveNext();
}
return this.IsFinished;
}
@ -112,10 +111,6 @@ namespace Coroutine {
return true;
}
internal bool IsWaitingForEvent() {
return this.current.IsWaitingForEvent();
}
/// <summary>
/// A delegate method used by <see cref="ActiveCoroutine.OnFinished"/>.
/// </summary>

View file

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

View file

@ -2,19 +2,19 @@ using System;
namespace Coroutine {
/// <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>
public struct Wait {
private readonly Event evt;
internal readonly Event Event;
private double seconds;
/// <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>
/// <param name="evt">The event to wait for</param>
public Wait(Event evt) {
this.evt = evt;
this.Event = evt;
this.seconds = 0;
}
@ -24,7 +24,7 @@ namespace Coroutine {
/// <param name="seconds">The amount of seconds to wait for</param>
public Wait(double seconds) {
this.seconds = seconds;
this.evt = null;
this.Event = null;
}
/// <summary>
@ -40,13 +40,5 @@ namespace Coroutine {
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.InvokeLater(new Wait(10), () => {
CoroutineHandler.InvokeLater(new Wait(5), () => {
Console.WriteLine("Raising test event");
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);
```
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
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(0, cr.Priority, "Default priority is not minimum");
for (var i = 0; i < 5; i++)
SimulateTime(1);
CoroutineHandler.Tick(1);
Assert.AreEqual(2, counter, "instruction after yield is not executed.");
Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value.");
Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value.");
@ -49,7 +49,7 @@ namespace Tests {
cr[0] = CoroutineHandler.Start(OnTimeTickNeverReturnYield());
cr[1] = CoroutineHandler.Start(OnTimeTickYieldBreak());
for (var i = 0; i < 5; i++)
SimulateTime(1);
CoroutineHandler.Tick(1);
Assert.AreEqual(3, counter, "Incorrect counter value.");
for (var i = 0; i < cr.Length; i++) {
@ -71,7 +71,7 @@ namespace Tests {
var cr = CoroutineHandler.Start(OnTimeTickYieldDefault());
for (var i = 0; i < 5; i++)
SimulateTime(1);
CoroutineHandler.Tick(1);
Assert.AreEqual(2, counter, "Incorrect counter value.");
Assert.AreEqual(true, cr.IsFinished, "Incorrect IsFinished value.");
@ -97,7 +97,7 @@ namespace Tests {
var cr = CoroutineHandler.Start(OnTimerTickInfinite());
cr.OnFinished += SetCounterToUnreachableValue;
for (var i = 0; i < 50; i++)
SimulateTime(1);
CoroutineHandler.Tick(1);
Assert.AreEqual(51, counter, "Incorrect counter value.");
Assert.AreEqual(false, cr.IsFinished, "Incorrect IsFinished value.");
@ -126,7 +126,7 @@ namespace Tests {
var cr = CoroutineHandler.Start(OnTimeTick());
cr.OnFinished += SetCounterToUnreachableValue;
SimulateTime(50);
CoroutineHandler.Tick(50);
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, counterParent, "Parent 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, counterGrandParent, "Grand 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.");
SimulateTime(1);
CoroutineHandler.Tick(1);
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, counterParent, "Parent 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(1, counterGrandParent, "Grand 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.");
SimulateTime(1);
CoroutineHandler.Tick(1);
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, counterParent, "Parent counter is invalid at time 4.");
@ -243,7 +243,7 @@ namespace Tests {
CoroutineHandler.Start(ShouldExecuteAfter());
CoroutineHandler.Start(ShouldExecuteBefore0(), priority: highPriority);
CoroutineHandler.Start(ShouldExecuteFinally(), priority: -1);
SimulateTime(10);
CoroutineHandler.Tick(10);
Assert.AreEqual(1, counterShouldExecuteAfter, $"ShouldExecuteAfter counter {counterShouldExecuteAfter} is invalid.");
Assert.AreEqual(1, counterShouldExecuteFinally, $"ShouldExecuteFinally counter {counterShouldExecuteFinally} is invalid.");
}
@ -270,10 +270,10 @@ namespace Tests {
CoroutineHandler.Start(IncrementCounter0Ever10Seconds());
CoroutineHandler.Start(IncrementCounter1Every5Seconds());
SimulateTime(3);
CoroutineHandler.Tick(3);
Assert.AreEqual(0, counter0, "Incorrect counter0 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(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.
// So the when we send 3 seconds in the last SimulateTime,
// the 3rd second was technically ignored.
SimulateTime(5);
CoroutineHandler.Tick(5);
Assert.AreEqual(1, counter0, "Incorrect counter0 value after 10 seconds.");
Assert.AreEqual(2, counter1, "Incorrect counter1 value after next 5 seconds.");
}
@ -293,9 +293,9 @@ namespace Tests {
counter++;
}, "Bird");
SimulateTime(5);
CoroutineHandler.Tick(5);
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(true, cr.IsFinished, "Incorrect IsFinished value.");
Assert.AreEqual(false, cr.WasCanceled, "Incorrect IsCanceled value.");
@ -313,7 +313,7 @@ namespace Tests {
var cr = CoroutineHandler.Start(CoroutineTakesMax500Ms());
for (var i = 0; i < 5; i++)
SimulateTime(50);
CoroutineHandler.Tick(50);
const int expected1 = 350;
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.");
}
private static void SimulateTime(double totalSeconds) {
CoroutineHandler.Tick(totalSeconds);
}
}
}