Just a few minutes ago, I committed some changes to Dive’s scheduler. No longer does the scheduler execute all expiring tasks willy-nilly; now it actually thinks!
Now the scheduler can delay the execution of tasks if previous tasks took up too much of the game loop. The scheduler is fed the time of the next expected frame (update call), and runs tasks until it is either out of tasks to run or it hits the next frame.
That isn’t all, though. The scheduler exhibits a few properties:
Not guaranteed to execute tasks on-time
Because the scheduler can defer execution of tasks when it runs out of time in the current frame, tasks might not always execute as soon as they expire.
Guaranteed to execute tasks after they expire
…but tasks will always execute after their expiry, never before.
Guaranteed to run at least one task per frame
There will always be at least one task that runs in a frame.
Task execution order is not guaranteed
Just because task a was scheduled before task b and they both expire at the same time, task b might execute before task a.
Tasks that were meant to expire a long time ago will be executed before tasks that just expired
To make sure a task doesn’t get held off forever, tasks are sorted by when they expired before the scheduler tries to execute them.
Testing the new scheduler
I tested out this new scheduler, and got some interesting results. Note that this test doesn’t give “real” results, because I can’t imagine anyone using this situation.
Before I wrote the new scheduling system, I used Dive’s new scripting system to write this bit of code:
alias(repeater, "timer(\"1\",\"repeater() repeater()\")") repeater
This creates an alias (see: function) which spawns timers at 1-second intervals that in turn spawn 2 more timers and so on and so forth.
When running, the old scheduler would freeze the engine after around 25 seconds and 30,000 scheduled tasks. The new scheduler, on the other hand, doesn’t freeze the engine at all and simply slows down the execution of the tasks, such that after about 25 seconds and 69,000 tasks the engine slows down to about 10 FPS but doesn’t crash (at least until it runs completely out of memory, which I have not left it open to do).
Update: It seems the slowdown isn’t even caused directly by the scheduler, but by .net’s memory management. Sometimes it takes 30 seconds and 70,000 tasks to bring the engine to just 30 FPS, but sometimes it starts at 65,000 and gets to 10 FPS.
Interestingly enough, the TPS (“ticks per second” or number of update calls per second) is barely affected, and the engine instead sacrifices draw calls. I realized later that this due to the way that the game loop is designed. When the engine starts coming under heavy load, it drops draw calls in favor of update calls.
If you have the debug overlay open, you will notice a “FrameSkip” value that when the engine is running normally is zero but when the engine is under load will usually hit up to 5. This is the number of frames per second that are being skipped. You can set a maximum for this value (which, when hit, will force a frame to be drawn) in config/engine.ini (the default is 5).
That’s all for now!