I’ve hunted for memory leaks before and can share a few things I am aware of:
- Most importantly, I once ran into a scenario where the memory would keep going up for a long time, and then just stop (it was at ~4 or 5GB, but I can’t remember). As I already had systems in place tracking every other possible source of memory leak I could think of - and they’d found plenty before that point but weren’t finding anymore - I interpret this to mean that Roblox doesn’t clean up all the memory immediately, but could clean it up if it became a problem. If your device is slowing down or crashing, however, you still have a memory leak (though see below for why your test might crash your device).
-
Destroy
is useful for garbage collection even if there is no parent because it disconnects all event listeners (which can be the source of memory leaks in some cases). This is especially important if you listen to the Blocked
event of a path as Roblox does a bunch of calculations in response to you connecting to that event (and it keeps doing those calculations so that it can detect when the path becomes Blocked). Note: If you have tons of paths, Roblox won’t lag your place, as it spreads out these calculations over time. Instead, it just might take literally seconds or minutes for the Blocked event to fire or for paths to be calculated.
- In my experience, some memory leaks can be tricky to find/track down (though I’m not sure to what degree this comes from the first point).
In your case, you probably haven’t found a memory leak. Here’s a one-script test I just ran (based off of yours):
local RunService = game:GetService('RunService')
local PathfindingService = game:GetService('PathfindingService')
local t = setmetatable({}, {__mode = "k"})
local function count()
local n = 0
for v in pairs(t) do n += 1 end
return n
end
local run = Instance.new("BoolValue")
run.Name = "Run"
run.Value = true
run.Parent = workspace
run.Changed:Connect(function()
print("Run:", run.Value and "On" or "Off")
end)
local created = 0
local computed = 0
local con = RunService.Stepped:Connect(function()
if not run.Value then return end
for i = 1, 5000 do
coroutine.wrap(function()
local path = PathfindingService:CreatePath()
t[path] = true
created += 1
path:ComputeAsync(Vector3.new(math.random(-100, 100), 0, math.random(-100, 100)), Vector3.new(math.random(-100, 100), 0, math.random(-100, 100)))
computed += 1
path:Destroy()
end)()
end
end)
while true do
print(created, created - computed, count())
wait(1)
end
The output for me looks like this:
0 0 0
5000 5000 5000
105000 78916 89260
235000 166617 185074
335000 236131 250916
435000 305997 329703
540000 383902 407627
650000 470091 517627
735000 535565 571018
850000 621407 655190
955000 697986 760190
1065000 778998 870190
1140000 835223 897965
1250000 919883 1007965
1345000 991109 1102965
1405000 1034381 1099801
1525000 1121367 1219801
Run: Off
1620000 1183593 1314801
1620000 1102203 1314801
1620000 1033202 1314801
1620000 960656 1314801
1620000 888509 1314801
1620000 814336 1314801
1620000 746703 1314801
1620000 671170 1314801
1620000 628796 1314801
1620000 589040 1314801
1620000 549304 1314801
1620000 509370 1314801
1620000 469640 1314801
1620000 430352 1314801
1620000 391075 1314801
1620000 352015 1314801
1620000 312745 1314801
1620000 272923 1314801
1620000 234011 1314801
1620000 193504 1314801
1620000 153198 1314801
1620000 113506 1314801
1620000 73886 1314801
1620000 34146 1314801
▶ 1620000 0 1314801 (x5)
Run: On
1635000 13185 1329801
1740000 96910 773962
1755000 107914 788962
Run: Off
Script timeout: exhausted allowed execution time
Stack Begin
Script 'Workspace.Script', Line 25
Stack End
1794984 138744 143875
1794984 111129 143875
1794984 90543 143875
1794984 51869 143875
▶ 1794984 0 143875 (x16)
Run: On
1824984 26703 35828
1829984 29706 32639
1919984 99055 104052
2024984 181455 192928
2144984 274957 299175
2244984 353156 382705
Run: Off
2269984 335416 380725
2269984 268388 380725
2269984 195548 380725
2269984 139757 380725
2269984 106598 380725
2269984 75787 380725
2269984 45059 380725
2269984 14946 380725
▶ 2269984 0 380725 (x22)
The table ‘t’ keeps track of how many paths are still in memory, but it’s a weak table so it doesn’t prevent them from being garbage collected. In the output we have:
- How many paths have been created
- How many paths have not finished computing
- How many path objects are still in memory
And also in the output:
- When I turned the Run BoolValue on/off
- A random script timeout error (I don’t know why it errored but we do see that
created
is not a multiple of 1000 after it occurred, so that’s interesting)
Now, for a moment I thought there was a memory leak (we end up with a bunch of paths in ‘t’ at the end), but just like I reported in my first point, sometimes Roblox simply doesn’t “feel like” collecting memory until you’re over some limit (though I haven’t experienced it leaving a bunch of them behind in weak tables before). Roblox was nearing 8GB of memory usage on my computer and I was just about to turn off the “Run” BoolValue again when all of a sudden it agreed that 8GB was too much and started collecting all this memory - the memory usage went down to ~3-4GB within seconds! You can see when this happened because the “count” returns 32639 at one point.
The reason that your test is likely to crash your device is that Roblox spreads out pathfinding over time so as not to lag your place, no matter how many pathfinding requests you give it. If you’re constantly generating new paths, Roblox won’t process them faster than you’re creating them, so you will end up with unlimited memory usage. You can see this in the output above, as well: after “Run: Off” displays, it takes some seconds before the remaining paths are processed. Naturally Roblox has to queue the paths (and threads) that haven’t gotten their result yet, which requires memory.