You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
GC, short for garbage collection, is usually referred to as the ability of the game to delete objects from the game without it costing a lot. The proc qdel() is used for this as opposed to byonds of proc del().
This guide will explain some of the ideas behind our GC system and how you can make your objects properly GC.
In the New of the faxmachine it gets added to a list of allfaxes. But it is never removed from that list. A fix would be to remove the fax from the list in Destroy.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
-
Introduction
GC, short for garbage collection, is usually referred to as the ability of the game to delete objects from the game without it costing a lot. The proc
qdel()
is used for this as opposed to byonds of procdel()
.This guide will explain some of the ideas behind our GC system and how you can make your objects properly GC.
If you just want to know how to properly write GC friendly code applicable for most cases then I'd advise reading Why do we use
qdel
instead ofdel
?, How doesqdel
work in some more detail (skipping the queue and destroy values part), How to ensure an object GCs and the How to check if something GCed properly? partsTLDR: It is all about the references to and from the to be deleted object. Clean them up. And always use
qdel
overdel
!Why do we use
qdel
instead ofdel
?Well, the answer first requires a bit of knowledge on how garbage collection works in SS13.
del
will do a so-called "hard delete" which means that byond will go through all the objects in the game and deletes any reference to the to-be-deleted object. As you can imagine this is a very costly procedure. As an alternative, SS13 developers introducedqdel
which stands for "queue del(ete)".qdel
will attempt to do a so-called "soft delete".Soft delete means that all references to an object will be removed smartly. This causes the object to stop existing since nobody knows about it anymore (sad init?). Doing it this way will remove the need for
del
if done correctly.How does
qdel
work in some more detailOnce
qdel
is called on an object it will send aCOMSIG_PARENT_QDELETING
signal (more on this later) and callDestroy
on said object. This is a proc on/datum
level and is used to remove all references that said object might still have itself and it can be used to remove references other objects have on itself.Destroy
is overridden by most of the objects inheriting from/datum
and they all MUST call the parent definition using..()
. Failure in doing so will cause issues (and currently won't even compile if you have the right tools installed).Destroy
also returns a value, a destroy hint. The normal value isQDEL_HINT_QUEUE
which will mean the deleted object will get queued in SSgarbage (more on that later). It is important to return a value, this is why you see. = ..()
orreturn ..()
often inDestroy
overrides.Lets for example take a look at the (current) implementation of
/obj/machinery/computer/monitor/Destroy()
Here we can see that it removes itself from a global list (clearing its reference from other objects) and deletes an internal object he made
power_monitor
usingQDEL_NULL
, which is just a define which callsqdel
on the given parameter and will then set the variable tonull
for you. Then finally it callsreturn ..()
. This is a normal flow ofDestroy
. First, clean up your stuff. Then call your parents implementation. This is done to avoid any dependencies issues and is the opposite of a normal constructor flow (Initialize/New
)The qdel queue
After
Destroy
is called, and theQDEL_HINT_QUEUE
is given, the object's\ref
value will be put in theSSgarbage
queue (a\ref
is used otherwise the object will still have a reference meaning it can't properly GC). This queue will be traversed over time to see if the objects in there still exist or not by looking up the reference usinglocate(refID)
. If no object is found (or in edge cases, an object is found but isn't the original object) then the object has properly been deleted and all is fine. If not? Then it will be moved to another queue, the hard delete queue, where it will again be checked the next tick and if it is still there then it will be hard deleted usingdel
. Not quite something that is desired but it has to be done otherwise the game will have a memory leak.Other Destroy return values
Besides
QDEL_HINT_QUEUE
there are a couple of other Destroy hints.QDEL_HINT_LETMELIVE
, should be quite self-explanatory. The object does not want to be destroyed and thus will not be destroyed (unlessqdel
is called with the force parameter on true). Useful for objects that never should be destroyed.QDEL_HINT_IWILLGC
, a very bold return value saying that it does not need to be queued as it knows it will GC without issues. Don't use this unless you are 100% sure this will GC in all cases.QDEL_HINT_HARDDEL
, meaning that it will be instantly queued up for the hard delete queue. Currently not used in the codebase.QDEL_HINT_HARDDEL_NOW
, will force a hard delete instantly on the object. This is used on objects which should be fully removed from the game. If they don't then stuff will break. Don't use this unless truly neededQDEL_HINT_FINDREFERENCE
, this one functions the same asQDEL_HINT_QUEUE
but will also callfind_references
on the object when it gets queued. This is used for testing onlyQDEL_HINT_IFFAIL_FINDREFERENCE
, Same idea asQDEL_HINT_FINDREFERENCE
except it callsfind_references
only when it fails.How to ensure an object GCs
Now onto how to make an object actually GC. As read above it is all about removing references to said object, and while doing that also remove references the object has to others.
This is done by looking at a created object and thinking. Okay, what values are stored in the object. Which of them are primitives and which are actual objects. Primitives are never references and thus don't need to be cleaned. And what to do with those objects?
Let's take the
/obj/machinery/computer/aifixer
as example. Here we assume that all inherited variables are being properly cleaned by our parent. So we only have two actual variables left:Here we have
occupant
which isnull
by default but could be an actual ai. And we haveactive
which is a primitive so we can ignore it.Okay, what do we do with
occupant
? Well, we of course need to clean it up when we get destroyed. But what if there is a player in it? This is something you need to ask yourself for each mob type reference if you plan on deleting it as well.The resulting Destroy code looks like this at the moment of writing this guide.
As you can see the
occupant
gets made into a ghost, if there is one, and then gets qdeleted.The aifixer object itself has to do nothing else to clean up it's references to other objects and other references to itself. The parent handles the rest namely:
Here you can see the machine gets removed from a global list which removes a reference to itself. Then it also stops processing if needed which also removes a reference to itself.
An example of how it should not be done is how the
/obj/machinery/photocopier/faxmachine
was (fixed in #16609):In the
New
of the faxmachine it gets added to a list ofallfaxes
. But it is never removed from that list. A fix would be to remove the fax from the list inDestroy
.Using signals to properly GC objects
Now, what if you have no clue who has you as a reference? This will make removing the reference to yourself impossible right?
Luckily we have a thing called signals. In special,
COMSIG_PARENT_QDELETING
is used here.Say you have an object that gets made from different sources which keep a reference to the object but the object does not have a reference back to the source. This is where the
COMSIG_PARENT_QDELETING
comes into play.Lets look at the
/obj/machinery/shieldgen
object. It has a list calleddeployed_shields
which can contain/obj/machinery/shield
objects. When it powers up the list gets filled with the shields it spawns and when it powers down or gets destroyed then the shields will be deleted and removed from the list. But what if the shield gets destroyed in between this?The shield itself has no reference to the shieldgen and thus is not able to fix the reference issue itself. This is why the shieldgen registers to the
COMSIG_PARENT_QDELETING
of the shields when creating them. When the said signal gets send on the shield then the shieldgen callsremove_shield
which removes the destroyed shield from itsdeployed_shields
list. Et voila! We solved the reference issue using signals!Some code to help visualize the text:
Any signal registered to an object will get processed properly in
datum/Destroy
so you do not need to unregister signals yourself. The same goes for timers and components linked to the object.How to check if something GCed properly?
Once you have written your code you can go test it in-game. Go and run through all the use cases of the object where it gets deleted. Then wait a few minutes (about 5 minutes usually) for the SSgarbage subsystem to have fired enough times to have soft/hard deleted your objects.
Once you're done go to the
Debug
tab in-game and click on theDisplay del() log
verb.This window displays all deletes that have happened and their information. (The landmarks are currently always hard deleted due to them returning the
QDEL_HINT_HARDDEL_NOW
value)Say we want to test if
/obj/machinery/computer/communications
GCs properly. Then we search for said type using CTRL + F.Et voila. As you can see it properly GCs in the use case I've used here.
For more complex objects you will need to go through a bunch of use cases and even then you are not guaranteed to have written GC perfect code. SS13 is a big game with a lot of variables that can mean that sometimes an object does not GC properly in a very specific situation. Here the special Destroy hints could be useful to debug that situation.
Find references when GC fails
When you uncomment certain defines, you'll be able to find references to a datum when it fails to GC:
I prefer to have all 3 uncommented, this will freeze your game but it will be the most accurate.
Afterwards, it will write the outcome in the
gc_debug.log
file in the logs directory.It is still possible that no reference gets found. This can mean multiple things, either the object still has a running proc. Or something else weird.
Hopefully, this guide helped you write more GC friendly code. If you have any remarks or suggestions then please leave a comment or message me on Discord.
Want more to read? A guide from TG about hard deletes has some good further explanation
Beta Was this translation helpful? Give feedback.
All reactions