I noticed this trend with how everyone seems to be clearing user data
-- normally a serverscript, just using local player to test
local httpS = game:GetService("HttpService")
local player = game:GetService("Players").LocalPlayer
function getSize(x)
local encode = httpS:JSONEncode(x)
local byte = string.len(encode)
return byte / 1024 -- kb
end
local UserData = {}
UserData[player] = {"aaa", "bb"} -- list from data store
wait (1)
-- plr left
UserData[player] = nil -- this is how everyone Ive seen clear data
print("ideal KB measured", getSize({}))
print("actual KB measured", getSize(UserData[player]))
output
IdealUserDataSize < (UserDataSize ++)
-- size will build up, sometimes known as a memory leak
This is the wrapper I made that “deep cleans”
-- call every few hours
local lockData = false
local function RedNeckWipe() -- ret Blind
lockData = true
local out = {}
local q = 0
for i, y in pairs(UserData) do
if UserData[y] ~= false then
-- plr is in server atm
q = q + 1
out[q] = UserData[i]
end
end
UserData = out
lockData = false
end
How have you guys been clearing dictionary data?
No wrong suggestions/answers
I know you said this was solved but just so you are properly informed, I think you are confusing garbage collection with data storage efficiency.
Once all refs to the table object containing the player’s data are cleared (one of these being setting UserData[player] to nil), the table that once was UserData[player] will be garbage collected and the memory will be freed, therefore this is not indicative of a memory leak.
It’s probably a good idea to note that JSON encoding the table is not a good way to check whether or not the memory is freed because it simply serializes a Lua data type to a string. JSON encoding nil will give you the string “null” which of course is represented by 4 bytes, one for each letter.
If you want to test out whether or not this garbage collection happens, you can do something like this using a table who has a metatable with a __mode metamethod set to v and add the table to be tested as a value inside of it:
local player = game:GetService('Players').PlayerAdded:Wait()
print(player)
local UserData = {}
local t = {'aaa', 'bb'}
UserData[player] = t
local gcTester = setmetatable({t, {}}, {__mode = 'v'})
UserData[player] = nil -- first reference
t = nil :: any -- second reference
-- we wait for the next gc cycle to happen
while gcTester[2] do
table.create(1) -- create a table just to increase memory usage to make the next gc cycle happen
task.wait()
end
task.wait(1) -- contingency
if not gcTester[1] then
print('Garbage collected') -- should always print
else
print('Not garbage collecte')
end
It isn’t necessarily too good to be true, doing this removes the reference to the table which allows it to be garbage collected (allows the memory to be freed) so if anything it can actually be the solution to a memory leak
The garbage collector is to some extent in your control, if you keep a “strong” (for lack of a better word) reference to the table (for example having it as an in-scope variable, a having it as a key or member in a table that doesn’t have a metatable without a __mode method), Lua can’t really safely assume the variable isn’t needed, therefore it will never be garbage collected until said references no longer exist
I’m going to answer your questions because I think there is some good information here and I’m halfway through writing it anyway
The garbage collector is responsible for freeing up memory when it can ascertain the memory taken up by an object is no longer needed. If there are no more strong references to the object, we can assume the object is not needed so it is accordingly removed from memory so that memory and freed up so the memory can be used for something else. It can’t be accessed when there are no more references to it, therefore so doesn’t make sense to have the object in memory, because it can never be accessed again.
In your original post, having UserData[player] be a table and never setting UserData[player] to nil leaks memory due to 2 factors:
Having the player as a key of your table is considered a strong reference to the player. It can be accessed using pairs for example. Therefore the player will never be garbage collected. This will be the case regardless of whether or not the player leaves the game, and regardless of whether or not you call Destroy on the player. How can the garbage collector know you never need to access the player again? There’s still a strong reference to it, so the garbage collector can never assume in 100% of cases that the player is garbage.
The player never being garbage collected means its corresponding value in the UserData table (being a table {“aaa”, “bb”}) is never garbage collected, because there is still a strong reference to it.
Setting the value to nil (which is the equivalent of removing the entry in the table) solves both of these issues:
Because the value that corresponds to UserData[player] no longer exists, that by logic removes the key as a strong reference to the player in the table.
Because the reference to the table that used to exist at UserData[player] is overwritten and longer exists here, that is one less reference to said table. If this was the only reference to the table, because the reference no longer exists, the table in no way can be accessed again. Then we run into our initial ascertation; that if an object can’t be accessed anymore, it makes no sense to keep it in memory.
So that is why setting the value to nil is the solution to a memory leak in your case.
I’m not 100% sure what you mean by this but I think there are 2 things:
If you meant why would we want to keep an object alive if there are no references to it, we don’t. That’s where the garbage collector comes in, to get rid of all objects that no longer have any point of reference.
The table in my initial reply was to show that the garbage collector would collect an object that no longer has any strong references. Setting the metatable’s __mode metamethod to “v” treats the reference in said table as a weak one, allowing it to be gc’d
If you meant why would we want to keep a reference to an instance parented to nil, preventing it from being garbage collected, there are a couple reasons:
If you want to access the object’s properties. Had the player been gc’d, the player would be nil, thereby giving you an attempt to index nil error, and none of the properties being accessible.
Using the player as a key would be another one. If you’re storing player data using the player as a key with the actual data as the value would prevent the player from being able to used as a key – nil can’t be a key in a table.
There are definitely more points that can be made but hopefully this clarifies a couple things
Very nicely explained and covers everything. If I may just chip in with something I saw in the OP’s first script:
getSize({}) vs getSize(UserData[player])
A new table and an emptied table don’t necessarily have the same size.
Each field (key-value pair) is assigned a slot of memory for keeping a reference to the value (which might be stored elsewhere). Array slots are a bit smaller than dictionary slots.
When the table is emptied, the value references are lost, and as cody said, they should eventually be garbage collected provided that there are no (other) strong references. From the Luau standpoint, the table is empty, and the memory occupied by stored objects is scheduled for freeing up.
But the number of allocated slots doesn’t shrink and may only grow (to the power of 2). Preallocated memory is quicker to write to, because it avoids expansions/rehashes, which is why table.create() exists.
So a table with 3 elements should have 4 slots, and an empty one no slots, hence an emptied one should be slightly bigger internally - by a couple of dozen bytes. I don’t know the exact sizes.