Skip to content

Guide to Garbage Collection

afterthought2 edited this page Dec 14, 2018 · 2 revisions

Garbage collection in ss13

When instances are no longer needed, it is desirable to remove them to free up resources. BYOND has two behaviors which facilitate this:

  • Instances are automatically collected and removed (eventually) if and only if there are no references to them. This is fast and generally desirable. Here a reference refers to a variable pointing to the instance (this includes the instance being a member of a list, and variables iin local scope of an active proc).
  • BYOND also has a del proc, which will remove the instance immediately. Unless there were no references to the instance already, this is likely to be very expensive, as BYOND apparently searches for for all references to it everywhere. The cost depends on the size of the world and possibly the number of references to the instance, but often takes on the order of a decisecond on our server hardware. There is no way for us to force this action to share resources, so this will cause lag that players will feel.

The takeaway here is:

Do not ever use del directly.

Fortunately, ss13 features an internal garbage collection system that allows you to handle garbage collection with greater flexibility. This is headed by the qdel proc. qdel may be called on any datum subtype or null, and does the following, in order (assuming the input is not null):

  1. It sets the datum's gc_destroyed var to GC_CURRENTLY_BEING_QDELETED. This makes the datum return TRUE in the QDELETED macro.
  2. It calls the datum's Destroy proc.
  3. It takes the returned value from the above call, and takes one of several possible further actions, depending on the value. We continue assuming the value was the default one, QDEL_HINT_QUEUE.
  4. The datum is queued on the garbage collection subsystem. For about two minutes, that subsystem will periodically check up on the datum. If it has been collected by BYOND's garbage collector and no longer exists, that's the end of the story.
  5. If after two minutes the datum has not been picked up, it is queued up to have del called on it. This might happen almost immediately, but need not: the subsystem tries to avoid having too many things del'd in a row, and will instead delete them in order as resources are available.

1-3 happen synchronously; no code will be executed between when you call qdel and 3 has finished. Your job, when handling garbage collection, is to make sure that calling qdel on your datum will result in it being garbage collected by BYOND before del is called on it; you do this by making sure to

Remove all references to a datum in its Destroy proc.

Below, we cover some common sources of references that require removal and techniques for how to handle them. First, though:

Things to do (and not do) in the Destroy proc

  • Destroy must generally call the parent type's Destroy proc. You do this using ..(). If you do not do this, you are asserting that your type does not require any of the reference removal its parent performs; this is almost certainly false unless you are also using a non-default garbage collection hint.
  • Destroy must always, in all circumstances, return a garbage collection hint. This hint will generally be the same as the parent type's, so a common idiom is
.../your_type/Destroy()
	your code
	. = ..()
  • You must never sleep in Destroy. This means that if you call a proc from Destroy, you must make sure that it does not sleep (unless it has waitfor set as FALSE).
  • Destroy should not affect the world apart from removing references to the instance and possibly clearing the instance's references to other stuff. This means that you should not cause explosions, print output to the usr, or spawn new instances there. These may all be things you want to have happen when your instance is deleted, in which case they should be put into a separate proc (e.g. die), which performs them and then calls qdel(src) afterwards.
  • Destroy should not duplicate actions performed by the parent types' Destroy procs. Typical examples are removing things from the contents, moving to nullspace to clear the loc's reference to an atom, removing an obj or obj/machinery from processing in a subsystem, and clearing the user's UI of an obj/item. These should only be done if they require special handling and the generic behavior is not suitable.
  • If you have registered for any listeners from the observer system, you need to unregister from all of them in Destroy by calling GLOB.whatever_event.unregister(arguments), where the arguments are most likely the same ones you registered with.
  • If you started processing on a subsystem using START_PROCESSING(SSwhatever, src), you must make sure to stop processing in Destroy using STOP_PROCESSING(SSwhatever, src). A few types automatically stop processing on relevant subsystems in base Destroy procs, so check beforehand.

Common things which require garbage collection handling, and how to handle them

Things in a type's own variables

Say you are creating a new datum type (or modifying an existing one). Check your datum's vars: are any of them storing a datum reference or a list of references? If so, you may need to handle them. Here is an example

/obj/item/foo
	var/obj/thing_inside_me
	var/list/my_favorite_objects = list()
	var/list/a_bunch_of_stuff_we_made = list()
	var/datum/global_datum_we_are_using
	var/datum/auxiliary/only_we_use_this = new()
	var/obj/machinery/machine_that_also_has_a_reference_to_us
  • thing_inside_me is an object we want to delete when we are deleted, and which has no references to us. However, we also need to clear the reference to it when we delete it, for otherwise it will fail to be garbage collected. The correct way of doing this is to call qdel(thing_inside_me), and then set thing_inside_me = null. There is a macro which does these two things for you: QDEL_NULL(thing_inside_me).
  • my_favorite_objects is a list of things we do not want to delete when we are deleted. There are two choices: generally you'll want to call Cut() on it, to remove references to the things in this list. Sometimes, there are multiple things with references to the same list instance, and you don't want to affect them: in that case, set my_favorite_objects = null.
  • a_bunch_of_stuff_we_made is a list of things we do want to delete when we are deleted. You will want to call qdel on all the items in it, and then Cut it or set it to null (the latter assuming nothing else is holding on to it). There is a macro which does all of this for you and avoids common pitfalls: QDEL_NULL_LIST(a_bunch_of_stuff_we_made).
  • global_datum_we_are_using is something we are referencing which should never be deleted. You may leave it alone, or set it to null if you wish. If instead it's something that might get deleted, but you don't want to delete yourself, set the variable to null.
  • only_we_use_this is a datum which we created just for us. Call QDEL_NULL(only_we_use_this); while it may be the case that after we are garbage collected this datum will be garbage collected automatically, the more common situation is that it will float around in nullspace forever because you never called qdel on it and something retained a reference to it.
  • machine_that_also_has_a_reference_to_us is an instance that also has an instance to us. This is a common and particularly difficult case which we discuss more below. You need to clear the reference to it, but you also need to first have it clear its reference to you. You also need to do the same in its Destroy.

Putting everything together,

/obj/item/foo/Destroy()
	QDEL_NULL(thing_inside_me)
	my_favorite_objects.Cut()
	QDEL_NULL_LIST(a_bunch_of_stuff_we_made)
	QDEL_NULL(only_we_use_this)
	if(machine_that_also_has_a_reference_to_us)
		machine_that_also_has_a_reference_to_us.reference_to_us = null
		machine_that_also_has_a_reference_to_us = null
	. = ..()

Removing references to an instance stored elsewhere: Prevention and being a team player

We have not yet addressed the main difficulty of handling garbage collection, which is making sure that anything with a reference to you has it removed in Destroy. The best way to do this is

When possible, minimize the number of variables referencing your instance directly.

To do this, check what is holding on to the reference, and whether it really needs it.

  • If you are holding a reference to something that is already easily accessible elsewhere, do you really need to? For example, if your machine has an input object, can you just check your contents for it instead of referencing it in a separate variable?
  • If you are holding a reference to assist with initialization, this may be a sign of poor initialization design. Try making all of the initialization code done by one of the objects, for example.
  • If you are holding a reference to something for the data, can you just copy the data instead? Only do this if it does not result in code duplication, though.
  • If you are holding a reference for very short periods of time, it may be possible to use a "soft reference" instead. A soft reference is a string obtained by using the text macro "\ref[instance]"; this can be resolved to a true reference by using locate(soft_reference). The danger is that when an instance is garbage collected, this string is reused, so there's no guarantee that you can resolve it consistently to what you started with.
  • Fortunately, there's a system for using soft references safely. You can create a weakref (an instance itself, of type /weakref) to an instance using the weakref(my_instance) proc call. The weakref can be safely stored (it holds no reference to my_instance, so it won't cause garbage collection failure), and resolved by calling our_weakref.resolve(). This will return either my_instance, if it exists, or null, if it doesn't. Check the return for null, and you are good to go. This mechanism can address all garbage collection issues, and may be a good choice for places where you need to hold on to a reference to a very generic type of object for a long time. It is also useful with timers and local scope (e.g. in process loops). You will want to avoid it in performance-sensitive code because it is somewhat more expensive than just using regular references, but this will usually not be an issue.

Sometimes holding on to references is unavoidable, though. In that case,

Be a good citizen and ensure that you clear your references when the referenced instance is qdel'd.

One way of doing this is to directly do it in the instance's Destroy proc. However, this assumes it has a way of finding you. Doing this with another reference back to you on the instance is sometimes possible (but not if it's of a very generic type). Even if it's possible, it may exacerbate the issue, as now you need to make sure that it clears this instance when you are deleted! Often a cleaner way of doing this is to use the observation system. Here is an example:

/obj/item/foo
	var/atom/friend

/obj/item/foo/proc/make_friend(atom/new_friend)
	friend = new_friend
	GLOB.destroyed_event.register(friend, src, .proc/remove_friend)

/obj/item/foo/proc/remove_friend(destroyed_instance)
	if(friend == destroyed_instance)
		GLOB.destroyed_event.unregister(friend, src, .proc/remove_friend)
		friend = null

/obj/item/foo/Destroy()
	if(friend)
		remove_friend(friend)
	. = ..()

Here we have /obj/item/foo holding on to a reference (friend) to an arbitrary atom indefinitely. To make sure that it doesn't stop friend from garbage collecting if qdel'd, it subscribes to be notified if friend is being deleted. This is done using GLOB.destroyed_event.register(friend, src, .proc/remove_friend): the first argument is the thing to be notified about, the second is who to notify (us), and the last argument is what proc on us to call in the event that friend is being destroyed. That proc will then be called from friend's Destroy proc automatically (it happens in datum/Destroy). The first argument of the called proc will be the thing being deleted; in this case friend.

When friend is destroyed, we remove our reference to it, but we make sure to unsubscribe from the listener first. We also clear the reference and unsubscribe if we are garbage collected, doing it in our Destroy.

Notice that in this code, we did not need to add any variables or logic to friend; all of the garbage collection is handled in our own code. This makes it more readable and easier to maintain.

Circular references

If faced with two instances, each with a reference to the other, you have the following options (in addition to removing one of the references, as discussed in the above section):

/obj/foo
	var/obj/bar/foos_bar

/obj/bar
	var/obj/foo/bars_foo
  • If when one is deleted, the other need not be, do:
/obj/foo/Destroy()
	if(foos_bar)
		foos_bar.bars_foo = null
		foos_bar = null
	. = ..()

/obj/bar/Destroy()
	if(bars_foo)
		bars_foo.foos_bar= null
		bars_foo = null
	. = ..()
  • If one of the instances should delete the other when deleted, but not the other way around, do:
/obj/foo/Destroy()
	qdel(foos_bar)
	. = ..()

/obj/bar/Destroy()
	if(bars_foo)
		bars_foo.foos_bar= null
		bars_foo = null
	. = ..()
  • If each should delete the other, do:
/obj/foo/Destroy()
	if(!QDELETED(foos_bar))
		qdel(foos_bar)
	foos_bar = null
	. = ..()

/obj/bar/Destroy()
	if(!QDELETED(bars_foo))
		qdel(bars_foo)
	bars_foo = null
	. = ..()

Calling qdel on an instance from within its own Destroy (or any proc called by it) will result in an error, which is why we perform the extra check above. It will not result in an infinite loop or call `Destroy more than once, though.

Sleeping, spawning, and timers

Procs which sleep or spawn may be guilty of causing garbage collection failures by holding on to references of deleted instances. This is particularly an issue if the time slept is over two minutes, but can add up from repeated or recursive calls. To avoid this, one strategy is to use weakrefs. Procs like this may also cause the instance calling them to fail to garbage collect; to avoid this, avoid using long sleeps in procs directly (use timers or subsystem processing instead).

Timers (added with the addtimer proc) are a system of calling procs on an instance after a delay. They will automatically avoid calling procs on deleted or deleting instances, thus being safer than sleep or spawn in that regard. It's possible to pass instances as arguments to the proc that the timer will call; these will then be stored by the timer, preventing those instances from garbage collecting. By using appropriate timer settings and using the global listeners described above, it is possible to have the timer removed when the instance is deleted, but this is difficult to arrange. A simpler solution is to

Use weakrefs as arguments in long timers.

Advanced usage

qdel can take additional positional arguments after the first. These are passed as arguments into Destroy.

Datums have a gc_destroyed var which monitors their garbage collection status. Typically this is used by the QDELETED(thing) macro, which returns TRUE if thing had qdel called on it or no longer exists and FALSE otherwise. It can also be checked directly for more fine-grained control, but this is rarely a good idea.

To delete an instance after a delay, use the QDEL_IN macro. If you want a listener to call qdel on an instance when an event is raised (or need this feature in other generic code), use the qdel_self proc. In other places, qdel(instance) is preferred to instance.qdel_self().

It is possible to pass several alternative garbage collection hints instead of the default one. Here is the list, along with notes on usage:

  • QDEL_HINT_QUEUE This is the default, as described above.
  • QDEL_HINT_LETMELIVE This will avoid deleting the instance. Use with mission-critical types meant to never be deleted (e.g. the dview mob). An error will be thrown if something calls qdel on the instance. If you use this, you probably should not call ..() in Destroy, or do anything there other than return this hint.
  • QDEL_HINT_IWILLGC The instance will be left alone, with the assumption that it will be picked up by BYOND's garbage collector because all references were removed. This might be used for performance reasons when a large number of instances are expected to be deleted at once, the assumption being that if they aren't collected, it's intolerable that they clog the del queue; however, this is unsafe and poor practice. A better use is in datums which should be destroyed if and only if there are no references to them (e.g. weakref datums).
  • QDEL_HINT_HARDDEL This will queue the instance for a del call directly. Note that it will be queued, so the del will not be synchronous. As of writing this, most /mob types use this hint for safety.
  • QDEL_HINT_HARDDEL_NOW This will call del on the instance without queuing. This is potentially expensive and should be avoided; use only when immediate destruction is necessary for server safety (e.g. clients return this hint, as do some controllers, depending on design).
  • QDEL_HINT_FINDREFERENCE This is a debugging hint that will search the world for references to the instance when it is deleted. Do not use outside of testing. This search is very slow, taking as long as several minutes to complete (much slower than calling del). When used for garbage collection debugging, it can be helpful, but will often miss certain reference types (temporary references, due to the long delay, and things in local scope). It will also return false positives, as it interrupts normal execution of code which may ordinarily clear them.
  • QDEL_HINT_IFFAIL_FINDREFERENCE Like above, but only called after the instance was queued for the regular amount of time and failed to be collected.

Documentation regarding setting up and using Git.

Making heads or tails of specific GitHub pages, for the uninitiated.

Tools

Documentation regarding tools external to DM and Git.

Documentation

Content creation guides

Standards

Standards and guidelines regarding commenting, contributor conduct and coding standards.

Clone this wiki locally