mirror of
https://github.com/ParadiseSS13/Paradise.git
synced 2026-07-15 09:03:23 +01:00
Merge pull request #1397 from Krausus/GarbageCollectorWasGarbage
Garbage Collector Fixes/Tweaks/Cleanup/Documenting
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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 << "<span class='warning'>Error: callproc_datum(): owner of proc no longer exists.</span>"
|
||||
return
|
||||
if(!hascall(A,procname))
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
*/
|
||||
@@ -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]"
|
||||
@@ -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 << "<b>GC: qdel turned [garbageCollector.del_everything ? "off" : "on"].</b>"
|
||||
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 << "<span class='notice'>No hard del()'d types found.</span>"
|
||||
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 = "<html><head><title>Deletion Profiler Report</title></head>"
|
||||
if(dels_profiled.len + gdels_profiled.len + ghdels_profiled.len)
|
||||
dat += "<a href='#DD'>Direct Deletions</a><br />"
|
||||
dat += "<a href='#SD'>GC Soft Deletions</a><br />"
|
||||
dat += "<a href='#HD'>GC Hard Deletions</a><br />"
|
||||
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)<br />"
|
||||
dat += "<table border='1'><tr><th>GC Hard Deletion Types</th></tr>"
|
||||
for(var/A in gc_hard_del_types)
|
||||
dat += "<tr><td>[A]</td></tr>"
|
||||
dat += "</table>"
|
||||
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 = "<table border='1' id='[anchorid]'><tr><th colspan='2'>[header]</th></tr>"
|
||||
for (var/t in L)
|
||||
dat +="<tr><td>[L[t]]</td><td>[t]</td></tr>"
|
||||
dat += "</table>"
|
||||
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
|
||||
*/
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user