[DNM] Runechat refactor (#20860)

* runechat optimisations

* Apply suggestions from code review

Co-authored-by: Henri215 <77684085+Henri215@users.noreply.github.com>

* length

* fixes

* reset this value

* amazing grace

* whitespace

* snowball fixes

---------

Co-authored-by: Henri215 <77684085+Henri215@users.noreply.github.com>
This commit is contained in:
S34N
2023-06-01 21:10:16 -05:00
committed by GitHub
co-authored by Henri215
parent 02db93e37e
commit 14d78b6988
4 changed files with 109 additions and 273 deletions
+8
View File
@@ -86,3 +86,11 @@
ss_id="processing_[#X]";\
}\
/datum/controller/subsystem/processing/##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
+5
View File
@@ -20,6 +20,11 @@
/// runs stoplag if tick_usage is above the limit
#define CHECK_TICK ( TICK_CHECK ? stoplag() : 0 )
/// Checks if a sleeping proc is running before or after the master controller
#define RUNNING_BEFORE_MASTER ( Master.last_run != null && Master.last_run != world.time )
/// Returns true if a verb ought to yield to the MC (IE: queue up to be processed by a subsystem)
#define VERB_SHOULD_YIELD ( TICK_CHECK || RUNNING_BEFORE_MASTER )
/// Returns true if tick usage is above 95, for high priority usage
#define TICK_CHECK_HIGH_PRIORITY ( TICK_USAGE > 95 )
/// runs stoplag if tick_usage is above 95, for high priority usage
+10 -228
View File
@@ -1,234 +1,16 @@
/// Controls how many buckets should be kept, each representing a tick. (30 seconds worth)
#define BUCKET_LEN (world.fps * 1 * 30)
/// Helper for getting the correct bucket for a given chatmessage
#define BUCKET_POS(scheduled_destruction) (((round((scheduled_destruction - SSrunechat.head_offset) / world.tick_lag) + 1) % BUCKET_LEN) || BUCKET_LEN)
/// Gets the maximum time at which messages will be handled in buckets, used for deferring to secondary queue
#define BUCKET_LIMIT (SSrunechat.head_offset + TICKS2DS(BUCKET_LEN + SSrunechat.practical_offset - 1))
/**
* # Runechat Subsystem
*
* Maintains a timer-like system to handle destruction of runechat messages. Much of this code is modeled
* after or adapted from the timer subsystem. Made by Bobbahbrown of /tg/station13
*
* Note that this has the same structure for storing and queueing messages as the timer subsystem does
* for handling timers: the bucket_list is a list of chatmessage datums, each of which are the head
* of a circularly linked list. Any given index in bucket_list could be null, representing an empty bucket.
*
* AA Note:
* One of the primary reasons for this is because each chatmessage has a timer attached to it, which is extra load on the GC
* At 150 population, the GC literally cannot keep up with processing 368,000 runechats and 368,000 extra timers in a 1 hour 30 minute round
* This also makes performance profiling a lot easier.
*
*/
SUBSYSTEM_DEF(runechat)
TIMER_SUBSYSTEM_DEF(runechat)
name = "Runechat"
flags = SS_TICKER | SS_NO_INIT
wait = 1
priority = FIRE_PRIORITY_RUNECHAT
offline_implications = "Runechat messages will no longer clear. Shuttle call recommended."
cpu_display = SS_CPUDISPLAY_HIGH
/// world.time of the first entry in the bucket list, effectively the 'start time' of the current buckets
var/head_offset = 0
/// Index of the first non-empty bucket
var/practical_offset = 1
/// world.tick_lag the bucket was designed for
var/bucket_resolution = 0
/// How many messages are in the buckets
var/bucket_count = 0
/// List of buckets, each bucket holds every message that has to be killed that byond tick
var/list/bucket_list = list()
/// Queue used for storing messages that are scheduled for deletion too far in the future for the buckets
var/list/datum/chatmessage/second_queue = list()
var/list/datum/callback/message_queue = list()
/datum/controller/subsystem/runechat/PreInit()
bucket_list.len = BUCKET_LEN
head_offset = world.time
bucket_resolution = world.tick_lag
/datum/controller/subsystem/runechat/get_stat_details()
return "ActMsgs:[bucket_count] SecQueue:[length(second_queue)]"
/datum/controller/subsystem/runechat/fire(resumed = FALSE)
// Store local references to datum vars as it is faster to access them this way
var/list/bucket_list = src.bucket_list
if (MC_TICK_CHECK)
return
// Check for when we need to loop the buckets, this occurs when
// the head_offset is approaching BUCKET_LEN ticks in the past
if (practical_offset > BUCKET_LEN)
head_offset += TICKS2DS(BUCKET_LEN)
practical_offset = 1
resumed = FALSE
// Store a reference to the 'working' chatmessage so that we can resume if the MC
// has us stop mid-way through processing
var/static/datum/chatmessage/cm
if (!resumed)
cm = null
// Iterate through each bucket starting from the practical offset
while (practical_offset <= BUCKET_LEN && head_offset + ((practical_offset - 1) * world.tick_lag) <= world.time)
var/datum/chatmessage/bucket_head = bucket_list[practical_offset]
if (!cm || !bucket_head || cm == bucket_head)
bucket_head = bucket_list[practical_offset]
cm = bucket_head
while (cm)
// If the chatmessage hasn't yet had its life ended then do that now
var/datum/chatmessage/next = cm.next
if (!cm.eol_complete)
cm.end_of_life()
else if (!QDELETED(cm)) // otherwise if we haven't deleted it yet, do so (this is after EOL completion)
qdel(cm)
if (MC_TICK_CHECK)
return
// Break once we've processed the entire bucket
cm = next
if (cm == bucket_head)
break
// Empty the bucket, check if anything in the secondary queue should be shifted to this bucket
bucket_list[practical_offset++] = null
var/i = 0
for (i in 1 to length(second_queue))
cm = second_queue[i]
if (cm.scheduled_destruction >= BUCKET_LIMIT)
i--
break
// Transfer the message into the bucket, performing necessary circular doubly-linked list operations
bucket_count++
var/bucket_pos = max(1, BUCKET_POS(cm.scheduled_destruction))
var/datum/timedevent/head = bucket_list[bucket_pos]
if (!head)
bucket_list[bucket_pos] = cm
cm.next = null
cm.prev = null
continue
if (!head.prev)
head.prev = head
cm.next = head
cm.prev = head.prev
cm.next.prev = cm
cm.prev.next = cm
if (i)
second_queue.Cut(1, i + 1)
cm = null
/datum/controller/subsystem/runechat/Recover()
bucket_list |= SSrunechat.bucket_list
second_queue |= SSrunechat.second_queue
/**
* Enters the runechat subsystem with this chatmessage, inserting it into the end-of-life queue
*
* This will also account for a chatmessage already being registered, and in which case
* the position will be updated to remove it from the previous location if necessary
*
* Arguments:
* * new_sched_destruction Optional, when provided is used to update an existing message with the new specified time
*/
/datum/chatmessage/proc/enter_subsystem(new_sched_destruction = 0)
// Get local references from subsystem as they are faster to access than the datum references
var/list/bucket_list = SSrunechat.bucket_list
var/list/second_queue = SSrunechat.second_queue
// When necessary, de-list the chatmessage from its previous position
if (new_sched_destruction)
if (scheduled_destruction >= BUCKET_LIMIT)
second_queue -= src
else
SSrunechat.bucket_count--
var/bucket_pos = BUCKET_POS(scheduled_destruction)
if (bucket_pos > 0)
var/datum/chatmessage/bucket_head = bucket_list[bucket_pos]
if (bucket_head == src)
bucket_list[bucket_pos] = next
if (prev != next)
prev.next = next
next.prev = prev
else
prev?.next = null
next?.prev = null
prev = next = null
scheduled_destruction = new_sched_destruction
// Ensure the scheduled destruction time is properly bound to avoid missing a scheduled event
scheduled_destruction = max(CEILING(scheduled_destruction, world.tick_lag), world.time + world.tick_lag)
// Handle insertion into the secondary queue if the required time is outside our tracked amounts
if (scheduled_destruction >= BUCKET_LIMIT)
BINARY_INSERT(src, SSrunechat.second_queue, datum/chatmessage, scheduled_destruction)
return
// Get bucket position and a local reference to the datum var, it's faster to access this way
var/bucket_pos = BUCKET_POS(scheduled_destruction)
// Get the bucket head for that bucket, increment the bucket count
var/datum/chatmessage/bucket_head = bucket_list[bucket_pos]
SSrunechat.bucket_count++
// If there is no existing head of this bucket, we can set this message to be that head
if (!bucket_head)
bucket_list[bucket_pos] = src
return
// Otherwise it's a simple insertion into the circularly doubly-linked list
if (!bucket_head.prev)
bucket_head.prev = bucket_head
next = bucket_head
prev = bucket_head.prev
next.prev = src
prev.next = src
/**
* Removes this chatmessage datum from the runechat subsystem
*/
/datum/chatmessage/proc/leave_subsystem()
// Attempt to find the bucket that contains this chat message
var/bucket_pos = BUCKET_POS(scheduled_destruction)
// Get local references to the subsystem's vars, faster than accessing on the datum
var/list/bucket_list = SSrunechat.bucket_list
var/list/second_queue = SSrunechat.second_queue
// Attempt to get the head of the bucket
var/datum/chatmessage/bucket_head
if (bucket_pos > 0)
bucket_head = bucket_list[bucket_pos]
// Decrement the number of messages in buckets if the message is
// the head of the bucket, or has a SD less than BUCKET_LIMIT implying it fits
// into an existing bucket, or is otherwise not present in the secondary queue
if(bucket_head == src)
bucket_list[bucket_pos] = next
SSrunechat.bucket_count--
else if(scheduled_destruction < BUCKET_LIMIT)
SSrunechat.bucket_count--
else
var/l = length(second_queue)
second_queue -= src
if(l == length(second_queue))
SSrunechat.bucket_count--
// Remove the message from the bucket, ensuring to maintain
// the integrity of the bucket's list if relevant
if(prev != next)
prev.next = next
next.prev = prev
else
prev?.next = null
next?.prev = null
prev = next = null
#undef BUCKET_LEN
#undef BUCKET_POS
#undef BUCKET_LIMIT
/datum/controller/subsystem/timer/runechat/fire(resumed)
. = ..() //poggers
while(length(message_queue))
var/datum/callback/queued_message = message_queue[length(message_queue)]
queued_message.Invoke()
message_queue.len--
if(MC_TICK_CHECK)
return
+86 -45
View File
@@ -1,9 +1,11 @@
/// How long the chat message's spawn-in animation will occur for
#define CHAT_MESSAGE_SPAWN_TIME 0.2 SECONDS
#define CHAT_MESSAGE_SPAWN_TIME (0.2 SECONDS)
/// How long the chat message will exist prior to any exponential decay
#define CHAT_MESSAGE_LIFESPAN 5 SECONDS
#define CHAT_MESSAGE_LIFESPAN (5 SECONDS)
/// How long the chat message's end of life fading animation will occur for
#define CHAT_MESSAGE_EOL_FADE 0.7 SECONDS
#define CHAT_MESSAGE_EOL_FADE (0.7 SECONDS)
/// Grace period for fade before we actually delete the chat message
#define CHAT_MESSAGE_GRACE_PERIOD (0.2 SECONDS)
/// Factor of how much the message index (number of messages) will account to exponential decay
#define CHAT_MESSAGE_EXP_DECAY 0.7
/// Factor of how much height will account to exponential decay
@@ -18,8 +20,13 @@
#define CHAT_LAYER_Z_STEP 0.0001
/// The number of z-layer 'slices' usable by the chat message layering
#define CHAT_LAYER_MAX_Z (CHAT_LAYER_MAX - CHAT_LAYER) / CHAT_LAYER_Z_STEP
/// Macro from Lummox used to get height from a MeasureText proc
#define WXH_TO_HEIGHT(x) text2num(copytext(x, findtextEx(x, "x") + 1))
/// Macro from Lummox used to get height from a MeasureText proc.
/// resolves the MeasureText() return value once, then resolves the height, then sets return_var to that.
#define WXH_TO_HEIGHT(measurement, return_var) \
do { \
var/_measurement = measurement; \
return_var = text2num(copytext(_measurement, findtextEx(_measurement, "x") + 1)); \
} while(FALSE);
/**
* # Chat Message Overlay
@@ -33,18 +40,14 @@
var/atom/message_loc
/// The client who heard this message
var/client/owned_by
/// Contains the scheduled destruction time, used for scheduling EOL
var/scheduled_destruction
/// Contains the time that the EOL for the message will be complete, used for qdel scheduling
var/eol_complete
/// Contains the approximate amount of lines for height decay
var/approx_lines
/// The current index used for adjusting the layer of each sequential chat message such that recent messages will overlay older ones
var/static/current_z_idx = 0
/// Contains the reference to the next chatmessage in the bucket, used by runechat subsystem
var/datum/chatmessage/next
/// Contains the reference to the previous chatmessage in the bucket, used by runechat subsystem
var/datum/chatmessage/prev
/// When we started animating the message
var/animate_start = 0
/// Our animation lifespan, how long this message will last
var/animate_lifespan = 0
/**
* Constructs a chat message overlay
@@ -59,7 +62,7 @@
*/
/datum/chatmessage/New(text, atom/target, mob/owner, italics, size, lifespan = CHAT_MESSAGE_LIFESPAN, symbol)
. = ..()
if (!istype(target))
if(!istype(target))
CRASH("Invalid target given for chatmessage")
if(QDELETED(owner) || !istype(owner) || !owner.client)
stack_trace("/datum/chatmessage created with [isnull(owner) ? "null" : "invalid"] mob owner")
@@ -68,14 +71,15 @@
INVOKE_ASYNC(src, PROC_REF(generate_image), text, target, owner, lifespan, italics, size, symbol)
/datum/chatmessage/Destroy()
if (owned_by)
if (owned_by.seen_messages)
if(REALTIMEOFDAY < (animate_start + animate_lifespan))
stack_trace("Del'd before we finished fading, with [(animate_start + animate_lifespan) - REALTIMEOFDAY] time left")
if(owned_by)
if(owned_by.seen_messages)
LAZYREMOVEASSOC(owned_by.seen_messages, message_loc, src)
owned_by.images.Remove(message)
owned_by = null
message_loc = null
message = null
leave_subsystem()
return ..()
/**
@@ -105,12 +109,12 @@
// Clip message
var/maxlen = CHAT_MESSAGE_MAX_LENGTH
var/datum/html/split_holder/s = split_html(text)
if (length_char(s.inner_text) > maxlen)
if(length_char(s.inner_text) > maxlen)
var/chattext = copytext_char(s.inner_text, 1, maxlen + 1) + "..."
text = jointext(s.opening, "") + chattext + jointext(s.closing, "")
// Calculate target color if not already present
if (!target.chat_color || target.chat_color_name != target.name)
if(!target.chat_color || target.chat_color_name != target.name)
target.chat_color = colorize_string(target.name)
target.chat_color_name = target.name
@@ -120,7 +124,7 @@
// Reject whitespace
var/static/regex/whitespace = new(@"^\s*$")
if (whitespace.Find(text))
if(whitespace.Find(text))
qdel(src)
return
@@ -137,25 +141,56 @@
// Approximate text height
var/static/regex/html_metachars = new(@"&[A-Za-z]{1,7};", "g")
var/complete_text = "<span class='center maptext[size ? " [size]" : ""]' style='[italics ? "font-style: italic; " : ""]color: [output_color]'>[symbol][text]</span>"
var/mheight = WXH_TO_HEIGHT(owned_by.MeasureText(complete_text, null, CHAT_MESSAGE_WIDTH))
var/mheight
WXH_TO_HEIGHT(owned_by.MeasureText(complete_text, null, CHAT_MESSAGE_WIDTH), mheight)
if(!VERB_SHOULD_YIELD)
return finish_image_generation(mheight, target, owner, complete_text, lifespan)
var/datum/callback/our_callback = CALLBACK(src, PROC_REF(finish_image_generation), mheight, target, owner, complete_text, lifespan)
SSrunechat.message_queue += our_callback
return
///finishes the image generation after the MeasureText() call in generate_image().
///necessary because after that call the proc can resume at the end of the tick and cause overtime.
/datum/chatmessage/proc/finish_image_generation(mheight, atom/target, mob/owner, complete_text, lifespan)
var/rough_time = REALTIMEOFDAY
approx_lines = max(1, mheight / CHAT_MESSAGE_APPROX_LHEIGHT)
// Translate any existing messages upwards, apply exponential decay factors to timers
message_loc = target
if (owned_by.seen_messages)
if(owned_by.seen_messages)
var/idx = 1
var/combined_height = approx_lines
for(var/msg in owned_by.seen_messages[message_loc])
var/datum/chatmessage/m = msg
animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME)
for(var/datum/chatmessage/m as anything in owned_by.seen_messages[message_loc])
combined_height += m.approx_lines
var/sched_remaining = m.scheduled_destruction - world.time
if (!m.eol_complete)
var/remaining_time = (sched_remaining) * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height)
m.enter_subsystem(world.time + remaining_time) // push updated time to runechat SS
var/time_alive = rough_time - m.animate_start
var/lifespan_until_fade = m.animate_lifespan - CHAT_MESSAGE_EOL_FADE
if(time_alive >= lifespan_until_fade) // If already fading out or dead, just shift upwards
animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL)
continue
// Ensure we don't accidentially spike alpha up or something silly like that
m.message.alpha = m.get_current_alpha(time_alive)
var/adjusted_lifespan_until_fade = lifespan_until_fade * (CHAT_MESSAGE_EXP_DECAY ** idx++) * (CHAT_MESSAGE_HEIGHT_DECAY ** combined_height)
m.animate_lifespan = adjusted_lifespan_until_fade + CHAT_MESSAGE_EOL_FADE
var/remaining_lifespan_until_fade = adjusted_lifespan_until_fade - time_alive
if(remaining_lifespan_until_fade > 0) // Still got some lifetime to go; stay faded in for the remainder, then fade out
animate(m.message, alpha = 255, time = remaining_lifespan_until_fade)
animate(alpha = 0, time = CHAT_MESSAGE_EOL_FADE)
else // Current time alive is beyond new adjusted lifespan, your time has come my son
animate(m.message, alpha = 0, time = CHAT_MESSAGE_EOL_FADE)
// We run this after the alpha animate, because we don't want to interrup it, but also don't want to block it by running first
// Sooo instead we do this. bit messy but it fuckin works
animate(m.message, pixel_y = m.message.pixel_y + mheight, time = CHAT_MESSAGE_SPAWN_TIME, flags = ANIMATION_PARALLEL)
// Reset z index if relevant
if (current_z_idx >= CHAT_LAYER_MAX_Z)
if(current_z_idx >= CHAT_LAYER_MAX_Z)
current_z_idx = 0
// Build message image
@@ -169,22 +204,33 @@
message.maptext_x = (CHAT_MESSAGE_WIDTH - owner.bound_width) * -0.5
message.maptext = complete_text
animate_start = rough_time
animate_lifespan = lifespan
// View the message
LAZYADDASSOC(owned_by.seen_messages, message_loc, src)
owned_by.images |= message
// Fade in
animate(message, alpha = 255, time = CHAT_MESSAGE_SPAWN_TIME)
// Stay faded in
animate(alpha = 255, time = lifespan - CHAT_MESSAGE_SPAWN_TIME - CHAT_MESSAGE_EOL_FADE)
// Fade out
animate(alpha = 0, time = CHAT_MESSAGE_EOL_FADE)
// Prepare for destruction
scheduled_destruction = world.time + (lifespan - CHAT_MESSAGE_EOL_FADE)
enter_subsystem()
// Register with the runechat SS to handle destruction
addtimer(CALLBACK(GLOBAL_PROC, GLOBAL_PROC_REF(qdel), src), lifespan + CHAT_MESSAGE_GRACE_PERIOD, TIMER_DELETE_ME, SSrunechat)
/**
* Applies final animations to overlay CHAT_MESSAGE_EOL_FADE deciseconds prior to message deletion
*/
/datum/chatmessage/proc/end_of_life(fadetime = CHAT_MESSAGE_EOL_FADE)
eol_complete = scheduled_destruction + fadetime
animate(message, alpha = 0, time = fadetime, flags = ANIMATION_PARALLEL)
enter_subsystem(eol_complete) // re-enter the runechat SS with the EOL completion time to QDEL self
/// Returns the current alpha of the message based on the time spent
/datum/chatmessage/proc/get_current_alpha(time_alive)
if(time_alive < CHAT_MESSAGE_SPAWN_TIME)
return (time_alive / CHAT_MESSAGE_SPAWN_TIME) * 255
var/lifespan_until_fade = animate_lifespan - CHAT_MESSAGE_EOL_FADE
if(time_alive <= lifespan_until_fade)
return 255
return (1 - ((time_alive - lifespan_until_fade) / CHAT_MESSAGE_EOL_FADE)) * 255
/**
* Creates a message overlay at a defined location for a given speaker
@@ -197,11 +243,6 @@
* * symbol - The symbol type of the message
*/
/mob/proc/create_chat_message(atom/movable/speaker, raw_message, italics = FALSE, size, symbol)
if(isobserver(src))
return
// Display visual above source
new /datum/chatmessage(raw_message, speaker, src, italics, size, CHAT_MESSAGE_LIFESPAN, symbol)