Hi!
Explanation
So basically I’m working on trying to minimize memory leaks by using my own module inspired by the ‘Maid Class’ that was explained about here.
I was able to make it work nicely up until I found out that if I never manually finish the tasks the task will hang around in memory forever. An example is if a UI gets deleted on the client and the script inside of it created a task via the Maid module but was never able to finish the task when it was meant to because that script is now gone.
There’s a module in my game called ‘Maid’ that is shared between the server and the client for garbage collection on many things like temporary events. Outside scripts require
the Maid module and can either create a task (Maid:CreateTask(Task, Script
) or “finish” a task (Maid:FinishTask(TaskIndex)
).
There is a table inside of the Maid module that holds all of the tasks with an index that is returned by CreateTask
and then the same index can be used to finish the same task by FinishTask
.
Problem
For some unknown reason line 52 of the Maid module (The AncestryChanged event) is not firing for me and it seems like it is mysteriously getting automatically disconnected, (Perhaps because when the Instance is being removed it also disconnects everything with a connection in CreateTask
?).
I notice at the same time right below it if I delete the script after it outputs “A” and before it outputs “B”, it will never print B. It prints B if I don’t delete it before the time ends.
Snippet from the Maid module:
local Connection
Connection = Script.AncestryChanged:Connect(function()
warn("Removed.") --This is never printing when removing the script passed.
if not Script.Parent then
Maid:FinishTask(Index)
Script:Destroy()
Connection:Disconnect()
Connection = nil
end
end)
coroutine.wrap(function()
print("A")
wait(5)
print("B", Connection)
end)()
Code
Maid module:
-- Mirror
local Maid = {}
-- Variables
Maid.Tasks = {}
-------------------------------
--[[
This module is used for basic disconnection and destroyal of both [Instances] and [Connections] when needed to keep
the game clean.
'Tasks' is the word used for values that are limited in lifetime and eventually need to be disconnected by the script.
These tasks can either be a single value (like :CreateTask(Instance or Connection)) or a table of multiple values
(like :CreateTask({Instance, Instance, Connection}). Single values can be used for single copies of items that need
to be disconnected in the future and tables can be used for groups of values that all need to be disconnected at once,
usually related to each other in some way.
NOTE: Only values directly inside of the table given will be disconnected. Any values nested under a table inside of
the primary table will be completely ignored.
Example usage:
local Task = Maid:CreateTask({
OnItemAdded = Instance.new("BindableEvent").Event,
OnItemRemoved = Instance.new("BindableEvent").Event
}
Item.AncestryChanged:Wait()
if not Item.Parent then
Maid:FinishTask(Task) -- Task in this case is actually just the index of the task. Looks much better.
end
]]
function Maid:GetTaskCount()
return #Maid.Tasks
end
function Maid:CreateTask(Task, Script)
assert(Task, "A task value is required to create a new task.")
assert(Script, "A reference to the script is required to create a new task.")
local Index = Maid:GetTaskCount() + 1
Maid.Tasks[Index] = Task
print("Task created: ".. Index ..".")
print(Script:GetFullName())
local Connection
Connection = Script.AncestryChanged:Connect(function() -- PSA: AncestryChanged isn't reliable.
warn("Removed.")
if not Script.Parent then
Maid:FinishTask(Index)
Script:Destroy()
Connection:Disconnect()
Connection = nil
end
end)
coroutine.wrap(function()
print("A")
wait(5)
print("B", Connection)
end)()
return Index
end
function Maid:FinishTask(Index)
local Task = Maid.Tasks[Index]
if Task then
if typeof(Task) == "table" then
for _Index, Value in pairs(Task) do
if typeof(Value) == "RBXScriptConnection" then
Maid.Tasks[Index][_Index]:Disconnect()
elseif typeof(Value) == "Instance" then
Maid.Tasks[Index][_Index]:Destroy()
end
Maid.Tasks[Index][_Index] = nil
end
elseif typeof(Task) == "RBXScriptConnection" then
Maid.Tasks[Index]:Disconnect()
elseif typeof(Task) == "Instance" then
Maid.Tasks[Index]:Destroy()
end
Maid.Tasks[Index] = nil
print("Task finished: ".. Index ..".")
end
end
coroutine.wrap(function() -- Temporary loop used to make sure it is all being GCed properly.
while true do
print("Tasks in memory: ".. Maid:GetTaskCount())
wait(10)
end
end)()
return Maid
Script used to test out the module:
-- Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Modules
local Maid = require(ReplicatedStorage.Maid)
---------------------------
local Tasks = {}
for i = 1, 5 do
local Task = Maid:CreateTask(workspace:GetPropertyChangedSignal("DistributedGameTime"):Connect(function()
print("Task set 1 running")
end), script)
table.insert(Tasks, Task)
end
--for _, Task in pairs(Tasks) do
-- Maid:FinishTask(Task)
--end
Extra question if it’s not too much to ask: What are the benefits of using metatables/OOP over not using it like shown above with a Maid object? Every other implementation of the Maid object that I’ve seen is always using metatables so I’m wondering if it’s just a preference or if there’s more use to it that can’t be pulled off without metatables.
Thank you!