Instance.IsDestroyed and Instance.Destroyed event

I move objects in and out of the world a lot, and I hook up a lot of connection listeners to these objects. However, I’m having trouble reliably making sure that I’m deallocating all references when the object is destroyed. It would be handy if there was a reliable way to guarantee an object is done being used so I can free up all RBXScriptConnection references that might still be lingering in memory.

22 Likes

Destroy automatically disconnects RBXScriptConnections for you.

1 Like

It doesn’t clear table references and any loops though.

And before you say, while Obj.Parent and wait( ) do end, sometimes the object is parented to nil and then back to other places.

5 Likes

Yeah this is what I’m saying.

The memory that was allocated for the RBXScriptConnection userdata doesn’t get garbage collected if there’s still a reference to it in an array.

I’m doing something very procedural and recursive with descendants and listeners, so it’s a huge pain to keep track of all of this stuff. Alternatively Roblox should just delete the connection object from lua arrays when objects are destroyed, but that might be unsafe.

1 Like

This sounds like a use case for weak tables.

3 Likes

I’ve never been able to use weak tables correctly.
What do I do to make the array delete itself and its references if nothing is pointing to the table?

Values will automatically be collected when they are no longer in use.

local weak = {__mode="v"}
local tab = setmetatable({}, weak)

You can also use k and kv where k makes the ‘key’ weak and v makes the ‘value’ weak.

2 Likes

That works, however, it will still be easier to have an event so we don’t have to continually poll for when it’s removed from the table.

Could you provide some tangible code examples of what you’re doing now vs what you would do with this feature?

Currently:

spawn( function ( )

while script and script.Parent and wait( ) do end

-- Cleansup admin stuff such as tables ( Destroying a modulescript doesnt stop code running, this line prevents me from moving my module into nil for safer storage )

end )

With this:

script.Destroyed:connect( function ( )

-- Cleansup admin stuff

end )

What defines an object as being in use or not? Is it just idle time?

No strong references* are made to the object.

* any reference that isn’t weak

2 Likes

Figures. So if the RBXScriptConnection is still connected, then it won’t be collected?

Destroy disconnects any connections.

It doesn’t delete the userdata that was allocated to represent the connection. The userdata does consume memory, I’ve been able to measure it.

I found a way to work around this issue, but it was really tricky for me to isolate that it was happening in the first place.

I was having trouble correctly detecting that an object was destroyed because the ancestorychanged connection was getting killed before I could check.

If you don’t already know, quick way to print when it’s destroyed:

function PrintDestroyed( Obj )
   spawn( function ( ) local a, b = Obj.Name, setmetatable( { Obj }, { __mode="v" } )
   while b[ 1 ] and wait( ) do end
   print( a .. " Destroyed" ) end )
end

Also, the event userdata wouldn’t gc until any references to it are cleared however it wouldn’t stop the object the event is for being gc’d as it has no references to the event object ( I assume ).

I have cleaned up your code, it was a bit confusing because of the way you structured it. I also believe you needed to dereference obj otherwise it will never print?

local collectV = {__mode='v'}
local function printDestroyed(obj)
  spawn(function ()
    local a, b = obj.Name, setmetatable({obj}, collectV)
    obj = nil
    repeat wait() until not b[1]
    print(a, "destroyed")
  end)
end
1 Like

Please know that weak tables are not reliable for this. Just because a weak reference is collected does not mean the referent is no longer in memory. Observe:

-- Create a weak table.
local a = setmetatable({}, {__mode="v"})

-- Create an instance an add it to the workspace.
-- We'll even give it some time to ensure that it's there.
local v = Instance.new("BoolValue")
v.Name = "UniqueName"
v.Parent = workspace
wait(1)

-- Ensure the only reference to the instance is weak.
a[1] = v
v = nil

-- Wait for reference to be collected.
local t = tick()
while #a > 0 do wait() end
print("ref collected after", tick()-t, "seconds")
print(workspace:FindFirstChild("UniqueName"), "still exists")

This happens because the Garbage Collector is not aware of the internal Roblox state. The GC only collected the userdata pointing to the internal state of the instance, so the internal state is still there. The game tree doesn’t maintain a set of strong references inside the Lua state that allow the GC to know that the instance still exists. The same applies to Connections; because the GC isn’t aware of the internal reference of a Connection to its instance, weak references to a Connection will be collected immediately.

5 Likes

That explains why I was having trouble making weaktables work with this. I figured there was something else going on behind the scenes that I wasn’t accounting for.
I don’t really want to use weaktables anyway, Roblox should just be accounting for this somehow.

Would it be infeasible for Roblox to just null out the userdata representation of connections if there was a reference to one in an array, or is that too unsafe? That would probably fix the problem I was having.

I ended up fixing this issue by isolating the userdata from the rest of my code with a proxy that keeps track of when the object gets destroyed.

local function isParentUnlocked(inst)
	if not inst.Parent then
		coroutine.yield()
		local unlocked = pcall(function ()
			local parent = inst.Parent
			local f = Instance.new("Folder")
			inst.Parent = f
			coroutine.yield()
			inst.Parent = nil
		end)
		return unlocked
	else
		return not inst:IsA("Terrain") 
	end
end
--
local function makeWeakConnection(object,signalName,func)
	local signal = object[signalName]
	local connection = signal:Connect(func)
	local proxy = { Connected = true }
	local ancestrySignal
	
	function proxy:Disconnect()
		if connection then
			connection:Disconnect()
			self.Connected = false
		end
	end
	
	local function onAncestoryChanged()
		if not (object:IsDescendantOf(game) and isParentUnlocked(object)) then
			proxy:Disconnect()
			ancestrySignal:Disconnect()
			proxy = nil
			connection = nil
			signal = nil
			object = nil
			ancestrySignal = nil
		end
	end
	
	ancestrySignal = object.AncestryChanged:Connect(onAncestoryChanged)
	
	return proxy
end
1 Like

AFAIK, the reference would gc as nothing was using the reference?