mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2025-12-13 03:02:38 +00:00
almost there
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
#define BUCKET_LEN (world.fps*1*60)
|
||||
/// Helper for getting the correct bucket for a given timer
|
||||
#define BUCKET_POS(timer) (((round((timer.timeToRun - SStimer.head_offset) / world.tick_lag)+1) % BUCKET_LEN)||BUCKET_LEN)
|
||||
/// <need description>
|
||||
/// Gets the maximum time at which timers will be invoked from buckets, used for deferring to secondary queue
|
||||
#define TIMER_MAX (world.time + TICKS2DS(min(BUCKET_LEN-(SStimer.practical_offset-DS2TICKS(world.time - SStimer.head_offset))-1, BUCKET_LEN-1)))
|
||||
/// Max float with integer precision
|
||||
#define TIMER_ID_MAX (2**24)
|
||||
@@ -11,10 +11,14 @@
|
||||
* # Timer Subsystem
|
||||
*
|
||||
* Handles creation, callbacks, and destruction of timed events.
|
||||
*
|
||||
* It is important to understand the 'buckets' used in the timer subsystem are really a timed event. Each bucket is a
|
||||
* circular doubly-linked list wherein the timed event at position n in the bucket list is a single timed event
|
||||
* in that bucket's circular list; this can be considered the entry point to that list.
|
||||
*/
|
||||
SUBSYSTEM_DEF(timer)
|
||||
name = "Timer"
|
||||
wait = 1 //SS_TICKER subsystem, so wait is in ticks
|
||||
wait = 1 // SS_TICKER subsystem, so wait is in ticks
|
||||
init_order = INIT_ORDER_TIMER
|
||||
priority = FIRE_PRIORITY_TIMER
|
||||
flags = SS_TICKER|SS_NO_INIT
|
||||
@@ -23,9 +27,9 @@ SUBSYSTEM_DEF(timer)
|
||||
var/list/datum/timedevent/second_queue = list()
|
||||
/// A hashlist dictionary used for storing unique timers
|
||||
var/list/hashes = list()
|
||||
/// world.time of the first entry in the bucket
|
||||
/// world.time of the first entry in the bucket list, effectively the 'start time' of the current buckets
|
||||
var/head_offset = 0
|
||||
/// Index of the first non-empty item in the bucket
|
||||
/// Index of the first non-empty bucket
|
||||
var/practical_offset = 1
|
||||
/// world.tick_lag the bucket was designed for
|
||||
var/bucket_resolution = 0
|
||||
@@ -53,16 +57,20 @@ SUBSYSTEM_DEF(timer)
|
||||
..("B:[bucket_count] P:[length(second_queue)] H:[length(hashes)] C:[length(clienttime_timers)] S:[length(timer_id_dict)]")
|
||||
|
||||
/datum/controller/subsystem/timer/fire(resumed = FALSE)
|
||||
// Store local references to datum vars as it is faster to access them
|
||||
var/lit = last_invoke_tick
|
||||
var/last_check = world.time - TICKS2DS(BUCKET_LEN*1.5)
|
||||
var/list/bucket_list = src.bucket_list
|
||||
var/last_check = world.time - TICKS2DS(BUCKET_LEN * 1.5)
|
||||
|
||||
// If there are no timers being tracked, then consider now to be the last invoked time
|
||||
if(!bucket_count)
|
||||
last_invoke_tick = world.time
|
||||
|
||||
// Check that we have invoked a callback in the last 1.5 minutes of BYOND time,
|
||||
// and throw a warning and reset buckets if this is true
|
||||
if(lit && lit < last_check && head_offset < last_check && last_invoke_warning < last_check)
|
||||
last_invoke_warning = world.time
|
||||
var/msg = "No regular timers processed in the last [BUCKET_LEN*1.5] ticks[bucket_auto_reset ? ", resetting buckets" : ""]!"
|
||||
var/msg = "No regular timers processed in the last [BUCKET_LEN * 1.5] ticks[bucket_auto_reset ? ", resetting buckets" : ""]!"
|
||||
message_admins(msg)
|
||||
WARNING(msg)
|
||||
if(bucket_auto_reset)
|
||||
@@ -75,7 +83,6 @@ SUBSYSTEM_DEF(timer)
|
||||
continue
|
||||
|
||||
log_world("Active timers at index [i]:")
|
||||
|
||||
var/datum/timedevent/bucket_node = bucket_head
|
||||
var/anti_loop_check = 1000
|
||||
do
|
||||
@@ -87,6 +94,7 @@ SUBSYSTEM_DEF(timer)
|
||||
for(var/I in second_queue)
|
||||
log_world(get_timer_debug_string(I))
|
||||
|
||||
// Process client-time timers
|
||||
var/next_clienttime_timer_index = 0
|
||||
for (next_clienttime_timer_index in 1 to length(clienttime_timers))
|
||||
if (MC_TICK_CHECK)
|
||||
@@ -99,7 +107,7 @@ SUBSYSTEM_DEF(timer)
|
||||
|
||||
var/datum/callback/callBack = ctime_timer.callBack
|
||||
if (!callBack)
|
||||
clienttime_timers.Cut(next_clienttime_timer_index,next_clienttime_timer_index+1)
|
||||
clienttime_timers.Cut(next_clienttime_timer_index,next_clienttime_timer_index + 1)
|
||||
CRASH("Invalid timer: [get_timer_debug_string(ctime_timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset], REALTIMEOFDAY: [REALTIMEOFDAY]")
|
||||
|
||||
ctime_timer.spent = REALTIMEOFDAY
|
||||
@@ -112,14 +120,14 @@ SUBSYSTEM_DEF(timer)
|
||||
else
|
||||
qdel(ctime_timer)
|
||||
|
||||
|
||||
// Remove invoked client-time timers
|
||||
if (next_clienttime_timer_index)
|
||||
clienttime_timers.Cut(1, next_clienttime_timer_index+1)
|
||||
|
||||
if (MC_TICK_CHECK)
|
||||
return
|
||||
|
||||
var/static/list/spent = list()
|
||||
var/static/list/invoked_timers = list()
|
||||
var/static/datum/timedevent/timer
|
||||
if (practical_offset > BUCKET_LEN)
|
||||
head_offset += TICKS2DS(BUCKET_LEN)
|
||||
@@ -146,7 +154,7 @@ SUBSYSTEM_DEF(timer)
|
||||
CRASH("Invalid timer: [get_timer_debug_string(timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]")
|
||||
|
||||
if (!timer.spent)
|
||||
spent += timer
|
||||
invoked_timers += timer
|
||||
timer.spent = world.time
|
||||
callBack.InvokeAsync()
|
||||
last_invoke_tick = world.time
|
||||
@@ -175,18 +183,18 @@ SUBSYSTEM_DEF(timer)
|
||||
|
||||
if (timer.callBack && !timer.spent)
|
||||
timer.callBack.InvokeAsync()
|
||||
spent += timer
|
||||
invoked_timers += timer
|
||||
bucket_count++
|
||||
else if(!QDELETED(timer))
|
||||
qdel(timer)
|
||||
continue
|
||||
|
||||
if (timer.timeToRun < head_offset + TICKS2DS(practical_offset-1))
|
||||
if (timer.timeToRun < head_offset + TICKS2DS(practical_offset - 1))
|
||||
bucket_resolution = null //force bucket recreation
|
||||
stack_trace("[i] Invalid timer state: Timer in long run queue that would require a backtrack to transfer to short run queue. [get_timer_debug_string(timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]")
|
||||
if (timer.callBack && !timer.spent)
|
||||
timer.callBack.InvokeAsync()
|
||||
spent += timer
|
||||
invoked_timers += timer
|
||||
bucket_count++
|
||||
else if(!QDELETED(timer))
|
||||
qdel(timer)
|
||||
@@ -210,12 +218,11 @@ SUBSYSTEM_DEF(timer)
|
||||
timer.prev.next = timer
|
||||
if (i)
|
||||
second_queue.Cut(1, i+1)
|
||||
|
||||
timer = null
|
||||
|
||||
bucket_count -= length(spent)
|
||||
|
||||
for (var/datum/timedevent/qtimer in spent)
|
||||
// Process each invoked timer
|
||||
bucket_count -= length(invoked_timers)
|
||||
for (var/datum/timedevent/qtimer in invoked_timers)
|
||||
if(QDELETED(qtimer))
|
||||
bucket_count++
|
||||
continue
|
||||
@@ -227,10 +234,11 @@ SUBSYSTEM_DEF(timer)
|
||||
qtimer.bucketEject()
|
||||
qtimer.timeToRun = (qtimer.flags & TIMER_CLIENT_TIME ? REALTIMEOFDAY : world.time) + qtimer.wait
|
||||
qtimer.bucketJoin()
|
||||
invoked_timers.len = 0
|
||||
|
||||
spent.len = 0
|
||||
|
||||
//formated this way to be runtime resistant
|
||||
/**
|
||||
* Generates a string with details about the timed event for debugging purposes
|
||||
*/
|
||||
/datum/controller/subsystem/timer/proc/get_timer_debug_string(datum/timedevent/TE)
|
||||
. = "Timer: [TE]"
|
||||
. += "Prev: [TE.prev ? TE.prev : "NULL"], Next: [TE.next ? TE.next : "NULL"]"
|
||||
@@ -241,12 +249,16 @@ SUBSYSTEM_DEF(timer)
|
||||
if(!TE.callBack)
|
||||
. += ", NO CALLBACK"
|
||||
|
||||
/**
|
||||
* Destroys the existing buckets and creates new buckets from the existing timed events
|
||||
*/
|
||||
/datum/controller/subsystem/timer/proc/reset_buckets()
|
||||
var/list/bucket_list = src.bucket_list
|
||||
var/list/bucket_list = src.bucket_list // Store local reference to datum var, this is faster
|
||||
var/list/alltimers = list()
|
||||
//collect the timers currently in the bucket
|
||||
|
||||
// Get all timers currently in the buckets
|
||||
for (var/bucket_head in bucket_list)
|
||||
if (!bucket_head)
|
||||
if (!bucket_head) // if bucket is empty for this tick
|
||||
continue
|
||||
var/datum/timedevent/bucket_node = bucket_head
|
||||
do
|
||||
@@ -254,25 +266,38 @@ SUBSYSTEM_DEF(timer)
|
||||
bucket_node = bucket_node.next
|
||||
while(bucket_node && bucket_node != bucket_head)
|
||||
|
||||
// Empty the list by zeroing and re-assigning the length
|
||||
bucket_list.len = 0
|
||||
bucket_list.len = BUCKET_LEN
|
||||
|
||||
// Reset values for the subsystem to their initial values
|
||||
practical_offset = 1
|
||||
bucket_count = 0
|
||||
head_offset = world.time
|
||||
bucket_resolution = world.tick_lag
|
||||
|
||||
// Add all timed events from the secondary queue as well
|
||||
alltimers += second_queue
|
||||
|
||||
// If there are no timers being tracked by the subsystem,
|
||||
// there is no need to do any further rebuilding
|
||||
if (!length(alltimers))
|
||||
return
|
||||
|
||||
// Sort all timers by time to run
|
||||
sortTim(alltimers, .proc/cmp_timer)
|
||||
|
||||
// Get the earliest timer, and if the TTR is earlier than the current world.time,
|
||||
// then set the head offset appropriately to be the earliest time tracked by the
|
||||
// current set of buckets
|
||||
var/datum/timedevent/head = alltimers[1]
|
||||
|
||||
if (head.timeToRun < head_offset)
|
||||
head_offset = head.timeToRun
|
||||
|
||||
// Iterate through each timed event and insert it into an appropriate bucket,
|
||||
// up unto the point that we can no longer insert into buckets as the TTR
|
||||
// is outside the range we are tracking, then insert the remainder into the
|
||||
// secondary queue
|
||||
var/new_bucket_count
|
||||
var/i = 1
|
||||
for (i in 1 to length(alltimers))
|
||||
@@ -280,34 +305,37 @@ SUBSYSTEM_DEF(timer)
|
||||
if (!timer)
|
||||
continue
|
||||
|
||||
var/bucket_pos = BUCKET_POS(timer)
|
||||
// Check that the TTR is within the range covered by buckets, when exceeded we've finished
|
||||
if (timer.timeToRun >= TIMER_MAX)
|
||||
i--
|
||||
break
|
||||
|
||||
|
||||
// Check that timer has a valid callback and hasn't been invoked
|
||||
if (!timer.callBack || timer.spent)
|
||||
WARNING("Invalid timer: [get_timer_debug_string(timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]")
|
||||
if (timer.callBack)
|
||||
qdel(timer)
|
||||
continue
|
||||
|
||||
// Insert the timer into the bucket, and perform necessary circular doubly-linked list operations
|
||||
new_bucket_count++
|
||||
var/bucket_pos = BUCKET_POS(timer)
|
||||
var/datum/timedevent/bucket_head = bucket_list[bucket_pos]
|
||||
if (!bucket_head)
|
||||
bucket_list[bucket_pos] = timer
|
||||
timer.next = null
|
||||
timer.prev = null
|
||||
continue
|
||||
|
||||
if (!bucket_head.prev)
|
||||
bucket_head.prev = bucket_head
|
||||
timer.next = bucket_head
|
||||
timer.prev = bucket_head.prev
|
||||
timer.next.prev = timer
|
||||
timer.prev.next = timer
|
||||
|
||||
// Cut the timers that are tracked by the buckets from the secondary queue
|
||||
if (i)
|
||||
alltimers.Cut(1, i+1)
|
||||
alltimers.Cut(1, i + 1)
|
||||
second_queue = alltimers
|
||||
bucket_count = new_bucket_count
|
||||
|
||||
@@ -318,17 +346,35 @@ SUBSYSTEM_DEF(timer)
|
||||
timer_id_dict |= SStimer.timer_id_dict
|
||||
bucket_list |= SStimer.bucket_list
|
||||
|
||||
/**
|
||||
* # Timed Event
|
||||
*
|
||||
* This is the actual timer, it contains the callback and necessary data to maintain
|
||||
* the timer.
|
||||
*
|
||||
* See the documentation for the timer subsystem for an explanation of the buckets referenced
|
||||
* below in next and prev
|
||||
*/
|
||||
/datum/timedevent
|
||||
/// ID used for timers when the TIMER_STOPPABLE flag is present
|
||||
var/id
|
||||
/// The callback to invoke after the timer completes
|
||||
var/datum/callback/callBack
|
||||
/// The time at which the callback should be invoked at
|
||||
var/timeToRun
|
||||
/// The length of the timer
|
||||
var/wait
|
||||
/// Unique hash generated when TIMER_UNIQUE flag is present
|
||||
var/hash
|
||||
/// Flags associated with the timer, see _DEFINES/subsystems.dm
|
||||
var/list/flags
|
||||
var/spent = 0 //time we ran the timer.
|
||||
var/name //for easy debugging.
|
||||
//cicular doublely linked list
|
||||
/// Time at which the timer was invoked or destroyed
|
||||
var/spent = 0
|
||||
/// An informative name generated for the timer, useful for bugging
|
||||
var/name
|
||||
/// Next timed event in the bucket
|
||||
var/datum/timedevent/next
|
||||
/// Previous timed event in the bucket
|
||||
var/datum/timedevent/prev
|
||||
|
||||
/datum/timedevent/New(datum/callback/callBack, wait, flags, hash)
|
||||
@@ -339,23 +385,27 @@ SUBSYSTEM_DEF(timer)
|
||||
src.flags = flags
|
||||
src.hash = hash
|
||||
|
||||
if (flags & TIMER_CLIENT_TIME)
|
||||
timeToRun = REALTIMEOFDAY + wait
|
||||
else
|
||||
timeToRun = world.time + wait
|
||||
// Determine time at which the timer's callback should be invoked
|
||||
timeToRun = (flags & TIMER_CLIENT_TIME ? REALTIMEOFDAY : world.time) + wait
|
||||
|
||||
// Include the timer in the hash table if the timer is unique
|
||||
if (flags & TIMER_UNIQUE)
|
||||
SStimer.hashes[hash] = src
|
||||
|
||||
// Generate ID for the timer if the timer is stoppable, include in the timer id dictionary
|
||||
if (flags & TIMER_STOPPABLE)
|
||||
id = num2text(nextid, 100)
|
||||
if (nextid >= SHORT_REAL_LIMIT)
|
||||
nextid += min(1, 2**round(nextid/SHORT_REAL_LIMIT))
|
||||
nextid += min(1, 2 ** round(nextid / SHORT_REAL_LIMIT))
|
||||
else
|
||||
nextid++
|
||||
SStimer.timer_id_dict[id] = src
|
||||
|
||||
name = "Timer: [id] (\ref[src]), TTR: [timeToRun], Flags: [jointext(bitfield2list(flags, list("TIMER_UNIQUE", "TIMER_OVERRIDE", "TIMER_CLIENT_TIME", "TIMER_STOPPABLE", "TIMER_NO_HASH_WAIT", "TIMER_LOOP")), ", ")], callBack: \ref[callBack], callBack.object: [callBack.object]\ref[callBack.object]([getcallingtype()]), callBack.delegate:[callBack.delegate]([callBack.arguments ? callBack.arguments.Join(", ") : ""])"
|
||||
// Generate debug-friendly name for timer
|
||||
var/static/list/bitfield_flags = list("TIMER_UNIQUE", "TIMER_OVERRIDE", "TIMER_CLIENT_TIME", "TIMER_STOPPABLE", "TIMER_NO_HASH_WAIT", "TIMER_LOOP")
|
||||
name = "Timer: [id] (\ref[src]), TTR: [timeToRun], Flags: [jointext(bitfield2list(flags, bitfield_flags), ", ")], \
|
||||
callBack: \ref[callBack], callBack.object: [callBack.object]\ref[callBack.object]([getcallingtype()]), \
|
||||
callBack.delegate:[callBack.delegate]([callBack.arguments ? callBack.arguments.Join(", ") : ""])"
|
||||
|
||||
if ((timeToRun < world.time || timeToRun < SStimer.head_offset) && !(flags & TIMER_CLIENT_TIME))
|
||||
CRASH("Invalid timer state: Timer created that would require a backtrack to run (addtimer would never let this happen): [SStimer.get_timer_debug_string(src)]")
|
||||
@@ -397,23 +447,39 @@ SUBSYSTEM_DEF(timer)
|
||||
prev = null
|
||||
return QDEL_HINT_IWILLGC
|
||||
|
||||
/**
|
||||
* Removes this timed event from any relevant buckets, or the secondary queue
|
||||
*/
|
||||
/datum/timedevent/proc/bucketEject()
|
||||
// Attempt to find bucket that contains this timed event
|
||||
var/bucketpos = BUCKET_POS(src)
|
||||
|
||||
// Store local references for the bucket list and secondary queue
|
||||
// This is faster than referencing them from the datum itself
|
||||
var/list/bucket_list = SStimer.bucket_list
|
||||
var/list/second_queue = SStimer.second_queue
|
||||
|
||||
// Attempt to get the head of the bucket
|
||||
var/datum/timedevent/buckethead
|
||||
if(bucketpos > 0)
|
||||
buckethead = bucket_list[bucketpos]
|
||||
|
||||
// Decrement the number of timers in buckets if the timed event is
|
||||
// the head of the bucket, or has a TTR less than TIMER_MAX implying it fits
|
||||
// into an existing bucket, or is otherwise not present in the secondary queue
|
||||
if(buckethead == src)
|
||||
bucket_list[bucketpos] = next
|
||||
SStimer.bucket_count--
|
||||
else if(timeToRun < TIMER_MAX || next || prev)
|
||||
else if(timeToRun < TIMER_MAX)
|
||||
SStimer.bucket_count--
|
||||
else
|
||||
var/l = length(second_queue)
|
||||
second_queue -= src
|
||||
if(l == length(second_queue))
|
||||
SStimer.bucket_count--
|
||||
|
||||
// Remove the timed event from the bucket, ensuring to maintain
|
||||
// the integrity of the bucket's list if relevant
|
||||
if(prev != next)
|
||||
prev.next = next
|
||||
next.prev = prev
|
||||
@@ -422,32 +488,41 @@ SUBSYSTEM_DEF(timer)
|
||||
next?.prev = null
|
||||
prev = next = null
|
||||
|
||||
/**
|
||||
* Attempts to add this timed event to a bucket, will enter the secondary queue
|
||||
* if there are no appropriate buckets at this time.
|
||||
*
|
||||
* Secondary queueing of timed events will occur when the timespan covered by the existing
|
||||
* buckets is exceeded by the time at which this timed event is scheduled to be invoked.
|
||||
* If the timed event is tracking client time, it will be added to a special bucket.
|
||||
*/
|
||||
/datum/timedevent/proc/bucketJoin()
|
||||
// Check if this timed event should be diverted to the client time bucket, or the secondary queue
|
||||
var/list/L
|
||||
|
||||
if (flags & TIMER_CLIENT_TIME)
|
||||
L = SStimer.clienttime_timers
|
||||
else if (timeToRun >= TIMER_MAX)
|
||||
L = SStimer.second_queue
|
||||
|
||||
if(L)
|
||||
BINARY_INSERT(src, L, datum/timedevent, src, timeToRun, COMPARE_KEY)
|
||||
return
|
||||
|
||||
//get the list of buckets
|
||||
// Get a local reference to the bucket list, this is faster than referencing the datum
|
||||
var/list/bucket_list = SStimer.bucket_list
|
||||
|
||||
//calculate our place in the bucket list
|
||||
// Find the correct bucket for this timed event
|
||||
var/bucket_pos = BUCKET_POS(src)
|
||||
|
||||
//get the bucket for our tick
|
||||
var/datum/timedevent/bucket_head = bucket_list[bucket_pos]
|
||||
SStimer.bucket_count++
|
||||
//empty bucket, we will just add ourselves
|
||||
|
||||
// If there is no timed event at this position, then the bucket is 'empty'
|
||||
// and we can just set this event to that position
|
||||
if (!bucket_head)
|
||||
bucket_list[bucket_pos] = src
|
||||
return
|
||||
//other wise, lets do a simplified linked list add.
|
||||
|
||||
// Otherwise, we merely add this timed event into the bucket, which is a
|
||||
// circularly doubly-linked list
|
||||
if (!bucket_head.prev)
|
||||
bucket_head.prev = bucket_head
|
||||
next = bucket_head
|
||||
@@ -455,7 +530,9 @@ SUBSYSTEM_DEF(timer)
|
||||
next.prev = src
|
||||
prev.next = src
|
||||
|
||||
///Returns a string of the type of the callback for this timer
|
||||
/**
|
||||
* Returns a string of the type of the callback for this timer
|
||||
*/
|
||||
/datum/timedevent/proc/getcallingtype()
|
||||
. = "ERROR"
|
||||
if (callBack.object == GLOBAL_PROC)
|
||||
@@ -486,8 +563,8 @@ SUBSYSTEM_DEF(timer)
|
||||
if(wait >= INFINITY)
|
||||
CRASH("Attempted to create timer with INFINITY delay")
|
||||
|
||||
// Generate hash if relevant for timed events with the TIMER_UNIQUE flag
|
||||
var/hash
|
||||
|
||||
if (flags & TIMER_UNIQUE)
|
||||
var/list/hashlist = list(callback.object, "([REF(callback.object)])", callback.delegate, flags & TIMER_CLIENT_TIME)
|
||||
if(!(flags & TIMER_NO_HASH_WAIT))
|
||||
@@ -497,11 +574,11 @@ SUBSYSTEM_DEF(timer)
|
||||
|
||||
var/datum/timedevent/hash_timer = SStimer.hashes[hash]
|
||||
if(hash_timer)
|
||||
if (hash_timer.spent) //it's pending deletion, pretend it doesn't exist.
|
||||
hash_timer.hash = null //but keep it from accidentally deleting us
|
||||
if (hash_timer.spent) // it's pending deletion, pretend it doesn't exist.
|
||||
hash_timer.hash = null // but keep it from accidentally deleting us
|
||||
else
|
||||
if (flags & TIMER_OVERRIDE)
|
||||
hash_timer.hash = null //no need having it delete it's hash if we are going to replace it
|
||||
hash_timer.hash = null // no need having it delete it's hash if we are going to replace it
|
||||
qdel(hash_timer)
|
||||
else
|
||||
if (hash_timer.flags & TIMER_STOPPABLE)
|
||||
|
||||
Reference in New Issue
Block a user