From d0b0dd9a46ea5a4fc2bf9877d246de22c98d170c Mon Sep 17 00:00:00 2001 From: Selis <12716288+ItsSelis@users.noreply.github.com> Date: Sat, 26 Oct 2024 13:27:22 +0200 Subject: [PATCH] Timer subsystem update (#16509) --- code/__defines/_helpers.dm | 3 + code/__defines/_lists.dm | 28 - code/__defines/math.dm | 2 + code/__defines/misc.dm | 1 - code/__defines/subsystems.dm | 120 ++- code/_helpers/_lists.dm | 58 ++ .../configuration/entries/general.dm | 3 + code/controllers/subsystems/timer.dm | 685 +++++++++++------- code/datums/datum.dm | 42 +- config/example/logging.txt | 3 + 10 files changed, 622 insertions(+), 323 deletions(-) diff --git a/code/__defines/_helpers.dm b/code/__defines/_helpers.dm index b2165492bd5..ffd7df34160 100644 --- a/code/__defines/_helpers.dm +++ b/code/__defines/_helpers.dm @@ -12,3 +12,6 @@ #define ICON_SIZE_X 32 /// The Y/Height dimension of ICON_SIZE. This will more than likely be the smaller axis. #define ICON_SIZE_Y 32 + +/// Takes a datum as input, returns its ref string +#define text_ref(datum) ref(datum) diff --git a/code/__defines/_lists.dm b/code/__defines/_lists.dm index 582c026f3c8..ebf923672dd 100644 --- a/code/__defines/_lists.dm +++ b/code/__defines/_lists.dm @@ -46,32 +46,4 @@ #define reverseList(L) reverseRange(L.Copy()) -// binary search sorted insert -// IN: Object to be inserted -// LIST: List to insert object into -// TYPECONT: The typepath of the contents of the list -// COMPARE: The variable on the objects to compare -#define BINARY_INSERT(IN, LIST, TYPECONT, COMPARE) \ - var/__BIN_CTTL = length(LIST);\ - if(!__BIN_CTTL) {\ - LIST += IN;\ - } else {\ - var/__BIN_LEFT = 1;\ - var/__BIN_RIGHT = __BIN_CTTL;\ - var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ - var/##TYPECONT/__BIN_ITEM;\ - while(__BIN_LEFT < __BIN_RIGHT) {\ - __BIN_ITEM = LIST[__BIN_MID];\ - if(__BIN_ITEM.##COMPARE <= IN.##COMPARE) {\ - __BIN_LEFT = __BIN_MID + 1;\ - } else {\ - __BIN_RIGHT = __BIN_MID;\ - };\ - __BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ - };\ - __BIN_ITEM = LIST[__BIN_MID];\ - __BIN_MID = __BIN_ITEM.##COMPARE > IN.##COMPARE ? __BIN_MID : __BIN_MID + 1;\ - LIST.Insert(__BIN_MID, IN);\ - } - #define islist(L) istype(L, /list) diff --git a/code/__defines/math.dm b/code/__defines/math.dm index f4539605f22..f3ad19e1093 100644 --- a/code/__defines/math.dm +++ b/code/__defines/math.dm @@ -28,6 +28,8 @@ #define ROUND_UP(x) ( -round(-(x))) #define CEILING(x, y) ( -round(-(x) / (y)) * (y) ) +#define ROUND_UP(x) ( -round(-(x))) + // round() acts like floor(x, 1) by default but can't handle other values #define FLOOR(x, y) ( round((x) / (y)) * (y) ) diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm index 270235992b7..3d2e7bdd028 100644 --- a/code/__defines/misc.dm +++ b/code/__defines/misc.dm @@ -31,7 +31,6 @@ #define MAX_CLIENT_FPS 200 // Some arbitrary defines to be used by self-pruning global lists. (see master_controller) -#define PROCESS_KILL 26 // Used to trigger removal from a processing list. #define MAX_GEAR_COST 15 // Used in chargen for accessory loadout limit. // For secHUDs and medHUDs and variants. The number is the location of the image on the list hud_list of humans. diff --git a/code/__defines/subsystems.dm b/code/__defines/subsystems.dm index b2838f79a2e..533d2c810c2 100644 --- a/code/__defines/subsystems.dm +++ b/code/__defines/subsystems.dm @@ -1,33 +1,78 @@ -//Timing subsystem -//Don't run if there is an identical unique timer active -//if the arguments to addtimer are the same as an existing timer, it doesn't create a new timer, and returns the id of the existing timer -#define TIMER_UNIQUE (1<<0) -//For unique timers: Replace the old timer rather then not start this one -#define TIMER_OVERRIDE (1<<1) -//Timing should be based on how timing progresses on clients, not the sever. -// tracking this is more expensive, -// should only be used in conjuction with things that have to progress client side, such as animate() or sound() -#define TIMER_CLIENT_TIME (1<<2) -//Timer can be stopped using deltimer() -#define TIMER_STOPPABLE (1<<3) -//To be used with TIMER_UNIQUE -//prevents distinguishing identical timers with the wait variable -#define TIMER_NO_HASH_WAIT (1<<4) -//Loops the timer repeatedly until qdeleted -//In most cases you want a subsystem instead -#define TIMER_LOOP (1<<5) +//! Defines for subsystems and overlays +//! +//! Lots of important stuff in here, make sure you have your brain switched on +//! when editing this file +//! ## Timing subsystem +/** + * Don't run if there is an identical unique timer active + * + * if the arguments to addtimer are the same as an existing timer, it doesn't create a new timer, + * and returns the id of the existing timer + */ +#define TIMER_UNIQUE (1<<0) + +///For unique timers: Replace the old timer rather then not start this one +#define TIMER_OVERRIDE (1<<1) + +/** + * Timing should be based on how timing progresses on clients, not the server. + * + * Tracking this is more expensive, + * should only be used in conjunction with things that have to progress client side, such as + * animate() or sound() + */ +#define TIMER_CLIENT_TIME (1<<2) + +///Timer can be stopped using deltimer() +#define TIMER_STOPPABLE (1<<3) + +///prevents distinguishing identical timers with the wait variable +/// +///To be used with TIMER_UNIQUE +#define TIMER_NO_HASH_WAIT (1<<4) + +///Loops the timer repeatedly until qdeleted +/// +///In most cases you want a subsystem instead, so don't use this unless you have a good reason +#define TIMER_LOOP (1<<5) + +///Delete the timer on parent datum Destroy() and when deltimer'd +#define TIMER_DELETE_ME (1<<6) + +///Empty ID define #define TIMER_ID_NULL -1 -#define INITIALIZATION_INSSATOMS 0 //New should not call Initialize -#define INITIALIZATION_INNEW_MAPLOAD 1 //New should call Initialize(TRUE) -#define INITIALIZATION_INNEW_REGULAR 2 //New should call Initialize(FALSE) +/// Used to trigger object removal from a processing list +#define PROCESS_KILL 26 -#define INITIALIZE_HINT_NORMAL 0 //Nothing happens -#define INITIALIZE_HINT_LATELOAD 1 //Call LateInitialize -#define INITIALIZE_HINT_QDEL 2 //Call qdel on the atom -//type and all subtypes should always call Initialize in New() +//! ## Initialization subsystem + +///New should not call Initialize +#define INITIALIZATION_INSSATOMS 0 +///New should call Initialize(TRUE) +#define INITIALIZATION_INNEW_MAPLOAD 1 +///New should call Initialize(FALSE) +#define INITIALIZATION_INNEW_REGULAR 2 + +//! ### Initialization hints + +///Nothing happens +#define INITIALIZE_HINT_NORMAL 0 +/** + * call LateInitialize at the end of all atom Initialization + * + * The item will be added to the late_loaders list, this is iterated over after + * initialization of subsystems is complete and calls LateInitalize on the atom + * see [this file for the LateIntialize proc](atom.html#proc/LateInitialize) + */ +#define INITIALIZE_HINT_LATELOAD 1 + +///Call qdel on the atom after initialization +#define INITIALIZE_HINT_QDEL 2 + +///type and all subtypes should always immediately call Initialize in New() #define INITIALIZE_IMMEDIATE(X) ##X/New(loc, ...){\ ..();\ if(!initialized) {\ @@ -51,27 +96,27 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G //! ### SS initialization hints /** - * Negative values incidate a failure or warning of some kind, positive are good. - * 0 and 1 are unused so that TRUE and FALSE are guarenteed to be invalid values. + * Negative values indicate a failure or warning of some kind, positive are good. + * 0 and 1 are unused so that TRUE and FALSE are guaranteed to be invalid values. */ /// Subsystem failed to initialize entirely. Print a warning, log, and disable firing. #define SS_INIT_FAILURE -2 -/// The default return value which must be overriden. Will succeed with a warning. +/// The default return value which must be overridden. Will succeed with a warning. #define SS_INIT_NONE -1 -/// Subsystem initialized sucessfully. +/// Subsystem initialized successfully. #define SS_INIT_SUCCESS 2 -/// Successful, but don't print anything. Useful if subsystem was disabled. +/// If your system doesn't need to be initialized (by being disabled or something) #define SS_INIT_NO_NEED 3 //! ### SS initialization load orders - // Subsystem init_order, from highest priority to lowest priority // Subsystems shutdown in the reverse of the order they initialize in // The numbers just define the ordering, they are meaningless otherwise. + #define INIT_ORDER_WEBHOOKS 50 #define INIT_ORDER_SQLITE 40 #define INIT_ORDER_GARBAGE 39 @@ -112,8 +157,6 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G #define INIT_ORDER_STATPANELS -98 #define INIT_ORDER_CHAT -100 //Should be last to ensure chat remains smooth during init. - - // Subsystem fire priority, from lowest to highest priority // If the subsystem isn't listed here it's either DEFAULT or PROCESS (if it's a processing subsystem child) #define FIRE_PRIORITY_PLAYERTIPS 5 @@ -144,4 +187,15 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G #define FIRE_PRIORITY_STATPANEL 390 #define FIRE_PRIORITY_CHAT 400 #define FIRE_PRIORITY_OVERLAYS 500 +#define FIRE_PRIORITY_TIMER 700 #define FIRE_PRIORITY_INPUT 1000 // This must always always be the max highest priority. Player input must never be lost. + +/** + Create a new timer and add it to the queue. + * Arguments: + * * callback the callback to call on timer finish + * * wait deciseconds to run the timer for + * * flags flags for this timer, see: code\__DEFINES\subsystems.dm + * * timer_subsystem the subsystem to insert this timer into +*/ +#define addtimer(args...) _addtimer(args, file = __FILE__, line = __LINE__) diff --git a/code/_helpers/_lists.dm b/code/_helpers/_lists.dm index e245198f810..cb8c9898459 100644 --- a/code/_helpers/_lists.dm +++ b/code/_helpers/_lists.dm @@ -895,3 +895,61 @@ var/global/list/json_cache = list() #define UNTYPED_LIST_ADD(list, item) (list += LIST_VALUE_WRAP_LISTS(item)) ///Remove an untyped item to a list, taking care to handle list items by wrapping them in a list to remove the footgun #define UNTYPED_LIST_REMOVE(list, item) (list -= LIST_VALUE_WRAP_LISTS(item)) + +/// Passed into BINARY_INSERT to compare keys +#define COMPARE_KEY __BIN_LIST[__BIN_MID] +/// Passed into BINARY_INSERT to compare values +#define COMPARE_VALUE __BIN_LIST[__BIN_LIST[__BIN_MID]] + +/**** + * Binary search sorted insert + * INPUT: Object to be inserted + * LIST: List to insert object into + * TYPECONT: The typepath of the contents of the list + * COMPARE: The object to compare against, usualy the same as INPUT + * COMPARISON: The variable on the objects to compare + * COMPTYPE: How should the values be compared? Either COMPARE_KEY or COMPARE_VALUE. + */ +#define BINARY_INSERT(INPUT, LIST, TYPECONT, COMPARE, COMPARISON, COMPTYPE) \ + do {\ + var/list/__BIN_LIST = LIST;\ + var/__BIN_CTTL = length(__BIN_LIST);\ + if(!__BIN_CTTL) {\ + __BIN_LIST += INPUT;\ + } else {\ + var/__BIN_LEFT = 1;\ + var/__BIN_RIGHT = __BIN_CTTL;\ + var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + var ##TYPECONT/__BIN_ITEM;\ + while(__BIN_LEFT < __BIN_RIGHT) {\ + __BIN_ITEM = COMPTYPE;\ + if(__BIN_ITEM.##COMPARISON <= COMPARE.##COMPARISON) {\ + __BIN_LEFT = __BIN_MID + 1;\ + } else {\ + __BIN_RIGHT = __BIN_MID;\ + };\ + __BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + };\ + __BIN_ITEM = COMPTYPE;\ + __BIN_MID = __BIN_ITEM.##COMPARISON > COMPARE.##COMPARISON ? __BIN_MID : __BIN_MID + 1;\ + __BIN_LIST.Insert(__BIN_MID, INPUT);\ + };\ + } while(FALSE) + +///Converts a bitfield to a list of numbers (or words if a wordlist is provided) +/proc/bitfield_to_list(bitfield = 0, list/wordlist) + var/list/return_list = list() + if(islist(wordlist)) + var/max = min(wordlist.len, 24) + var/bit = 1 + for(var/i in 1 to max) + if(bitfield & bit) + return_list += wordlist[i] + bit = bit << 1 + else + for(var/bit_number = 0 to 23) + var/bit = 1 << bit_number + if(bitfield & bit) + return_list += bit + + return return_list diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm index 70b568bb760..981e59067f0 100644 --- a/code/controllers/configuration/entries/general.dm +++ b/code/controllers/configuration/entries/general.dm @@ -60,6 +60,9 @@ /// logs graffiti /datum/config_entry/flag/log_graffiti +/// logs all timers in buckets on automatic bucket reset (Useful for timer debugging) +/datum/config_entry/flag/log_timers_on_bucket_reset + // FIXME: Unused ///datum/config_entry/string/nudge_script_path // where the nudge.py script is located // default = "nudge.py" diff --git a/code/controllers/subsystems/timer.dm b/code/controllers/subsystems/timer.dm index fc43b81fd39..c314fa7d38b 100644 --- a/code/controllers/subsystems/timer.dm +++ b/code/controllers/subsystems/timer.dm @@ -1,32 +1,54 @@ -#define BUCKET_LEN (round(10*(60/world.tick_lag), 1)) //how many ticks should we keep in the bucket. (1 minutes worth) -#define BUCKET_POS(timer) (((round((timer.timeToRun - SStimer.head_offset) / world.tick_lag)+1) % BUCKET_LEN)||BUCKET_LEN) -#define TIMER_MAX (world.time + TICKS2DS(min(BUCKET_LEN-(SStimer.practical_offset-DS2TICKS(world.time - SStimer.head_offset))-1, BUCKET_LEN-1))) -#define TIMER_ID_MAX (2**24) //max float with integer precision +/// Controls how many buckets should be kept, each representing a tick. (1 minutes worth) +#define BUCKET_LEN (world.fps*1*60) +/// Helper for getting the correct bucket for a given timer +#define BUCKET_POS(timer) (((ROUND_UP((timer.timeToRun - timer.timer_subsystem.head_offset) / world.tick_lag)+1) % BUCKET_LEN) || BUCKET_LEN) +/// Gets the maximum time at which timers will be invoked from buckets, used for deferring to secondary queue +#define TIMER_MAX(timer_ss) (timer_ss.head_offset + TICKS2DS(BUCKET_LEN + timer_ss.practical_offset - 1)) +/** + * # Timer Subsystem + * + * Handles creation, callbacks, and destruction of timed events. + * + * It is important to understand the buckets used in the timer subsystem are just a series of doubly-linked + * lists. The object at a given index in bucket_list is a /datum/timedevent, the head of a list, which has prev + * and next references for the respective elements in that bucket's 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 - var/list/datum/timedevent/second_queue = list() //awe, yes, you've had first queue, but what about second queue? + /// Queue used for storing timers that do not fit into the current buckets + var/list/datum/timedevent/second_queue = list() + /// A hashlist dictionary used for storing unique timers var/list/hashes = list() - - var/head_offset = 0 //world.time of the first entry in the the bucket. - var/practical_offset = 1 //index of the first non-empty item in the bucket. - var/bucket_resolution = 0 //world.tick_lag the bucket was designed for - var/bucket_count = 0 //how many timers are in the buckets - - var/list/bucket_list = list() //list of buckets, each bucket holds every timer that has to run that byond tick. - - var/list/timer_id_dict = list() //list of all active timers assoicated to their timer id (for easy lookup) - - var/list/clienttime_timers = list() //special snowflake timers that run on fancy pansy "client time" - + /// 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 wrap around pivot for buckets. buckets before this are later running buckets wrapped around from the end of the bucket list. + var/practical_offset = 1 + /// world.tick_lag the bucket was designed for + var/bucket_resolution = 0 + /// How many timers are in the buckets + var/bucket_count = 0 + /// List of buckets, each bucket holds every timer that has to run that byond tick + var/list/bucket_list = list() + /// List of all active timers associated to their timer ID (for easy lookup) + var/list/timer_id_dict = list() + /// Special timers that run in real-time, not BYOND time; these are more expensive to run and maintain + var/list/clienttime_timers = list() + /// Contains the last time that a timer's callback was invoked, or the last tick the SS fired if no timers are being processed var/last_invoke_tick = 0 + /// Keeps track of the next index to work on for client timers + var/next_clienttime_timer_index = 0 + /// Contains the last time that a warning was issued for not invoking callbacks var/static/last_invoke_warning = 0 + /// Boolean operator controlling if the timer SS will automatically reset buckets if it fails to invoke callbacks for an extended period of time var/static/bucket_auto_reset = TRUE + /// How many times bucket was reset + var/bucket_reset_count = 0 /datum/controller/subsystem/timer/PreInit() bucket_list.len = BUCKET_LEN @@ -34,48 +56,59 @@ SUBSYSTEM_DEF(timer) bucket_resolution = world.tick_lag /datum/controller/subsystem/timer/stat_entry(msg) - msg = "B:[bucket_count] P:[length(second_queue)] H:[length(hashes)] C:[length(clienttime_timers)] S:[length(timer_id_dict)]" + msg = "B:[bucket_count] P:[length(second_queue)] H:[length(hashes)] C:[length(clienttime_timers)] S:[length(timer_id_dict)] RST:[bucket_reset_count]" return ..() -/datum/controller/subsystem/timer/fire(resumed = FALSE) - var/lit = last_invoke_tick - var/last_check = world.time - TICKS2DS(BUCKET_LEN*1.5) - var/list/bucket_list = src.bucket_list - - if(!bucket_count) - last_invoke_tick = world.time - - 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" : ""]!" - message_admins(msg) - WARNING(msg) - if(bucket_auto_reset) - bucket_resolution = 0 - - log_world("Timer bucket reset. world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") +/datum/controller/subsystem/timer/proc/dump_timer_buckets(full = TRUE) + var/list/to_log = list("Timer bucket reset. world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") + if (full) for (var/i in 1 to length(bucket_list)) var/datum/timedevent/bucket_head = bucket_list[i] if (!bucket_head) continue - log_world("Active timers at index [i]:") - + to_log += "Active timers at index [i]:" var/datum/timedevent/bucket_node = bucket_head - var/anti_loop_check = 1000 + var/anti_loop_check = 1 do - log_world(get_timer_debug_string(bucket_node)) + to_log += get_timer_debug_string(bucket_node) bucket_node = bucket_node.next anti_loop_check-- while(bucket_node && bucket_node != bucket_head && anti_loop_check) - log_world("Active timers in the second_queue queue:") + + to_log += "Active timers in the second_queue queue:" for(var/I in second_queue) - log_world(get_timer_debug_string(I)) + to_log += get_timer_debug_string(I) - var/next_clienttime_timer_index = 0 - var/len = length(clienttime_timers) + // Dump all the logged data to the world log + log_world(to_log.Join("\n")) - for (next_clienttime_timer_index in 1 to len) +/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/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" : ""]!" + message_admins(msg) + WARNING(msg) + if(bucket_auto_reset) + bucket_resolution = 0 + dump_timer_buckets(CONFIG_GET(flag/log_timers_on_bucket_reset)) + + // Process client-time timers + if (next_clienttime_timer_index) + clienttime_timers.Cut(1, next_clienttime_timer_index+1) + next_clienttime_timer_index = 0 + for (next_clienttime_timer_index in 1 to length(clienttime_timers)) if (MC_TICK_CHECK) next_clienttime_timer_index-- break @@ -86,142 +119,106 @@ SUBSYSTEM_DEF(timer) var/datum/callback/callBack = ctime_timer.callBack if (!callBack) - 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]") + 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 callBack.InvokeAsync() - if(ctime_timer.flags & TIMER_LOOP) + if(ctime_timer.flags & TIMER_LOOP) // Re-insert valid looping client timers into the client timer list. + if (QDELETED(ctime_timer)) // Don't re-insert timers deleted inside their callbacks. + continue ctime_timer.spent = 0 ctime_timer.timeToRun = REALTIMEOFDAY + ctime_timer.wait - BINARY_INSERT(ctime_timer, clienttime_timers, datum/timedevent, timeToRun) + BINARY_INSERT(ctime_timer, clienttime_timers, /datum/timedevent, ctime_timer, timeToRun, COMPARE_KEY) else qdel(ctime_timer) - + // Remove invoked client-time timers if (next_clienttime_timer_index) clienttime_timers.Cut(1, next_clienttime_timer_index+1) + next_clienttime_timer_index = 0 - if (MC_TICK_CHECK) - return - - var/static/list/spent = list() - var/static/datum/timedevent/timer + // Check for when we need to loop the buckets, this occurs when + // the head_offset is approaching BUCKET_LEN ticks in the past if (practical_offset > BUCKET_LEN) head_offset += TICKS2DS(BUCKET_LEN) practical_offset = 1 resumed = FALSE + // Check for when we have to reset buckets, typically from auto-reset if ((length(bucket_list) != BUCKET_LEN) || (world.tick_lag != bucket_resolution)) reset_buckets() bucket_list = src.bucket_list resumed = FALSE - if (!resumed) - timer = null - - while (practical_offset <= BUCKET_LEN && head_offset + ((practical_offset-1)*world.tick_lag) <= world.time) - var/datum/timedevent/head = bucket_list[practical_offset] - if (!timer || !head || timer == head) - head = bucket_list[practical_offset] - timer = head - while (timer) + // Iterate through each bucket starting from the practical offset + while (practical_offset <= BUCKET_LEN && head_offset + ((practical_offset - 1) * world.tick_lag) <= world.time) + var/datum/timedevent/timer + while ((timer = bucket_list[practical_offset])) var/datum/callback/callBack = timer.callBack if (!callBack) - bucket_resolution = null //force bucket recreation - CRASH("Invalid timer: [get_timer_debug_string(timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") + stack_trace("Invalid timer: [get_timer_debug_string(timer)] world.time: [world.time], \ + head_offset: [head_offset], practical_offset: [practical_offset], bucket_joined: [timer.bucket_joined]") + if (!timer.spent) + bucket_resolution = null // force bucket recreation + return + timer.bucketEject() //pop the timer off of the bucket list. + + // Invoke callback if possible if (!timer.spent) - spent += timer timer.spent = world.time callBack.InvokeAsync() last_invoke_tick = world.time - if (MC_TICK_CHECK) - return - - timer = timer.next - if (timer == head) - break - - - bucket_list[practical_offset++] = null - - //we freed up a bucket, lets see if anything in second_queue needs to be shifted to that bucket. - var/i = 0 - var/L = length(second_queue) - for (i in 1 to L) - timer = second_queue[i] - if (timer.timeToRun >= TIMER_MAX) - i-- - break - - if (timer.timeToRun < head_offset) - bucket_resolution = null //force bucket recreation - stack_trace("[i] Invalid timer state: Timer in long run queue with a time to run less then head_offset. [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 - bucket_count++ - else if(!QDELETED(timer)) - qdel(timer) - continue - - 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 - bucket_count++ - else if(!QDELETED(timer)) - qdel(timer) - continue - - bucket_count++ - var/bucket_pos = max(1, 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 - if (i) - second_queue.Cut(1, i+1) - - timer = null - - bucket_count -= length(spent) - - for(var/datum/timedevent/qtimer as anything in spent) - if(QDELETED(qtimer)) - bucket_count++ - continue - if(!(qtimer.flags & TIMER_LOOP)) - qdel(qtimer) - else - bucket_count++ - qtimer.spent = 0 - qtimer.bucketEject() - if(qtimer.flags & TIMER_CLIENT_TIME) - qtimer.timeToRun = REALTIMEOFDAY + qtimer.wait + if (timer.flags & TIMER_LOOP) // Prepare valid looping timers to re-enter the queue + if(QDELETED(timer)) // If a loop is deleted in its callback, we need to avoid re-inserting it. + continue + timer.spent = 0 + timer.timeToRun = world.time + timer.wait + timer.bucketJoin() else - qtimer.timeToRun = world.time + qtimer.wait - qtimer.bucketJoin() + qdel(timer) - spent.len = 0 + if (MC_TICK_CHECK) + break -//formated this way to be runtime resistant + if (!bucket_list[practical_offset]) + // Empty the bucket, check if anything in the secondary queue should be shifted to this bucket + bucket_list[practical_offset] = null // Just in case + practical_offset++ + var/i = 0 + for (i in 1 to length(second_queue)) + timer = second_queue[i] + if (timer.timeToRun >= TIMER_MAX(src)) + i-- + break + + // Check for timers that are scheduled to run in the past + if (timer.timeToRun < head_offset) + bucket_resolution = null // force bucket recreation + stack_trace("[i] Invalid timer state: Timer in long run queue with a time to run less then head_offset. \ + [get_timer_debug_string(timer)] world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") + break + + // Check for timers that are not capable of being scheduled to run without rebuilding buckets + 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]") + break + + timer.bucketJoin() + if (i) + second_queue.Cut(1, i+1) + if (MC_TICK_CHECK) + break + +/** + * 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"]" @@ -232,12 +229,19 @@ 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 + WARNING("Timer buckets has been reset, this may cause timer to lag") + bucket_reset_count++ + + 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 @@ -245,25 +249,45 @@ 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 + + for (var/datum/timedevent/t as anything in alltimers) + t.timer_subsystem = src // Recovered timers need to be reparented + t.bucket_joined = FALSE + t.bucket_pos = -1 + t.prev = null + t.next = null + + // 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, GLOBAL_PROC_REF(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)) @@ -271,19 +295,25 @@ SUBSYSTEM_DEF(timer) if (!timer) continue - var/bucket_pos = BUCKET_POS(timer) - if (timer.timeToRun >= TIMER_MAX) + // Check that the TTR is within the range covered by buckets, when exceeded we've finished + if (timer.timeToRun >= TIMER_MAX(src)) 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]") + 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 doubly-linked list operations new_bucket_count++ + var/bucket_pos = BUCKET_POS(timer) + timer.bucket_pos = bucket_pos + timer.bucket_joined = TRUE + var/datum/timedevent/bucket_head = bucket_list[bucket_pos] if (!bucket_head) bucket_list[bucket_pos] = timer @@ -291,89 +321,126 @@ SUBSYSTEM_DEF(timer) timer.prev = null continue - if (!bucket_head.prev) - bucket_head.prev = bucket_head + bucket_head.prev = timer timer.next = bucket_head - timer.prev = bucket_head.prev - timer.next.prev = timer - timer.prev.next = timer + timer.prev = null + bucket_list[bucket_pos] = 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 /datum/controller/subsystem/timer/Recover() - second_queue |= SStimer.second_queue - hashes |= SStimer.hashes - timer_id_dict |= SStimer.timer_id_dict - bucket_list |= SStimer.bucket_list + // Find the current timer sub-subsystem in global and recover its buckets etc + var/datum/controller/subsystem/timer/timerSS = null + for(var/global_var in global.vars) + if (istype(global.vars[global_var],src.type)) + timerSS = global.vars[global_var] + hashes = timerSS.hashes + timer_id_dict = timerSS.timer_id_dict + + bucket_list = timerSS.bucket_list + second_queue = timerSS.second_queue + + // The buckets are FUBAR + reset_buckets() + +/** + * # 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 + /// The source of the timedevent, whatever called addtimer + var/source + /// 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 + /// Holds info about this timer, stored from the moment it was created + /// Used to create a visible "name" whenever the timer is stringified + var/list/timer_info + /// Next timed event in the bucket var/datum/timedevent/next + /// Previous timed event in the bucket var/datum/timedevent/prev + /// The timer subsystem this event is associated with + var/datum/controller/subsystem/timer/timer_subsystem + /// Boolean indicating if timer joined into bucket + var/bucket_joined = FALSE + /// Initial bucket position + var/bucket_pos = -1 -/datum/timedevent/New(datum/callback/callBack, wait, flags, hash) +/datum/timedevent/New(datum/callback/callBack, wait, flags, datum/controller/subsystem/timer/timer_subsystem, hash, source) var/static/nextid = 1 id = TIMER_ID_NULL src.callBack = callBack src.wait = wait src.flags = flags src.hash = hash + src.source = source + src.timer_subsystem = timer_subsystem || SStimer - 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 + timer_subsystem.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 >= TIMER_ID_MAX) - nextid += min(1, 2**round(nextid/TIMER_ID_MAX)) + if (nextid >= SHORT_REAL_LIMIT) + nextid += min(1, 2 ** round(nextid / SHORT_REAL_LIMIT)) else nextid++ - SStimer.timer_id_dict[id] = src + timer_subsystem.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(", ") : ""])" - - if ((timeToRun < world.time || timeToRun < SStimer.head_offset) && !(flags & TIMER_CLIENT_TIME)) + if ((timeToRun < world.time || timeToRun < timer_subsystem.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)]") if (callBack.object != GLOBAL_PROC && !QDESTROYING(callBack.object)) - LAZYADD(callBack.object.active_timers, src) + LAZYADD(callBack.object._active_timers, src) bucketJoin() /datum/timedevent/Destroy() ..() if (flags & TIMER_UNIQUE && hash) - SStimer.hashes -= hash + timer_subsystem.hashes -= hash - if (callBack && callBack.object && callBack.object != GLOBAL_PROC && callBack.object.active_timers) - callBack.object.active_timers -= src - UNSETEMPTY(callBack.object.active_timers) + if (callBack && callBack.object && callBack.object != GLOBAL_PROC && callBack.object._active_timers) + callBack.object._active_timers -= src + UNSETEMPTY(callBack.object._active_timers) callBack = null if (flags & TIMER_STOPPABLE) - SStimer.timer_id_dict -= id + timer_subsystem.timer_id_dict -= id if (flags & TIMER_CLIENT_TIME) if (!spent) spent = world.time - SStimer.clienttime_timers -= src + timer_subsystem.clienttime_timers -= src return QDEL_HINT_IWILLGC if (!spent) @@ -388,64 +455,143 @@ 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() - var/bucketpos = BUCKET_POS(src) - var/list/bucket_list = SStimer.bucket_list - var/list/second_queue = SStimer.second_queue + // Store local references for the bucket list and secondary queue + // This is faster than referencing them from the datum itself + var/list/bucket_list = timer_subsystem.bucket_list + var/list/second_queue = timer_subsystem.second_queue + + // Attempt to get the head of the bucket var/datum/timedevent/buckethead - if(bucketpos > 0) - buckethead = bucket_list[bucketpos] + if(bucket_pos > 0) + buckethead = bucket_list[bucket_pos] + + // 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) - SStimer.bucket_count-- + bucket_list[bucket_pos] = next + timer_subsystem.bucket_count-- + else if(bucket_joined) + timer_subsystem.bucket_count-- else var/l = length(second_queue) second_queue -= src if(l == length(second_queue)) - SStimer.bucket_count-- - if(prev != next) + timer_subsystem.bucket_count-- + + // Remove the timed event from the bucket, ensuring to maintain + // the integrity of the bucket's list if relevant + if (prev && prev.next == src) prev.next = next + if (next && next.prev == src) next.prev = prev - else - prev?.next = null - next?.prev = null prev = next = null + bucket_pos = -1 + bucket_joined = FALSE +/datum/timedevent/proc/operator""() + if(!length(timer_info)) + return "Event not filled" + var/static/list/bitfield_flags = list("TIMER_UNIQUE", "TIMER_OVERRIDE", "TIMER_CLIENT_TIME", "TIMER_STOPPABLE", "TIMER_NO_HASH_WAIT", "TIMER_LOOP") +#if defined(TIMER_DEBUG) + var/list/callback_args = timer_info[10] + return "Timer: [timer_info[1]] ([text_ref(src)]), TTR: [timer_info[2]], wait:[timer_info[3]] Flags: [jointext(bitfield_to_list(timer_info[4], bitfield_flags), ", ")], \ + callBack: [text_ref(timer_info[5])], callBack.object: [timer_info[6]][timer_info[7]]([timer_info[8]]), \ + callBack.delegate:[timer_info[9]]([callback_args ? callback_args.Join(", ") : ""]), source: [timer_info[11]]" +#else + return "Timer: [timer_info[1]] ([text_ref(src)]), TTR: [timer_info[2]], wait:[timer_info[3]] Flags: [jointext(bitfield_to_list(timer_info[4], bitfield_flags), ", ")], \ + callBack: [text_ref(timer_info[5])], callBack.object: [timer_info[6]]([timer_info[7]]), \ + callBack.delegate:[timer_info[8]], source: [timer_info[9]]" +#endif + +/** + * 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() +#if defined(TIMER_DEBUG) + // Generate debug-friendly list for timer, more complex but also more expensive + timer_info = list( + 1 = id, + 2 = timeToRun, + 3 = wait, + 4 = flags, + 5 = callBack, /* Safe to hold this directly because it's never del'd */ + 6 = "[callBack.object]", + 7 = text_ref(callBack.object), + 8 = getcallingtype(), + 9 = callBack.delegate, + 10 = callBack.arguments ? callBack.arguments.Copy() : null, + 11 = "[source]" + ) +#else + // Generate a debuggable list for the timer, simpler but wayyyy cheaper, string generation (and ref/copy memes) is a bitch and this saves a LOT of time + timer_info = list( + 1 = id, + 2 = timeToRun, + 3 = wait, + 4 = flags, + 5 = callBack, /* Safe to hold this directly because it's never del'd */ + 6 = "[callBack.object]", + 7 = getcallingtype(), + 8 = callBack.delegate, + 9 = "[source]" + ) +#endif + + if (bucket_joined) + stack_trace("Bucket already joined! [src]") + + // 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 - + L = timer_subsystem.clienttime_timers + else if (timeToRun >= TIMER_MAX(timer_subsystem)) + L = timer_subsystem.second_queue if(L) - BINARY_INSERT(src, L, datum/timedevent, timeToRun) + BINARY_INSERT(src, L, /datum/timedevent, src, timeToRun, COMPARE_KEY) return - //get the list of buckets - var/list/bucket_list = SStimer.bucket_list + // Get a local reference to the bucket list, this is faster than referencing the datum + var/list/bucket_list = timer_subsystem.bucket_list - //calculate our place in the bucket list - var/bucket_pos = BUCKET_POS(src) + // Find the correct bucket for this timed event + bucket_pos = BUCKET_POS(src) + + if (bucket_pos < timer_subsystem.practical_offset && timeToRun < (timer_subsystem.head_offset + TICKS2DS(BUCKET_LEN))) + WARNING("Bucket pos in past: bucket_pos = [bucket_pos] < practical_offset = [timer_subsystem.practical_offset] \ + && timeToRun = [timeToRun] < [timer_subsystem.head_offset + TICKS2DS(BUCKET_LEN)], Timer: [src]") + bucket_pos = timer_subsystem.practical_offset // Recover bucket_pos to avoid timer blocking queue - //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 + timer_subsystem.bucket_count++ + + // 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_joined = TRUE bucket_list[bucket_pos] = src return - //other wise, lets do a simplified linked list add. - if (!bucket_head.prev) - bucket_head.prev = bucket_head - next = bucket_head - prev = bucket_head.prev - next.prev = src - prev.next = src + // Otherwise, we merely add this timed event into the bucket, which is a + // doubly-linked list + bucket_joined = TRUE + bucket_head.prev = src + next = bucket_head + prev = null + bucket_list[bucket_pos] = src + +/** + * Returns a string of the type of the callback for this timer + */ /datum/timedevent/proc/getcallingtype() . = "ERROR" if (callBack.object == GLOBAL_PROC) @@ -453,62 +599,82 @@ SUBSYSTEM_DEF(timer) else . = "[callBack.object.type]" -/proc/addtimer(datum/callback/callback, wait = 0, flags = 0) - if (!callback) - CRASH("addtimer called without a callback") +/** + * Create a new timer and insert it in the queue. + * You should not call this directly, and should instead use the addtimer macro, which includes source information. + * + * Arguments: + * * callback the callback to call on timer finish + * * wait deciseconds to run the timer for + * * flags flags for this timer, see: code\__DEFINES\subsystems.dm + * * timer_subsystem the subsystem to insert this timer into + */ +/proc/_addtimer(datum/callback/callback, wait = 0, flags = 0, datum/controller/subsystem/timer/timer_subsystem, file, line) + ASSERT(istype(callback), "addtimer called [callback ? "with an invalid callback ([callback])" : "without a callback"]") + ASSERT(isnum(wait), "addtimer called with a non-numeric wait ([wait])") if (wait < 0) stack_trace("addtimer called with a negative wait. Converting to [world.tick_lag]") if (callback.object != GLOBAL_PROC && QDELETED(callback.object) && !QDESTROYING(callback.object)) - stack_trace("addtimer called with a callback assigned to a qdeleted object. In the future such timers will not be supported and may refuse to run or run with a 0 wait") + stack_trace("addtimer called with a callback assigned to a qdeleted object. In the future such timers will not \ + be supported and may refuse to run or run with a 0 wait") - wait = max(CEILING(wait, world.tick_lag), world.tick_lag) + if (flags & TIMER_CLIENT_TIME) // REALTIMEOFDAY has a resolution of 1 decisecond + wait = max(CEILING(wait, 1), 1) // so if we use tick_lag timers may be inserted in the "past" + else + wait = max(CEILING(wait, world.tick_lag), world.tick_lag) if(wait >= INFINITY) CRASH("Attempted to create timer with INFINITY delay") - var/hash + timer_subsystem = timer_subsystem || SStimer + // Generate hash if relevant for timed events with the TIMER_UNIQUE flag + var/hash if (flags & TIMER_UNIQUE) - var/list/hashlist - if(flags & TIMER_NO_HASH_WAIT) - hashlist = list(callback.object, "(\ref[callback.object])", callback.delegate, flags & TIMER_CLIENT_TIME) - else - hashlist = list(callback.object, "(\ref[callback.object])", callback.delegate, wait, flags & TIMER_CLIENT_TIME) + var/list/hashlist = list(callback.object, "([REF(callback.object)])", callback.delegate, flags & TIMER_CLIENT_TIME) + if(!(flags & TIMER_NO_HASH_WAIT)) + hashlist += wait hashlist += callback.arguments hash = hashlist.Join("|||||||") - var/datum/timedevent/hash_timer = SStimer.hashes[hash] + var/datum/timedevent/hash_timer = timer_subsystem.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 its hash if we are going to replace it qdel(hash_timer) else if (hash_timer.flags & TIMER_STOPPABLE) . = hash_timer.id return else if(flags & TIMER_OVERRIDE) - stack_trace("TIMER_OVERRIDE used without TIMER_UNIQUE") + stack_trace("TIMER_OVERRIDE used without TIMER_UNIQUE") //this is also caught by grep. - var/datum/timedevent/timer = new(callback, wait, flags, hash) + var/datum/timedevent/timer = new(callback, wait, flags, timer_subsystem, hash, file && "[file]:[line]") return timer.id -/proc/deltimer(id) +/** + * Delete a timer + * + * Arguments: + * * id a timerid or a /datum/timedevent + */ +/proc/deltimer(id, datum/controller/subsystem/timer/timer_subsystem) if (!id) return FALSE if (id == TIMER_ID_NULL) CRASH("Tried to delete a null timerid. Use TIMER_STOPPABLE flag") - if (!istext(id)) - if (istype(id, /datum/timedevent)) - qdel(id) - return TRUE + if (istype(id, /datum/timedevent)) + qdel(id) + return TRUE + timer_subsystem = timer_subsystem || SStimer //id is string - var/datum/timedevent/timer = SStimer.timer_id_dict[id] - if (timer && !timer.spent) + var/datum/timedevent/timer = timer_subsystem.timer_id_dict[id] + if (timer && (!timer.spent || timer.flags & TIMER_DELETE_ME)) qdel(timer) return TRUE return FALSE @@ -534,7 +700,32 @@ SUBSYSTEM_DEF(timer) return null return timer.timeToRun - (timer.flags & TIMER_CLIENT_TIME ? REALTIMEOFDAY : world.time) +/** + * Update the delay on an existing LOOPING timer + * Will come into effect on the next process + * + * Arguments: + * * id a timerid or a /datum/timedevent + * * new_wait the new wait to give this looping timer + */ +/proc/updatetimedelay(id, new_wait, datum/controller/subsystem/timer/timer_subsystem) + if (!id) + return + if (id == TIMER_ID_NULL) + CRASH("Tried to update the wait of null timerid. Use TIMER_STOPPABLE flag") + if (istype(id, /datum/timedevent)) + var/datum/timedevent/timer = id + timer.wait = new_wait + return + timer_subsystem = timer_subsystem || SStimer + //id is string + var/datum/timedevent/timer = timer_subsystem.timer_id_dict[id] + if(!timer || timer.spent) + return + if(!(timer.flags & TIMER_LOOP)) + CRASH("Tried to update the wait of a non looping timer. This is not supported") + timer.wait = new_wait + #undef BUCKET_LEN #undef BUCKET_POS #undef TIMER_MAX -#undef TIMER_ID_MAX diff --git a/code/datums/datum.dm b/code/datums/datum.dm index a50909a3b47..2c37b78811b 100644 --- a/code/datums/datum.dm +++ b/code/datums/datum.dm @@ -22,7 +22,7 @@ var/list/open_uis /// Active timers with this datum as the target - var/list/active_timers + var/list/_active_timers /// Status traits attached to this datum. associative list of the form: list(trait name (string) = list(source1, source2, source3,...)) var/list/_status_traits @@ -69,20 +69,35 @@ // Create and destroy is weird and I wanna cover my bases var/harddel_deets_dumped = FALSE -// Default implementation of clean-up code. -// This should be overridden to remove all references pointing to the object being destroyed. -// Return the appropriate QDEL_HINT; in most cases this is QDEL_HINT_QUEUE. +/** + * Default implementation of clean-up code. + * + * This should be overridden to remove all references pointing to the object being destroyed, if + * you do override it, make sure to call the parent and return its return value by default + * + * Return an appropriate [QDEL_HINT][QDEL_HINT_QUEUE] to modify handling of your deletion; + * in most cases this is [QDEL_HINT_QUEUE]. + * + * The base case is responsible for doing the following + * * Erasing timers pointing to this datum + * * Erasing compenents on this datum + * * Notifying datums listening to signals from this datum that we are going away + * + * Returns [QDEL_HINT_QUEUE] + */ /datum/proc/Destroy(force=FALSE) + //SHOULD_CALL_PARENT(TRUE) + //SHOULD_NOT_SLEEP(TRUE) + tag = null + weak_reference = null //ensure prompt GCing of weakref. - //clear timers - var/list/timers = active_timers - active_timers = null - for(var/datum/timedevent/timer as anything in timers) - if (timer.spent) - continue - qdel(timer) - - weak_reference = null // Clear this reference to ensure it's kept for as brief duration as possible. + if(_active_timers) + var/list/timers = _active_timers + _active_timers = null + for(var/datum/timedevent/timer as anything in timers) + if (timer.spent && !(timer.flags & TIMER_DELETE_ME)) + continue + qdel(timer) //BEGIN: ECS SHIT signal_enabled = FALSE @@ -114,7 +129,6 @@ UnregisterSignal(target, signal_procs[target]) //END: ECS SHIT - tag = null SStgui.close_uis(src) #ifdef REFERENCE_TRACKING diff --git a/config/example/logging.txt b/config/example/logging.txt index a3c2a9caeb1..3d5e436d2b1 100644 --- a/config/example/logging.txt +++ b/config/example/logging.txt @@ -44,6 +44,9 @@ LOG_PDA ## log graffiti drawings LOG_GRAFFITI +## Log all timers on timer auto reset +# LOG_TIMERS_ON_BUCKET_RESET + ## log world.log messages # LOG_WORLD_OUTPUT