From 3f75fdb42df661f0e94f85885ec3d4fde5dfcd0f Mon Sep 17 00:00:00 2001 From: MrStonedOne Date: Wed, 11 Jan 2017 08:16:26 -0800 Subject: [PATCH] Addtimer refactor: Officially faster then byond's internal sleep system --- code/__DEFINES/MC.dm | 11 +- code/__DEFINES/math.dm | 5 + code/__HELPERS/cmp.dm | 6 +- code/__HELPERS/time.dm | 11 +- code/controllers/subsystem/garbage.dm | 2 + code/controllers/subsystem/timer.dm | 250 +++++++++++++++++++++----- 6 files changed, 239 insertions(+), 46 deletions(-) diff --git a/code/__DEFINES/MC.dm b/code/__DEFINES/MC.dm index 2ea7258816b..87374b6672c 100644 --- a/code/__DEFINES/MC.dm +++ b/code/__DEFINES/MC.dm @@ -45,5 +45,12 @@ #define SS_POST_FIRE_TIMING 128 //Timing subsystem -#define TIMER_NORMAL "normal" -#define TIMER_UNIQUE "unique" \ No newline at end of file +//Don't run if there is an identical unique timer active +#define TIMER_UNIQUE 0x1 +//For unique timers: Replace the old timer rather then not start this one +#define TIMER_OVERRIDE 0x2 +//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 0x4 + diff --git a/code/__DEFINES/math.dm b/code/__DEFINES/math.dm index d454c0a0676..4726d210012 100644 --- a/code/__DEFINES/math.dm +++ b/code/__DEFINES/math.dm @@ -17,3 +17,8 @@ #define TICK_USAGE_TO_MS(starting_tickusage) (TICK_DELTA_TO_MS(world.tick_usage-starting_tickusage)) #define PERCENT(val) (round(val*100, 0.1)) + +//time of day but automatically adjusts to the server going into the next day within the same round. +//for when you need a reliable time number that doesn't depend on byond time. +#define REALTIMEOFDAY (world.timeofday + (MIDNIGHT_ROLLOVER * MIDNIGHT_ROLLOVER_CHECK)) +#define MIDNIGHT_ROLLOVER_CHECK ( rollovercheck_last_timeofday != world.timeofday ? update_midnight_rollover() : midnight_rollovers ) diff --git a/code/__HELPERS/cmp.dm b/code/__HELPERS/cmp.dm index 1cad2a5e5e4..f963f09a35c 100644 --- a/code/__HELPERS/cmp.dm +++ b/code/__HELPERS/cmp.dm @@ -40,8 +40,12 @@ var/cmp_field = "name" /proc/cmp_subsystem_priority(datum/subsystem/a, datum/subsystem/b) return a.priority - b.priority +/proc/cmp_timer(datum/timedevent/a, datum/timedevent/b) + return a.timeToRun - b.timeToRun + /proc/cmp_clientcolour_priority(datum/client_colour/A, datum/client_colour/B) return B.priority - A.priority /proc/cmp_clockscripture_priority(datum/clockwork_scripture/A, datum/clockwork_scripture/B) - return initial(A.sort_priority) - initial(B.sort_priority) \ No newline at end of file + return initial(A.sort_priority) - initial(B.sort_priority) + diff --git a/code/__HELPERS/time.dm b/code/__HELPERS/time.dm index 8d000ce6a57..22ff1c2bef9 100644 --- a/code/__HELPERS/time.dm +++ b/code/__HELPERS/time.dm @@ -22,4 +22,13 @@ //returns timestamp in a sql and ISO 8601 friendly format /proc/SQLtime() - return time2text(world.realtime, "YYYY-MM-DD hh:mm:ss") \ No newline at end of file + return time2text(world.realtime, "YYYY-MM-DD hh:mm:ss") + + +/var/midnight_rollovers = 0 +/var/rollovercheck_last_timeofday = 0 +/proc/update_midnight_rollover() + if (world.timeofday < rollovercheck_last_timeofday) //TIME IS GOING BACKWARDS! + return midnight_rollovers++ + return midnight_rollovers + diff --git a/code/controllers/subsystem/garbage.dm b/code/controllers/subsystem/garbage.dm index 73de5173c60..6d24e127294 100644 --- a/code/controllers/subsystem/garbage.dm +++ b/code/controllers/subsystem/garbage.dm @@ -224,6 +224,8 @@ var/datum/subsystem/garbage_collector/SSgarbage // Return the appropriate QDEL_HINT; in most cases this is QDEL_HINT_QUEUE. /datum/proc/Destroy(force=FALSE) tag = null + for(var/thing in SStimer.timer_src_dict[src]) + qdel(thing) return QDEL_HINT_QUEUE /datum/var/gc_destroyed //Time when this object was destroyed. diff --git a/code/controllers/subsystem/timer.dm b/code/controllers/subsystem/timer.dm index 11edde6a064..2313bbac961 100644 --- a/code/controllers/subsystem/timer.dm +++ b/code/controllers/subsystem/timer.dm @@ -1,90 +1,256 @@ +#define BUCKET_LEN (world.fps*10*60) //how many ticks should we keep in the bucket. (10 minutes worth) +#define BUCKET_POS(timer) (round((timer.timeToRun - SStimer.head_offset) / world.tick_lag) + 1) var/datum/subsystem/timer/SStimer /datum/subsystem/timer name = "Timer" - wait = 2 //SS_TICKER subsystem, so wait is in ticks + wait = 1 //SS_TICKER subsystem, so wait is in ticks init_order = 1 display_order = 3 - can_fire = 0 //start disabled - flags = SS_FIRE_IN_LOBBY|SS_TICKER|SS_POST_FIRE_TIMING|SS_NO_INIT + + flags = SS_FIRE_IN_LOBBY|SS_TICKER|SS_NO_INIT var/list/datum/timedevent/processing var/list/hashes + var/head_offset = 0 //world.time of the first entry in the the bucket. + var/practical_offset = 0 //index of the first non-empty item in the bucket. + var/bucket_resolution = 0 //world.tick_lag the bucket was designed for + + var/list/buckets //list of buckets, each bucket holds every timer that has to run that byond tick. + + var/list/timer_id_dict //list of all active timers assoicated to their timer id (for easy lookup) + + var/list/timer_src_dict //list of everything with an active timer attached to it as key with a list of those timers as the value (for destroy) + + var/list/clienttime_timers //special snowflake timers that run on fancy pansy "client time" + /datum/subsystem/timer/New() processing = list() hashes = list() + + timer_id_dict = list() + timer_src_dict = list() + + clienttime_timers = list() + NEW_SS_GLOBAL(SStimer) /datum/subsystem/timer/stat_entry(msg) - ..("P:[processing.len]") + ..("P:[length(processing)] H:[length(hashes)] C:[length(clienttime_timers)]") /datum/subsystem/timer/fire() - if(!processing.len) - can_fire = 0 //nothing to do, lets stop firing. - return - for(var/datum/timedevent/event in processing) - if(event.timeToRun <= world.time) - event.callback.InvokeAsync() - qdel(event) - if (MC_TICK_CHECK) - return + if (length(clienttime_timers)) + for (var/thing in clienttime_timers) + var/datum/timedevent/event = thing + if (event.timeToRun <= REALTIMEOFDAY) + var/datum/callback/callback = event.callback + qdel(event) //this is right, we delete the event before running it so that stack overflows don't cause us to repeatively run the same event + callback.InvokeAsync() + + if (MC_TICK_CHECK) + return + + if (head_offset + (world.tick_lag * BUCKET_LEN) < world.time || length(src.buckets) != BUCKET_LEN || world.tick_lag != bucket_resolution) + refill_buckets() + + var/list/buckets = src.buckets + while (practical_offset <= BUCKET_LEN && head_offset + (practical_offset*world.tick_lag) <= world.time && !MC_TICK_CHECK) + var/datum/timedevent/event = buckets[practical_offset] + while (event) + var/datum/callback/callback = event.callback + if (!callback) + stack_trace("FAILURE! [event]||[event.timeToRun]|[qdeleted(event)]|||[world.time]||[head_offset]||[practical_offset]") + if (event.timeToRun > world.time) + stack_trace("HOLY JESUS! SHIT BE FUCKED [event]||[event.timeToRun]||[world.time]||[event.callback.object]||[event.callback.delegate]||[head_offset]||[practical_offset]") + qdel(event) //same as above + callback.InvokeAsync() + if (MC_TICK_CHECK) + return + event = buckets[practical_offset] + + practical_offset++ + +/datum/subsystem/timer/proc/refill_buckets() + sortTim(processing, /proc/cmp_timer) + src.buckets = new(BUCKET_LEN) + practical_offset = 1 + bucket_resolution = world.tick_lag + var/list/buckets = src.buckets + var/new_offset + for (var/thing in processing) + var/datum/timedevent/event = thing + if (!event) + processing -= event + continue + + if (isnull(new_offset)) + new_offset = event.timeToRun + + var/bucket_pos = round((event.timeToRun - new_offset) / world.tick_lag) + 1 + if (bucket_pos > BUCKET_LEN) + break + + var/datum/timedevent/bucket_head = buckets[bucket_pos] + if (!bucket_head) + buckets[bucket_pos] = event + continue + + if (!bucket_head.prev) + bucket_head.prev = bucket_head + event.next = bucket_head + event.prev = bucket_head.prev + event.next.prev = event + event.prev.next = event + head_offset = new_offset + /datum/subsystem/timer/Recover() processing |= SStimer.processing hashes |= SStimer.hashes /datum/timedevent + var/id var/datum/callback/callback var/timeToRun - var/id + var/datum/timerid var/hash + var/list/flags + + //cicular doublely linked list + var/datum/timedevent/next + var/datum/timedevent/prev + var/static/nextid = 1 -/datum/timedevent/New() +/datum/timedevent/New(datum/callback/callback, timeToRun, flags, hash) id = nextid++ + src.callback = callback + src.timeToRun = timeToRun + src.flags = flags + + if (hash) + src.hash = hash + SStimer.hashes[hash] = src + + SStimer.timer_id_dict["timerid[id]"] = src + + if (callback.object != GLOBAL_PROC) + SStimer.timer_src_dict[callback.object] = src + + if (flags & TIMER_CLIENT_TIME) + SStimer.clienttime_timers += src + return + + SStimer.processing += src + + //get the list of buckets + var/list/buckets = SStimer.buckets + //calculate our place in the bucket list + var/bucket_pos = BUCKET_POS(src) + //we are too far aways from needing to run to be in the bucket list, refill_buckets() will handle us. + debug_admins("Timer insert: [bucket_pos] for [timeToRun] at [world.time] and [SStimer.head_offset]") + if (bucket_pos > length(buckets)) + return + //get the bucket for our tick + var/datum/timedevent/event = buckets[bucket_pos] + //empty bucket, we will just add ourself + if (!event) + buckets[bucket_pos] = src + if (bucket_pos < SStimer.practical_offset) + SStimer.practical_offset = bucket_pos + return + //other wise, lets do a simplified linked list add. + if (!event.prev) + event.prev = event + + next = event + prev = event.prev + next.prev = src + prev.next = src + /datum/timedevent/Destroy() + if (hash) + SStimer.hashes -= hash + + SStimer.timer_id_dict -= "timerid[id]" + + if (callback && callback.object != GLOBAL_PROC) + SStimer.timer_src_dict -= callback.object + + callback = null + + if (flags & TIMER_CLIENT_TIME) + SStimer.clienttime_timers -= src + return QDEL_HINT_IWILLGC + SStimer.processing -= src - SStimer.hashes -= hash + + if (prev == next && next) + next.prev = null + prev.next = null + else + if (prev) + prev.next = next + + if (next) + next.prev = prev + + var/bucketpos = BUCKET_POS(src) + var/datum/timedevent/buckethead + var/list/buckets = SStimer.buckets + + if (bucketpos > 0 && bucketpos <= length(buckets)) + buckethead = buckets[bucketpos] + if (buckethead == src) + buckets[bucketpos] = next + + prev = null + next = null + return QDEL_HINT_IWILLGC -/proc/addtimer(datum/callback/callback, wait, unique = TIMER_NORMAL) + +/proc/addtimer(datum/callback/callback, wait, flags) if (!callback) return - if (!SStimer.can_fire) - SStimer.can_fire = 1 - var/datum/timedevent/event = new() - event.callback = callback - event.timeToRun = world.time + wait - var/list/hashlist = args.Copy() - - hashlist[1] = "[callback.object](\ref[callback.object])" - hashlist.Insert(2, callback.delegate, callback.arguments) - event.hash = jointext(hashlist, null) - - if(unique == TIMER_UNIQUE) - var/datum/timedevent/hash_event = SStimer.hashes[event.hash] - if(hash_event) - return hash_event.id - - SStimer.hashes[event.hash] = event if (wait <= 0) callback.InvokeAsync() - SStimer.hashes -= event.hash return - // If we are unique (or we're not checking that), add the timer and return the id. - SStimer.processing += event + var/hash + + if (flags & TIMER_UNIQUE) + var/list/hashlist = list(callback.object, "(\ref[callback.object])", callback.delegate, wait, flags & TIMER_CLIENT_TIME) + hashlist += callback.arguments + hash = hashlist.Join("|||||||") + + var/datum/timedevent/hash_event = SStimer.hashes[hash] + if(hash_event) + if (flags & TIMER_OVERRIDE) + qdel(hash_event) + else + return hash_event.id + + var/timeToRun = world.time + wait + if (flags & TIMER_CLIENT_TIME) + timeToRun = REALTIMEOFDAY + wait + + var/datum/timedevent/event = new(callback, timeToRun, flags, hash) + return event.id + /proc/deltimer(id) - for(var/datum/timedevent/event in SStimer.processing) - if(event.id == id) - qdel(event) - return 1 - return 0 + if (!id) + return FALSE + var/datum/timedevent/event = SStimer.timer_id_dict["timerid[id]"] + if (event) + qdel(event) + return TRUE + return FALSE