Skip to content

Commit

Permalink
Maploader Optimisations (#2365)
Browse files Browse the repository at this point in the history
## About The Pull Request

Ports:
tgstation/tgstation#69632
tgstation/tgstation#70118
tgstation/tgstation#70729

Aiming to also port:
tgstation/tgstation#70966
tgstation/tgstation#71074
tgstation/tgstation#77495

## Why It's Good For The Game

Anything we can do to speedup and/or reduce planetgen and maploading lag
is great for the game

## Changelog

:cl:
/:cl:

<!-- Both :cl:'s are required for the changelog to work! You can put
your name to the right of the first :cl: if you want to overwrite your
GitHub username as author ingame. -->
<!-- You can use multiple of the same prefix (they're only used for the
icon ingame) and delete the unneeded ones. Despite some of the tags,
changelogs should generally represent how a player might be affected by
the changes rather than a summary of the PR's contents. -->

---------

Co-authored-by: LemonInTheDark <[email protected]>
  • Loading branch information
MarkSuckerberg and LemonInTheDark authored Oct 18, 2023
1 parent 75fe3dc commit c956324
Show file tree
Hide file tree
Showing 43 changed files with 605 additions and 333 deletions.
2 changes: 1 addition & 1 deletion check_regex.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ standards:

- exactly:
[
298,
295,
"non-bitwise << uses",
'(?<!\d)(?<!\d\s)(?<!<)<<(?!=|\s\d|\d|<|\/)',
]
Expand Down
2 changes: 2 additions & 0 deletions code/__DEFINES/dcs/signals.dm
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
// start global signals with "!", this used to be necessary but now it's just a formatting choice
/// from base of datum/controller/subsystem/mapping/proc/add_new_zlevel(): (list/args)
#define COMSIG_GLOB_NEW_Z "!new_z"
/// sent after world.maxx and/or world.maxy are expanded: (has_exapnded_world_maxx, has_expanded_world_maxy)
#define COMSIG_GLOB_EXPANDED_WORLD_BOUNDS "!expanded_world_bounds"
/// called after a successful var edit somewhere in the world: (list/args)
#define COMSIG_GLOB_VAR_EDIT "!var_edit"
/// called after an explosion happened : (epicenter, devastation_range, heavy_impact_range, light_impact_range, took, orig_dev_range, orig_heavy_range, orig_light_range)
Expand Down
2 changes: 2 additions & 0 deletions code/__DEFINES/is_helpers.dm
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#define isatom(A) (isloc(A))

#define isdatum(thing) (istype(thing, /datum))

#define isweakref(D) (istype(D, /datum/weakref))

//Turfs
Expand Down
2 changes: 2 additions & 0 deletions code/__DEFINES/maths.dm
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

#define CEILING(x, y) (-round(-(x) / (y)) * (y))

#define ROUND_UP(x) (-round(-(x)))

// round() acts like floor(x, 1) by default but can't handle other values
#define FLOOR(x, y) (round((x) / (y)) * (y))

Expand Down
7 changes: 7 additions & 0 deletions code/__DEFINES/misc.dm
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ GLOBAL_LIST_EMPTY(bloody_footprints_cache)
//subtypesof(), typesof() without the parent path
#define subtypesof(typepath) (typesof(typepath) - typepath)

/// Takes a datum as input, returns its ref string, or a cached version of it
/// This allows us to cache \ref creation, which ensures it'll only ever happen once per datum, saving string tree time
/// It is slightly less optimal then a []'d datum, but the cost is massively outweighed by the potential savings
/// It will only work for datums mind, for datum reasons
/// : because of the embedded typecheck
#define text_ref(datum) (isdatum(datum) ? (datum:cached_ref ||= "\ref[datum]") : ("\ref[datum]"))

//Gets the turf this atom inhabits
#define get_turf(A) (get_step(A, 0))

Expand Down
2 changes: 1 addition & 1 deletion code/__DEFINES/typeids.dm
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
#define TYPEID_NORMAL_LIST "f"
//helper macros
#define GET_TYPEID(ref) (((length(ref) <= 10) ? "TYPEID_NULL" : copytext(ref, 4, -7)))
#define IS_NORMAL_LIST(L) (GET_TYPEID("\ref[L]") == TYPEID_NORMAL_LIST)
#define IS_NORMAL_LIST(L) (GET_TYPEID(text_ref(L)) == TYPEID_NORMAL_LIST)


51 changes: 43 additions & 8 deletions code/__HELPERS/_lists.dm
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,49 @@

return null

/// Takes a weighted list (see above) and expands it into raw entries
/// This eats more memory, but saves time when actually picking from it
/proc/expand_weights(list/list_to_pick)
var/list/values = list()
for(var/item in list_to_pick)
var/value = list_to_pick[item]
if(!value)
continue
values += value

var/gcf = greatest_common_factor(values)

var/list/output = list()
for(var/item in list_to_pick)
var/value = list_to_pick[item]
if(!value)
continue
for(var/i in 1 to value / gcf)
output += item
return output

/// Takes a list of numbers as input, returns the highest value that is cleanly divides them all
/// Note: this implementation is expensive as heck for large numbers, I only use it because most of my usecase
/// Is < 10 ints
/proc/greatest_common_factor(list/values)
var/smallest = min(arglist(values))
for(var/i in smallest to 1 step -1)
var/safe = TRUE
for(var/entry in values)
if(entry % i != 0)
safe = FALSE
break
if(safe)
return i

/// Pick a random element from the list and remove it from the list.
/proc/pick_n_take(list/list_to_pick)
RETURN_TYPE(list_to_pick[_].type)
if(list_to_pick.len)
var/picked = rand(1,list_to_pick.len)
. = list_to_pick[picked]
list_to_pick.Cut(picked,picked+1) //Cut is far more efficient that Remove()

// Allows picks with non-integer weights and also 0
// precision 1000 means it works up to 3 decimal points
/proc/pickweight_float(list/L, default=1, precision=1000)
Expand All @@ -268,14 +311,6 @@

return null

//Pick a random element from the list and remove it from the list.
/proc/pick_n_take(list/L)
RETURN_TYPE(L[_].type)
if(L.len)
var/picked = rand(1,L.len)
. = L[picked]
L.Cut(picked,picked+1) //Cut is far more efficient that Remove()

//Returns the top(last) element from the list and removes it from the list (typical stack function)
/proc/pop(list/L)
if(L.len)
Expand Down
6 changes: 3 additions & 3 deletions code/__HELPERS/icons.dm
Original file line number Diff line number Diff line change
Expand Up @@ -1198,7 +1198,7 @@ GLOBAL_DATUM_INIT(dummySave, /savefile, new("tmp/dummySave.sav")) //Cache of ico
if(isicon(icon) && isfile(icon))
//icons compiled in from 'icons/path/to/dmi_file.dmi' at compile time are weird and arent really /icon objects,
///but they pass both isicon() and isfile() checks. theyre the easiest case since stringifying them gives us the path we want
var/icon_ref = "\ref[icon]"
var/icon_ref = text_ref(icon)
var/locate_icon_string = "[locate(icon_ref)]"

icon_path = locate_icon_string
Expand All @@ -1209,7 +1209,7 @@ GLOBAL_DATUM_INIT(dummySave, /savefile, new("tmp/dummySave.sav")) //Cache of ico
// the rsc reference returned by fcopy_rsc() will be stringifiable to "icons/path/to/dmi_file.dmi"
var/rsc_ref = fcopy_rsc(icon)

var/icon_ref = "\ref[rsc_ref]"
var/icon_ref = text_ref(rsc_ref)

var/icon_path_string = "[locate(icon_ref)]"

Expand All @@ -1219,7 +1219,7 @@ GLOBAL_DATUM_INIT(dummySave, /savefile, new("tmp/dummySave.sav")) //Cache of ico
var/rsc_ref = fcopy_rsc(icon)
//if its the text path of an existing dmi file, the rsc reference returned by fcopy_rsc() will be stringifiable to a dmi path

var/rsc_ref_ref = "\ref[rsc_ref]"
var/rsc_ref_ref = text_ref(rsc_ref)
var/rsc_ref_string = "[locate(rsc_ref_ref)]"

icon_path = rsc_ref_string
Expand Down
23 changes: 22 additions & 1 deletion code/__HELPERS/text.dm
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,24 @@
return copytext(text, 1, i + 1)
return ""

//Returns a string with reserved characters and spaces after the first and last letters removed
//Like trim(), but very slightly faster. worth it for niche usecases
/proc/trim_reduced(text)
var/starting_coord = 1
var/text_len = length(text)
for (var/i in 1 to text_len)
if (text2ascii(text, i) > 32)
starting_coord = i
break

for (var/i = text_len, i >= starting_coord, i--)
if (text2ascii(text, i) > 32)
return copytext(text, starting_coord, i + 1)

if(starting_coord > 1)
return copytext(text, starting_coord)
return ""

/**
* Truncate a string to the given length
*
Expand All @@ -255,7 +273,7 @@
/proc/trim(text, max_length)
if(max_length)
text = copytext_char(text, 1, max_length)
return trim_left(trim_right(text))
return trim_reduced(text)

//Returns a string with the first element of the string capitalized.
/proc/capitalize(t)
Expand Down Expand Up @@ -735,6 +753,9 @@ GLOBAL_LIST_INIT(binary, list("0","1"))
base = text("[]\herself", rest)
if("hers")
base = text("[]\hers", rest)
else // Someone fucked up, if you're not a macro just go home yeah?
// This does technically break parsing, but at least it's better then what it used to do
return base

. = base
if(rest)
Expand Down
12 changes: 7 additions & 5 deletions code/__HELPERS/unsorted.dm
Original file line number Diff line number Diff line change
Expand Up @@ -1364,19 +1364,21 @@ GLOBAL_DATUM_INIT(dview_mob, /mob/dview, new)

return "{[time_high]-[time_mid]-[GUID_VERSION][time_low]-[GUID_VARIANT][time_clock]-[node_id]}"

// \ref behaviour got changed in 512 so this is necesary to replicate old behaviour.
// If it ever becomes necesary to get a more performant REF(), this lies here in wait
// #define REF(thing) (thing && istype(thing, /datum) && (thing:datum_flags & DF_USE_TAG) && thing:tag ? "[thing:tag]" : "\ref[thing]")
/**
* \ref behaviour got changed in 512 so this is necesary to replicate old behaviour.
* If it ever becomes necesary to get a more performant REF(), this lies here in wait
* #define REF(thing) (thing && isdatum(thing) && (thing:datum_flags & DF_USE_TAG) && thing:tag ? "[thing:tag]" : text_ref(thing))
**/
/proc/REF(input)
if(istype(input, /datum))
if(isdatum(input))
var/datum/thing = input
if(thing.datum_flags & DF_USE_TAG)
if(!thing.tag)
stack_trace("A ref was requested of an object with DF_USE_TAG set but no tag: [thing]")
thing.datum_flags &= ~DF_USE_TAG
else
return "\[[url_encode(thing.tag)]\]"
return "\ref[input]"
return text_ref(input)

// Makes a call in the context of a different usr
// Use sparingly
Expand Down
8 changes: 4 additions & 4 deletions code/controllers/subsystem/garbage.dm
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ SUBSYSTEM_DEF(garbage)
var/type = D.type
var/datum/qdel_item/I = items[type]

log_world("## TESTING: GC: -- \ref[D] | [type] was unable to be GC'd --")
log_world("## TESTING: GC: -- [text_ref(D)] | [type] was unable to be GC'd --")
#ifdef TESTING
for(var/c in GLOB.admins) //Using testing() here would fill the logs with ADMIN_VV garbage
var/client/admin = c
Expand Down Expand Up @@ -258,7 +258,7 @@ SUBSYSTEM_DEF(garbage)
return
var/queue_time = world.time

var/refid = "\ref[D]"
var/refid = text_ref(D)
if (D.gc_destroyed <= 0)
D.gc_destroyed = queue_time

Expand All @@ -271,7 +271,7 @@ SUBSYSTEM_DEF(garbage)
++delslasttick
++totaldels
var/type = D.type
var/refID = "\ref[D]"
var/refID = text_ref(D)
var/datum/qdel_item/I = items[type]

if (!force && I.qdel_flags & QDEL_ITEM_SUSPENDED_FOR_LAG)
Expand Down Expand Up @@ -389,7 +389,7 @@ SUBSYSTEM_DEF(garbage)
D.find_references()
if (QDEL_HINT_IFFAIL_FINDREFERENCE) //qdel will, if REFERENCE_TRACKING is enabled and the object fails to collect, display all references to this object.
SSgarbage.Queue(D)
SSgarbage.reference_find_on_fail["\ref[D]"] = TRUE
SSgarbage.reference_find_on_fail[text_ref(D)] = TRUE
#endif
else
#ifdef TESTING
Expand Down
15 changes: 7 additions & 8 deletions code/controllers/subsystem/statpanel.dm
Original file line number Diff line number Diff line change
Expand Up @@ -153,17 +153,16 @@ SUBSYSTEM_DEF(statpanels)
list("CPU:", world.cpu),
list("Instances:", "[num2text(world.contents.len, 10)]"),
list("World Time:", "[world.time]"),
list("Globals:", GLOB.stat_entry(), "\ref[GLOB]"),
list("[config]:", config.stat_entry(), "\ref[config]"),
list("Globals:", GLOB.stat_entry(), text_ref(GLOB)),
list("[config]:", config.stat_entry(), text_ref(config)),
list("Byond:", "(FPS:[world.fps]) (TickCount:[world.time/world.tick_lag]) (TickDrift:[round(Master.tickdrift,1)]([round((Master.tickdrift/(world.time/world.tick_lag))*100,0.1)]%)) (Internal Tick Usage: [round(MAPTICK_LAST_INTERNAL_TICK_USAGE,0.1)]%)"),
list("Master Controller:", Master.stat_entry(), "\ref[Master]"),
list("Failsafe Controller:", Failsafe.stat_entry(), "\ref[Failsafe]"),
list("Master Controller:", Master.stat_entry(), text_ref(Master)),
list("Failsafe Controller:", Failsafe.stat_entry(), text_ref(Failsafe)),
list("","")
)
for(var/ss in Master.subsystems)
var/datum/controller/subsystem/sub_system = ss
mc_data[++mc_data.len] = list("\[[sub_system.state_letter()]][sub_system.name]", sub_system.stat_entry(), "\ref[sub_system]")
mc_data[++mc_data.len] = list("Camera Net", "Cameras: [GLOB.cameranet.cameras.len] | Chunks: [GLOB.cameranet.chunks.len]", "\ref[GLOB.cameranet]")
for(var/datum/controller/subsystem/sub_system as anything in Master.subsystems)
mc_data[++mc_data.len] = list("\[[sub_system.state_letter()]][sub_system.name]", sub_system.stat_entry(), text_ref(sub_system))
mc_data[++mc_data.len] = list("Camera Net", "Cameras: [GLOB.cameranet.cameras.len] | Chunks: [GLOB.cameranet.chunks.len]", text_ref(GLOB.cameranet))
mc_data_encoded = url_encode(json_encode(mc_data))

/atom/proc/remove_from_cache()
Expand Down
4 changes: 2 additions & 2 deletions code/controllers/subsystem/timer.dm
Original file line number Diff line number Diff line change
Expand Up @@ -511,8 +511,8 @@ SUBSYSTEM_DEF(timer)
/datum/timedevent/proc/bucketJoin()
// Generate debug-friendly name for timer
var/static/list/bitfield_flags = list("TIMER_UNIQUE", "TIMER_OVERRIDE", "TIMER_CLIENT_TIME", "TIMER_STOPPABLE", "TIMER_NO_HASH_WAIT", "TIMER_LOOP")
name = "Timer: [id] (\ref[src]), TTR: [timeToRun], wait:[wait] Flags: [jointext(bitfield_to_list(flags, bitfield_flags), ", ")], \
callBack: \ref[callBack], callBack.object: [callBack.object]\ref[callBack.object]([getcallingtype()]), \
name = "Timer: [id] ([text_ref(src)]), TTR: [timeToRun], wait:[wait] Flags: [jointext(bitfield_to_list(flags, bitfield_flags), ", ")], \
callBack: [text_ref(callBack)], callBack.object: [callBack.object][text_ref(callBack.object)]([getcallingtype()]), \
callBack.delegate:[callBack.delegate]([callBack.arguments ? callBack.arguments.Join(", ") : ""]), source: [source]"

if (bucket_joined)
Expand Down
2 changes: 1 addition & 1 deletion code/controllers/subsystem/vis_overlays.dm
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ SUBSYSTEM_DEF(vis_overlays)
else
overlay = _create_new_vis_overlay(icon, iconstate, layer, plane, dir, alpha, add_appearance_flags)
overlay.cache_expiration = -1
var/cache_id = "\ref[overlay]@{[world.time]}"
var/cache_id = "[text_ref(overlay)]@{[world.time]}"
vis_overlay_cache[cache_id] = overlay
. = overlay
if(overlay == null)
Expand Down
5 changes: 5 additions & 0 deletions code/datums/datum.dm
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
/// Datum level flags
var/datum_flags = NONE

/// A cached version of our \ref
/// The brunt of \ref costs are in creating entries in the string tree (a tree of immutable strings)
/// This avoids doing that more then once per datum by ensuring ref strings always have a reference to them after they're first pulled
var/cached_ref

/// A weak reference to another datum
var/datum/weakref/weak_reference

Expand Down
Loading

0 comments on commit c956324

Please sign in to comment.