[MIRROR] Biddle Verbs: Queues the Most Expensive Verbs for the Next Tick if the Server Is Overloaded [MDB IGNORE] (#15329)

* Biddle Verbs: Queues the Most Expensive Verbs for the Next Tick if the Server Is Overloaded (#65589)

This pr goes through: /client/Click(), /client/Topic(), /mob/living/verb/resist(), /mob/verb/quick_equip(), /mob/verb/examinate(), and /mob/verb/mode() and makes them queue their functionality to a subsystem to execute in the next tick if the server is overloaded. To do this a new subsystem is made to handle most verbs called SSverb_manager, if the server is overloaded the verb queues itself in the subsystem and returns, then near the start of the next tick that verb is resumed with the provided callback. The verbs are called directly after SSinput, and the subsystem does not yield until its queue is completely finished.

The exception are clicks from player input since they are extremely important for the feeling of responsiveness. I considered not queuing them but theyre too expensive not to, suffering from a death of a thousand cuts performance wise from many many things in the process adding up. Instead clicks are executed at the very start of the next tick, as the first action that SSinput completes, before player movement is processed even.

A few months ago, before I died I was trying to figure out why games at midpop (40-50 people) had non zero and consistent time dilation without maptick being consistently above 28% (which is when the MC stops yielding for maptick if its overloaded). I found it out, started working on this pr, then promptly died. luckily im a bit less dead now

the current MC has a problem: the cost of verbs is completely and totally invisible to it, it cannot account for them. Why is this bad? because verbs are the last thing to execute in the tick, after the MC and SendMaps have finished executing.
tick diagram2
If the MC is overloaded and uses 100% of the time it allots itself this means that if SendMaps uses the amount its expected to take, verbs have at most 2% of the tick to execute in before they are overtiming and thus delaying the start of the next tick. This is bad, and im 99% sure this is the majority of our overtime.

Take Click() for example. Click isnt listed as a verb but since its called as a result of client commands its executed at the end of the tick like other verbs. in this random 80 pop sybil round profile i had saved on my computer sybil 80 pop (2).txt /client/Click() has an overtime of only 1.8 seconds, which isnt that bad. however it has a self cpu of 2.5 seconds meaning 1.8/2.5 = 72% of its time is overtiming, and it also is calling 80.2 seconds worth of total cpu, which means that more than 57.7 seconds of overtime is attributed to just /client/Click() executing at the very end of a tick. the reason why this isnt obvious is just because the verbs themselves typically dont have high enough self cpu to get high enough on the rankings of overtiming procs to be noticed, all of their overtime is distributed among a ton of procs they call in the chain.

Since i cant guarantee the MC resumes at the very start of the next tick due to other sleeping procs almost always resuming first: I time the duration between clicks being queued up for the next tick and when theyre actually executed. if it exceeds 20 milliseconds of added latency (less than one tenth the average human reaction time) clicks will execute immediately instead of queuing, this should make instances where a player can notice the added latency a vanishingly small minority of cases. still, this should be tm'd

* Biddle Verbs: Queues the Most Expensive Verbs for the Next Tick if the Server Is Overloaded

Co-authored-by: Kylerace <kylerlumpkin1@gmail.com>
This commit is contained in:
SkyratBot
2022-07-31 22:03:59 +00:00
committed by GitHub
co-authored by Kylerace
parent 417c73b057
commit d8da1153b7
26 changed files with 376 additions and 95 deletions
+66 -8
View File
@@ -1,15 +1,28 @@
SUBSYSTEM_DEF(input)
VERB_MANAGER_SUBSYSTEM_DEF(input)
name = "Input"
wait = 1 //SS_TICKER means this runs every tick
init_order = INIT_ORDER_INPUT
init_stage = INITSTAGE_EARLY
flags = SS_TICKER
priority = FIRE_PRIORITY_INPUT
runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY
use_default_stats = FALSE
var/list/macro_set
/datum/controller/subsystem/input/Initialize()
///running average of how many clicks inputted by a player the server processes every second. used for the subsystem stat entry
var/clicks_per_second = 0
///count of how many clicks onto atoms have elapsed before being cleared by fire(). used to average with clicks_per_second.
var/current_clicks = 0
///acts like clicks_per_second but only counts the clicks actually processed by SSinput itself while clicks_per_second counts all clicks
var/delayed_clicks_per_second = 0
///running average of how many movement iterations from player input the server processes every second. used for the subsystem stat entry
var/movements_per_second = 0
///running average of the amount of real time clicks take to truly execute after the command is originally sent to the server.
///if a click isnt delayed at all then it counts as 0 deciseconds.
var/average_click_delay = 0
/datum/controller/subsystem/verb_manager/input/Initialize()
setup_default_macro_sets()
initialized = TRUE
@@ -19,7 +32,7 @@ SUBSYSTEM_DEF(input)
return ..()
// This is for when macro sets are eventualy datumized
/datum/controller/subsystem/input/proc/setup_default_macro_sets()
/datum/controller/subsystem/verb_manager/input/proc/setup_default_macro_sets()
macro_set = list(
"Any" = "\"KeyDown \[\[*\]\]\"",
"Any+UP" = "\"KeyUp \[\[*\]\]\"",
@@ -29,12 +42,57 @@ SUBSYSTEM_DEF(input)
)
// Badmins just wanna have fun ♪
/datum/controller/subsystem/input/proc/refresh_client_macro_sets()
/datum/controller/subsystem/verb_manager/input/proc/refresh_client_macro_sets()
var/list/clients = GLOB.clients
for(var/i in 1 to clients.len)
var/client/user = clients[i]
user.set_macros()
/datum/controller/subsystem/input/fire()
for(var/mob/user as anything in GLOB.keyloop_list)
user.focus?.keyLoop(user.client)
/datum/controller/subsystem/verb_manager/input/can_queue_verb(datum/callback/verb_callback/incoming_callback, control)
//make sure the incoming verb is actually something we specifically want to handle
if(control != "mapwindow.map")
return FALSE
if(average_click_delay >= MAXIMUM_CLICK_LATENCY || !..())
current_clicks++
average_click_delay = MC_AVG_FAST_UP_SLOW_DOWN(average_click_delay, 0)
return FALSE
return TRUE
///stupid workaround for byond not recognizing the /atom/Click typepath for the queued click callbacks
/atom/proc/_Click(location, control, params)
if(usr)
Click(location, control, params)
/datum/controller/subsystem/verb_manager/input/fire()
var/moves_this_run = 0
var/deferred_clicks_this_run = 0 //acts like current_clicks but doesnt count clicks that dont get processed by SSinput
for(var/datum/callback/verb_callback/queued_click as anything in verb_queue)
if(!istype(queued_click))
stack_trace("non /datum/callback/verb_callback instance inside SSinput's verb_queue!")
continue
average_click_delay = MC_AVG_FAST_UP_SLOW_DOWN(average_click_delay, (REALTIMEOFDAY - queued_click.creation_time) SECONDS)
queued_click.InvokeAsync()
current_clicks++
deferred_clicks_this_run++
verb_queue.Cut() //is ran all the way through every run, no exceptions
for(var/mob/user in GLOB.keyloop_list)
moves_this_run += user.focus?.keyLoop(user.client)//only increments if a player changes their movement input from the last tick
clicks_per_second = MC_AVG_SECONDS(clicks_per_second, current_clicks, wait TICKS)
delayed_clicks_per_second = MC_AVG_SECONDS(delayed_clicks_per_second, deferred_clicks_this_run, wait TICKS)
movements_per_second = MC_AVG_SECONDS(movements_per_second, moves_this_run, wait TICKS)
current_clicks = 0
/datum/controller/subsystem/verb_manager/input/stat_entry(msg)
. = ..()
. += "M/S:[round(movements_per_second,0.01)] | C/S:[round(clicks_per_second,0.01)]([round(delayed_clicks_per_second,0.01)] | CD: [round(average_click_delay,0.01)])"
@@ -1,53 +1,5 @@
SUBSYSTEM_DEF(speech_controller)
/// verb_manager subsystem just for handling say's
VERB_MANAGER_SUBSYSTEM_DEF(speech_controller)
name = "Speech Controller"
wait = 1
flags = SS_TICKER|SS_NO_INIT
priority = FIRE_PRIORITY_SPEECH_CONTROLLER//has to be high priority, second in priority ONLY to SSinput
runlevels = RUNLEVELS_DEFAULT | RUNLEVEL_LOBBY
///used so that an admin can force all speech verbs to execute immediately instead of queueing
var/FOR_ADMINS_IF_BROKE_immediately_execute_all_speech = FALSE
///list of the form: list(client mob, message that mob is queued to say, other say arguments (if any)).
///this is our process queue, processed every tick.
var/list/queued_says_to_execute = list()
///queues mob_to_queue into our process list so they say(message) near the start of the next tick
/datum/controller/subsystem/speech_controller/proc/queue_say_for_mob(mob/mob_to_queue, message, message_type)
if(!TICK_CHECK || FOR_ADMINS_IF_BROKE_immediately_execute_all_speech)
process_single_say(mob_to_queue, message, message_type)
return TRUE
queued_says_to_execute += list(list(mob_to_queue, message, message_type))
return TRUE
/datum/controller/subsystem/speech_controller/fire(resumed)
/// cache for sanic speed (lists are references anyways)
var/list/says_to_process = queued_says_to_execute.Copy()
queued_says_to_execute.Cut()//we should be going through the entire list every single iteration
for(var/list/say_to_process as anything in says_to_process)
var/mob/mob_to_speak = say_to_process[MOB_INDEX]//index 1 is the mob, 2 is the message, 3 is the message category
var/message = say_to_process[MESSAGE_INDEX]
var/message_category = say_to_process[CATEGORY_INDEX]
process_single_say(mob_to_speak, message, message_category)
///used in fire() to process a single mobs message through the relevant proc.
///only exists so that sleeps in the message pipeline dont cause the whole queue to wait
/datum/controller/subsystem/speech_controller/proc/process_single_say(mob/mob_to_speak, message, message_category)
set waitfor = FALSE
switch(message_category)
if(SPEECH_CONTROLLER_QUEUE_SAY_VERB)
mob_to_speak.say(message)
if(SPEECH_CONTROLLER_QUEUE_WHISPER_VERB)
mob_to_speak.whisper(message)
if(SPEECH_CONTROLLER_QUEUE_EMOTE_VERB)
mob_to_speak.emote("me",1,message,TRUE)
+121
View File
@@ -0,0 +1,121 @@
/**
* SSverb_manager, a subsystem that runs every tick and runs through its entire queue without yielding like SSinput.
* this exists because of how the byond tick works and where user inputted verbs are put within it.
*
* see TICK_ORDER.md for more info on how the byond tick is structured.
*
* The way the MC allots its time is via TICK_LIMIT_RUNNING, it simply subtracts the cost of SendMaps (MAPTICK_LAST_INTERNAL_TICK_USAGE)
* plus TICK_BYOND_RESERVE from the tick and uses up to that amount of time (minus the percentage of the tick used by the time it executes subsystems)
* on subsystems running cool things like atmospherics or Life or SSInput or whatever.
*
* Without this subsystem, verbs are likely to cause overtime if the MC uses all of the time it has alloted for itself in the tick, and SendMaps
* uses as much as its expected to, and an expensive verb ends up executing that tick. This is because the MC is completely blind to the cost of
* verbs, it can't account for it at all. The only chance for verbs to not cause overtime in a tick where the MC used as much of the tick
* as it alloted itself and where SendMaps costed as much as it was expected to is if the verb(s) take less than TICK_BYOND_RESERVE percent of
* the tick, which isnt much. Not to mention if SendMaps takes more than 30% of the tick and the MC forces itself to take at least 70% of the
* normal tick duration which causes ticks to naturally overrun even in the absence of verbs.
*
* With this subsystem, the MC can account for the cost of verbs and thus stop major overruns of ticks. This means that the most important subsystems
* like SSinput can start at the same time they were supposed to, leading to a smoother experience for the player since ticks arent riddled with
* minor hangs over and over again.
*/
SUBSYSTEM_DEF(verb_manager)
name = "Verb Manager"
wait = 1
flags = SS_TICKER | SS_NO_INIT
priority = FIRE_PRIORITY_DELAYED_VERBS
runlevels = RUNLEVEL_INIT | RUNLEVELS_DEFAULT
///list of callbacks to procs called from verbs or verblike procs that were executed when the server was overloaded and had to delay to the next tick.
///this list is ran through every tick, and the subsystem does not yield until this queue is finished.
var/list/datum/callback/verb_callback/verb_queue = list()
///running average of how many verb callbacks are executed every second. used for the stat entry
var/verbs_executed_per_second = 0
///if TRUE we treat usr's with holders just like usr's without holders. otherwise they always execute immediately
var/can_queue_admin_verbs = FALSE
///if this is true all verbs immediately execute and dont queue. in case the mc is fucked or something
var/FOR_ADMINS_IF_VERBS_FUCKED_immediately_execute_all_verbs = FALSE
///used for subtypes to determine if they use their own stats for the stat entry
var/use_default_stats = TRUE
///if TRUE this will... message admins every time a verb is queued to this subsystem for the next tick with stats.
///for obvious reasons dont make this be TRUE on the code level this is for admins to turn on
var/message_admins_on_queue = FALSE
/**
* queue a callback for the given verb/verblike proc and any given arguments to the specified verb subsystem, so that they process in the next tick.
* intended to only work with verbs or verblike procs called directly from client input, use as part of TRY_QUEUE_VERB() and co.
*
* returns TRUE if the queuing was successful, FALSE otherwise.
*/
/proc/_queue_verb(datum/callback/verb_callback/incoming_callback, tick_check, datum/controller/subsystem/verb_manager/subsystem_to_use = SSverb_manager, ...)
if(TICK_USAGE < tick_check \
|| QDELETED(incoming_callback) \
|| QDELETED(incoming_callback.object) \
|| !ismob(usr) \
|| QDELING(usr))
return FALSE
if(!istype(subsystem_to_use))
return FALSE
var/list/args_to_check = args.Copy()
args_to_check.Cut(2, 4)//cut out tick_check and subsystem_to_use
//any subsystem can use the additional arguments to refuse queuing
if(!subsystem_to_use.can_queue_verb(arglist(args_to_check)))
return FALSE
return subsystem_to_use.queue_verb(incoming_callback)
/**
* subsystem-specific check for whether a callback can be queued.
* intended so that subsystem subtypes can verify whether
*
* subtypes may include additional arguments here if they need them! you just need to include them properly
* in TRY_QUEUE_VERB() and co.
*/
/datum/controller/subsystem/verb_manager/proc/can_queue_verb(datum/callback/verb_callback/incoming_callback)
if(usr.client?.holder && !can_queue_admin_verbs \
|| FOR_ADMINS_IF_VERBS_FUCKED_immediately_execute_all_verbs \
|| !initialized \
|| !(runlevels & Master.current_runlevel))
return FALSE
return TRUE
/**
* queue a callback for the given proc, so that it is invoked in the next tick.
* intended to only work with verbs or verblike procs called directly from client input, use as part of TRY_QUEUE_VERB()
*
* returns TRUE if the queuing was successful, FALSE otherwise.
*/
/datum/controller/subsystem/verb_manager/proc/queue_verb(datum/callback/verb_callback/incoming_callback)
. = FALSE //errored
if(message_admins_on_queue)
message_admins("[name] verb queuing: tick usage: [TICK_USAGE]%, proc: [incoming_callback.delegate], object: [incoming_callback.object], usr: [usr]")
verb_queue += incoming_callback
return TRUE
/datum/controller/subsystem/verb_manager/fire(resumed)
var/executed_verbs = 0
for(var/datum/callback/verb_callback/verb_callback as anything in verb_queue)
if(!istype(verb_callback))
stack_trace("non /datum/callback/verb_callback inside [name]'s verb_queue!")
continue
verb_callback.InvokeAsync()
executed_verbs++
verb_queue.Cut()
verbs_executed_per_second = MC_AVG_SECONDS(verbs_executed_per_second, executed_verbs, wait TICKS)
/datum/controller/subsystem/verb_manager/stat_entry(msg)
. = ..()
if(use_default_stats)
. += "V/S: [round(verbs_executed_per_second, 0.01)]"