Destroy() Can Now Replicate to Clients

At this point it’s probably better if you circle back and just do task.delay(1, obj.Destroy, obj) as the namecall optimization won’t be saving you any time when using a closure means you’d be doing 2 function calls anyways.

1 Like

Benchmark time. (Note: this benchmarks task.spawn, but task.delay should be about the same, minus the scheduling cost.)

local n = 500

local instance = Instance.new('Folder')
instance:Destroy()

function spawnLookup()
	for i = 1, n do
		task.spawn(instance.Destroy, instance)
	end
end

local destroy = game.Destroy
function spawnCached()
	for i = 1, n do
		task.spawn(destroy, instance)
	end
end

function closure()
	for i = 1, n do
		task.spawn(function() instance:Destroy() end)
	end
end

function closureNoUpvalues()
	for i = 1, n do
		task.spawn(function(instance) instance:Destroy() end, instance)
	end
end

Caching game.Destroy yields the best performance, while looking up game.Destroy every time results in the slowest performance. However everything overlaps so heavily that in practice it doesn’t really matter.

Try me.

Here’s what happens if you call namecall directly using some implementation-specific Hot Garbage. Please note: this is scary as hell. You don’t want to see how I did this.

So namecall is a normal instance:Destroy() call, without using the task library. __namecall is a direct __namecall metamethod call, without using the task library, and without setting the namecall method at all (it is set once, to Destroy, using a newproxy __namecall, and then not set again).

cursed uses the task library, and is the most efficient way to schedule a (precisely) delayed destruction of an Instance.

Alright so scheduling namecall ~directly using the task library is absolutely possible and it has huge performance benefits. That’s literally what I’m doing.

Okay I’ll be honest this is funny. I was complaining about how this wasn’t possible and here it is, right in front of me.

I might make a community resource, you’ll see how I did this soon.

4 Likes

There is also an edge case where connections connected to an instance after the instance is destroyed are not disconnected, this should be fixed with this new update as well.

local b = workspace.Baseplate

local c1 = b.Touched:Connect(function()
	
end)

b:Destroy()

task.defer(function()
      print(c1.Connected) --> false
end)

local c2 = b.Touched:Connect(function()
	
end)

while c2.Connected do
     print("connection still connected...")
     task.wait(1)
end
1 Like

Note that in certain cases (garbage collection, etc) you won’t observe the Connected property turn false until the next time the event is fired. Your callback won’t get called and the Connected property will get set to false. That may be what you’re observing.

Of course, you won’t be able to trigger the Touched event after something is destroyed, try with GetPropertyChangedSignal.

I misread your reply. After something is destroyed, connections that are added should not be disconnected. Some Instances are still somewhat functional after being destroyed, so sometimes this is wanted for one reason or another.

Connections linked to an instance still being kept alive even if the instance is destroyed is bad and unexpected behavior in an almost every single case. Once an instance is destroyed, there is no reason for you to use it in almost any cases (except clean up references to it, etc…). Maids and janitors make this process easy and cleaning up in general.

Even the developer hub recommends so and this isn’t surprising anyways:

There’s a reason that you can still call methods, get and set fields, etc. on destroyed objects. Destroyed Instances are permanently removed from the DataModel and all connections are cleaned up, but they aren’t removed from all userland code.

You can actually call Destroy multiple times on an Instance.

local inst = Instance.new('Folder')

local conn = inst.AncestryChanged:Connect(function() end)
print(conn.Connected) -- true
inst:Destroy()
print(conn.Connected) -- false

conn = inst.AncestryChanged:Connect(function() end)
print(conn.Connected) -- true
inst:Destroy()
print(conn.Connected) -- false

Yes, I’m the author of one of the fastest maids (which I haven’t published because nobody will care).

The reason you can do so is primarily because of backwards compatibility with old games who have bad code like that but at the same time many developers sometimes use certain properties of an instance to clean references of that instance, which isn’t a problem!

Ideally for destroyed instances, their data should be read only and should be kept immutable (an error can be raised when trying to mutate it), this also encourages proper usage of the data of an instance. Now this may bring up another problem with having references to these data (those which are of reference types) but that is okay since they would be collected when those references are dead.

Newly created connections should never be alive on an destroyed instance, and the behavior of Destroy should be kept consistent (i.e an instance shouldn’t be destroyed multiple times – all the steps needed to do destroy an instance should be run once and not repeatedly, the current behavior of Destroy doesn’t respect this properly which concerns me slightly due to lack of precaution. This also allows you to check if an instance is already destroyed by having just a simply code like:

local function IsInstanceDestroyed(inst)
    return not (inst.AncestryChanged:Connect(function() end)).Connected
end

According to what I said above, cleaning up references to an destroyed instance properly may look like so:

local instanceRefs = {...}

local function Cleanup(instance)
    -- We're just fetching the instance's unique id (notice 
    -- we aren't modifying it, but only using the data to
    -- clean the reference)
    instanceRefs[instance:GetAttribute("UniqueId")] = nil

    -- In another case, just this is fine..
   instanceRefs[instance] = nil

   -- BAD code - mutating an attribute of a destroyed instance!
    instanceRefs[instance:GetAttribute("UniqueId")] = nil
    instance:SetAttribute("UniqueId", nil) -- don't need it anymore..
end

instance.Destroying:Connect(function()
     Cleanup(instance)
end)

I’m not sure if this is directly related since I’ve never tested this before, but I recently tested this so I’ll report it here (I’m not regular).

It seems that the Destroying event isn’t fired for children when their parent is destroyed.

Just drop this into a game (just two scripts and 10 lines total)
Script.rbxm (723 Bytes)

This calls Destroy on the parent script and its Destroying event is fired properly, but its child’s Destroying event isn’t called it seems.

ee9w8a_125601
f55tsg_125611
qxsws4_125621

Not sure if this is intentional or not, or if this update causes this, but the documentation on Destroy states that it “calls Destroy on all children” which doesn’t seem to be the case as the event is not fired. This just makes it difficult to do stuff on destroy of scripts that don’t have Destroy called on them directly. In my case this is in ScreenGuis that have ResetOnSpawn enabled as the event is only fired on the ui itself.

I should expect either none of those to print or both, but I did do some testing and found out that it seems to just be scripts which are listening to their own Destroying event and are being automatically cleaned up because their parent was destroyed. But the first print shows that scripts are capable of listening to their own destroy, just not if they’re children, which I find inconsistent.

This is actually consistent with the behavior that the Event Listeners a Script connects will not run once the Script is removed from the DataModel. If you read the documentation here: Script you will find that we say a Lua Script will run if

  • Disabled property is false
  • The Script object is a descendant of the Workspace or
    ServerScriptService

and terminate if these conditions are no longer met.

To be more specific, let’s go through the example you provided: When we have the following in the DataModel

ServerScriptService
    -Script1
        -Script2

and the Scripts look like

--Script1
script.Destroying:Connect(function()
	print("Destroying")
end)

task.delay(5, function()
	script:Destroy()
end)
--Script2
script.Destroying:Connect(function()
	print("Destroying 2")
	part1:Destroy()
end)

and Script1 calls Destroy(), Script1's parent gets set to nil. Afterwards, Script2's Destroying event will fire.

Note that by the time Script2's Destroying signal fires, Script1 has already been reparented to nil. This means that, Script2, as the child of Script1, is also no longer part of the DataModel. More importantly, Script2 is also no longer a descendant of Workspace or ServerScriptService. Thus, the Event Listeners connected by Script2 will no longer run.

So it’s not that Script2:Destroying doesn’t fire - the issue is that the event listener connected by Script2 got disconnected when its ancestor changed from ServerScriptService to nil, when Script1 was destroyed.

I figured this was the case when I saw it was firing and I could listen to it from another script, however my use case was with a PlayerGui which gets reset on spawn, so the only way to be able to listen to it being destroyed is listening to the Destroying event of the UI itself? Mainly because connections were still being listened to from the destroyed script because it wasn’t cleaned up (something listening to Destroying allows me to do).

This is because the script listening is actually a module script which even when destroyed runs its code (or at least still responds to events):

Also this was a weird way to handle it and I already changed a lot of how this works so that this doesn’t affect it at all, but I just thought it was a little odd. Thanks for the reply!

(To anyone reading this, don’t put module scripts in UI’s that reset on spawn, that’s the root cause of my issues)

No worries, glad I could help and glad you’ve already got a solution! :slight_smile:

Your use case is still somewhat confusing to me, so I’m not really sure I can give you useful tips for how to address it. That being said, writing games on Roblox is your expertise, so your solution is likely better than what I could come up with :smiley:

and yes, I totally agree that the engine sometimes behaves in surprising ways xD

I have an question will wait() be fixed because like it is slower and laggy it should be speed as task.wait.

Here are some results:

No. wait is deprecated, the reason it sticks around is to support legacy scripts and if we changed the behavior it would break some of those legacy scripts. If you want the better behavior use task.wait

What do you mean? Just use task.wait instead of wait, it is way more reliable and fast.

The Destroying event doesn’t fire when you delete objects in the explorer in Studio, which is a problem for my plugin. I can’t listen to users deleting specific things in Studio, only with some hacky workarounds.

Greetings Developers,

It’s finally time for Instance:Destroy() to be replicated by default! We will roll this change out May 16, 2022

This means that setting ReplicateInstanceDestroySetting under Workspace to Default will replicate Instance:Destroy(). If you don’t want your experience to replicate Instance:Destroy() , you will have to set ReplicateInstanceDestroySetting under Workspace to Disabled.

Remember, the plan is to eventually enable Instance:Destroy() for all experiences, so please update your experiences to take advantage of this feature, or let us know what’s blocking you from enabling this feature!

4 Likes

Will Destroy be called on a Character when it resets?
Will Destroy be called on parts when they fall too low on a map?

Cool stuff, updating the roblox functions.

Is there a way to check if the parent property is locked or any other property is locked?
I’d like to destroy all objects within a folder upon one of them being destroyed but it errors whenever the first object being destroyed tries to get destroyed again, checking it’s parent being nil doesnt work since it’s activated on destroying event

1 Like

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.