Garbage collection step never runs if yield is long enough?

So I was testing around for any memory leaks recently and I came across some pretty odd behavior. All these code examples I’m about to go over are done in a single Script in an empty place.

Make a weak table. Put a value in it. Wait an arbitrary amount of time and check the weak table. If it no longer exists its been garbage collected.

local gc_table = setmetatable({}, {__mode = "v"})
gc_table[1] = { "eek!"}
wait(5)
print(gc_table)

This works how I imagine it does. The GC step runs at some point between yielding control of the thread and returning back to it.

Now try this?

local gc_table = setmetatable({}, {__mode = "v"})
wait(5) -- wait before setting a value in the gc_table
gc_table[1] = { "eek!"}
wait(5) -- gc step run when?
print(gc_table) --- it didnt run? where is the strong ref otherwise?

This prints out the entry? I understand that the garbage collector runs somewhat periodically and it can be arbitrary. But I’ve run this many many times with wildly different durations for the second wait call and it changes nothing. I tried using all the old and new scheduler functions and running some code before and after and throwing it in loops and putting it all in a coroutine and all that jazz to see if I could get the behavior to change but nothing seemed to work except this:

local gc_table = setmetatable({}, {__mode = "v"})
wait(1) -- wait not 5 seconds but 1???
gc_table[1] = { "eek!"}
wait(5) -- gc step run when?
print(gc_table) --- why does it run now?

So now I have concluded that I have literally no idea what’s going on internally. I slept on it thinking I was seeing things but I am no longer asleep and this behavior is still here. If I make the first wait duration arbitrarily small enough it will run the GC step? Can someone explain what exactly I am failing to see here or is this just an engine bug?

Thanks in advance.

2 Likes

I answered this in a another thread:

1 Like

I thought that might be the case and that’s why I added what I thought was computationally expensive code in different threads before and after the call. I was also under the impression that calling collectgarbage(count) or gcinfo() might force a GC step like Quenty in your thread but it appears that your

game:GetService("RunService"):BindToRenderStep("",0,function()end)

code made the GC step run. Thanks!

1 Like

I would like to note that even this line doesn’t always work since the nature of the solution is to introduce enough activity for the GC step to run. In my particular case, it appears that for continuous checking I have to include a loop to instance 10 parts every Stepped frame to build enough activity. I should add that anyone testing for memory leaks like this should include references you know will be collected as controls to be sure the GC step ran in the first place.