diff --git a/code/__defines/MC.dm b/code/__defines/MC.dm index 5173da844c6..2c4360add9c 100644 --- a/code/__defines/MC.dm +++ b/code/__defines/MC.dm @@ -18,6 +18,15 @@ #define MC_AVG_FAST_UP_SLOW_DOWN(average, current) (average > current ? MC_AVERAGE_SLOW(average, current) : MC_AVERAGE_FAST(average, current)) #define MC_AVG_SLOW_UP_FAST_DOWN(average, current) (average < current ? MC_AVERAGE_SLOW(average, current) : MC_AVERAGE_FAST(average, current)) +///creates a running average of "things elapsed" per time period when you need to count via a smaller time period. +///eg you want an average number of things happening per second but you measure the event every tick (50 milliseconds). +///make sure both time intervals are in the same units. doesn't work if current_duration > total_duration or if total_duration == 0 +#define MC_AVG_OVER_TIME(average, current, total_duration, current_duration) ((((total_duration) - (current_duration)) / (total_duration)) * (average) + (current)) + +#define MC_AVG_MINUTES(average, current, current_duration) (MC_AVG_OVER_TIME(average, current, 1 MINUTES, current_duration)) + +#define MC_AVG_SECONDS(average, current, current_duration) (MC_AVG_OVER_TIME(average, current, 1 SECONDS, current_duration)) + #define NEW_SS_GLOBAL(varname) if(varname != src){if(istype(varname)){Recover();qdel(varname);}varname = src;} #define START_PROCESSING(Processor, Datum) if (!(Datum.datum_flags & DF_ISPROCESSING)) {Datum.datum_flags |= DF_ISPROCESSING;Processor.processing += Datum} @@ -70,18 +79,35 @@ #define SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/##X);\ /datum/controller/subsystem/##X/New(){\ - NEW_SS_GLOBAL(SS##X);\ - PreInit();\ + NEW_SS_GLOBAL(SS##X);\ + PreInit();\ }\ /datum/controller/subsystem/##X +#define TIMER_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/timer/##X);\ +/datum/controller/subsystem/timer/##X/New(){\ + NEW_SS_GLOBAL(SS##X);\ + PreInit();\ +}\ +/datum/controller/subsystem/timer/##X/fire() {..() /*just so it shows up on the profiler*/} \ +/datum/controller/subsystem/timer/##X + #define PROCESSING_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/processing/##X);\ /datum/controller/subsystem/processing/##X/New(){\ - NEW_SS_GLOBAL(SS##X);\ - PreInit();\ + NEW_SS_GLOBAL(SS##X);\ + PreInit();\ }\ +/datum/controller/subsystem/processing/##X/fire() {..() /*just so it shows up on the profiler*/} \ /datum/controller/subsystem/processing/##X +#define VERB_MANAGER_SUBSYSTEM_DEF(X) GLOBAL_REAL(SS##X, /datum/controller/subsystem/verb_manager/##X);\ +/datum/controller/subsystem/verb_manager/##X/New(){\ + NEW_SS_GLOBAL(SS##X);\ + PreInit();\ +}\ +/datum/controller/subsystem/verb_manager/##X/fire() {..() /*just so it shows up on the profiler*/} \ +/datum/controller/subsystem/verb_manager/##X + // Boilerplate code for multi-step processors. See machines.dm for example use. #define INTERNAL_PROCESS_STEP(this_step, initial_step, proc_to_call, cost_var, next_step)\ if(current_step == this_step || (initial_step && !resumed)) /* So we start at step 1 if not resumed.*/ {\ diff --git a/code/__defines/callbacks.dm b/code/__defines/callbacks.dm index 126ef1ea1cf..91d036b9441 100644 --- a/code/__defines/callbacks.dm +++ b/code/__defines/callbacks.dm @@ -1,4 +1,8 @@ -#define GLOBAL_PROC "some_magic_bullshit" - +#define GLOBAL_PROC "some_magic_bullshit" +/// A shorthand for the callback datum, [documented here](datum/callback.html) #define CALLBACK new /datum/callback + #define INVOKE_ASYNC world.ImmediateInvokeAsync + +/// like CALLBACK but specifically for verb callbacks +#define VERB_CALLBACK new /datum/callback/verb_callback diff --git a/code/__defines/subsystems.dm b/code/__defines/subsystems.dm index 533d2c810c2..b3ef3aeda3d 100644 --- a/code/__defines/subsystems.dm +++ b/code/__defines/subsystems.dm @@ -188,6 +188,8 @@ var/global/list/runlevel_flags = list(RUNLEVEL_LOBBY, RUNLEVEL_SETUP, RUNLEVEL_G #define FIRE_PRIORITY_CHAT 400 #define FIRE_PRIORITY_OVERLAYS 500 #define FIRE_PRIORITY_TIMER 700 +#define FIRE_PRIORITY_SPEECH_CONTROLLER 900 +#define FIRE_PRIORITY_DELAYED_VERBS 950 #define FIRE_PRIORITY_INPUT 1000 // This must always always be the max highest priority. Player input must never be lost. /** diff --git a/code/__defines/verb_manager.dm b/code/__defines/verb_manager.dm new file mode 100644 index 00000000000..11ea6ada4d8 --- /dev/null +++ b/code/__defines/verb_manager.dm @@ -0,0 +1,36 @@ +/** + * verb queuing thresholds. remember that since verbs execute after SendMaps the player wont see the effects of the verbs on the game world + * until SendMaps executes next tick, and then when that later update reaches them. thus most player input has a minimum latency of world.tick_lag + player ping. + * however thats only for the visual effect of player input, when a verb processes the actual latency of game state changes or semantic latency is effectively 1/2 player ping, + * unless that verb is queued for the next tick in which case its some number probably smaller than world.tick_lag. + * so some verbs that represent player input are important enough that we only introduce semantic latency if we absolutely need to. + * its for this reason why player clicks are handled in SSinput before even movement - semantic latency could cause someone to move out of range + * when the verb finally processes but it was in range if the verb had processed immediately and overtimed. + */ + +///queuing tick_usage threshold for verbs that are high enough priority that they only queue if the server is overtiming. +///ONLY use for critical verbs +#define VERB_OVERTIME_QUEUE_THRESHOLD 100 +///queuing tick_usage threshold for verbs that need lower latency more than most verbs. +#define VERB_HIGH_PRIORITY_QUEUE_THRESHOLD 95 +///default queuing tick_usage threshold for most verbs which can allow a small amount of latency to be processed in the next tick +#define VERB_DEFAULT_QUEUE_THRESHOLD 85 + +///attempt to queue this verb process if the server is overloaded. evaluates to FALSE if queuing isnt necessary or if it failed. +///_verification_args... are only necessary if the verb_manager subsystem youre using checks them in can_queue_verb() +///if you put anything in _verification_args that ISNT explicitely put in the can_queue_verb() override of the subsystem youre using, +///it will runtime. +#define TRY_QUEUE_VERB(_verb_callback, _tick_check, _subsystem_to_use, _verification_args...) (_queue_verb(_verb_callback, _tick_check, _subsystem_to_use, _verification_args)) +///queue wrapper for TRY_QUEUE_VERB() when you want to call the proc if the server isnt overloaded enough to queue +#define QUEUE_OR_CALL_VERB(_verb_callback, _tick_check, _subsystem_to_use, _verification_args...) \ + if(!TRY_QUEUE_VERB(_verb_callback, _tick_check, _subsystem_to_use, _verification_args)) {\ + _verb_callback:InvokeAsync() \ + }; + +//goes straight to SSverb_manager with default tick threshold +#define DEFAULT_TRY_QUEUE_VERB(_verb_callback, _verification_args...) (TRY_QUEUE_VERB(_verb_callback, VERB_DEFAULT_QUEUE_THRESHOLD, null, _verification_args)) +#define DEFAULT_QUEUE_OR_CALL_VERB(_verb_callback, _verification_args...) QUEUE_OR_CALL_VERB(_verb_callback, VERB_DEFAULT_QUEUE_THRESHOLD, null, _verification_args) + +//default tick threshold but nondefault subsystem +#define TRY_QUEUE_VERB_FOR(_verb_callback, _subsystem_to_use, _verification_args...) (TRY_QUEUE_VERB(_verb_callback, VERB_DEFAULT_QUEUE_THRESHOLD, _subsystem_to_use, _verification_args)) +#define QUEUE_OR_CALL_VERB_FOR(_verb_callback, _subsystem_to_use, _verification_args...) QUEUE_OR_CALL_VERB(_verb_callback, VERB_DEFAULT_QUEUE_THRESHOLD, _subsystem_to_use, _verification_args) diff --git a/code/controllers/subsystems/speech_controller.dm b/code/controllers/subsystems/speech_controller.dm new file mode 100644 index 00000000000..e293c89a9bb --- /dev/null +++ b/code/controllers/subsystems/speech_controller.dm @@ -0,0 +1,5 @@ +/// verb_manager subsystem just for handling say's +VERB_MANAGER_SUBSYSTEM_DEF(speech_controller) + name = "Speech Controller" + wait = 1 + priority = FIRE_PRIORITY_SPEECH_CONTROLLER//has to be high priority, second in priority ONLY to SSinput diff --git a/code/controllers/subsystems/verb_manager.dm b/code/controllers/subsystems/verb_manager.dm new file mode 100644 index 00000000000..1e3cce0a9af --- /dev/null +++ b/code/controllers/subsystems/verb_manager.dm @@ -0,0 +1,167 @@ +/** + * 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 allotted 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 allotted 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 isn't 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 aren't 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_LOBBY | 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 don't 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 don't make this be TRUE on the code level this is for admins to turn on + var/message_admins_on_queue = FALSE + + ///always queue if possible. overrides can_queue_admin_verbs but not FOR_ADMINS_IF_VERBS_FUCKED_immediately_execute_all_verbs + var/always_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(QDELETED(incoming_callback)) + var/destroyed_string + if(!incoming_callback) + destroyed_string = "callback is null." + else + destroyed_string = "callback was deleted [DS2TICKS(world.time - incoming_callback.gc_destroyed)] ticks ago. callback was created [DS2TICKS(world.time) - incoming_callback.creation_time] ticks ago." + + stack_trace("_queue_verb() returned false because it was given a deleted callback! [destroyed_string]") + return FALSE + + if(!istext(incoming_callback.object) && QDELETED(incoming_callback.object)) //just in case the object is GLOBAL_PROC + var/destroyed_string + if(!incoming_callback.object) + destroyed_string = "callback.object is null." + else + destroyed_string = "callback.object was deleted [DS2TICKS(world.time - incoming_callback.object.gc_destroyed)] ticks ago. callback was created [DS2TICKS(world.time) - incoming_callback.creation_time] ticks ago." + + stack_trace("_queue_verb() returned false because it was given a callback acting on a qdeleted object! [destroyed_string]") + return FALSE + + //we want unit tests to be able to directly call verbs that attempt to queue, and since unit tests should test internal behavior, we want the queue + //to happen as if it was actually from player input if its called on a mob. +#ifdef UNIT_TESTS + if(QDELETED(usr) && ismob(incoming_callback.object)) + incoming_callback.user = WEAKREF(incoming_callback.object) + var/datum/callback/new_us = CALLBACK(arglist(list(GLOBAL_PROC, GLOBAL_PROC_REF(_queue_verb)) + args.Copy())) + return world.push_usr(incoming_callback.object, new_us) + +#else + + if(QDELETED(usr) || isnull(usr.client)) + stack_trace("_queue_verb() returned false because it wasn't called from player input!") + return FALSE + +#endif + + if(!istype(subsystem_to_use)) + stack_trace("_queue_verb() returned false because it was given an invalid subsystem to queue for!") + return FALSE + + if((TICK_USAGE < tick_check) && !subsystem_to_use.always_queue) + 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(always_queue && !FOR_ADMINS_IF_VERBS_FUCKED_immediately_execute_all_verbs) + return TRUE + + if((usr.client?.holder && !can_queue_admin_verbs) \ + || (!subsystem_initialized && !(flags & SS_NO_INIT)) \ + || FOR_ADMINS_IF_VERBS_FUCKED_immediately_execute_all_verbs \ + || !(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) + run_verb_queue() + +/// runs through all of this subsystems queue of verb callbacks. +/// goes through the entire verb queue without yielding. +/// used so you can flush the queue outside of fire() without interfering with anything else subtype subsystems might do in fire(). +/datum/controller/subsystem/verb_manager/proc/run_verb_queue() + 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 SECONDS) + //note that wait SECONDS is incorrect if this is called outside of fire() but because byond is garbage i need to add a timer to rustg to find a valid solution + +/datum/controller/subsystem/verb_manager/stat_entry(msg) + . = ..() + if(use_default_stats) + . += "V/S: [round(verbs_executed_per_second, 0.01)]" diff --git a/code/datums/verb_callbacks.dm b/code/datums/verb_callbacks.dm new file mode 100644 index 00000000000..77f817017d2 --- /dev/null +++ b/code/datums/verb_callbacks.dm @@ -0,0 +1,27 @@ +///like normal callbacks but they also record their creation time for measurement purposes +///they also require the same usr/user that made the callback to both still exist and to still have a client in order to execute +/datum/callback/verb_callback + ///the tick this callback datum was created in. used for testing latency + var/creation_time = 0 + +/datum/callback/verb_callback/New(thingtocall, proctocall, ...) + creation_time = DS2TICKS(world.time) + . = ..() + +#ifndef UNIT_TEST +/datum/callback/verb_callback/Invoke(...) + var/mob/our_user = user?.resolve() + if(QDELETED(our_user) || isnull(our_user.client)) + return + var/mob/temp = usr + . = ..() + usr = temp + +/datum/callback/verb_callback/InvokeAsync(...) + var/mob/our_user = user?.resolve() + if(QDELETED(our_user) || isnull(our_user.client)) + return + var/mob/temp = usr + . = ..() + usr = temp +#endif diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index d4eff6fd9a1..c311ab322d0 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -143,7 +143,21 @@ if("openLink") src << link(href_list["link"]) - ..() //redirect to hsrc.Topic() + if (hsrc) + var/datum/real_src = hsrc + if(QDELETED(real_src)) + return + + //fun fact: Topic() acts like a verb and is executed at the end of the tick like other verbs. So we have to queue it if the server is + //overloaded + if(hsrc && hsrc != holder && DEFAULT_TRY_QUEUE_VERB(VERB_CALLBACK(src, PROC_REF(_Topic), hsrc, href, href_list))) + return + ..() //redirect to hsrc.Topic() + +///dumb workaround because byond doesnt seem to recognize the Topic() typepath for /datum/proc/Topic() from the client Topic, +///so we cant queue it without this +/client/proc/_Topic(datum/hsrc, href, list/href_list) + return hsrc.Topic(href, href_list) //This stops files larger than UPLOAD_LIMIT being sent from client to server via input(), client.Import() etc. /client/AllowUpload(filename, filelength) diff --git a/code/modules/mob/say.dm b/code/modules/mob/say.dm index a5b7a6f00d9..eb34f0421b7 100644 --- a/code/modules/mob/say.dm +++ b/code/modules/mob/say.dm @@ -17,6 +17,8 @@ set name = "Say" set category = "IC" set hidden = 1 + set instant = TRUE + //VOREStation Addition Start if(forced_psay) psay(message) @@ -24,7 +26,10 @@ //VOREStation Addition End client?.stop_thinking() - usr.say(message) + //queue this message because verbs are scheduled to process after SendMaps in the tick and speech is pretty expensive when it happens. + //by queuing this for next tick the mc can compensate for its cost instead of having speech delay the start of the next tick + if(message) + QUEUE_OR_CALL_VERB_FOR(VERB_CALLBACK(src, TYPE_PROC_REF(/mob, say), message), SSspeech_controller) /mob/verb/me_verb(message as message) set name = "Me" diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm index ed654a4c6f4..5acbaa887a0 100644 --- a/code/modules/tgui/tgui.dm +++ b/code/modules/tgui/tgui.dm @@ -353,8 +353,7 @@ log_tgui(user, "Action: [act_type] [href_list["payload"]], Window: [window.id], Source: [src_object]") #endif process_status() - if(src_object.tgui_act(act_type, payload, src, state)) - SStgui.update_uis(src_object) + DEFAULT_QUEUE_OR_CALL_VERB(VERB_CALLBACK(src, PROC_REF(on_act_message), act_type, payload, state)) return FALSE switch(type) if("ready") @@ -382,3 +381,10 @@ log_tgui(user, "Fallback Triggered: [href_list["payload"]], Window: [window.id], Source: [src_object]") #endif src_object.tgui_fallback(payload) + +/// Wrapper for behavior to potentially wait until the next tick if the server is overloaded +/datum/tgui/proc/on_act_message(act_type, payload, state) + if(QDELETED(src) || QDELETED(src_object)) + return + if(src_object.tgui_act(act_type, payload, src, state)) + SStgui.update_uis(src_object) diff --git a/vorestation.dme b/vorestation.dme index aa8c5dbd2f4..f41c66779d3 100644 --- a/vorestation.dme +++ b/vorestation.dme @@ -148,6 +148,7 @@ #include "code\__defines\typeids.dm" #include "code\__defines\unit_tests.dm" #include "code\__defines\update_icons.dm" +#include "code\__defines\verb_manager.dm" #include "code\__defines\vore.dm" #include "code\__defines\vote.dm" #include "code\__defines\vv.dm" @@ -368,6 +369,7 @@ #include "code\controllers\subsystems\shuttles.dm" #include "code\controllers\subsystems\skybox.dm" #include "code\controllers\subsystems\sounds.dm" +#include "code\controllers\subsystems\speech_controller.dm" #include "code\controllers\subsystems\sqlite.dm" #include "code\controllers\subsystems\SSvote.dm" #include "code\controllers\subsystems\statpanel.dm" @@ -379,6 +381,7 @@ #include "code\controllers\subsystems\time_track.dm" #include "code\controllers\subsystems\timer.dm" #include "code\controllers\subsystems\transcore_vr.dm" +#include "code\controllers\subsystems\verb_manager.dm" #include "code\controllers\subsystems\vis_overlays.dm" #include "code\controllers\subsystems\vote.dm" #include "code\controllers\subsystems\webhooks.dm" @@ -426,6 +429,7 @@ #include "code\datums\riding.dm" #include "code\datums\soul_link.dm" #include "code\datums\sun.dm" +#include "code\datums\verb_callbacks.dm" #include "code\datums\weakrefs.dm" #include "code\datums\autolathe\arms.dm" #include "code\datums\autolathe\arms_vr.dm"