As stated in the title, I want to know how to know if a script or a certain code-block of a script is causing a memory leak.
What are the different ways to detect a memory leak? If there are visual ways, how?
How can they be pinpointed to know which script / part of a script is causing them?
How can something be classified as a memory leak?
What are the different ways to deal with memory leaks?
For context, my previous game that I made has faced this problem: as the game proceeded to run longer, the different parts of the game started to lag noticeably!. Since I’m onto a new project, I want to be able to prevent this from happening again.
I saw this post (Best Practices Handbook) as I’m trying to increase my scripting proficiency and noticed this:
I knew then that this was the problem I encountered on my previous project.
How would I also know when to close the connection? This is one example:
-- For the quest Triggers
for i, questTriggerInstance in ipairs(questTriggers) do
if questTriggerInstance:IsA("BasePart") then
-- Connection
questTriggerInstance.Touched:Connect(function(hit)
local player = PlayersService:GetPlayerFromCharacter(hit.Parent)
if hit.Parent:FindFirstChildWhichIsA("Humanoid") and debounce[player.Name] == nil and PlayersService:FindFirstChild(hit.Parent.Name) then
debounce[player.Name] = true
initiateStartQuest(questTriggerInstance, player)
task.wait(5) -- Wait before starting another quest
debounce[player.Name] = nil
end
end)
end
end
How can memory cleanup be used in this case? If memory leaks are indeed present.
I’ve seen different modules like Janitor but don’t know when and how to apply them.
If you want to close an event after it has occurred, you can simply use the Event:Once() that you had mentioned or look up tutorials on Janitor or Maid, but I’d recommend doing that for multiple events not just a single instance.
How often does this code run?
Do you delete quest trigger Instances?
In your case you can just create connection and disconnect it as soon as someone has touched trigger
for i, questTriggerInstance in ipairs(questTriggers) do
if questTriggerInstance:IsA("BasePart") then
local connection
connection = questTriggerInstance.Touched:Connect(function(hit) -- connection will be save in this thread
local player = PlayersService:GetPlayerFromCharacter(hit.Parent)
if hit.Parent:FindFirstChildWhichIsA("Humanoid") and debounce[player.Name] == nil and PlayersService:FindFirstChild(hit.Parent.Name) then
debounce[player.Name] = true
initiateStartQuest(questTriggerInstance, player)
task.wait(5) -- Wait before starting another quest
debounce[player.Name] = nil
connection:Disconnect()
end
end)
end
end
Any resources? I’ve been trying to look for them but all they are doing are kinda just showcases of those modules and how I need to use them but not how to use them.
Can you show me the full picture?
I just want to understand how often do you make those connections
And what if you have restarted quest? Can you restart the quest?
What about canceling the quest?
To use Janitor, all you really have to do is this:
local MyJanitor = Janitor.new()
-- Say imaginary part exists
Janitor:Add(Part.Touched:Connect(function()
-- Your code here
-- Cleanup and the method below is the equivalent of :Disconnect()
MyJanitor:Cleanup()
end))
-- Alternatively,
MyJanitor()
To give more context, the MyJanitor:Add() is simply adding an event to a list of events inside the Janitor. There are three arguments in the method which is the object, method name, and the name. The method name i.e Stop or Play, is just a string and the Janitor will handle things, for example:
In the documentation, if you wrote this in your code this tween will stop or if you add an animation track with method name Play, the animation will play.
The name in the third argument really is just a name of the object, like an index per say. It’s like in a table recorded with lists and names.
If you need more information than this, ask anything.
You can notice that memory in your game just skyrocket when you have memory leak like this:
local part = Instance.new("Part")
part.Parent = workspace
part.Anchored = true
while task.wait() do
part.Touched:Connect(function() -- this takes memory and it won't be released unless you delete part.
print("lol")
end)
end
This is very helpful since I could do (2.) using the logic you’ve provided which would hopefully prevent any memory leak for the code I gave as an example:
By the way, for the local connection variable, couldn’t I just declare it on the scope where all subsequent code-blocks can access it (but I think multiple code-blocks might change it at the same time) ? Like this:
local connection
-- [[Other variables and functions which
--would be accessible anywhere within the script]]
for i, questTriggerInstance in ipairs(questTriggers) do
if questTriggerInstance:IsA("BasePart") then
connection = questTriggerInstance.Touched:Connect(function(hit) -- connection will be save in this thread
-- Same logic
-- Same logic
connection:Disconnect() -- Main point in focus
end
end)
end
end
Or should I just declare that variable on that specific scope instead, i.e., not accessible in other blocks.
BTW, Thanks a lot for your replies! You’ve been helping me wrap my head around this easily!
It’s probably better to keep it within the specific scope as the previous event will be lost forever in reference, and may cause leaks cause the global connection variable keeps changing.
All of these replies have been pretty relevant. Knowing when connections need to be disconnected can be confusing at first. Not all connections need to be disconnected. If you want a Touched event to run throughout the entirety of your game, there is no reason to disconnect it. Disconnect events that are no longer needed and still retain memory.
It’s also worth mentioning that if your connection is connected to a part (ex: Part.Touched, Part.Changed, etc.), you can destroy the part to garbage collect all events linked to it. This includes destroying parents/ancestors of the part. If you wanted to make a .Touched event only for a specific part, and then delete the part when your done; You don’t need to clean up any events.
Regarding connection helpers, I usually use Maid or Trove, or a combination of both. They are pretty similar in their functionality. But, you have to decide when to use them. Don’t use connection helpers for a script that involves disconnecting one or two events. Just disconnect those events like you normally would. There is no point in using Maid or Trove for small tasks.
There are a lot of potential causes for memory leaks, but I’ll detail a few major ones.
Players and characters.
See, players and characters don’t actually get destroyed when they leave or respawn - their parents are just set to nil.
This means they will indefinitely stay in memory - over time, a server can get saturated with these connections and experience degradation.
This behavior will change somewhere this year though, just keep this in mind.
Being inexperienced.
This is something that others here have already mentioned, but please disconnect or destroy instances that you know you’re done with.
Don’t make unnecessary references to those instances too, or the GC won’t clean them up for you.
Also keep in mind that destroying instances also disconnect any events connected to them.
Eg: if you have a treasure chest that opens when someone holds down a button near it with a ProximityPrompt, you can probably just destroy the prompt, or at least disconnect the prompt event after the player is done with it. Or even better, just destroy the chest after a while.
Understanding these will help you understand the patterns behind what causes memory leaks, and how you can fix them.
Just search up on any GC tutorial on Roblox, and you’ll find loads of guides for them.
No, not all memory leaks will result in a huge spike in memory.
A gradually increasing memory usage can still be indicative of a memory leak.
It also applies vice versa - just because there is a huge spike in memory, doesn’t mean there is a memory leak.
Maybe you just required a huge module that might take up a lot of memory, or you have just loaded in a huge map into your workspace.
This is exactly the reason my previous game experience a whole ton of lag after 10-20 minutes of playing and players killing each other with magic. My cleanup of the parts that were used for the magic VFX etc. were good and responsive, but I didn’t know that I would also have to cleanup the characters after respawn which clogged up memory after some time! Especially with the modifications I did with the character like adding different stuff.
These kinds of topics should be discussed more and be put in practice early on for the new scripters! Browsing through devforum and youtube, there aren’t much friendly introductory forums related to clean-ups (and not really much videos about it). Most are too complex to understand especially with jargons.
Nevertheless, learning to utilize those modules— in the correct cases (for bigger cleanups and not small tasks, as you’ve stated)— will surely be of big help for those like me who wants to be able to code up to industry-standard!
You can prevent this from happening by setting workspace.PlayerCharacterDestroyBehavior to true.
This forces player and character instances to be destroyed once they respawn or leave.
This will be opt-out and eventually default behavior somewhere in the future, but as it stands, it’s opt-in, so you’ll have to enable it yourself.
Please do this - it’s a really big problem for server longevity for practically every Roblox game in existence.
CharacterRemoved and PlayerRemoving will still work as per normal, though if you have any deferred tasks within those events, they may fail as the instances may be destroyed.
or you can just do what i do and come up with your custom character spawn behavior looooool
If you have event listeners or loops that aren’t created at the start of runtime, make sure to close them. Loops can be broken with break or return, and listeners can be ended by MyFunction:Disconnect().
A trick that can be used to replace old loops is to use newproxy(), which generates a unique but otherwise empty value. At the start of each iteration, a loop can check if a newproxy value has been updated, and if it has, it will self-terminate.
Example:
PL_Proxy = newproxy()
function PrintLoop(PL_String)
local CheckProxy = newproxy()
PL_Proxy = CheckProxy
while true do
if CheckProxy ~= PL_Proxy then break end
print(PL_String)
task.wait(1)
end
print("Ending print loop "..PL_String)
end