diff --git a/code/controllers/subsystems/timer.dm b/code/controllers/subsystems/timer.dm index b1ad33f7f91..eb0fdfb08d6 100644 --- a/code/controllers/subsystems/timer.dm +++ b/code/controllers/subsystems/timer.dm @@ -12,7 +12,7 @@ var/datum/controller/subsystem/timer/SStimer flags = SS_FIRE_IN_LOBBY|SS_TICKER|SS_NO_INIT - var/list/datum/timedevent/processing = list() + var/list/datum/timedevent/second_queue = list() //awe, yes, you've had first queue, but what about second queue? var/list/hashes = list() var/head_offset = 0 //world.time of the first entry in the the bucket. @@ -30,42 +30,37 @@ var/datum/controller/subsystem/timer/SStimer var/static/last_invoke_warning = 0 var/static/bucket_auto_reset = TRUE - var/static/times_flushed = 0 - var/static/times_crashed = 0 - /datum/controller/subsystem/timer/New() NEW_SS_GLOBAL(SStimer) bucket_list.len = BUCKET_LEN /datum/controller/subsystem/timer/stat_entry(msg) - ..("B:[bucket_count] P:[length(processing)] H:[length(hashes)] C:[length(clienttime_timers)][times_crashed ? " F:[times_crashed]" : ""]") + ..("B:[bucket_count] P:[length(second_queue)] H:[length(hashes)] C:[length(clienttime_timers)]") /datum/controller/subsystem/timer/fire(resumed = FALSE) var/lit = last_invoke_tick - var/last_check = world.time - TIMER_NO_INVOKE_WARNING + var/last_check = world.time - TICKS2DS(BUCKET_LEN*1.5) var/list/bucket_list = src.bucket_list - var/static/list/spent = list() if(!bucket_count) last_invoke_tick = world.time - if(lit && lit < last_check && last_invoke_warning < last_check) + 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 [TIMER_NO_INVOKE_WARNING] ticks[bucket_auto_reset ? ", resetting buckets" : ""]!" - times_crashed++ + 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_ss(name, "Timer bucket reset. world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset], times_flushed: [times_flushed], length(spent): [length(spent)]") + + log_ss(name, "Timer bucket reset. world.time: [world.time], head_offset: [head_offset], practical_offset: [practical_offset]") for (var/i in 1 to length(bucket_list)) var/datum/timedevent/bucket_head = bucket_list[i] if (!bucket_head) continue - + log_ss(name, "Active timers at index [i]:") - + var/datum/timedevent/bucket_node = bucket_head var/anti_loop_check = 1000 do @@ -73,8 +68,8 @@ var/datum/controller/subsystem/timer/SStimer bucket_node = bucket_node.next anti_loop_check-- while(bucket_node && bucket_node != bucket_head && anti_loop_check) - log_ss(name, "Active timers in the processing queue:") - for(var/I in processing) + log_ss(name, "Active timers in the second_queue queue:") + for(var/I in second_queue) log_ss(name, get_timer_debug_string(I)) var/next_clienttime_timer_index = 0 @@ -96,67 +91,121 @@ var/datum/controller/subsystem/timer/SStimer ctime_timer.spent = REALTIMEOFDAY callBack.InvokeAsync() + if(ctime_timer.flags & TIMER_LOOP) ctime_timer.spent = 0 ctime_timer.timeToRun = REALTIMEOFDAY + ctime_timer.wait BINARY_INSERT(ctime_timer, clienttime_timers, datum/timedevent, timeToRun) else qdel(ctime_timer) - next_clienttime_timer_index-- //ctime_timer is removed from clienttime_timers by its Destroy + if (next_clienttime_timer_index) clienttime_timers.Cut(1, next_clienttime_timer_index+1) - var/static/datum/timedevent/timer - var/static/datum/timedevent/head + if (MC_TICK_CHECK) + return - if (practical_offset > BUCKET_LEN || (!resumed && length(bucket_list) != BUCKET_LEN || world.tick_lag != bucket_resolution)) - shift_buckets() + var/static/list/spent = list() + var/static/datum/timedevent/timer + if (practical_offset > BUCKET_LEN) + head_offset += TICKS2DS(BUCKET_LEN) + practical_offset = 1 + resumed = FALSE + + if ((length(bucket_list) != BUCKET_LEN) || (world.tick_lag != bucket_resolution)) + reset_buckets() bucket_list = src.bucket_list resumed = FALSE + if (!resumed) timer = null - head = null - while (practical_offset <= BUCKET_LEN && head_offset + (practical_offset*world.tick_lag) <= world.time && !MC_TICK_CHECK) + 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] - if (!head) - practical_offset++ - if (MC_TICK_CHECK) - break - continue timer = head - do + while (timer) var/datum/callback/callBack = timer.callBack if (!callBack) - qdel(timer) bucket_resolution = null //force bucket recreation - CRASH("Invalid timer: [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]") + 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 - timer.spent = TRUE + timer.spent = world.time callBack.InvokeAsync() last_invoke_tick = world.time - timer = timer.next - if (MC_TICK_CHECK) return - while (timer && timer != head) - timer = null - bucket_list[practical_offset++] = null - if (MC_TICK_CHECK) - return - times_flushed++ + 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 + CRASH("[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 + CRASH("[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/spent_timer in spent) - var/datum/timedevent/qtimer = spent_timer + for (var/i in spent) + var/datum/timedevent/qtimer = i if(QDELETED(qtimer)) bucket_count++ continue @@ -182,7 +231,7 @@ var/datum/controller/subsystem/timer/SStimer if(QDELETED(TE)) . += ", QDELETED" -/datum/controller/subsystem/timer/proc/shift_buckets() +/datum/controller/subsystem/timer/proc/reset_buckets() var/list/bucket_list = src.bucket_list var/list/alltimers = list() //collect the timers currently in the bucket @@ -203,31 +252,37 @@ var/datum/controller/subsystem/timer/SStimer head_offset = world.time bucket_resolution = world.tick_lag - alltimers += processing + alltimers += second_queue if (!length(alltimers)) return - sortTim(alltimers, .proc/cmp_timer) + 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 + var/new_bucket_count + var/i = 1 + for (i in 1 to length(alltimers)) + var/datum/timedevent/timer = alltimers[i] if (!timer) - timers_to_remove += timer continue - var/bucket_pos = max(1, BUCKET_POS(timer)) + var/bucket_pos = BUCKET_POS(timer) + if (timer.timeToRun >= TIMER_MAX) + i-- + break + - timers_to_remove += timer //remove it from the big list once we are done 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 - bucket_count++ + + new_bucket_count++ var/datum/timedevent/bucket_head = bucket_list[bucket_pos] if (!bucket_head) bucket_list[bucket_pos] = timer @@ -241,12 +296,14 @@ var/datum/controller/subsystem/timer/SStimer timer.prev = bucket_head.prev timer.next.prev = timer timer.prev.next = timer - - processing = (alltimers - timers_to_remove) + if (i) + alltimers.Cut(1, i+1) + second_queue = alltimers + bucket_count = new_bucket_count /datum/controller/subsystem/timer/Recover() - processing |= SStimer.processing + second_queue |= SStimer.second_queue hashes |= SStimer.hashes timer_id_dict |= SStimer.timer_id_dict bucket_list |= SStimer.bucket_list @@ -282,25 +339,28 @@ var/datum/controller/subsystem/timer/SStimer if (flags & TIMER_UNIQUE) SStimer.hashes[hash] = src if (flags & TIMER_STOPPABLE) - do - if (nextid >= TIMER_ID_MAX) - nextid = 1 - id = nextid++ - while(SStimer.timer_id_dict["timerid" + num2text(id, 8)]) - SStimer.timer_id_dict["timerid" + num2text(id, 8)] = src + id = num2text(nextid, 100) + if (nextid >= TIMER_ID_MAX) + nextid += min(1, 2**round(nextid/TIMER_ID_MAX)) + else + nextid++ + SStimer.timer_id_dict[id] = src name = "Timer: " + num2text(id, 8) + ", 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 (callBack.object != GLOBAL_PROC) + + 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)]") + + if (callBack.object != GLOBAL_PROC && !QDESTROYING(callBack.object)) LAZYADD(callBack.object.active_timers, src) bucketJoin() /datum/timedevent/Destroy() ..() - if (flags & TIMER_UNIQUE) + if (flags & TIMER_UNIQUE && 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) @@ -308,10 +368,12 @@ var/datum/controller/subsystem/timer/SStimer callBack = null if (flags & TIMER_STOPPABLE) - SStimer.timer_id_dict -= "timerid[id]" + SStimer.timer_id_dict -= id if (flags & TIMER_CLIENT_TIME) - SStimer.clienttime_timers -= src + if (!spent) + spent = world.time + SStimer.clienttime_timers -= src return QDEL_HINT_IWILLGC if (!spent) @@ -327,27 +389,29 @@ var/datum/controller/subsystem/timer/SStimer return QDEL_HINT_IWILLGC /datum/timedevent/proc/bucketEject() - 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/bucket_list = SStimer.bucket_list - - if(bucketpos in 1 to length(bucket_list)) + var/list/second_queue = SStimer.second_queue + var/datum/timedevent/buckethead + if(bucketpos > 0) buckethead = bucket_list[bucketpos] + if(buckethead == src) + bucket_list[bucketpos] = next + SStimer.bucket_count-- + else if(timeToRun < TIMER_MAX || next || prev) SStimer.bucket_count-- else - SStimer.processing -= src - - if (buckethead == src) - bucket_list[bucketpos] = next + var/l = length(second_queue) + second_queue -= src + if(l == length(second_queue)) + SStimer.bucket_count-- + if(prev != next) + prev.next = next + next.prev = prev + else + prev?.next = null + next?.prev = null + prev = next = null /datum/timedevent/proc/bucketJoin() var/list/L @@ -355,7 +419,7 @@ var/datum/controller/subsystem/timer/SStimer if (flags & TIMER_CLIENT_TIME) L = SStimer.clienttime_timers else if (timeToRun >= TIMER_MAX) - L = SStimer.processing + L = SStimer.second_queue if(L) BINARY_INSERT(src, L, datum/timedevent, timeToRun) @@ -421,17 +485,17 @@ var/datum/controller/subsystem/timer/SStimer hash_timer.hash = null SStimer.hashes -= hash else - if (flags & TIMER_OVERRIDE) qdel(hash_timer) else if (hash_timer.flags & TIMER_STOPPABLE) . = hash_timer.id return + else if(flags & TIMER_OVERRIDE) + crash_with("TIMER_OVERRIDE used without TIMER_UNIQUE") var/datum/timedevent/timer = new(callback, wait, flags, hash) - if (flags & TIMER_STOPPABLE) - return timer.id + return timer.id /proc/deltimer(id) if (!id) @@ -445,7 +509,7 @@ var/datum/controller/subsystem/timer/SStimer qdel(id) return TRUE - var/datum/timedevent/timer = SStimer.timer_id_dict["timerid[id]"] + var/datum/timedevent/timer = SStimer.timer_id_dict[id] if (timer && !timer.spent) qdel(timer) return TRUE @@ -462,7 +526,7 @@ var/datum/controller/subsystem/timer/SStimer if (!istext(id) && istype(id, /datum/timedevent)) timer = id else - timer = SStimer.timer_id_dict["timerid[id]"] + timer = SStimer.timer_id_dict[id] if(!timer) return FALSE diff --git a/code/game/objects/structures/watercloset.dm b/code/game/objects/structures/watercloset.dm index cf884e50537..146e69cc196 100644 --- a/code/game/objects/structures/watercloset.dm +++ b/code/game/objects/structures/watercloset.dm @@ -206,7 +206,7 @@ else if(ismist) ismist = 1 mymist = new /obj/effect/mist(loc) - addtimer(CALLBACK(src, .proc/clear_mist), 250, TIMER_OVERRIDE) + addtimer(CALLBACK(src, .proc/clear_mist), 250, TIMER_OVERRIDE|TIMER_UNIQUE) /obj/machinery/shower/proc/clear_mist() if (!on)