Engine Bug: Core Memory/default engine leak

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:

  1. Enter the place.
  2. Use the Run prompt like in the picture.
  3. Press F9 (on windows) and check Memory/CoreMemory/default if using RunService:BindToRenderStepped() or Memory/CoreMemory/luasignal if using RunService.RenderStepped:Connect().

tut1.PNG

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 ():

Screenshot from reproductory prototype

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

scripts scripts2

  1. 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.
  2. Projectiles , a simple Folder containing some testing projectile models.
  3. 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

9 Likes

I tested out your reproduction example, and noted that inside your script you forget to de-reference the SpeedMult dictionary for the speeds of the parts. This causes the memory leak of which you are referencing in your post. (The instances stay within memory forever.)

It references the Model/Instance inside the index here:
image

It is never de-referenced when the Model/Instance reaches the end, causing the Instance to remain in memory (default memory) forever:
image

The fix:


I also added types to the script below and changed math.random to use Random.new, switched to Pivots for moving the Models, as well as, doing some other minor fixes.
If you wish to improve your script further, I would suggest making speedMult and projectileTable one table that holds the both in a numbered dictionary (E.g: {{speedMult = 5, Model = projModel}}) and ipairs through it and remove with table.remove and insert with table.insert which would allow you to better improve readability and have less references.

Fixed & Improved Script
local RunService:RunService = game:GetService("RunService")

local shapeSpawnerModel:Model = workspace.ShapeSpawner
local startPart:Part = shapeSpawnerModel.Start --A part in workspace from where the projectiles will START.
local endPart:Part = shapeSpawnerModel.End --A part in workspace where projectiles STOP.

local spawnFreq:number = 100 -- number of projectiles per second
local timer:number = 0 -- used to keep track of spawn timers

local projectilesModels:{[number]: Model} = game.ReplicatedStorage.Projectiles:GetChildren() -- Folder with all possible Projectile models
local projectileTable:{[Model]: Model} = {} -- Table to store projectile references
local speedMults:{[Model]: number} = {} -- Table to store projectile speeds
local running:boolean = false -- Indicates spawner state
local renderSteppedListener = nil
local Rng:Random = Random.new()

local function step(step:number): ()
	--Spawning of new projectiles
	timer += step
	if timer > 1/spawnFreq then
		--Picking a random Projectile Model to spawn
		local proj:Model = projectilesModels[Rng:NextInteger(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 = Color3.new(Rng:NextInteger(0,1), Rng:NextInteger(0,1), Rng:NextInteger(0,1))
		--Picking a random spawn position placed on the startPart (green rectangle) 
		local x:number = startPart.Size.Y/2 * Rng:NextNumber(-1,1)
		local y:number = startPart.Size.Y/2 * Rng:NextNumber(-1,1)
		local initCF:CFrame = CFrame.new(startPart.Position + Vector3.new(0, 0, x) + Vector3.new(0, y, 0))
		--Projectile Instance creation
		local projModel:Model = proj:Clone()
		projModel.PrimaryPart.Color = color
		projModel:PivotTo(initCF)
		projModel.Parent = workspace
		
		--Saving the projectile's ref within a table, and a random speed associated to it.
		projectileTable[projModel] = projModel
		speedMults[projModel] = Rng:NextNumber(1, 2)
		
		-- Instead of resetting to 0, we start from this for accurate frequencies.
		timer -= 1/spawnFreq
	end
	
	--Moving of projectiles
	for _:Model, projModel:Model in pairs(projectileTable) do
		-- projModel would NOT be nil in this case.
		projModel:PivotTo(projModel:GetPivot() - Vector3.new(speedMults[projModel] * step*10,0,0))
		
		--Testing if end reached
		if projModel.PrimaryPart.Position.X < endPart.Position.X then
			projectileTable[projModel]:Destroy()
			projectileTable[projModel] = nil
			speedMults[projModel] = nil
		end
	end
end

--Basic function to start the spawning process
local function run(): ()
	if running then return end
	running = true
	timer = 0
	projectileTable = {}
	speedMults = {}
	renderSteppedListener = RunService.RenderStepped:Connect(step)
end

--Basic function to stop spawning + clean all projectiles
local function stop(): ()
	if not running then return end
	running = false
	if renderSteppedListener then
		renderSteppedListener:Disconnect()
		renderSteppedListener = nil
	end
	for _:Model, projModel:Model in pairs(projectileTable) do
		projectileTable[projModel] = nil
		speedMults[projModel] = nil
		projModel:Destroy()
	end
end

return {run = run,
	stop = stop}

After the fix: (Memory stabilizes and no longer continues upwards.)

Un-copylocked Place with scripts:

Also note that I’ve found apparently a different memory leak within the script?
LuaSignal:


I believe this to be your real culprit as it consumes more memory than your default memory leak. I’d assume this is from something else not being garbage collected correctly and would probably rewrite the script to find it.

4 Likes

Alternatively, attributes can be used to store projectile speeds on the models.

Having said that, it looks like there’s still some sort of signal leak here - this is unlikely to be a script bug since nothing in the script connects to anything in particular. We’ll investigate.

8 Likes

Can confirm this is an engine bug; it doesn’t have anything to do with signals though, the tracking is misleading here - we’ll fix that. We might be able to disable a flag tomorrow to fix the leak, if not this would need to wait till next week unfortunately. The leak is due to the use of Model’s primary CFrame.

update upon reading the code, unfortunately there’s no flag to disable. We’ll try to fix it asap but that unfortunately means “next week”.

10 Likes

Hello @Taratect_Queen , thank you for trying to look into this.

You are right about the speedMults never being dereferenced, however this is my mistake. I have edited the post many times to make it simpler for people to view, initially the Projectiles were objects, and the speedMult was an attribute so it was properly cleaned.

Nevertheless, the leak that is happening (about 2-3MB/s) is not from that. The video and the screenshots I took were from before the speedMult leak taking place in the first place, but apologies for that.

The reason you are not leaking within the CoreMemory/default is not because of your fix, it’s because of the fact that you are using RunService.RenderStepped:Connect().

I mentioned in the post that:

Therefore the leak in your case is happening in luasignal as you mentioned, and it is indeed the engine bug that I was talking about. To make sure, try “unfixing” what you did, and letting the speedMult references persist, you will see no difference in performance (This is because the table references are too small to matter, and they are never tracked within CoreMemory/default or CoreMemory/luasignal, they are tracked in LuaHeap instead)

So all in all, the changes you made to the script didn’t really change much, and what you mention here:

Are the actual leak I was talking about. It’s just that based on the RunService RenderStepped approach you chose, it will leak in different parts of CoreMemory. I hope this is clear enough, let me know if it’s understandable.

1 Like

Thanks for the reply @zeuxcg ,
You are right, I had looked into this more yesterday, and I built a caching mechanism to stop creating/destroying instances, and found out that wasn’t the problem. Also, I tried changing from RunService to while loops and that still caused leaks. Therefore I reduced it to a much more minimal state, and also figured it was from the CFrame being set (Keep in mind, this happens even if it’s a model, or a simple part and we use Part.CFrame, so it’s not related to the actual :SetPrimaryPartCFrame() method). Seems like some metadata that is kept within the data structures is never dereferenced.

I was just too tired and wanted to change my post next day :stuck_out_tongue: , I’m glad it got noticed in the meanwhile.

Also, I don’t know how helpful this can be, but I also tried changing from CFrame to simple Position changing, and the leak persists but on a smaller scale (probably related to the fact that Position contains 3 values while CFrames 12 , so the leak’s slope is 4 times smaller).

2 Likes

Just want to add to what @Daronion said. If you leak lua tables then the memory shows up in the LuaHeap section. If you leak Instances, it would show up in the Instances section.

Screenshots from post

In either case, those sections seemed fine so that’s why engine bug was assumed.

EDIT: This was meant as reply to @Taratect_Queen, mb.

The memory leak in question is specific to Model’s primary CFrame. If you can observe the leak without changing that at all, please share your modified repro.

2 Likes

Definitely.
It’s a slightly more complex repro where the Projectiles are a Class that has a Part, but apart from that everything is the same:

  1. BindToRenderStepped_Example_Parts_Only.rbxl (35.1 KB)
    → Using Part.CFrame movement only (6.2 MB leak over 90 seconds):

  2. BindToRenderStepped_Example_Parts_Only_POSITION.rbxl (35.1 KB)
    → Using Part.Position movement only (6.1 MB leak over 90 seconds)

1 Like

I believe it’s just the output widget - if I comment out your print(collectgarbage), the memory usage seems stable. I’m not sure if we limit the size of the output scrollback in any way, if not it would make sense that the more you print the more memory Studio needs.

3 Likes

Yes you are right, I had no idea that the output widget’s memory is shown in the CoreMemory/default.

Thanks for the info! :grin:

2 Likes

This will be fixed in next week’s update, sorry for the inconvenience.

Unfortunately there’s not a good workaround for model movement until then. The best you could do is welding together the model and moving it via CFrame changes, but that only works for models in a WorldRoot.

6 Likes