Merge pull request #1397 from Krausus/GarbageCollectorWasGarbage

Garbage Collector Fixes/Tweaks/Cleanup/Documenting
This commit is contained in:
Fox-McCloud
2015-06-28 06:36:04 -04:00
19 changed files with 444 additions and 285 deletions
+3 -1
View File
@@ -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,
+1 -1
View File
@@ -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
*/
+2 -2
View File
@@ -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))