diff --git a/code/controllers/ProcessScheduler/core/_stubs.dm b/code/controllers/ProcessScheduler/core/_stubs.dm
index 326fd29ac2a..94f4cc1fc4b 100644
--- a/code/controllers/ProcessScheduler/core/_stubs.dm
+++ b/code/controllers/ProcessScheduler/core/_stubs.dm
@@ -25,14 +25,3 @@
world << "Diary: \[[diaryType]:[type]] [text]"
else
world << "Log: \[[type]] [text]"
-
-/**
- * var/disposed
- *
- * In goonstation, disposed is set to 1 after an object enters the delete queue
- * or the object is placed in an object pool (effectively out-of-play so to speak)
- */
-/datum/var/disposed
-// Garbage collection (controller).
-/datum/var/gcDestroyed
-/datum/var/timeDestroyed
\ No newline at end of file
diff --git a/code/controllers/ProcessScheduler/core/updateQueueWorker.dm b/code/controllers/ProcessScheduler/core/updateQueueWorker.dm
index 66f66bbcc01..39737ef0213 100644
--- a/code/controllers/ProcessScheduler/core/updateQueueWorker.dm
+++ b/code/controllers/ProcessScheduler/core/updateQueueWorker.dm
@@ -31,7 +31,7 @@ datum/updateQueueWorker/proc/doWork()
var/datum/object = objects[objects.len] // Pull out the object
objects.len-- // Remove the object from the list
- if (istype(object) && !isturf(object) && !object.disposed && isnull(object.gcDestroyed)) // We only work with real objects
+ if (istype(object) && !isturf(object) && isnull(object.gcDestroyed)) // We only work with real objects
call(object, procName)(arglist(arguments))
// If there's nothing left to execute
diff --git a/code/controllers/Processes/machinery.dm b/code/controllers/Processes/machinery.dm
index 52274a519af..940530ab713 100644
--- a/code/controllers/Processes/machinery.dm
+++ b/code/controllers/Processes/machinery.dm
@@ -8,7 +8,7 @@
//#endif
for(var/obj/machinery/M in machines)
- if(M && !M.gcDestroyed)
+ if(M && isnull(M.gcDestroyed))
#ifdef PROFILE_MACHINES
var/time_start = world.timeofday
#endif
@@ -29,6 +29,7 @@
machine_profiling[M.type] += (time_end - time_start)
#endif
+ else
+ machines -= M
scheck()
-
diff --git a/code/controllers/Processes/pipenet.dm b/code/controllers/Processes/pipenet.dm
index 56a068f54ca..b59373a88a1 100644
--- a/code/controllers/Processes/pipenet.dm
+++ b/code/controllers/Processes/pipenet.dm
@@ -4,9 +4,9 @@
/datum/controller/process/pipenet/doWork()
for(var/datum/pipe_network/pipeNetwork in pipe_networks)
- if(istype(pipeNetwork) && !pipeNetwork.disposed)
+ if(istype(pipeNetwork) && isnull(pipeNetwork.gcDestroyed))
pipeNetwork.process()
scheck()
continue
-
- pipe_networks.Remove(pipeNetwork)
+ else
+ pipe_networks -= pipeNetwork
diff --git a/code/controllers/Processes/power_machinery.dm b/code/controllers/Processes/power_machinery.dm
index ce17c0583a6..c7710c8e5bd 100644
--- a/code/controllers/Processes/power_machinery.dm
+++ b/code/controllers/Processes/power_machinery.dm
@@ -8,18 +8,15 @@ var/global/list/power_machinery_profiling = list()
schedule_interval = 20 // every 2 seconds
/datum/controller/process/power_machinery/doWork()
- for(var/i = 1 to power_machines.len)
- if(i > power_machines.len)
- break
- var/obj/machinery/M = power_machines[i]
- if(istype(M) && !M.gcDestroyed)
+ for(var/obj/machinery/M in power_machines)
+ if(istype(M) && isnull(M.gcDestroyed))
#ifdef PROFILE_MACHINES
var/time_start = world.timeofday
#endif
if(M.process() == PROCESS_KILL)
M.inMachineList = 0
- power_machines.Remove(M)
+ power_machines -= M
continue
if(M && M.use_power)
@@ -34,12 +31,10 @@ var/global/list/power_machinery_profiling = list()
power_machinery_profiling[M.type] += (time_end - time_start)
#endif
else
- if(!power_machines.Remove(M))
- power_machines.Cut(i,i+1)
+ power_machines -= M
else
- if(M)
+ if(istype(M))
M.inMachineList = 0
- if(!power_machines.Remove(M))
- power_machines.Cut(i,i+1)
+ power_machines -= M
scheck()
diff --git a/code/controllers/Processes/powernet.dm b/code/controllers/Processes/powernet.dm
index 1edf194915a..185dbcf0606 100644
--- a/code/controllers/Processes/powernet.dm
+++ b/code/controllers/Processes/powernet.dm
@@ -4,9 +4,9 @@
/datum/controller/process/powernet/doWork()
for(var/datum/powernet/powerNetwork in powernets)
- if(istype(powerNetwork) && !powerNetwork.disposed)
+ if(istype(powerNetwork) && isnull(powerNetwork.gcDestroyed))
powerNetwork.reset()
scheck()
continue
-
- powernets.Remove(powerNetwork)
+ else
+ powernets -= powerNetwork
diff --git a/code/controllers/garbage.dm b/code/controllers/garbage.dm
deleted file mode 100644
index b9f2a2eea75..00000000000
--- a/code/controllers/garbage.dm
+++ /dev/null
@@ -1,201 +0,0 @@
-#define GC_COLLECTIONS_PER_TICK 300 // Was 100.
-#define GC_COLLECTION_TIMEOUT (30 SECONDS)
-#define GC_FORCE_DEL_PER_TICK 60
-//#define GC_DEBUG
-
-var/list/gc_hard_del_types = list()
-var/datum/garbage_collector/garbageCollector
-
-/client/proc/gc_dump_hdl()
- set name = "(GC) Hard Del List"
- set desc = "List types that are hard del()'d by the GC."
- set category = "Debug"
-
- if(!check_rights(R_DEBUG))
- return
-
- if(!gc_hard_del_types || !gc_hard_del_types.len)
- usr << "No hard del()'d types found."
-
- for(var/A in gc_hard_del_types)
- usr << "[A] = [gc_hard_del_types[A]]"
-
-/datum/garbage_collector
- var/list/queue = new
- var/del_everything = 0
-
- // To let them know how hardworking am I :^).
- var/dels_count = 0
- var/hard_dels = 0
- var/soft_dels = 0
-
-/datum/garbage_collector/proc/addTrash(const/atom/movable/AM)
- if(!istype(AM))
- return
-
- if(del_everything)
- del(AM)
- hard_dels++
- dels_count++
- return
-
- queue["\ref[AM]"] = world.timeofday
-
-/datum/garbage_collector/proc/process()
- var/remainingCollectionPerTick = GC_COLLECTIONS_PER_TICK
- var/remainingForceDelPerTick = GC_FORCE_DEL_PER_TICK
- var/collectionTimeScope = world.timeofday - GC_COLLECTION_TIMEOUT
- while(queue.len && --remainingCollectionPerTick >= 0)
- var/refID = queue[1]
- var/destroyedAtTime = queue[refID]
-
- if(destroyedAtTime > collectionTimeScope)
- break
-
- var/atom/movable/AM = locate(refID)
- if(AM) // Something's still referring to the qdel'd object. del it.
- if(isnull(AM.gcDestroyed))
- queue -= refID
- continue
- if(remainingForceDelPerTick <= 0)
- break
-
- #ifdef GC_DEBUG
- WARNING("gc process force delete [AM.type]")
- #endif
-
- AM.hard_deleted = 1
- gc_hard_del_types |= AM.type
- del AM
-
- hard_dels++
- remainingForceDelPerTick--
-
-#ifdef GC_DEBUG
-#undef GC_DEBUG
-#endif
-
-#undef GC_FORCE_DEL_PER_TICK
-#undef GC_COLLECTION_TIMEOUT
-#undef GC_COLLECTIONS_PER_TICK
-
-/datum/garbage_collector/proc/dequeue(id)
- if (queue)
- queue -= id
-
- dels_count++
-
-/datum/garbage_collector/proc/hardDel(const/datum/D)
- WARNING("GC hard-delling [D.type].")
- gc_hard_del_types |= D.type
- del(D)
- garbageCollector.hard_dels++
- garbageCollector.dels_count++
-
-/*
- * NEVER USE THIS FOR ANYTHING OTHER THAN /atom/movable
- * OTHER TYPES CANNOT BE QDEL'D BECAUSE THEIR LOC IS LOCKED OR THEY DON'T HAVE ONE.
- */
-/proc/qdel(var/datum/D, ignore_pooling = 0)
- if(isnull(D))
- return
-
- if(isnull(garbageCollector))
- del(D)
- return
-
- if(!istype(D))
- WARNING("qdel() passed object of type [D.type]. qdel() can only handle /datum/ types.")
- del(D) //no clue what could even do this, but just in case.
- return
-
- if(isnull(D.gcDestroyed))
- // Let our friend know they're about to get fucked up.
- var/hint = D.Destroy()
-
- switch(hint)
- if(QDEL_HINT_QUEUE) //qdel should queue the object for deletion
- garbageCollector.addTrash(D)
- if(QDEL_HINT_LETMELIVE) //qdel should let the object live after calling destroy.
- return
- if(QDEL_HINT_IWILLGC) //functionally the same as the above. qdel should assume the object will gc on its own, and not check it.
- return
- if (QDEL_HINT_HARDDEL_NOW) //qdel should assume this object won't gc, and hard del it post haste.
- garbageCollector.hardDel(D)
- if (QDEL_HINT_PUTINPOOL) //qdel will put this object in the pool.
- PlaceInPool(D,0)
- else
- // world << "WARNING GC DID NOT GET A RETURN VALUE FOR [D], [D.type]!"
- garbageCollector.addTrash(D)
-
-/datum/controller
- var/processing = 0
- var/iteration = 0
- var/processing_interval = 0
-
-/datum/controller/proc/recover() // If we are replacing an existing controller (due to a crash) we attempt to preserve as much as we can.
-
-/*
- * Like Del(), but for qdel.
- * Called BEFORE qdel moves shit.
- */
-/datum/proc/Destroy()
- return QDEL_HINT_HARDDEL_NOW //qdel can't handle datums, therefore tell it to immediately del
-
-/client/proc/qdel_toggle()
- set name = "Toggle qdel Behavior"
- set desc = "Toggle qdel usage between normal and force del()."
- set category = "Debug"
-
- garbageCollector.del_everything = !garbageCollector.del_everything
- world << "GC: qdel turned [garbageCollector.del_everything ? "off" : "on"]."
- log_admin("[key_name(usr)] turned qdel [garbageCollector.del_everything ? "off" : "on"].")
- message_admins("\blue [key_name(usr)] turned qdel [garbageCollector.del_everything ? "off" : "on"].", 1)
-
-/*/client/var/running_find_references
-
-/atom/verb/find_references()
- set category = "Debug"
- set name = "Find References"
- set background = 1
- set src in world
-
- if(!usr || !usr.client)
- return
-
- if(usr.client.running_find_references)
- testing("CANCELLED search for references to a [usr.client.running_find_references].")
- usr.client.running_find_references = null
- return
-
- if(alert("Running this will create a lot of lag until it finishes. You can cancel it by running it again. Would you like to begin the search?", "Find References", "Yes", "No") == "No")
- return
- qdel(src)
- // Remove this object from the list of things to be auto-deleted.
- if(garbageCollector)
- garbageCollector.queue -= "\ref[src]"
-
- usr.client.running_find_references = type
- testing("Beginning search for references to a [type].")
- var/list/things = list()
- for(var/client/thing)
- things += thing
- for(var/datum/thing)
- things += thing
- for(var/atom/thing)
- things += thing
- for(var/event/thing)
- things += thing
- testing("Collected list of things in search for references to a [type]. ([things.len] Thing\s)")
- for(var/datum/thing in things)
- if(!usr.client.running_find_references) return
- for(var/varname in thing.vars)
- var/variable = thing.vars[varname]
- if(variable == src)
- testing("Found [src.type] \ref[src] in [thing.type]'s [varname] var.")
- else if(islist(variable))
- if(src in variable)
- testing("Found [src.type] \ref[src] in [thing.type]'s [varname] list var.")
- testing("Completed search for references to a [type].")
- usr.client.running_find_references = null
-*/
\ No newline at end of file
diff --git a/code/controllers/master_controller.dm b/code/controllers/master_controller.dm
index e03a20c2f0f..9a9072bb0d4 100644
--- a/code/controllers/master_controller.dm
+++ b/code/controllers/master_controller.dm
@@ -10,6 +10,13 @@ var/global/last_tick_duration = 0
var/global/air_processing_killed = 0
var/global/pipe_processing_killed = 0
+/datum/controller
+ var/processing = 0
+ var/iteration = 0
+ var/processing_interval = 0
+
+/datum/controller/proc/recover() // If we are replacing an existing controller (due to a crash) we attempt to preserve as much as we can.
+
datum/controller/game_controller
var/breather_ticks = 2 //a somewhat crude attempt to iron over the 'bumps' caused by high-cpu use by letting the MC have a breather for this many ticks after every loop
var/minimum_ticks = 20 //The minimum length of time between MC ticks
@@ -288,12 +295,13 @@ datum/controller/game_controller/proc/process()
/datum/controller/game_controller/proc/process_bots()
for(var/obj/machinery/bot/Bot in aibots)
- if(!Bot.gc_destroyed)
+ if(Bot && isnull(Bot.gcDestroyed))
last_thing_processed = Bot.type
spawn(0)
Bot.bot_process()
continue
- aibots -= Bot
+ else
+ aibots -= Bot
/datum/controller/game_controller/proc/processObjects()
for (var/obj/Object in processing_objects)
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index e73508e2678..67e148b32b3 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -1,6 +1,3 @@
-var/global/list/del_profiling = list()
-var/global/list/gdel_profiling = list()
-var/global/list/ghdel_profiling = list()
/atom
layer = 2
var/level = 2
@@ -30,9 +27,6 @@ var/global/list/ghdel_profiling = list()
//Detective Work, used for the duplicate data points kept in the scanners
var/list/original_atom
- // Garbage collection
- var/gc_destroyed=null
-
var/allow_spin = 1 //Set this to 1 for a _target_ that is being thrown at; if an atom has this set to 1 then atoms thrown AT it will not spin; currently used for the singularity. -Fox
/atom/Destroy()
@@ -40,7 +34,7 @@ var/global/list/ghdel_profiling = list()
if(reagents)
- reagents.Destroy()
+ qdel(reagents)
reagents = null
// Idea by ChuckTheSheep to make the object even more unreferencable.
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index 25be7e055f1..cceb4db37c3 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -16,7 +16,6 @@
var/mob/pulledby = null
var/area/areaMaster
- var/hard_deleted = 0
/atom/movable/New()
. = ..()
@@ -28,41 +27,6 @@
loc = null
return QDEL_HINT_QUEUE
-/proc/delete_profile(var/type, code = 0)
- if(!ticker || !ticker.current_state < 3) return
- switch(code)
- if(0)
- if (!("[type]" in del_profiling))
- del_profiling["[type]"] = 0
-
- del_profiling["[type]"] += 1
- if(1)
- if (!("[type]" in ghdel_profiling))
- ghdel_profiling["[type]"] = 0
-
- ghdel_profiling["[type]"] += 1
- if(2)
- if (!("[type]" in gdel_profiling))
- gdel_profiling["[type]"] = 0
-
- gdel_profiling["[type]"] += 1
- if(garbageCollector)
- garbageCollector.soft_dels++
-
-/atom/movable/Del()
- if (gcDestroyed)
- garbageCollector.dequeue("\ref[src]")
-
- if (hard_deleted)
- delete_profile("[type]", 1)
- else
- delete_profile("[type]", 2)
- else // direct del calls or nulled explicitly.
- delete_profile("[type]", 0)
- Destroy()
-
- ..()
-
// Used in shuttle movement and AI eye stuff.
// Primarily used to notify objects being moved by a shuttle/bluespace fuckup.
/atom/movable/proc/setLoc(var/T, var/teleported=0)
diff --git a/code/game/objects/objs.dm b/code/game/objects/objs.dm
index 5c09ac57c0d..0882cff2921 100644
--- a/code/game/objects/objs.dm
+++ b/code/game/objects/objs.dm
@@ -249,7 +249,7 @@ a {
/obj/singularity_act()
ex_act(1.0)
- if(src && isnull(gc_destroyed))
+ if(src && isnull(gcDestroyed))
qdel(src)
return 2
diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm
index 92d1bf5a331..ed7b5377bc7 100644
--- a/code/modules/admin/admin_verbs.dm
+++ b/code/modules/admin/admin_verbs.dm
@@ -142,8 +142,10 @@ var/list/admin_verbs_debug = list(
/client/proc/callproc,
/client/proc/callproc_datum,
/client/proc/toggledebuglogs,
- /client/proc/qdel_toggle, // /vg/
+ /client/proc/qdel_toggle,
/client/proc/gc_dump_hdl,
+ /client/proc/gc_toggle_profiling,
+ /client/proc/gc_show_del_report,
/client/proc/debugNatureMapGenerator,
/client/proc/check_bomb_impacts,
/client/proc/test_movable_UI,
diff --git a/code/modules/admin/verbs/debug.dm b/code/modules/admin/verbs/debug.dm
index bf381efbd9e..c3f0bdb0b01 100644
--- a/code/modules/admin/verbs/debug.dm
+++ b/code/modules/admin/verbs/debug.dm
@@ -101,7 +101,7 @@ But you can call procs that are of type /mob/living/carbon/human/proc/ for that
if(!lst)
return
- if(!A || A.gc_destroyed)
+ if(!A || !isnull(A.gcDestroyed))
usr << "Error: callproc_datum(): owner of proc no longer exists."
return
if(!hascall(A,procname))
diff --git a/code/modules/events/tgevents/brand_intelligence.dm b/code/modules/events/tgevents/brand_intelligence.dm
index 661adb95f0c..30f359417ec 100644
--- a/code/modules/events/tgevents/brand_intelligence.dm
+++ b/code/modules/events/tgevents/brand_intelligence.dm
@@ -33,7 +33,7 @@
/datum/event/brand_intelligence/tick()
- if(!originMachine || originMachine.gc_destroyed || originMachine.shut_up || originMachine.wires.IsAllCut()) //if the original vending machine is missing or has it's voice switch flipped
+ if(!originMachine || !isnull(originMachine.gcDestroyed) || originMachine.shut_up || originMachine.wires.IsAllCut()) //if the original vending machine is missing or has it's voice switch flipped
for(var/obj/machinery/vending/saved in infectedMachines)
saved.shoot_inventory = 0
if(originMachine)
@@ -44,7 +44,7 @@
if(!vendingMachines.len) //if every machine is infected
for(var/obj/machinery/vending/upriser in infectedMachines)
- if(prob(70) && !upriser.gc_destroyed)
+ if(prob(70) && isnull(upriser.gcDestroyed))
var/mob/living/simple_animal/hostile/mimic/copy/M = new(upriser.loc, upriser, null, 1) // it will delete upriser on creation and override any machine checks
M.faction = list("profit")
M.speak = rampant_speeches.Copy()
diff --git a/code/modules/garbage collection/__gc_info.dm b/code/modules/garbage collection/__gc_info.dm
new file mode 100644
index 00000000000..96e3a0ea8dc
--- /dev/null
+++ b/code/modules/garbage collection/__gc_info.dm
@@ -0,0 +1,69 @@
+/*
+
+===============================================================================
+ How Garbage Collection Works
+===============================================================================
+
+In BYOND, there are exactly two ways anything gets deleted:
+
+- A "soft delete", which occurs when an object's reference count hits 0, meaning it is not being referenced by anything
+ in the world (more on references further down). When an object is unreferenced, nothing can access it, so it has no
+ reason to exist, and BYOND's garbage collector simply deletes the object. This is fast.
+
+- A "hard delete", which occurs when the del keyword is used to directly delete an object. This forces BYOND to find
+ every reference to the object being deleted, and null them all out. This is slow.
+
+A reference is anything that refers to an object, and can thus be used to access it. A variable on another object, a
+variable in a proc, an argument to a proc, the src of a running proc, the locs of contained objects, another object's
+contents list (but not the world's automatic contents list), anything. BYOND keeps track of how many references there
+are to an object - the reference count goes up when you make a new reference to an object, and back down when that
+reference gets set to null, goes out of scope, or is deleted. When the reference count hits 0, there are no remaining
+references to the object, and it is deleted by BYOND's garbage collector.
+
+When you have something that exists in the world, and you want it to stop existing (maybe it got blown up, or eaten by
+the singularity, or whatever), it needs to be deleted. You can use the del keyword to make sure it's deleted instantly,
+but del is slow, and you want things to be deleted as quickly as possible, especially if you're deleting a whole lot of
+them. You want soft deletes.
+
+That's where the garbage collection system comes in - it prepares things to be soft deleted, and hard deletes anything
+that can't be. There are two main procs involved in this process:
+
+/proc/qdel(datumToDelete)
+ This is, effectively, a replacement for del that tells an object to prepare itself to be soft deleted by calling its
+ Destroy() proc. Depending on the qdel hint returned by Destroy(), qdel will queue the object in the garbage collector
+ (to be hard deleted if it isn't soft deleted), directly delete the object, pool the object, or ignore the object and
+ assume it will handle deleting itself. An object passed into qdel will have its gcDestroyed var set, so
+ isnull(gcDestroyed) will be true if an object is not being destroyed, and false if it is (which means you should get
+ rid of the reference you have to it).
+
+ Note that qdel can only work with datum-based objects, which excludes the world (deleting this shuts down the world),
+ clients (deleting these disconnect the client), lists, and savefiles. If any of these are passed to qdel, they will
+ be directly deleted, just as if they had been passed straight to del, so it should never be unsafe to qdel anything
+ you could del.
+
+/datum/proc/Destroy()
+ This is, effectively, a replacement for Del() (with some exceptions) which is also responsible for nulling out
+ references to or on the object it is called on. Like Del, the Destroy proc will always be called, even if the object
+ is getting destroyed by a direct del instead of qdel; in that case, isnull(gcDestroyed) will be true, and some
+ references can safely be ignored.
+
+ The exceptions where Destroy cannot replace Del are for the same non-datum types mentioned under qdel, above. Those
+ should use a Del proc for any necessary cleanup, as a Destroy proc on them will not automatically get called.
+
+ When called by qdel, Destroy is expected to return a qdel hint, which determines whether the object should be directly
+ deleted, queued for deletion, pooled, or ignored entirely; which of these are appropriate will vary, though objects
+ should be pooled or queued whenever possible. The full list of qdel hints are in the code for the qdel proc.
+
+As mentioned above, gcDestroyed can be used to tell whether an object is being destroyed. This is important, because if
+an object is being destroyed, it WILL still exist, and any code that references it should stop referencing it as soon as
+possible. In the same places that you check whether a reference still exists, you should also check for something like
+isnull(myRefVar.gcDestroyed), which will be false if your object is being destroyed, meaning you should throw out the
+reference immediately.
+
+The inner workings of the GC itself and the stuff related to testing it probably don't need a detailed description - if
+you intend to work with them, it would be best to read the code to understand what they do.
+
+Pooling is related to GC, in that it's meant to reduce deletion overhead, but it does so by re-using objects instead of
+deleting them at all. Explaining pooling is outside the scope of this file.
+
+*/
\ No newline at end of file
diff --git a/code/modules/garbage collection/garbage_collector.dm b/code/modules/garbage collection/garbage_collector.dm
new file mode 100644
index 00000000000..97d67c30ac8
--- /dev/null
+++ b/code/modules/garbage collection/garbage_collector.dm
@@ -0,0 +1,166 @@
+// Main garbage collection code
+
+// For general information about how the GC works and how to use it, see __gc_info.dm
+
+#define GC_COLLECTIONS_PER_TICK 300 // Was 100.
+#define GC_COLLECTION_TIMEOUT (30 SECONDS)
+#define GC_FORCE_DEL_PER_TICK 60
+//#define GC_DEBUG
+
+// A list of types that were queued in the GC, and had to be soft deleted; used in testing
+var/list/gc_hard_del_types = list()
+var/datum/garbage_collector/garbageCollector
+
+// The time a datum was destroyed by the GC, or null if it hasn't been
+/datum/var/gcDestroyed
+// Whether a datum was hard-deleted by the GC; 0 if not, 1 if it was queued, -1 if directly deleted
+/datum/var/hard_deleted = 0
+
+/datum/garbage_collector
+ var/list/queue = new
+ var/del_everything = 0
+
+ // To let them know how hardworking am I :^).
+ var/dels_count = 0
+ var/hard_dels = 0
+ var/soft_dels = 0
+
+/datum/garbage_collector/proc/addTrash(var/datum/D)
+ if(!istype(D) || del_everything)
+ del(D)
+ hard_dels++
+ dels_count++
+ return
+
+ queue -= "\ref[D]" // If this is a re-used ref, remove the old ref from the queue
+ queue["\ref[D]"] = world.time
+
+/datum/garbage_collector/proc/process()
+ var/remainingCollectionPerTick = GC_COLLECTIONS_PER_TICK
+ var/remainingForceDelPerTick = GC_FORCE_DEL_PER_TICK
+ var/collectionTimeScope = world.time - GC_COLLECTION_TIMEOUT
+ while(queue.len && --remainingCollectionPerTick >= 0)
+ var/refID = queue[1]
+ var/destroyedAtTime = queue[refID]
+
+ if(destroyedAtTime > collectionTimeScope)
+ break
+
+ var/datum/D = locate(refID)
+ // If the object still exists, and it's the same object, hard del it
+ if(D && D.gcDestroyed == destroyedAtTime)
+ if(remainingForceDelPerTick <= 0)
+ break
+
+ #ifdef GC_DEBUG
+ gcwarning("GC process force delete [D.type]")
+ #endif
+
+ hardDel(D)
+ queue.Cut(1, 2)
+
+ remainingForceDelPerTick--
+ else // Otherwise, it was GC'd - remove it from the queue
+ queue.Cut(1, 2)
+ soft_dels++
+ dels_count++
+
+#ifdef GC_DEBUG
+#undef GC_DEBUG
+#endif
+
+#undef GC_FORCE_DEL_PER_TICK
+#undef GC_COLLECTION_TIMEOUT
+#undef GC_COLLECTIONS_PER_TICK
+
+/datum/garbage_collector/proc/hardDel(var/datum/D)
+ gc_hard_del_types |= D.type
+ D.hard_deleted = 1
+ if(!D.gcDestroyed)
+ spawn(-1)
+ D.Destroy()
+ D.gcDestroyed = world.time
+ del(D)
+ hard_dels++
+ dels_count++
+
+// Effectively replaces del for any datum-based type
+/proc/qdel(var/datum/D)
+ if(isnull(D))
+ return
+
+ if(isnull(garbageCollector))
+ del(D)
+ return
+
+ if(!istype(D)) // A non-datum was passed into qdel - just delete it outright.
+ // warning("qdel() passed object of type [D.type]. qdel() can only handle /datum/ types.")
+ del(D)
+ return
+
+ if(isnull(D.gcDestroyed))
+ // Let our friend know they're about to get fucked up.
+ var/hint
+ D.gcDestroyed = world.time
+ try
+ hint = D.Destroy()
+ catch(var/exception/e)
+ if(istype(e))
+ gcwarning("qdel() caught runtime destroying [D.type]: [e] in [e.file], line [e.line]")
+ else
+ gcwarning("qdel() caught runtime destroying [D.type]: [e]")
+ // Destroy runtimed? Panic! Hard delete!
+ D.hard_deleted = -1
+ del(D)
+ if(garbageCollector)
+ garbageCollector.dels_count++
+ return
+ if(!isnull(D.gcDestroyed) && D.gcDestroyed != world.time)
+ gcwarning("Sleep detected in Destroy() call of [D.type]")
+ D.gcDestroyed = world.time
+
+ switch(hint)
+ if(QDEL_HINT_QUEUE) //qdel should queue the object for deletion
+ garbageCollector.addTrash(D)
+ if(QDEL_HINT_LETMELIVE) //qdel should let the object live after calling destroy.
+ return
+ if(QDEL_HINT_IWILLGC) //functionally the same as the above. qdel should assume the object will gc on its own, and not check it.
+ return
+ if (QDEL_HINT_HARDDEL_NOW) //qdel should assume this object won't gc, and hard del it post haste.
+ D.hard_deleted = -1 // -1 means "this hard del skipped the queue", used for profiling
+ del(D)
+ if(garbageCollector)
+ garbageCollector.dels_count++
+ if (QDEL_HINT_PUTINPOOL) //qdel will put this object in the pool.
+ PlaceInPool(D,0)
+ else
+ // world << "WARNING GC DID NOT GET A RETURN VALUE FOR [D], [D.type]!"
+ garbageCollector.addTrash(D)
+
+/*
+ * Like Del(), but for qdel.
+ * Called BEFORE qdel moves shit.
+ */
+/datum/proc/Destroy()
+ tag = null
+ return QDEL_HINT_HARDDEL_NOW // By default, assume that queueing any given datum is unsafe
+
+// If something gets deleted directly, make sure its Destroy proc is still called
+/datum/Del()
+ if(isnull(gcDestroyed)) // Not GC'd
+ try
+ Destroy()
+ catch(var/exception/e)
+ if(istype(e))
+ gcwarning("Del() caught runtime destroying [type]: [e] in [e.file], line [e.line]")
+ else
+ gcwarning("Del() caught runtime destroying [type]: [e]")
+ if(del_profiling)
+ delete_profile(src)
+ else
+ if(del_profiling)
+ delete_profile(src)
+ return ..()
+
+/proc/gcwarning(msg)
+ world.log << "## GC WARNING: [msg]"
diff --git a/code/modules/garbage collection/gc_testing.dm b/code/modules/garbage collection/gc_testing.dm
new file mode 100644
index 00000000000..34467b15b5a
--- /dev/null
+++ b/code/modules/garbage collection/gc_testing.dm
@@ -0,0 +1,171 @@
+// Garbage collection testing/debugging/profiling code
+
+/client/proc/qdel_toggle()
+ set name = "(GC) Toggle Queueing"
+ set desc = "Toggle qdel usage between normal and force del()."
+ set category = "Debug"
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ garbageCollector.del_everything = !garbageCollector.del_everything
+ // world << "GC: qdel turned [garbageCollector.del_everything ? "off" : "on"]."
+ log_admin("[key_name(usr)] turned qdel [garbageCollector.del_everything ? "off" : "on"].")
+ message_admins("\blue [key_name(usr)] turned qdel [garbageCollector.del_everything ? "off" : "on"].", 1)
+
+/client/proc/gc_toggle_profiling()
+ set name = "(GC) Toggle Profiling"
+ set desc = "Toggle profiling of deletion methods"
+ set category = "Debug"
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ del_profiling = !del_profiling
+ log_admin("[key_name(usr)] turned deletion profiling [del_profiling ? "on" : "off"].")
+ message_admins("\blue [key_name(usr)] turned deletion profiling [del_profiling ? "on" : "off"].", 1)
+
+/client/proc/gc_show_del_report()
+ set name = "(GC) Show Del Report"
+ set desc = "Show report of deletions seen while profiling"
+ set category = "Debug"
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ delete_profiler_report()
+
+/client/proc/gc_dump_hdl()
+ set name = "(GC) Hard Del List"
+ set desc = "List types that are hard del()'d by the GC."
+ set category = "Debug"
+
+ if(!check_rights(R_DEBUG))
+ return
+
+ if(!gc_hard_del_types || !gc_hard_del_types.len)
+ usr << "No hard del()'d types found."
+ return
+
+ usr << "Types hard del()'d by the GC:"
+ for(var/A in gc_hard_del_types)
+ usr << "[A]"
+
+// Profiling stuff
+var/global/del_profiling = 0
+var/global/list/dels_profiled = list()
+var/global/list/gdels_profiled = list()
+var/global/list/ghdels_profiled = list()
+
+/proc/delete_profile(var/datum/D)
+ if(!ticker || (ticker.current_state < 3)) return
+ var/code = 0
+ var/type = D.type
+ if(isnull(D.gcDestroyed))
+ code = 0
+ else if(D.hard_deleted == -1)
+ code = 0 // A non-queued hard deletion is counted as a straight deletion
+ else if(D.hard_deleted)
+ code = 1
+ else
+ code = 2
+ switch(code)
+ if(0) // Directly deleted (skipped the GC queue entirely)
+ if (!("[type]" in dels_profiled))
+ dels_profiled["[type]"] = 0
+
+ dels_profiled["[type]"] += 1
+ if(1) // Hard-deleted by the GC
+ if (!("[type]" in ghdels_profiled))
+ ghdels_profiled["[type]"] = 0
+
+ ghdels_profiled["[type]"] += 1
+ if(2) // qdel'd and garbage collected by BYOND
+ if (!("[type]" in gdels_profiled))
+ gdels_profiled["[type]"] = 0
+
+ gdels_profiled["[type]"] += 1
+
+/proc/delete_profiler_report()
+ var/dat = "
Deletion Profiler Report"
+ if(dels_profiled.len + gdels_profiled.len + ghdels_profiled.len)
+ dat += "Direct Deletions
"
+ dat += "GC Soft Deletions
"
+ dat += "GC Hard Deletions
"
+ dat += delete_profiler_sortedlist(dels_profiled, "Direct Deletions", "DD")
+ dat += delete_profiler_sortedlist(gdels_profiled, "GC Soft Deletions", "SD")
+ dat += delete_profiler_sortedlist(ghdels_profiled, "GC Hard Deletions", "HD")
+ else
+ dat += "(No deletions profiled; listing types that have been hard-deleted by GC)
"
+ dat += "| GC Hard Deletion Types |
"
+ for(var/A in gc_hard_del_types)
+ dat += "| [A] |
"
+ dat += "
"
+ usr << browse(dat, "window=delete_profiler_report;size=600x480")
+
+/proc/delete_profiler_sortedlist(var/list/L, var/header, var/anchorid)
+ L = L.Copy()
+ // Yes, this is a terrible sort, but I'm too lazy to find a good one
+ var/v,i,j,s
+ for(i = 1 to L.len-1)
+ s=i
+ v = L[L[i]]
+ for(j = i + 1 to L.len)
+ if(L[L[j]] > v)
+ s = j
+ v = L[L[j]]
+ L.Swap(i,s)
+
+ var/dat = "| [header] |
"
+ for (var/t in L)
+ dat +="| [L[t]] | [t] |
"
+ dat += "
"
+ return dat
+
+/*/client/var/running_find_references
+
+/atom/verb/find_references()
+ set category = "Debug"
+ set name = "Find References"
+ set background = 1
+ set src in world
+
+ if(!usr || !usr.client)
+ return
+
+ if(usr.client.running_find_references)
+ testing("CANCELLED search for references to a [usr.client.running_find_references].")
+ usr.client.running_find_references = null
+ return
+
+ if(alert("Running this will create a lot of lag until it finishes. You can cancel it by running it again. Would you like to begin the search?", "Find References", "Yes", "No") == "No")
+ return
+ qdel(src)
+ // Remove this object from the list of things to be auto-deleted.
+ if(garbageCollector)
+ garbageCollector.queue -= "\ref[src]"
+
+ usr.client.running_find_references = type
+ testing("Beginning search for references to a [type].")
+ var/list/things = list()
+ for(var/client/thing)
+ things += thing
+ for(var/datum/thing)
+ things += thing
+ for(var/atom/thing)
+ things += thing
+ for(var/event/thing)
+ things += thing
+ testing("Collected list of things in search for references to a [type]. ([things.len] Thing\s)")
+ for(var/datum/thing in things)
+ if(!usr.client.running_find_references) return
+ for(var/varname in thing.vars)
+ var/variable = thing.vars[varname]
+ if(variable == src)
+ testing("Found [src.type] \ref[src] in [thing.type]'s [varname] var.")
+ else if(islist(variable))
+ if(src in variable)
+ testing("Found [src.type] \ref[src] in [thing.type]'s [varname] list var.")
+ testing("Completed search for references to a [type].")
+ usr.client.running_find_references = null
+*/
\ No newline at end of file
diff --git a/code/modules/power/solar.dm b/code/modules/power/solar.dm
index 2f1390e583f..93c1a961073 100644
--- a/code/modules/power/solar.dm
+++ b/code/modules/power/solar.dm
@@ -150,7 +150,7 @@ var/list/solars_list = list()
/obj/machinery/power/solar/ex_act(severity, target)
..()
- if(!gc_destroyed)
+ if(isnull(gcDestroyed))
switch(severity)
if(2)
if(prob(50) && broken())
@@ -501,7 +501,7 @@ var/list/solars_list = list()
/obj/machinery/power/solar_control/ex_act(severity, target)
..()
- if(!gc_destroyed)
+ if(isnull(gcDestroyed))
switch(severity)
if(2)
if(prob(50))
diff --git a/paradise.dme b/paradise.dme
index 5d50bd49163..89f0aaec637 100644
--- a/paradise.dme
+++ b/paradise.dme
@@ -128,7 +128,6 @@
#include "code\controllers\configuration.dm"
#include "code\controllers\emergency_shuttle_controller.dm"
#include "code\controllers\failsafe.dm"
-#include "code\controllers\garbage.dm"
#include "code\controllers\hooks-defs.dm"
#include "code\controllers\hooks.dm"
#include "code\controllers\master_controller.dm"
@@ -1141,6 +1140,8 @@
#include "code\modules\food\recipes_grill.dm"
#include "code\modules\food\recipes_microwave.dm"
#include "code\modules\food\recipes_oven.dm"
+#include "code\modules\garbage collection\garbage_collector.dm"
+#include "code\modules\garbage collection\gc_testing.dm"
#include "code\modules\genetics\side_effects.dm"
#include "code\modules\holiday\christmas.dm"
#include "code\modules\holiday\holiday.dm"