Memory Management & Leaks

Keeping memory usage under control is easy but usually looked over!

by RaterixRGL

Author Avatar

Memory Management.

Keeping memory usage down is extremely important. Most people don't realize that memory usage becomes a problem as time goes on. Creating major scripts, modules, or anything that wants to use memory will eventually creep up and cause a major problem. Even if it is inside of a module script, the main scripts take the memory usage from the module script because the main script is the primary threat. The more memory usage a script uses, the more the interpreter has to work. All forms of code use an interpreter, and the more memory usage that you have, the more variables that are in play, all affect the performance and resource usage of your script. Not only does it affect the performance of your script, but it affects the performance of your game. This is extremely minor in small scripts but the larger your script is and the more usage your script uses, like let's say a very high activity script is around 90% activity, would definitely notice major benefits with memory management. Anything that stores in the memory that is not garbage collected will cause this issue. Here's an example of variables that will not be cleaned up with garbage collection for the main thread, yet you could trigger it to be cleansed for the main thread.

local x = 10
local y = 50 -- Some variable, ie a NumberValue stored.
x = x + y
...
-- Multiple references to x, but no further references to y.

This is a baseline example of the mistake, y is never referenced again yet it’s taking up space in memory. Lua uses standard lexical scoping, but we won’t get into the explanation of that because this is meant as a simple tutorial. Please read 4.2 – Local Variables and Blocks of the Programming in Lua documents, found on Lua’s official website, lua.org or any other alternative source. Below is another example of the same mistake, yet being made with Roblox code.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local Pizza = ReplicatedStorage:WaitForChild("Pizza") -- Calling this above the for loop saves resources because it never needs to get Pizza from ReplicatedStorage ever again, it’s stored in memory.
for k,v in pairs(Players:GetPlayers()) do
    if (v.Character) then -- This is equal to "if (v.Character ~= nil) then", but more efficient.
        Pizza:Clone().Parent = v.Character
    end
end-- No further references to pizza.

Because of the fact that we have no further references to pizza, that means we have “Pizza” in memory, but it’s doing nothing but wasting space. Not only that, but we may want to use the word “Pizza” for other code, but not use what’s in ReplicatedStorage. Executing this code using a local function actually is worse because the entire for loop now is stored in memory allowing for further use rather than being discarded. To correct this mistake, you use a do-end block.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
do
    local Pizza = ReplicatedStorage:WaitForChild("Pizza")
    for k,v in pairs(Players:GetPlayers()) do
        if (v.Character) then
            Pizza:Clone().Parent = v.Character
        end
    end
end

This method clears “Pizza” from memory, allowing clean garbage collection. Using a do-end block will also effectively allow you to re-use the “Pizza” variable without overwriting it or accidentally referencing the wrong thing.

Multiple do-end block chains.

You can even chain do-end blocks inside each other, and it’s recommended to do so the larger your script gets. Below is an example of multiple do-end chains in one script. No optimizations are made, it’s just an example.

do
    print("First batch.")
    local function Pizza()
        print("Pizza was made.")
    end
    do
        Pizza()
        print("Second batch.")
        Pizza()
    end
end

Keep a keen eye at using this system, actively try your best to use it, a do-end block is vital at cleaning up those pesky little things using up the memory. You can even use a do-end block for entire sections of code, like OOP, and OOP tables, or just any table can be sanitized.

Table sanitization.

Tables that have unnecessary data in them, or even functions. Functions like table.remove are helpful to clear parts of a table you don’t need, or you can overwrite a named variable when remove doesn’t work.

local Pizza = {Size = 10,Cooking = true}
local function Cook_Pizza()
    ...
end
local function Give_Pizza_To_User()
    ...
end

After GivePizzaTo_User is called, the “Cooking” variable no-longer is needed, so you can call

Pizza.Cooking = nil

Doing so will save memory. It isn’t as much as variable wiping, but it at least helps in some form. If however, you have a Connection inside a table, do not write it to nil. First, call :Disconnect on the connection, then write it to nil.

Connections, and Connection memory leaks.

TheRobloxPlayer2509 made a guide on this game a while ago explaining memory leaks, however I have expanded upon what he posted greatly, and he was incorrect about a few topics, unfortunately. A do-end bracket is not another thread. Connections are one of the hardest things on memory, I'm not exactly sure why but for some reason creating a connection uses an extensive amount of memory. Because of this, you want to make sure that your connections are as low as possible. Disconnect them whenever you could, and recreate them if need be. It's difficult to explain how important it is to remove connections. Let's say you have a major system out of connections, like a 50 to 100 panels on the ground and they all have a touch sensor. A better option would be to only connect the players legs if there’s less than the (amount of panels * (players * 2)) (maybe 2.5 since legs can touch walls, or the HumanoidRootPart, benchmark your game). However, if you’re trying to make a connection for more than just a player (like a ball rolling), then you need a dynamic system that updates when the objects in play are near and has the ability to touch the panels. You can use a looping function that checks every one second for the position of the objects, and hook up connections to the nearby panels.

... -- Prior game code.
do
    local PanelUpdateTBP = true -- This should be under a do-end block connecting with your game code that will set PanelUpdateTBP to false.
    local function PanelUpdate()
        local function Touch_Function(Part) -- Inside the PanelUpdate function because this is the only place it gets connected.
            ...
        end
        local PanelConnections = {} -- Preserve connections in memory.
        local PanelPositions = {}
        for k,v in pairs(Panels) do
            table.insert(PanelPositions,v.Position) -- These panels are static, dump the position in memory, no need to keep re-reading the part.
        end
        local PPMax = #PanelPositions -- Reserving in memory so you save processing power.
        while (wait(1)) and (PanelUpdateTBP == true)) do -- When PanelUpdateTBP sets to false, this while loop gets destroyed and no-longer processed in the spawn function. It is extremely important to terminate all loops from any additional threads made.
            for k,v in pairs(Objects_In_Play) do -- All objects that will touch the panels (ie in a fixed minigame box), legs of a player, a ball, car wheels, etc.
                for x,m in pairs(PanelPositions) do
                    -- Distance checking code using .magnitude for v.Position and m, or your own method.
                    if (Distance < 25) then
                        if (PanelConnections[x] == nil) then
                            PannelConnections[x] = v.Touched:Connect(Touch_Function)
                        end
                        break -- Stop processing, we've set a connection. No need to search through the other positions.
                    elseif (x == PPMax) and (PanelConnections[x] ~= nil) then -- End of position list, clear the connection if it exists. (x == PPMax) gets called first, so the "and" part isn't even processed unless (x == PPMax) is true.
                        PanelConnections[x]:Disconnect()
                        PanelConnections[x] = nil -- Removes the RBXScriptConnection object from the table.
                    end
                end
            end
        end
        for k,v in pairs(PanelConnections) do -- Disconnect all connections after the while loop ends.
            v:Disconnect()
        end
        -- This is the end of PanelUpdate, all local tables in this function get garbage collected after this.
    end
    -- Past this, the function Touch_Function doesn’t exist in global memory, only in PanelUpdate.
    spawn(PanelUpdate)
    ... -- Additional game code that sets PanelUpdateTBP to false, ie the end-game function.
    local function EndGame()
        PanelUpdateTBP = false
        ...
    end
end -- Past this, PanelUpdate does not exist within memory after it is terminated.

In this extremely large code bracket, I’ve included multiple batches of information, like disconnecting loops that exist in threads, and clearing out connections. All tips and information has been commented, give them a read.

Connection Leaks.

Running a connection that never gets disconnected, will never get disconnected until the script is disabled. Below is an example of a connection that will never get disconnected via script but can be disconnected when the Part is Destroyed:

Part.Touched:Connect(function(Obj)
    ...
end)

If the above part is ever Destroyed, the connections to it also get Disconnected. The function Destroy is the only method that effectively clears connections Below is an example of a connection that will never get disconnected:

workspace.ChildAdded:Connect(function(Obj)
    ...
end)

Never use methods like this unless you strictly specify that these functions are necessary, and beneficial to your code. You always want to call :Disconnect on the function, and to do so you need to reserve the Connection hook into memory.

local Workspace_ChildAddedConnection = workspace.ChildAdded:Connect(function(Obj)
    ...
end)
... -- Additional game code.
Workspace_ChildAddedConnection:Disconnect() -- Disconnect called, ie end of minigame round, vehicle turned off, etc.
Workspace_ChildAddedConnection = nil -- Removes the RBXScriptConnection object. A do-end bracket would make Workspace_ChildAddedConnection nil and destroy the variable once the section of code exits the bracket.

When memory variables provide benefits in performance.

There’s some cases where clearing a variable shouldn’t be done, and using extra memory for a reserved object is necessary. For example, requiring a module is more intensive than keeping it in memory depending on the size and use case of your script. If you are fairly certain that you don’t need a module, then wipe it from memory, be done with it. If you know for a fact you need to keep the module in memory and you will keep going back to it, leave it in memory, and even store the functions as memory objects. Storing module functions as memory objects actually provides a performance benefit.

local Module = require(ServerScriptService:WaitForChild("Module"))
EatFunction = Module.EatFunction

Thread leaks.

Using spawn, or a coroutine on a function that is not local will become a permanent part of the memory and never garbage collected, especially if the local function is not a do-end bracket. You also must ensure the function itself has memory protections, like a do-end bracket, and any forms of infinite processing (ie a while loop, or repeat) has an ability to be cancelled. (ie while (Pizza_Pie == true) do) Below is an example function to correctly create a thread using spawn.

do
    local function Pizza()
        print("Pizza pizza!")
    end
    spawn(Pizza)
end

You could also set Pizza to nil, but remember, Lua still counts Pizza as a variable, even if it’s nil. A do-end bracket is the most effective method. Also, remember, setting a function to nil does not end the function. It will clear from the working set memory once the function itself actually ends.

Final thoughts.

It’s important to read up the Programming in Lua documentation for efficient programming. You can find the official version at https://www.lua.org/pil/contents.html Even if it’s hard to read, take your time. Look up specific things, and take your time if necessary.

Thanks for reading my guide, this will be updated when necessary.

View in-game to comment, award, and more!