TL;DR
The Roblox Engine is leaking memory when you create and destroy instances in RenderStepped ( Either with RunService:BindToRenderStepped()
or with RunService.RenderStepped:Connect()
)
Repro
We have created two places to illustrate the bug:
- Enter the place.
- Use the Run prompt like in the picture.
- Press F9 (on windows) and check Memory/CoreMemory/default if using
RunService:BindToRenderStepped()
or Memory/CoreMemory/luasignal if usingRunService.RenderStepped:Connect()
.
RenderStepped_Connect_Example_V2.rbxl (36.0 KB
BindToRenderStepped_Example_V2.rbxl (36.0 KB)
Proof of Concept Video:
Introduction
Hello everyone. In the past month me and my brother’s game (@Redridge) started getting many dislikes. We went ahead and checked the game analytics and we noticed there were many clients that were running out of memory. When entering the game and noticed there was a memory leak happening under the CoreMemory / default section ():
We have inspected this for a long period of time, thinking this must have been our error, following posts such as Properly using RunService along with preventing Memory Leaks
and Garbage Collection and Memory Leaks in Roblox - What you should know.
Repro Explanation
Components Used
-
SpawnController, a script that controls the spawning, moving and removing of projectiles via RunService (Bind or Listener).
This also prints the current script’s Lua memory usage. If you run this you can see that the Lua memory is not increasing (which can also be confirmed by checking F9->Memory/PlaceMemory/LuaHeap) and the instances are also being properly garbage collected (F9->Memory/PlaceMemory/Instances)
We can see that the F9->Memory/CoreMemory/default which is not related to us keeps increasing by about 100 MB per 1 min and a half. - Projectiles , a simple Folder containing some testing projectile models.
- A simple Local Script that is used to connect the ‘Run’ and ‘Stop’ prompts to the SpawnController module.
Scripts
Spawn Controller
local RunService = game:GetService("RunService")
local shapeSpawnerModel = workspace.ShapeSpawner
local startPart = shapeSpawnerModel.Start --A part in workspace from where the projectiles will START.
local endPart = shapeSpawnerModel.End --A part in workspace where projectiles STOP.
local spawnFreq = 100 -- number of projectiles per second
local timer = 0 -- used to keep track of spawn timers
local projectilesModels = game.ReplicatedStorage.Projectiles:GetChildren() -- Folder with all possible Projectile models
local projectileTable = {} -- Table to store projectile references
local speedMults = {} -- Table to store projectile speeds
local running = false -- Indicates spawner state
function step(step)
--Spawning of new projectiles
timer += step
if timer > 1/spawnFreq then
--Picking a random Projectile Model to spawn
local proj = projectilesModels[math.random(1, #projectilesModels)]
--Picking a random color between (0,0,0), (0,0,1), (0,1,0), (0,1,1), (1,0,0), (1,0,1), (1,1,0), (1,1,1)
local color = Color3.new(math.random(0,1), math.random(0,1), math.random(0,1))
--Picking a random spawn position placed on the startPart (green rectangle)
local x = startPart.Size.Y/2 * math.random(-10,10)/10
local y = startPart.Size.Y/2 * math.random(-10,10)/10
local initCF = CFrame.new(startPart.Position + Vector3.new(0, 0, x) + Vector3.new(0, y, 0))
--Projectile Instance creation
local projModel = proj:Clone()
projModel:SetPrimaryPartCFrame(initCF)
projModel.PrimaryPart.Color = color
projModel.Parent = workspace
--Saving the projectile's ref within a table, and a random speed associated to it.
projectileTable[projModel] = projModel
speedMults[projModel] = math.random(10, 20)/10
-- Instead of resetting to 0, we start from this for accurate frequencies.
timer = timer - 1/spawnFreq
-- Printing the total memory used by the LUA scripts. Never passes 2 MB => No LUA leak.
print(collectgarbage("count"))
end
--Moving of projectiles
for k, projModel in pairs(projectileTable) do
--Check if projObj still exists, if not remove its reference and skip to next iteration.
if projModel == nil then projectileTable[projModel] = nil continue end
projModel:SetPrimaryPartCFrame(projModel:GetPrimaryPartCFrame() - Vector3.new(speedMults[k] * step*10,0,0))
--Testing if end reached
if projModel.PrimaryPart.Position.X < endPart.Position.X then
projectileTable[projModel]:Destroy()
projectileTable[projModel] = nil
end
end
end
--Basic function to start the spawning process
function run()
if running then return end
running = true
timer = 0
projectileTable = {}
speedMults = {}
RunService:BindToRenderStep("Step", Enum.RenderPriority.Camera.Value, step)
end
--Basic function to stop spawning + clean all projectiles
function stop()
if not running then return end
running = false
RunService:UnbindFromRenderStep("Step")
for k, projModel in pairs(projectileTable) do
projectileTable[projModel] = nil
speedMults[projModel] = nil
projModel:Destroy()
end
end
return {
run = run,
stop = stop
}
Local Script
main = require(game.ReplicatedStorage.Common.SpawnController)
workspace.Run.ProximityPrompt.Triggered:Connect(function()
main.run()
end)
workspace.Stop.ProximityPrompt.Triggered:Connect(function()
main.stop()
end)
Any help on this would be appreciated.
CC: @Redridge @Bug-Support