mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-17 21:48:58 +03:00
<!-- This is a semi-strict format, you can add/remove sections as needed but the order/format should be kept the same Remove these comments before submitting --> # Excerpt <!-- Explain this PR in as much detail as applicable Some example prompts to consider: How might this affect the game? The codebase? What might be some alternatives to this? How/Who does this benefit/hurt [the game/codebase]? --> thou shall be thy axe that continues the work - to behead all `EntityQueryEnumerator`s, and thus i require it. 'tis imperative i have at thee and perform the duties. if the plague bearing shitcode is not cut at thy roots, it shall only nest a deeper grave and spread locusts. --- # Description It has been a reoccuring theme that someone irresponsibly uses `EntityQueryEnumerator` and then suddenly server performance is worse. A lot of these cases involve using EQE to iterate a timer on a component, to start or stop an effect after a delay. Rather than iterating `frames * n` times per second FOR EVERY UNIQUE SYSTEM THAT DOES THIS (QUITE A FEW), we instead iterate `frames` per second regardless of systems using the Event Scheduler. The Event Scheduler itself is a list of events that wait to be triggered after a delay. Rather than iterating through all of them, they are sorted in order of occurance using a PriorityQueue. I love priority queues because they sort as you enqueue to them, and apparently the sort complexity is logarithmic? But mostly because of the former. I chose to write the scheduler the way I did because the choice to use async seems too big for me alone. So this system is synchronous and updates on game time. This is mostly a practical optimisation. The code which I have written is almost certainly not optimal, but the simple act of replacing EQE delays will significantly improve server performance anyway. Rust monsters feel free to rewrite the event scheduler to be more performant. NOTE: For some reason PriorityQueue is banned on the client, and configuring it requires editing a RobustToolbox file. So for now this system is restricted to Content.Server until we start using our engine fork. --- # TODO <!-- A list of everything you have to do before this PR is "complete" You probably won't have to complete everything before merging but it's good to leave future references --> - [x] Working queue of events ordered by execution times - [x] A function to enqueue any-event-defined-ever into the scheduler with a delay - [x] Delay the event, and then fire it - [x] Implement retroactive schedule cancelling In a future PR: - Add ```System.Collection.Generic.PriorityQueue`2``` to whitelist in `RobustToolbox/Robust.Server/ContentPack/Sandbox.yml` - - Shared files had to be relocated to server because they were banned on the client and would cause an exception. - Investigate insert performance as more systems are added to use the EventScheduler - - MLGTASTICa rose the idea of 'buckets' as an optimisation. - - I theorise multiple priority queues for different types of events might also work. --- <!-- This is default collapsed, readers click to expand it and see all your media The PR media section can get very large at times, so this is a good way to keep it clean The title is written using HTML tags The title must be within the <summary> tags or you won't see it --> <details><summary><h1>Media</h1></summary> <p> Demonstration: https://youtu.be/CGB6SDWGc-Q Response to MLGTASTICa, stress testing: https://youtu.be/30OA06Pzhtk </p> </details> --- # Changelog <!-- You can add an author after the `🆑` to change the name that appears in the changelog (ex: `🆑 Death`) Leaving it blank will default to your GitHub display name This includes all available types for the changelog --> - add: Added the EventScheduler system that lets you raise an event at a certain time or after a delay without killing performance. - fix: Optimised EMP by migrating from EQE to the new EventScheduler system.
184 lines
7.2 KiB
C#
184 lines
7.2 KiB
C#
using Content.Shared.EventScheduler;
|
|
using Robust.Shared.Timing;
|
|
|
|
namespace Content.Server.EventScheduler;
|
|
|
|
public sealed class EventSchedulerSystem : SharedEventSchedulerSystem
|
|
{
|
|
//TODO: move server files to shared after System.Collection.Generic.PriorityQueue`2 is whitelisted in sandbox.yml in RobustToolbox
|
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
|
|
private uint _id = 0;
|
|
private uint NextId() { return _id++; }
|
|
|
|
private Dictionary<uint, DelayedEvent> _eventDict = new();
|
|
private static PriorityQueue<uint, TimeSpan> _eventQueue = new(_comparer);
|
|
private static EventSchedulerComparer _comparer = new();
|
|
|
|
private void Enqueue(DelayedEvent delayedEvent, TimeSpan time)
|
|
{
|
|
_eventDict.Add(delayedEvent.Id, delayedEvent);
|
|
_eventQueue.Enqueue(delayedEvent.Id, time);
|
|
}
|
|
|
|
private void Dequeue(out DelayedEvent? delayedEvent)
|
|
{
|
|
var id = _eventQueue.Dequeue();
|
|
delayedEvent = _eventDict[id];
|
|
_eventDict.Remove(id);
|
|
}
|
|
|
|
private void Dequeue()
|
|
{
|
|
Dequeue(out _);
|
|
}
|
|
|
|
private bool TryRequeue(DelayedEvent delayedEvent, TimeSpan time, bool useDelay = false)
|
|
{
|
|
var curId = delayedEvent.Id;
|
|
|
|
// if we can't get the event for whatever reason, consider the requeuing a failure
|
|
if (!_eventDict.TryGetValue(curId, out _))
|
|
{
|
|
Log.Warning($"Couldn't reschedule event for {delayedEvent.Uid}, missing a value!");
|
|
|
|
return false;
|
|
}
|
|
|
|
// if we cannot remove the event for whatever reason, consider requeuing a failure
|
|
if (!_eventQueue.Remove(curId, out _, out var originalTime))
|
|
{
|
|
Log.Warning($"Couldn't reschedule event for {delayedEvent.Uid}, failed to remove!");
|
|
|
|
return false;
|
|
}
|
|
|
|
// if we the delay behaviour, consider the original time as the starting point and our input as a delay
|
|
if (useDelay)
|
|
time += originalTime;
|
|
|
|
// finally requeue
|
|
_eventQueue.Enqueue(curId, time);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wraps an Event to be raised at a specific time in the future.
|
|
/// </summary>
|
|
/// <typeparam name="TEvent"></typeparam>
|
|
/// <param name="uid">The EntityUid which the Event will be raised for.</param>
|
|
/// <param name="eventArgs">The Event to be passed to the scheduler.</param>
|
|
/// <param name="time">The time at which the Event will be raised.</param>
|
|
/// <returns>A DelayedEvent instance. Keep this if you want to conditionally reschedule your Event.</returns>
|
|
public DelayedEvent ScheduleEvent<TEvent>(EntityUid uid, ref TEvent eventArgs, TimeSpan time)
|
|
where TEvent : notnull
|
|
{
|
|
var delayedEvent = new DelayedEvent(NextId(), uid, eventArgs);
|
|
Enqueue(delayedEvent, time);
|
|
|
|
Log.Debug($"Scheduled event: '{eventArgs.GetType()}' at uid: ({uid}) for time: ({time})");
|
|
|
|
return delayedEvent;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wraps an Event to be raised after a time has elapsed.
|
|
/// </summary>
|
|
/// <typeparam name="TEvent"></typeparam>
|
|
/// <param name="uid">The EntityUid which the Event will be raised for.</param>
|
|
/// <param name="eventArgs">The Event to be passed to the scheduler.</param>
|
|
/// <param name="delay">A delay after which the Event will be raised.</param>
|
|
/// <returns>A DelayedEvent instance. Keep this if you want to conditionally reschedule your Event.</returns>
|
|
public DelayedEvent DelayEvent<TEvent>(EntityUid uid, ref TEvent eventArgs, TimeSpan delay)
|
|
where TEvent : notnull
|
|
{
|
|
return ScheduleEvent(uid, ref eventArgs, _gameTiming.CurTime + delay);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Takes an existing DelayedEvent and reschedules it to a specific time, as long as it hasn't already been raised yet.
|
|
/// </summary>
|
|
/// <typeparam name="TEvent"></typeparam>
|
|
/// <param name="delayedEvent">The DelayedEvent instance which you want to reschedule.</param>
|
|
/// <param name="time">The new time at which the Event will be raised.</param>
|
|
/// <returns>Returns true if the DelayedEvent exists, false otherwise.</returns>
|
|
public bool TryRescheduleDelayedEvent(DelayedEvent delayedEvent, TimeSpan time)
|
|
{
|
|
if (!TryRequeue(delayedEvent, time))
|
|
return false;
|
|
|
|
Log.Debug($"Rescheduled event: '{delayedEvent.EventArgs.GetType()}' at uid: ({delayedEvent.Uid}) for time: ({time})");
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Takes an existing DelayedEvent and postpones it by a certain time, as long as it hasn't already been raised yet.
|
|
/// </summary>
|
|
/// <typeparam name="TEvent"></typeparam>
|
|
/// <param name="delayedEvent">The DelayedEvent instance which you want to reschedule.</param>
|
|
/// <param name="delay">A delay that is added to the Event's scheduled raise time.</param>
|
|
/// <returns>Returns true if the DelayedEvent exists, false otherwise.</returns>
|
|
public bool TryPostponeDelayedEvent(DelayedEvent delayedEvent, TimeSpan delay)
|
|
{
|
|
if (!TryRequeue(delayedEvent, delay, true))
|
|
return false;
|
|
|
|
Log.Debug($"Postponed event: '{delayedEvent.EventArgs.GetType()}' at uid: ({delayedEvent.Uid}) by delay: ({delay})");
|
|
return true;
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
|
|
/*
|
|
huh?? weren't you supposed to make an optimised system to replace frameTime?
|
|
- no, this replaces EQE, uses frametime to stay in sync with game tick
|
|
but it should only iterate 1/frame if no events occur on the frame
|
|
REGARDLESS OF HOW MANY SYSTEMS USE IT <-------
|
|
*/
|
|
const uint failsafe = 1000;
|
|
uint iterationCount = 0;
|
|
while (true)
|
|
{
|
|
// this should never happen
|
|
iterationCount++;
|
|
if (iterationCount >= failsafe)
|
|
{
|
|
Log.Warning($"Event processing hit safety limit of {failsafe} events in one frame - possible infinite loop detected!");
|
|
break;
|
|
}
|
|
|
|
// mostly a getter for values we're dealing with, if there are no queued events break
|
|
if (!_eventQueue.TryPeek(out var index, out var time)
|
|
|| !_eventDict.TryGetValue(index, out var current))
|
|
break;
|
|
|
|
// if the pointed event has been cancelled, get the next event
|
|
if (current.Cancelled)
|
|
{
|
|
Dequeue();
|
|
|
|
Log.Debug($"Cancelled event '{current.EventArgs.GetType()}' at uid: ({current.Uid})!");
|
|
continue;
|
|
}
|
|
|
|
// if the pointed event can be triggered, raise it and get the next event
|
|
// this is in case >1 event is raised at the same time, allowing them to trigger on the same frame
|
|
if (_gameTiming.CurTime >= time)
|
|
{
|
|
Dequeue();
|
|
|
|
try { RaiseLocalEvent(current.Uid, current.EventArgs); }
|
|
catch (Exception ex) { Log.Error($"Error processing event for entity {current.Uid}: {ex}"); }
|
|
|
|
Log.Debug($"Raised event '{current.EventArgs.GetType()}' at uid: ({current.Uid})!");
|
|
continue;
|
|
}
|
|
|
|
// exit loop if nothing happens
|
|
break;
|
|
}
|
|
}
|
|
}
|