From b386cec388054a4dd49e710a0ccdcb21c7c2e241 Mon Sep 17 00:00:00 2001 From: Krausus Date: Sat, 27 Jun 2015 01:56:57 -0400 Subject: [PATCH] Initial GC fixes/tweaks/cleanup/documenting --- .../ProcessScheduler/core/_stubs.dm | 11 - .../core/updateQueueWorker.dm | 2 +- code/controllers/Processes/machinery.dm | 5 +- code/controllers/Processes/pipenet.dm | 6 +- code/controllers/Processes/power_machinery.dm | 17 +- code/controllers/Processes/powernet.dm | 6 +- code/controllers/garbage.dm | 201 ------------------ code/controllers/master_controller.dm | 12 +- code/game/atoms.dm | 8 +- code/game/atoms_movable.dm | 36 ---- code/game/objects/objs.dm | 2 +- code/modules/admin/admin_verbs.dm | 4 +- code/modules/admin/verbs/debug.dm | 2 +- .../events/tgevents/brand_intelligence.dm | 4 +- code/modules/garbage collection/__gc_info.dm | 69 ++++++ .../garbage collection/garbage_collector.dm | 166 +++++++++++++++ code/modules/garbage collection/gc_testing.dm | 171 +++++++++++++++ code/modules/power/solar.dm | 4 +- paradise.dme | 3 +- 19 files changed, 444 insertions(+), 285 deletions(-) delete mode 100644 code/controllers/garbage.dm create mode 100644 code/modules/garbage collection/__gc_info.dm create mode 100644 code/modules/garbage collection/garbage_collector.dm create mode 100644 code/modules/garbage collection/gc_testing.dm 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 += "" + for(var/A in gc_hard_del_types) + dat += "" + dat += "
GC Hard Deletion Types
[A]
" + 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 = "" + for (var/t in L) + dat +="" + dat += "
[header]
[L[t]][t]
" + 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 36cdcf096ae..c5aa9efdd1e 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"