Merge pull request #22933 from MrStonedOne/addtimerbuckets

Addtimer refactor: Officially faster than byond's internal sleep system
This commit is contained in:
Joan Lung
2017-01-18 15:06:23 -05:00
committed by GitHub
6 changed files with 327 additions and 49 deletions
+9 -2
View File
@@ -45,5 +45,12 @@
#define SS_POST_FIRE_TIMING 128
//Timing subsystem
#define TIMER_NORMAL "normal"
#define TIMER_UNIQUE "unique"
//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
+5
View File
@@ -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 )
+5 -1
View File
@@ -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)
return initial(A.sort_priority) - initial(B.sort_priority)
+10 -1
View File
@@ -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")
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
+10 -1
View File
@@ -170,7 +170,9 @@ var/datum/subsystem/garbage_collector/SSgarbage
switch(hint)
if (QDEL_HINT_QUEUE) //qdel should queue the object for deletion.
SSgarbage.QueueForQueuing(D)
if (QDEL_HINT_LETMELIVE, QDEL_HINT_IWILLGC) //qdel should let the object live after calling destory.
if (QDEL_HINT_IWILLGC)
return
if (QDEL_HINT_LETMELIVE) //qdel should let the object live after calling destory.
if(!force)
D.gc_destroyed = null //clear the gc variable (important!)
return
@@ -224,6 +226,13 @@ 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
var/list/timers = active_timers
active_timers = null
for(var/thing in timers)
var/datum/timedevent/timer = thing
if (timer.spent)
continue
qdel(timer)
return QDEL_HINT_QUEUE
/datum/var/gc_destroyed //Time when this object was destroyed.
+288 -44
View File
@@ -1,90 +1,334 @@
#define BUCKET_LEN (world.fps*1*60) //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)
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/bucket_count = 0 //how many timers are in the buckets
var/list/bucket_list //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/clienttime_timers //special snowflake timers that run on fancy pansy "client time"
/datum/subsystem/timer/New()
processing = list()
hashes = list()
timer_id_dict = list()
clienttime_timers = list()
NEW_SS_GLOBAL(SStimer)
/datum/subsystem/timer/stat_entry(msg)
..("P:[processing.len]")
..("B:[bucket_count] 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.
if (length(clienttime_timers))
for (var/thing in clienttime_timers)
var/datum/timedevent/timer = thing
if (timer.spent)
qdel(timer)
continue
if (timer.timeToRun <= REALTIMEOFDAY)
var/datum/callback/callBack = timer.callBack
timer.spent = TRUE
callBack.InvokeAsync()
qdel(timer)
if (MC_TICK_CHECK)
return
if (head_offset + (world.tick_lag * BUCKET_LEN) < world.time || length(src.bucket_list) != BUCKET_LEN || world.tick_lag != bucket_resolution)
shift_buckets()
var/list/bucket_list = src.bucket_list
var/static/list/spent = list()
LOOP_OUTER
while (practical_offset <= BUCKET_LEN && head_offset + (practical_offset*world.tick_lag) <= world.time && !MC_TICK_CHECK)
var/datum/timedevent/head = bucket_list[practical_offset]
if (!head)
practical_offset++
if (MC_TICK_CHECK)
break
continue
var/datum/timedevent/timer = head
do
var/datum/callback/callBack = timer.callBack
if (!callBack || timer.spent)
qdel(timer)
bucket_resolution = null //force bucket recreation
CRASH("Invalid timer: timer.timeToRun=[timer.timeToRun]||qdeleted(timer)=[qdeleted(timer)]||world.time=[world.time]||head_offset=[head_offset]||practical_offset=[practical_offset]||timer.spent=[timer.spent]")
spent += timer
timer.spent = TRUE
callBack.InvokeAsync()
timer = timer.next
if (MC_TICK_CHECK)
if (!timer || timer == head)
break
if (head.prev)
head.prev.next = timer
if (timer.prev)
timer.prev.next = head
timer.prev = head.prev
bucket_list[practical_offset] = timer
break LOOP_OUTER
while (timer && timer != head)
bucket_list[practical_offset++] = null
if (MC_TICK_CHECK)
break
timer_id_dict -= spent
bucket_count -= length(spent)
for (var/timer in spent)
qdel(timer)
spent.len = 0
/datum/subsystem/timer/proc/shift_buckets()
var/list/bucket_list = src.bucket_list
var/list/alltimers = list()
//collect the timers currently in the bucket
for (var/bucket_head in bucket_list)
if (!bucket_head)
continue
var/datum/timedevent/bucket_node = bucket_head
do
alltimers += bucket_node
bucket_node = bucket_node.next
while(bucket_node && bucket_node != bucket_head)
bucket_list = new(BUCKET_LEN)
src.bucket_list = bucket_list //cache update
practical_offset = 1
bucket_count = 0
head_offset = world.time
bucket_resolution = world.tick_lag
alltimers += processing
if (!length(alltimers))
return
for(var/datum/timedevent/event in processing)
if(event.timeToRun <= world.time)
event.callback.InvokeAsync()
qdel(event)
if (MC_TICK_CHECK)
return
sortTim(alltimers, .proc/cmp_timer)
var/datum/timedevent/head = alltimers[1]
if (head.timeToRun < head_offset)
head_offset = head.timeToRun
var/list/timers_to_remove = list()
for (var/thing in alltimers)
var/datum/timedevent/timer = thing
if (!timer)
timers_to_remove += timer
continue
var/bucket_pos = BUCKET_POS(timer)
if (bucket_pos > BUCKET_LEN)
break
timers_to_remove += timer //remove it from the big list once we are done
if (!timer.callBack || timer.spent)
continue
bucket_count++
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
processing = (alltimers - timers_to_remove)
/datum/subsystem/timer/Recover()
processing |= SStimer.processing
hashes |= SStimer.hashes
timer_id_dict |= SStimer.timer_id_dict
bucket_list |= SStimer.bucket_list
/datum/var/list/active_timers
/datum/timedevent
var/datum/callback/callback
var/timeToRun
var/id
var/datum/callback/callBack
var/timeToRun
var/hash
var/list/flags
var/spent = FALSE //set to true right before running.
//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)
LAZYINITLIST(callBack.object.active_timers)
callBack.object.active_timers += src
if (flags & TIMER_CLIENT_TIME)
SStimer.clienttime_timers += src
return
//get the list of buckets
var/list/bucket_list = SStimer.bucket_list
//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, shift_buckets() will handle us.
if (bucket_pos > length(bucket_list))
SStimer.processing += src
return
//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 (!bucket_head)
bucket_list[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 (!bucket_head.prev)
bucket_head.prev = bucket_head
next = bucket_head
prev = bucket_head.prev
next.prev = src
prev.next = src
/datum/timedevent/Destroy()
SStimer.processing -= src
SStimer.hashes -= hash
if (hash)
SStimer.hashes -= hash
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_CLIENT_TIME)
SStimer.clienttime_timers -= src
return QDEL_HINT_IWILLGC
if (!spent)
if (prev == next && next)
next.prev = null
prev.next = null
else
if (prev)
prev.next = next
if (next)
next.prev = prev
SStimer.timer_id_dict -= "timerid[id]"
var/bucketpos = BUCKET_POS(src)
var/datum/timedevent/buckethead
var/list/bucket_list = SStimer.bucket_list
if (bucketpos > 0 && bucketpos <= length(bucket_list))
buckethead = bucket_list[bucketpos]
SStimer.bucket_count--
else
SStimer.processing -= src
if (buckethead == src)
bucket_list[bucketpos] = next
else
if (prev && prev.next == src)
prev.next = null
if (next && next.prev == src)
next.prev = 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_timer = SStimer.hashes[hash]
if(hash_timer)
if (flags & TIMER_OVERRIDE)
qdel(hash_timer)
else
return hash_timer.id
var/timeToRun = world.time + wait
if (flags & TIMER_CLIENT_TIME)
timeToRun = REALTIMEOFDAY + wait
var/datum/timedevent/timer = new(callback, timeToRun, flags, hash)
return timer.id
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
if (!istext(id))
if (istype(id, /datum/timedevent))
qdel(id)
return TRUE
var/datum/timedevent/timer = SStimer.timer_id_dict["timerid[id]"]
if (timer && !timer.spent)
qdel(timer)
return TRUE
return FALSE
#undef BUCKET_LEN
#undef BUCKET_POS