Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ports various area/turf performance improvements from /tg/ #2493

Merged
merged 12 commits into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions code/__DEFINES/time.dm
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
#define GARBAGEDAY "Garbage Day"
#define MONKEYDAY "Monkey Day"
#define MOTH_WEEK "Moth Week"
#define IAN_HOLIDAY "Ian's Birthday"
/*

Days of the week to make it easier to reference them.
Expand Down
48 changes: 46 additions & 2 deletions code/__HELPERS/_lists.dm
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,37 @@
///Remove an untyped item to a list, taking care to handle list items by wrapping them in a list to remove the footgun
#define UNTYPED_LIST_REMOVE(list, item) (list -= LIST_VALUE_WRAP_LISTS(item))

/*
* ## Lazylists
*
* * What is a lazylist?
*
* True to its name a lazylist is a lazy instantiated list.
* It is a list that is only created when necessary (when it has elements) and is null when empty.
*
* * Why use a lazylist?
*
* Lazylists save memory - an empty list that is never used takes up more memory than just `null`.
*
* * When to use a lazylist?
*
* Lazylists are best used on hot types when making lists that are not always used.
*
* For example, if you were adding a list to all atoms that tracks the names of people who touched it,
* you would want to use a lazylist because most atoms will never be touched by anyone.
*
* * How do I use a lazylist?
*
* A lazylist is just a list you defined as `null` rather than `list()`.
* Then, you use the LAZY* macros to interact with it, which are essentially null-safe ways to interact with a list.
*
* Note that you probably should not be using these macros if your list is not a lazylist.
* This will obfuscate the code and make it a bit harder to read and debug.
*
* Generally speaking you shouldn't be checking if your lazylist is `null` yourself, the macros will do that for you.
* Remember that LAZYLEN (and by extension, length) will return 0 if the list is null.
*/

///Initialize the lazylist
#define LAZYINITLIST(L) if (!L) { L = list(); }
///If the provided list is empty, set it to null
Expand Down Expand Up @@ -60,14 +91,27 @@
#define LAZYCLEARLIST(L) if(L) L.Cut()
///Returns the list if it's actually a valid list, otherwise will initialize it
#define SANITIZE_LIST(L) ( islist(L) ? L : list() )
#define reverseList(L) reverse_range(L.Copy())

/// Performs an insertion on the given lazy list with the given key and value. If the value already exists, a new one will not be made.
#define LAZYORASSOCLIST(lazy_list, key, value) \
LAZYINITLIST(lazy_list); \
LAZYINITLIST(lazy_list[key]); \
lazy_list[key] |= value;

///Ensures the length of a list is at least I, prefilling it with V if needed. if V is a proc call, it is repeated for each new index so that list() can just make a new list for each item.
#define LISTASSERTLEN(L, I, V...) \
if (length(L) < I) { \
var/_OLD_LENGTH = length(L); \
L.len = I; \
/* Convert the optional argument to a if check */ \
for (var/_USELESS_VAR in list(V)) { \
for (var/_INDEX_TO_ASSIGN_TO in _OLD_LENGTH+1 to I) { \
L[_INDEX_TO_ASSIGN_TO] = V; \
} \
} \
}

#define reverseList(L) reverse_range(L.Copy())

/// Passed into BINARY_INSERT to compare keys
#define COMPARE_KEY __BIN_LIST[__BIN_MID]
/// Passed into BINARY_INSERT to compare values
Expand Down
10 changes: 4 additions & 6 deletions code/__HELPERS/areas.dm
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@
var/area/old_area = the_turf.loc

//keep rack of all areas affected by turf changes
affected_areas[old_area.name] = old_area

Check warning on line 155 in code/__HELPERS/areas.dm

View workflow job for this annotation

GitHub Actions / Run Linters

OD2304: Invalid index operation. datum[] index operations are not valid starting in BYOND 515.1641

//move the turf to its new area and unregister it from the old one
the_turf.change_area(old_area, newA)
Expand All @@ -178,7 +178,7 @@
//convert map to list
var/list/area/area_list = list()
for(var/area_name in affected_areas)
area_list += affected_areas[area_name]

Check warning on line 181 in code/__HELPERS/areas.dm

View workflow job for this annotation

GitHub Actions / Run Linters

OD2304: Invalid index operation. datum[] index operations are not valid starting in BYOND 515.1641
SEND_GLOBAL_SIGNAL(COMSIG_AREA_CREATED, newA, area_list, creator)
to_chat(creator, span_notice("You have created a new area, named [newA.name]. It is now weather proof, and constructing an APC will allow it to be powered."))
creator.log_message("created a new area: [AREACOORD(creator)] (previously \"[oldA.name]\")", LOG_GAME)
Expand Down Expand Up @@ -273,13 +273,11 @@
// Now their turfs
var/list/turfs = list()
for(var/area/pull_from as anything in areas_to_pull)
var/list/our_turfs = pull_from.get_contained_turfs()
if(target_z == 0)
turfs += our_turfs
if (target_z == 0)
for (var/list/zlevel_turfs as anything in pull_from.get_zlevel_turf_lists())
turfs += zlevel_turfs
else
for(var/turf/turf_in_area as anything in our_turfs)
if(target_z == turf_in_area.z)
turfs += turf_in_area
turfs += pull_from.get_turfs_by_zlevel(target_z)
return turfs


Expand Down
49 changes: 31 additions & 18 deletions code/controllers/subsystem/area_contents.dm
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#define ALLOWED_LOOSE_TURFS 500
#define ALLOWED_LOOSE_TURFS 100
/**
* Responsible for managing the sizes of area.contained_turfs and area.turfs_to_uncontain
* These lists do not check for duplicates, which is fine, but it also means they can balloon in size over time
Expand All @@ -17,8 +17,11 @@ SUBSYSTEM_DEF(area_contents)
var/total_clearing_from = 0
var/total_to_clear = 0
for(var/area/to_clear as anything in marked_for_clearing)
total_to_clear += length(to_clear.turfs_to_uncontain)
total_clearing_from += length(to_clear.contained_turfs)
for (var/area_zlevel in 1 to length(to_clear.turfs_to_uncontain_by_zlevel))
if (length(to_clear.turfs_to_uncontain_by_zlevel[area_zlevel]))
total_to_clear += length(to_clear.turfs_to_uncontain_by_zlevel[area_zlevel])
if (length(to_clear.turfs_by_zlevel) >= area_zlevel) //this should always be true, but stat_entry is no place for runtimes. fire() can handle that
total_clearing_from += length(to_clear.turfs_by_zlevel[area_zlevel])
msg = "A:[length(currentrun)] MR:[length(marked_for_clearing)] TC:[total_to_clear] CF:[total_clearing_from]"
return ..()

Expand All @@ -29,8 +32,10 @@ SUBSYSTEM_DEF(area_contents)

while(length(currentrun))
var/area/test = currentrun[length(currentrun)]
if(length(test.turfs_to_uncontain) > ALLOWED_LOOSE_TURFS)
marked_for_clearing |= test
for (var/area_zlevel in 1 to length(test.turfs_to_uncontain_by_zlevel))
if(length(test.turfs_to_uncontain_by_zlevel[area_zlevel]) > ALLOWED_LOOSE_TURFS)
marked_for_clearing |= test
break
currentrun.len--
if(MC_TICK_CHECK)
return
Expand All @@ -39,19 +44,27 @@ SUBSYSTEM_DEF(area_contents)
while(length(marked_for_clearing))
var/area/clear = marked_for_clearing[length(marked_for_clearing)]

// The operation of cutting large lists can be expensive
// It scales almost directly with the size of the list we're cutting with
// Because of this, we're gonna stick to cutting 1 entry at a time
// There's no reason to batch it I promise, this is faster. No overtime too
var/amount_cut = 0
var/list/cut_from = clear.turfs_to_uncontain
for(amount_cut in 1 to length(cut_from))
clear.contained_turfs -= cut_from[amount_cut]
if(MC_TICK_CHECK)
cut_from.Cut(1, amount_cut + 1)
return

clear.turfs_to_uncontain = list()
for (var/area_zlevel in 1 to length(clear.turfs_to_uncontain_by_zlevel))
if (!length(clear.turfs_to_uncontain_by_zlevel[area_zlevel]))
continue
if (length(clear.turfs_by_zlevel) < area_zlevel)
stack_trace("[clear]([clear.type])'s turfs_by_zlevel is length [length(clear.turfs_by_zlevel)] but we are being asked to remove turfs from zlevel [area_zlevel] from it.")
clear.turfs_to_uncontain_by_zlevel[area_zlevel] = list()
continue

// The operation of cutting large lists can be expensive
// It scales almost directly with the size of the list we're cutting with
// Because of this, we're gonna stick to cutting 1 entry at a time
// There's no reason to batch it I promise, this is faster. No overtime too
var/amount_cut = 0
var/list/cut_from = clear.turfs_to_uncontain_by_zlevel[area_zlevel]
for(amount_cut in 1 to length(cut_from))
clear.turfs_by_zlevel[area_zlevel] -= cut_from[amount_cut]
if(MC_TICK_CHECK)
cut_from.Cut(1, amount_cut + 1)
return

clear.turfs_to_uncontain_by_zlevel = list()
marked_for_clearing.len--

#undef ALLOWED_LOOSE_TURFS
29 changes: 9 additions & 20 deletions code/controllers/subsystem/job.dm
Original file line number Diff line number Diff line change
Expand Up @@ -964,35 +964,24 @@ SUBSYSTEM_DEF(job)


/datum/controller/subsystem/job/proc/get_last_resort_spawn_points()
//bad mojo
var/area/shuttle/arrival/arrivals_area = GLOB.areas_by_type[/area/shuttle/arrival]
if(arrivals_area)
//first check if we can find a chair
var/obj/structure/chair/shuttle_chair = locate() in arrivals_area
if(shuttle_chair)
return shuttle_chair

//last hurrah
if(!isnull(arrivals_area))
var/list/turf/available_turfs = list()
for(var/turf/arrivals_turf in arrivals_area)
if(!arrivals_turf.is_blocked_turf(TRUE))
for (var/list/zlevel_turfs as anything in arrivals_area.get_zlevel_turf_lists())
for (var/turf/arrivals_turf as anything in zlevel_turfs)
var/obj/structure/chair/shuttle_chair = locate() in arrivals_turf
if(!isnull(shuttle_chair))
return shuttle_chair
if(arrivals_turf.is_blocked_turf(TRUE))
continue
available_turfs += arrivals_turf

if(length(available_turfs))
return pick(available_turfs)

//pick an open spot on arrivals and dump em
var/list/arrivals_turfs = shuffle(get_area_turfs(/area/shuttle/arrival))
if(length(arrivals_turfs))
for(var/turf/arrivals_turf in arrivals_turfs)
if(!arrivals_turf.is_blocked_turf(TRUE))
return arrivals_turf
//last chance, pick ANY spot on arrivals and dump em
return pick(arrivals_turfs)

stack_trace("Unable to find last resort spawn point.")
return GET_ERROR_ROOM


///Lands specified mob at a random spot in the hallways
/datum/controller/subsystem/job/proc/DropLandAtRandomHallwayPoint(mob/living/living_mob)
var/turf/spawn_turf = get_safe_random_station_turf(typesof(/area/station/hallway))
Expand Down
14 changes: 9 additions & 5 deletions code/controllers/subsystem/mapping.dm
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ SUBSYSTEM_DEF(mapping)
// Cache for sonic speed
var/list/unused_turfs = src.unused_turfs
var/list/world_contents = GLOB.areas_by_type[world.area].contents
var/list/world_turf_contents = GLOB.areas_by_type[world.area].contained_turfs
var/list/world_turf_contents_by_z = GLOB.areas_by_type[world.area].turfs_by_zlevel
var/list/lists_to_reserve = src.lists_to_reserve
var/index = 0
while(index < length(lists_to_reserve))
Expand All @@ -197,10 +197,12 @@ SUBSYSTEM_DEF(mapping)
LAZYINITLIST(unused_turfs["[T.z]"])
unused_turfs["[T.z]"] |= T
var/area/old_area = T.loc
old_area.turfs_to_uncontain += T
LISTASSERTLEN(old_area.turfs_to_uncontain_by_zlevel, T.z, list())
old_area.turfs_to_uncontain_by_zlevel[T.z] += T
T.turf_flags = UNUSED_RESERVATION_TURF
world_contents += T
world_turf_contents += T
LISTASSERTLEN(world_turf_contents_by_z, T.z, list())
world_turf_contents_by_z[T.z] += T
packet.len--
packetlen = length(packet)

Expand Down Expand Up @@ -981,12 +983,14 @@ GLOBAL_LIST_EMPTY(the_station_areas)
// Faster
if(space_guaranteed)
var/area/global_area = GLOB.areas_by_type[world.area]
global_area.contained_turfs += Z_TURFS(z_level)
LISTASSERTLEN(global_area.turfs_by_zlevel, z_level, list())
global_area.turfs_by_zlevel[z_level] = Z_TURFS(z_level)
return

for(var/turf/to_contain as anything in Z_TURFS(z_level))
var/area/our_area = to_contain.loc
our_area.contained_turfs += to_contain
LISTASSERTLEN(our_area.turfs_by_zlevel, z_level, list())
our_area.turfs_by_zlevel[z_level] += to_contain

/datum/controller/subsystem/mapping/proc/update_plane_tracking(datum/space_level/update_with)
// We're essentially going to walk down the stack of connected z levels, and set their plane offset as we go
Expand Down
19 changes: 12 additions & 7 deletions code/controllers/subsystem/shuttle.dm
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ SUBSYSTEM_DEF(shuttle)
var/datum/turf_reservation/proposal = SSmapping.request_turf_block_reservation(
transit_width,
transit_height,
1,
z_size = 1, //if this is changed the turf uncontain code below has to be updated to support multiple zs
reservation_type = /datum/turf_reservation/transit,
turf_type_override = transit_path,
)
Expand Down Expand Up @@ -666,17 +666,22 @@ SUBSYSTEM_DEF(shuttle)
if(!midpoint)
qdel(proposal)
return FALSE

var/area/old_area = midpoint.loc
old_area.turfs_to_uncontain += proposal.reserved_turfs
var/area/shuttle/transit/A = new()
A.parallax_movedir = travel_dir
A.contents = proposal.reserved_turfs
A.contained_turfs = proposal.reserved_turfs
LISTASSERTLEN(old_area.turfs_to_uncontain_by_zlevel, bottomleft.z, list())
old_area.turfs_to_uncontain_by_zlevel[bottomleft.z] += proposal.reserved_turfs

var/area/shuttle/transit/new_area = new()
new_area.parallax_movedir = travel_dir
new_area.contents = proposal.reserved_turfs
LISTASSERTLEN(new_area.turfs_by_zlevel, bottomleft.z, list())
new_area.turfs_by_zlevel[bottomleft.z] = proposal.reserved_turfs

var/obj/docking_port/stationary/transit/new_transit_dock = new(midpoint)
new_transit_dock.reserved_area = proposal
new_transit_dock.name = "Transit for [M.shuttle_id]/[M.name]"
new_transit_dock.owner = M
new_transit_dock.assigned_area = A
new_transit_dock.assigned_area = new_area

// Add 180, because ports point inwards, rather than outwards
new_transit_dock.setDir(angle2dir(dock_angle))
Expand Down
2 changes: 1 addition & 1 deletion code/datums/station_traits/negative_traits.dm
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@
/datum/station_trait/revolutionary_trashing/proc/trash_this_place()
for(var/area/station/command/area_to_trash in GLOB.areas)

for(var/turf/current_turf as anything in area_to_trash.get_contained_turfs())
for(var/turf/current_turf as anything in area_to_trash.get_turfs_from_all_zlevels())
if(isclosedturf(current_turf))
continue
if(prob(25))
Expand Down
Loading
Loading