Files
Bubberstation/code/modules/mob/mob_say.dm
Kylerace 77c2b7f50c 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
2022-07-31 14:56:18 -07:00

181 lines
6.4 KiB
Plaintext

//Speech verbs.
///what clients use to speak. when you type a message into the chat bar in say mode, this is the first thing that goes off serverside.
/mob/verb/say_verb(message as text)
set name = "Say"
set category = "IC"
set instant = TRUE
if(GLOB.say_disabled) //This is here to try to identify lag problems
to_chat(usr, span_danger("Speech is currently admin-disabled."))
return
//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, /atom/movable/proc/say, message), SSspeech_controller)
///Whisper verb
/mob/verb/whisper_verb(message as text)
set name = "Whisper"
set category = "IC"
set instant = TRUE
if(GLOB.say_disabled) //This is here to try to identify lag problems
to_chat(usr, span_danger("Speech is currently admin-disabled."))
return
if(message)
QUEUE_OR_CALL_VERB_FOR(VERB_CALLBACK(src, /mob/proc/whisper, message), SSspeech_controller)
/**
* Whisper a message.
*
* Basic level implementation just speaks the message, nothing else.
*/
/mob/proc/whisper(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language, ignore_spam = FALSE, forced, filterproof)
if(!message)
return
say(message, language = language)
///The me emote verb
/mob/verb/me_verb(message as text)
set name = "Me"
set category = "IC"
if(GLOB.say_disabled) //This is here to try to identify lag problems
to_chat(usr, span_danger("Speech is currently admin-disabled."))
return
message = trim(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN))
QUEUE_OR_CALL_VERB_FOR(VERB_CALLBACK(src, /mob/proc/emote, "me", 1, message, TRUE), SSspeech_controller)
///Speak as a dead person (ghost etc)
/mob/proc/say_dead(message)
var/name = real_name
var/alt_name = ""
if(GLOB.say_disabled) //This is here to try to identify lag problems
to_chat(usr, span_danger("Speech is currently admin-disabled."))
return
var/jb = is_banned_from(ckey, "Deadchat")
if(QDELETED(src))
return
if(jb)
to_chat(src, span_danger("You have been banned from deadchat."))
return
if (src.client)
if(src.client.prefs.muted & MUTE_DEADCHAT)
to_chat(src, span_danger("You cannot talk in deadchat (muted)."))
return
if(SSlag_switch.measures[SLOWMODE_SAY] && !HAS_TRAIT(src, TRAIT_BYPASS_MEASURES) && src == usr)
if(!COOLDOWN_FINISHED(client, say_slowmode))
to_chat(src, span_warning("Message not sent due to slowmode. Please wait [SSlag_switch.slowmode_cooldown/10] seconds between messages.\n\"[message]\""))
return
COOLDOWN_START(client, say_slowmode, SSlag_switch.slowmode_cooldown)
if(src.client.handle_spam_prevention(message,MUTE_DEADCHAT))
return
var/mob/dead/observer/O = src
if(isobserver(src) && O.deadchat_name)
name = "[O.deadchat_name]"
else
if(mind?.name)
name = "[mind.name]"
else
name = real_name
if(name != real_name)
alt_name = " (died as [real_name])"
var/spanned = say_quote(say_emphasis(message))
var/source = "<span class='game'><span class='prefix'>DEAD:</span> <span class='name'>[name]</span>[alt_name]"
var/rendered = " <span class='message'>[emoji_parse(spanned)]</span></span>"
log_talk(message, LOG_SAY, tag="DEAD")
if(SEND_SIGNAL(src, COMSIG_MOB_DEADSAY, message) & MOB_DEADSAY_SIGNAL_INTERCEPT)
return
var/displayed_key = key
if(client?.holder?.fakekey)
displayed_key = null
deadchat_broadcast(rendered, source, follow_target = src, speaker_key = displayed_key)
///Check if this message is an emote
/mob/proc/check_emote(message, forced)
if(message[1] == "*")
emote(copytext(message, length(message[1]) + 1), intentional = !forced)
return TRUE
///Check if the mob has a hivemind channel
/mob/proc/hivecheck()
return FALSE
///The amount of items we are looking for in the message
#define MESSAGE_MODS_LENGTH 6
/mob/proc/check_for_custom_say_emote(message, list/mods)
var/customsaypos = findtext(message, "*")
if(!customsaypos)
return message
if (is_banned_from(ckey, "Emote"))
return copytext(message, customsaypos + 1)
mods[MODE_CUSTOM_SAY_EMOTE] = lowertext(copytext_char(message, 1, customsaypos))
message = copytext(message, customsaypos + 1)
if (!message)
mods[MODE_CUSTOM_SAY_ERASE_INPUT] = TRUE
message = "an interesting thing to say"
return message
/**
* Extracts and cleans message of any extenstions at the begining of the message
* Inserts the info into the passed list, returns the cleaned message
*
* Result can be
* * SAY_MODE (Things like aliens, channels that aren't channels)
* * MODE_WHISPER (Quiet speech)
* * MODE_SING (Singing)
* * MODE_HEADSET (Common radio channel)
* * RADIO_EXTENSION the extension we're using (lots of values here)
* * RADIO_KEY the radio key we're using, to make some things easier later (lots of values here)
* * LANGUAGE_EXTENSION the language we're trying to use (lots of values here)
*/
/mob/proc/get_message_mods(message, list/mods)
for(var/I in 1 to MESSAGE_MODS_LENGTH)
// Prevents "...text" from being read as a radio message
if (length(message) > 1 && message[2] == message[1])
continue
var/key = message[1]
var/chop_to = 2 //By default we just take off the first char
if(key == "#" && !mods[WHISPER_MODE])
mods[WHISPER_MODE] = MODE_WHISPER
else if(key == "%" && !mods[MODE_SING])
mods[MODE_SING] = TRUE
else if(key == ";" && !mods[MODE_HEADSET])
if(stat == CONSCIOUS) //necessary indentation so it gets stripped of the semicolon anyway.
mods[MODE_HEADSET] = TRUE
else if((key in GLOB.department_radio_prefixes) && length(message) > length(key) + 1 && !mods[RADIO_EXTENSION])
mods[RADIO_KEY] = lowertext(message[1 + length(key)])
mods[RADIO_EXTENSION] = GLOB.department_radio_keys[mods[RADIO_KEY]]
chop_to = length(key) + 2
else if(key == "," && !mods[LANGUAGE_EXTENSION])
for(var/ld in GLOB.all_languages)
var/datum/language/LD = ld
if(initial(LD.key) == message[1 + length(message[1])])
// No, you cannot speak in xenocommon just because you know the key
if(!can_speak_language(LD))
return message
mods[LANGUAGE_EXTENSION] = LD
chop_to = length(key) + length(initial(LD.key)) + 1
if(!mods[LANGUAGE_EXTENSION])
return message
else
return message
message = trim_left(copytext_char(message, chop_to))
if(!message)
return
return message