From 079f8ac51554bb338ac5826c9d06c8d4bc10be80 Mon Sep 17 00:00:00 2001 From: LemonInTheDark <58055496+LemonInTheDark@users.noreply.github.com> Date: Thu, 3 Feb 2022 02:57:52 -0800 Subject: [PATCH] Adds moveloop bucketing, uses queues for the singulo rather then sleeps (#64418) Adds a basic bucketing system to move loops. This should hopefully save a lot of cpu time, and allow for more load while gaining better smoothness. The idea is very similar to SStimer, but my implementation is much more simple, since I have to worry less about long delays and redundant buckets. Insertion needs to be cheaper too, since I'm making a system that by design holds a lot of looping things It comes with some minor tradeoffs, we can't have constant rechecking of loops if a move "fails", not that we really want that anyway We also lose direct control over the timer var, but I think that's better, don't want people manipulating that directly Not that it even really worked very well back when we did have it Removes the sleep from singularity code Rather then using sleep to store the state of our iteration, we instead queue the iteration in a list. We then use a custom singulo processing subsystem to call our "digest" proc several times per full eat, with the hope of staying on top of our queue This rarely happens because the queue is too large, god why is a sm powered singulo 24x24 tiles. I've also A: cached our dist checks, and B: Added dist checks to prevent attempting to pull things out of range This might look a bit worse, but it saves a lot of work Oh right and I made the singulo unable to eat while it still has tiles to digest. The hope is to prevent overwork and list explosion. Hopefully this will prevent singulo server stoppage, though I've seen some other worrying things in testing. --- code/__DEFINES/movement.dm | 4 + code/__HELPERS/_lists.dm | 37 ++++++++ .../subsystem/movement/movement.dm | 92 +++++++++++++++---- .../subsystem/movement/movement_types.dm | 7 +- .../subsystem/processing/singulo.dm | 6 ++ code/datums/components/singularity.dm | 77 +++++++++++----- code/modules/power/singularity/singularity.dm | 10 +- tgstation.dme | 1 + 8 files changed, 186 insertions(+), 48 deletions(-) create mode 100644 code/controllers/subsystem/processing/singulo.dm diff --git a/code/__DEFINES/movement.dm b/code/__DEFINES/movement.dm index 5b2ea68d853..10f9e67025f 100644 --- a/code/__DEFINES/movement.dm +++ b/code/__DEFINES/movement.dm @@ -35,6 +35,10 @@ GLOBAL_VAR_INIT(glide_size_multiplier, 1.0) ///Should we override the loop's glide? #define MOVEMENT_LOOP_IGNORE_GLIDE (1<<2) +//Index defines for movement bucket data packets +#define MOVEMENT_BUCKET_TIME 1 +#define MOVEMENT_BUCKET_LIST 2 + /** * currently_z_moving defines. Higher numbers mean higher priority. * This one is for falling down open space from stuff such as deleted tile, pit grate... diff --git a/code/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm index 7c034257ce6..570d4523f7c 100644 --- a/code/__HELPERS/_lists.dm +++ b/code/__HELPERS/_lists.dm @@ -133,6 +133,43 @@ };\ } while(FALSE) +#define SORT_FIRST_INDEX(list) (list[1]) +#define SORT_VAR_NO_TYPE(varname) var/varname +/**** + * Even more custom binary search sorted insert, using defines instead of vars + * INPUT: Item to be inserted + * LIST: List to insert INPUT into + * TYPECONT: A define setting the var to the typepath of the contents of the list + * COMPARE: The item to compare against, usualy the same as INPUT + * COMPARISON: A define that takes an item to compare as input, and returns their comparable value + * COMPTYPE: How should the list be compared? Either COMPARE_KEY or COMPARE_VALUE. + */ +#define BINARY_INSERT_DEFINE(INPUT, LIST, TYPECONT, COMPARE, COMPARISON, COMPTYPE) \ + do {\ + var/list/__BIN_LIST = LIST;\ + var/__BIN_CTTL = length(__BIN_LIST);\ + if(!__BIN_CTTL) {\ + __BIN_LIST += INPUT;\ + } else {\ + var/__BIN_LEFT = 1;\ + var/__BIN_RIGHT = __BIN_CTTL;\ + var/__BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + ##TYPECONT(__BIN_ITEM);\ + while(__BIN_LEFT < __BIN_RIGHT) {\ + __BIN_ITEM = COMPTYPE;\ + if(##COMPARISON(__BIN_ITEM) <= ##COMPARISON(COMPARE)) {\ + __BIN_LEFT = __BIN_MID + 1;\ + } else {\ + __BIN_RIGHT = __BIN_MID;\ + };\ + __BIN_MID = (__BIN_LEFT + __BIN_RIGHT) >> 1;\ + };\ + __BIN_ITEM = COMPTYPE;\ + __BIN_MID = ##COMPARISON(__BIN_ITEM) > ##COMPARISON(COMPARE) ? __BIN_MID : __BIN_MID + 1;\ + __BIN_LIST.Insert(__BIN_MID, INPUT);\ + };\ + } while(FALSE) + ///Returns a list in plain english as a string /proc/english_list(list/input, nothing_text = "nothing", and_text = " and ", comma_text = ", ", final_comma_text = "" ) var/total = length(input) diff --git a/code/controllers/subsystem/movement/movement.dm b/code/controllers/subsystem/movement/movement.dm index 21342ac8f78..ebf441749e8 100644 --- a/code/controllers/subsystem/movement/movement.dm +++ b/code/controllers/subsystem/movement/movement.dm @@ -2,17 +2,28 @@ SUBSYSTEM_DEF(movement) name = "Movement Loops" flags = SS_NO_INIT|SS_BACKGROUND|SS_TICKER wait = 1 //Fire each tick - ///The list of datums we're processing - var/list/processing = list() - ///Used to make pausing possible - var/list/currentrun = list() + /* + A breif aside about the bucketing system here + + The goal is to allow for higher loads of semi long delays while reducing cpu usage + Bucket insertion and management are much less complex then what you might see in SStimer + This is intentional, as we loop our delays much more often then that ss is designed for + We also have much shorter term timers, so we need to worry about redundant buckets much less + */ + ///Assoc list of "target time" -> list(things to process). Used for quick lookup + var/list/buckets = list() + ///Sorted list of list(target time, bucket to process) + var/list/sorted_buckets = list() ///The time we started our last fire at var/canonical_time = 0 ///The visual delay of the subsystem var/visual_delay = 1 /datum/controller/subsystem/movement/stat_entry(msg) - msg = "P:[length(processing)]" + var/total_len = 0 + for(var/list/bucket as anything in sorted_buckets) + total_len += length(bucket[MOVEMENT_BUCKET_LIST]) + msg = "B:[length(sorted_buckets)] E:[total_len]" return ..() /datum/controller/subsystem/movement/Recover() @@ -22,30 +33,75 @@ SUBSYSTEM_DEF(movement) var/our_name = typenames[length(typenames)] //Get the last name in the list, IE the subsystem identifier var/datum/controller/subsystem/movement/old_version = global.vars["SS[our_name]"] - processing = old_version.processing - currentrun = old_version.currentrun + buckets = old_version.buckets + sorted_buckets = old_version.sorted_buckets /datum/controller/subsystem/movement/fire(resumed) if(!resumed) canonical_time = world.time - currentrun = processing.Copy() - var/list/running = currentrun //Cache for... you've heard this before - while(running.len) - var/datum/move_loop/loop = running[running.len] - running.len-- - if(loop.timer <= canonical_time) - loop.process() //This shouldn't get nulls, if it does, runtime - if (MC_TICK_CHECK) + for(var/list/bucket_info as anything in sorted_buckets) + var/time = bucket_info[MOVEMENT_BUCKET_TIME] + if(time > canonical_time || MC_TICK_CHECK) return + pour_bucket(bucket_info) + +/// Processes a bucket of movement loops (This should only ever be called by fire(), it exists to prevent runtime fuckery) +/datum/controller/subsystem/movement/proc/pour_bucket(list/bucket_info) + var/list/processing = bucket_info[MOVEMENT_BUCKET_LIST] // Cache for lookup speed + while(processing.len) + var/datum/move_loop/loop = processing[processing.len] + processing.len-- + loop.process() //This shouldn't get nulls, if it does, runtime + if(!QDELETED(loop)) //Re-Insert the loop + loop.timer = world.time + loop.delay + queue_loop(loop) + if (MC_TICK_CHECK) + break + + if(length(processing)) + return // Still work to be done + var/bucket_time = bucket_info[MOVEMENT_BUCKET_TIME] + smash_bucket(1, bucket_time) // We assume we're the first bucket in the queue right now visual_delay = MC_AVERAGE_FAST(visual_delay, max((world.time - canonical_time) / wait, 1)) +/// Removes a bucket from our system. You only need to pass in the time, but if you pass in the index of the list you save us some work +/datum/controller/subsystem/movement/proc/smash_bucket(index, bucket_time) + if(!index) + for(var/i in 1 to length(sorted_buckets)) + var/list/bucket_info = sorted_buckets[i] + if(bucket_info[MOVEMENT_BUCKET_TIME] != bucket_time) + continue + index = i + break + sorted_buckets.Cut(index, index + 1) //Removes just this list + //Removes the assoc lookup too + buckets -= "[bucket_time]" + +/datum/controller/subsystem/movement/proc/queue_loop(datum/move_loop/loop) + var/target_time = loop.timer + var/string_time = "[target_time]" + if(buckets[string_time]) + buckets[string_time] += loop + else + buckets[string_time] = list(loop) + // This makes buckets and sorted buckets point to the same place, allowing for quicker inserts + var/list/new_bucket = list(list(target_time, buckets[string_time])) + BINARY_INSERT_DEFINE(new_bucket, sorted_buckets, SORT_VAR_NO_TYPE, list(target_time), SORT_FIRST_INDEX, COMPARE_KEY) + +/datum/controller/subsystem/movement/proc/dequeue_loop(datum/move_loop/loop) + var/list/our_entries = buckets["[loop.timer]"] + our_entries -= loop + if(!our_entries) + smash_bucket(bucket_time = loop.timer) // We can't pass an index in for context because we don't know our position + /datum/controller/subsystem/movement/proc/add_loop(datum/move_loop/add) - processing += add add.start_loop() + if(QDELETED(add)) + return + queue_loop(add) /datum/controller/subsystem/movement/proc/remove_loop(datum/move_loop/remove) - processing -= remove - currentrun -= remove + dequeue_loop(remove) remove.stop_loop() diff --git a/code/controllers/subsystem/movement/movement_types.dm b/code/controllers/subsystem/movement/movement_types.dm index 7ce34591133..d1ea92376fd 100644 --- a/code/controllers/subsystem/movement/movement_types.dm +++ b/code/controllers/subsystem/movement/movement_types.dm @@ -20,6 +20,8 @@ ///Delay between each move in deci-seconds var/delay = 1 ///The next time we should process + ///Used primarially as a hint to be reasoned about by our [controller], and as the id of our bucket + ///Should not be modified directly outside of [start_loop] var/timer = 0 ///Is this loop running or not var/running = FALSE @@ -71,10 +73,9 @@ extra_info = null return ..() -///Exists as a helper so outside code can modify delay while also modifying timer +///Exists as a helper so outside code can modify delay in a sane way /datum/move_loop/proc/set_delay(new_delay) delay = max(new_delay, world.tick_lag) - timer = world.time + delay /datum/move_loop/process() var/old_delay = delay //The signal can sometimes change delay @@ -93,7 +94,6 @@ SEND_SIGNAL(src, COMSIG_MOVELOOP_POSTPROCESS, success, delay * visual_delay) - timer = world.time + delay if(QDELETED(src) || !success) //Can happen return @@ -399,7 +399,6 @@ /datum/move_loop/has_target/dist_bound/move() if(!check_dist()) //If we're too close don't do the move - timer = world.time //Make sure to move as soon as possible return FALSE return TRUE diff --git a/code/controllers/subsystem/processing/singulo.dm b/code/controllers/subsystem/processing/singulo.dm new file mode 100644 index 00000000000..882d8e70f1a --- /dev/null +++ b/code/controllers/subsystem/processing/singulo.dm @@ -0,0 +1,6 @@ +/// Very rare subsystem, provides any active singularities with the timings and seclusion they need to succeed +PROCESSING_SUBSYSTEM_DEF(singuloprocess) + name = "Singularity" + wait = 0.5 + priority = FIRE_PRIORITY_DEFAULT + stat_tag = "SIN" diff --git a/code/datums/components/singularity.dm b/code/datums/components/singularity.dm index 96fc734b7d8..04d5690d733 100644 --- a/code/datums/components/singularity.dm +++ b/code/datums/components/singularity.dm @@ -40,7 +40,11 @@ /// If specified, the singularity will slowly move to this target var/atom/target + /// List of turfs we have yet to consume, but need to + var/list/turf/turfs_to_consume = list() + /// The time that has elapsed since our last move/eat call + var/time_since_last_eat /datum/component/singularity/Initialize( bsa_targetable = TRUE, @@ -65,7 +69,7 @@ src.singularity_size = singularity_size /datum/component/singularity/RegisterWithParent() - START_PROCESSING(SSdcs, src) + START_PROCESSING(SSsinguloprocess, src) // The singularity stops drifting for no man! parent.AddElement(/datum/element/forced_gravity, FALSE) @@ -104,7 +108,7 @@ return ..() /datum/component/singularity/UnregisterFromParent() - STOP_PROCESSING(SSdcs, src) + STOP_PROCESSING(SSsinguloprocess, src) parent.RemoveElement(/datum/element/bsa_blocker) parent.RemoveElement(/datum/element/forced_gravity) @@ -122,9 +126,17 @@ )) /datum/component/singularity/process(delta_time) - if (roaming) - move() - eat() + // We want to move and eat once a second, but want to process our turf consume queue the rest of the time + time_since_last_eat += delta_time + digest() + if(TICK_CHECK) + return + if(time_since_last_eat > 1) // Delta time is in seconds for "reasons" + time_since_last_eat = 0 + if (roaming) + move() + eat() + digest() // Try and process as much as you can with the time we have left /datum/component/singularity/proc/block_blob() SIGNAL_HANDLER @@ -166,33 +178,50 @@ thing.singularity_act(singularity_size, parent) /datum/component/singularity/proc/eat() + turfs_to_consume |= spiral_range_turfs(grav_pull, parent) + +/datum/component/singularity/proc/digest() var/atom/atom_parent = parent - for (var/_tile in spiral_range_turfs(grav_pull, parent)) - var/turf/tile = _tile - if (!tile || !isturf(atom_parent.loc)) + if(!isturf(atom_parent.loc)) + return + + // We use a static index for this to prevent infinite runtimes. + // Maybe a might overengineered, but let's be safe yes? + var/static/cached_index = 0 + if(cached_index) + var/old_index = cached_index + cached_index = 0 // Prevents infinite Cut() runtimes. Sorry MSO + turfs_to_consume.Cut(1, old_index + 1) + + for (cached_index in 1 to length(turfs_to_consume)) + var/turf/tile = turfs_to_consume[cached_index] + var/dist_to_tile = get_dist(tile, parent) + + if(grav_pull < dist_to_tile) //If we've exited the singulo's range already, just skip us continue - if (get_dist(tile, parent) > consume_range) - tile.singularity_pull(src, singularity_size) - else + + var/in_consume_range = (dist_to_tile <= consume_range) + if (in_consume_range) consume(src, tile) + else + tile.singularity_pull(parent, singularity_size) - for (var/_thing in tile) - var/atom/thing = _thing - - // Because we can possibly yield in the middle of iteration, let's make sure what were looking at is still there - // Without this, you get "Qdeleted thing being thrown around" - if (QDELETED(thing)) + for (var/atom/movable/thing as anything in tile) + if(thing == parent) continue + if (in_consume_range) + consume(src, thing) + else + thing.singularity_pull(parent, singularity_size) - if (isturf(atom_parent.loc) && thing != parent) - var/atom/movable/movable_thing = thing - if (get_dist(movable_thing, parent) > consume_range) - movable_thing.singularity_pull(parent, singularity_size) - else - consume(src, movable_thing) + if(TICK_CHECK) //Yes this means the singulo can eat all of its host subsystem's cpu, but like it's the singulo, and it was gonna do that anyway + turfs_to_consume.Cut(1, cached_index + 1) + cached_index = 0 + return - CHECK_TICK + turfs_to_consume.Cut() + cached_index = 0 /datum/component/singularity/proc/move() var/drifting_dir = pick(GLOB.alldirs - last_failed_movement) diff --git a/code/modules/power/singularity/singularity.dm b/code/modules/power/singularity/singularity.dm index 77669d05dd5..feb309ce0cf 100644 --- a/code/modules/power/singularity/singularity.dm +++ b/code/modules/power/singularity/singularity.dm @@ -36,6 +36,8 @@ var/move_self = TRUE ///If the singularity has eaten a supermatter shard and can go to stage six var/consumed_supermatter = FALSE + /// How long it's been since the singulo last acted, in seconds + var/time_since_act = 0 flags_1 = SUPERMATTER_IGNORES_1 resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | FREEZE_PROOF @@ -46,7 +48,7 @@ energy = starting_energy - START_PROCESSING(SSobj, src) + START_PROCESSING(SSsinguloprocess, src) SSpoints_of_interest.make_point_of_interest(src) var/datum/component/singularity/new_component = AddComponent( @@ -76,7 +78,7 @@ /obj/singularity/Destroy() - STOP_PROCESSING(SSobj, src) + STOP_PROCESSING(SSsinguloprocess, src) return ..() /obj/singularity/attack_tk(mob/user) @@ -146,6 +148,10 @@ energy -= round(((energy + 1) / 4), 1) /obj/singularity/process(delta_time) + time_since_act += delta_time + if(time_since_act < 2) + return + time_since_act = 0 if(current_size >= STAGE_TWO) if(prob(event_chance)) event() diff --git a/tgstation.dme b/tgstation.dme index e7bf67ab420..7e99466b6e1 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -515,6 +515,7 @@ #include "code\controllers\subsystem\processing\projectiles.dm" #include "code\controllers\subsystem\processing\quirks.dm" #include "code\controllers\subsystem\processing\reagents.dm" +#include "code\controllers\subsystem\processing\singulo.dm" #include "code\controllers\subsystem\processing\station.dm" #include "code\controllers\subsystem\processing\tramprocess.dm" #include "code\controllers\subsystem\processing\wet_floors.dm"