mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-02-02 04:19:46 +00:00
* Fix: Avoid timer scheduling too far events into short queue (#64242) Previously it was possible for events to enter the short queue when the timer is offset by more than BUCKET_LEN Now it is forced to schedule events into the second queue if the timer is processing slower then world time goes allowing the timer to keep up This PR provides a better definition of TIMER_MAX to avoid scheduling timed events that are more than one window of buckets away in terms of timeToRun into buckets queue and properly passing them into the second queue. Ports ss220-space/Paradise#578 Should be merged with/after #64138 Detailed explanation The timer subsystem mainly uses two concepts, buckets, and second queue Buckets is a fixed-length list of linked lists, where each "bucket" contains timers scheduled to run on the same tick The second queue is a simple list containing sorted timers that scheduled too far in future To process buckets, the timer uses two variables named head_offset and practical_offset head_offset determines the offset of the first bucket in time while practical_offset determines offset from bucket list beginning There are two equations responsible for determining where timed event would end up scheduled TIMER_MAX and BUCKET_POS TIMER_MAX determines the maximum value of timeToRun for timed event to schedule into buckets and not the second queue While BUCKET_POS determines where to put timed event relative to current head_offset Let's look at BUCKET_POS first BUCKET_POS(timer) = (((round((timer.timeToRun - SStimer.head_offset) / world.tick_lag)+1) % BUCKET_LEN)||BUCKET_LEN) Let's imagine we have our tick_lag set to 0.5, due to that we will have BUCKET_LEN = (10 / 0.5) * 60 = 1200 And head_offset of 100, that would make any timed event with timeToRun = 100 + 600N to get bucket_pos of 1 Now let's look at the current implementation of TIMER_MAX TIMER_MAX = (world.time + TICKS2DS(min(BUCKET_LEN-(SStimer.practical_offset-DS2TICKS(world.time - SStimer.head_offset))-1, BUCKET_LEN-1))) Let's say our world.time = 100 and practical_offset = 1 for now So TIMER_MAX = 100 + min(1200 - (1 - (100 - 100)/0.5) - 1, 1200 - 1) * 0.5 = 100 + 1198 * 0.5 = 699 As you might see, in that example we're fine and no events can be scheduled in buckets past boundary But let's now imagine a situation: some high priority subsystem lagged and caused the timer not to fire for a bit Now our world.time = 200 and practical_offset = 1 still So now our TIMER_MAX would be calculated as follow TIMER_MAX = 200 + min(Q, 1199) * 0.5 Where Q = 1200 - 1 - (1 - (200 - 100) / 0.5) = 1200 - 1 - 1 + (200 - 100) / 0.5 = 1398 Which is bigger then 1199, so we will choose 1199 instead TIMER_MAX = 200 + 599.5 = 799.5 Let's now schedule repetitive timed event with timeToRun = world.time + 500 It will be scheduled into buckets since, 700 < TIMER_MAX BUCKET_POS will be ((700 - 100) / 0.5 + 1) % 1200 = 1 Let's run the timer subsystem During the execution of that timer, we will try to reschedule it for the next fire at timeToRun = world.time + 500 Which would end up adding it in the same bucket we are currently processing, locking subsystem in a loop till suspending On next tick we will try to continue and will reschedule at timeToRun = world.time + 0.5 + 500 Which would end up in bucket 2, constantly blocking the timer from processing normally Why It's Good For The Game Increases chances of smooth experience Changelog cl Semoro fix: Avoid timer scheduling too far events into short queue /cl * Fix: Avoid timer scheduling too far events into short queue Co-authored-by: Aziz Chynaliev <azizonkg@gmail.com>