Reproducible sound.samplePlayer intermittent memory leak

I have a simple Lua test project attached to demonstrate this issue I’ve been chasing for months: samplePlayers sometimes won’t release their memory, leading to a crash. This can be seen both in the Simulator (I’m on Mac) and on-device.

This is in SDK 2.6.2, but is not new: I believe it has been happening for over a year.

This leak is intermittent: the same code can run for HOURS with no leak. Then all of a sudden it will start to leak. When it does start, oddly, rebooting the Simulator or Playdate doesn’t reliably seem to help. It usually just keeps leaking—and may keep leaking for hours across multiple reboots. And then, like magic, the leak stops again. (Which can make it feel like recent code changes are somehow causing the leak to happen or not—but I finally know that’s not the case.)

Therefore, you may need to run this over a long period of time to see the leak begin (or to see it not happen as the case may be). Rebooting the app during testing is fine: it does not need to be run in one long session. The leak can begin in the middle of a previously-clean run, or immediately on launch. This app can run fine for hours—sometimes. Seems to be just luck. (Autorefreshing the malloc map view will make it very visible.) It will lead to a crash within seconds when it happens.

I’ve seen this with both PCM and ADPCM WAV files.

The attached project just reloads the same 1.7 MB sound and plays a bit of it, 5 times a second, always re-using the same global samplePlayer variable. (The same issue happens with less frequent playback and smaller files—this version just makes the issue more dramatic.)

This code makes my best attempt to clean up memory: before each new instance, the old one is stopped, assigned to nil, and then I even force a collectgarbage(). (I’m not sure any of that cleanup is actually needed: it doesn’t seem to help. But the attempt is made.)

Here’s the core of what the app does, over and over via 200 ms timer:

soundPlayer:stop()
soundPlayer = nil
collectgarbage()

soundPlayer = snd.sampleplayer.new("TestSound")
soundChannel:addSource(soundPlayer)
soundPlayer:play()

Sample Player Memory Test.zip (5.7 MB)

I’m not 100% sure my code is following best memory management practices—maybe a leak is expected? But what seems wrong regardless is that it either leaks or not, arbitrarily.

2 Likes

My guess is it's memory fragmentation. If you allocate a big chunk, free it, and some tiny allocation winds up in the middle of that space before you allocate the big chunk again, it can't use that space any more, it has to move farther down the heap. We added the sample:new() and sample:load() to help with this--instead of freeing and reallocating, you create fixed buffers for the large samples you'll need and load them in:

1 Like

Thanks—I know what you mean, but that’s not what I’m seeing in the malloc map: when this problem happens, big chunks are not left unused: the memory just fills up solid without gaps.

(And the problem happens with much smaller sounds too—although this test uses a bigger file to make the crash happen sooner: seconds instead of minutes. It’s easy to see when memory starts to fill up with small sounds, though—even without waiting for the full crash. Just not AS easy as seeing the effect of a big sound.)

Here’s a malloc map screen recording of that test app: at first things are humming along for a minute and a half, then it suddenly fills up RAM and crashes. It’s not that the freed-up chunks aren’t big enough to re-use; space stops getting freed up at all.

(Right now, this app tends to crash within 1 to 15 minutes of each launch. But there are days when the exact same app crashes right after launch, every single time. And then, for hours/days of multiple launches, the same app will work flawlessly without leaking. Same is true of my actual game.)
Sample Player Memory Test Recording.mov.zip (12.7 MB)

yep, you're right, that's not fragmentation. :thinking: I wasn't able to reproduce it but maybe I didn't leave it running long enough. I'll give it another go.

1 Like

It’s amazing how long I have to let it run sometimes to see the problem... but one thing that makes testing easier: it doesn’t seem to be extended run time (per se) that shows the issue, just the passage OF time.

So running this test app for a couple minutes every so often—even fully quitting/relaunching the Simulator in between—will still show the problem. Eventually! And then when it does, the problem will persist a long time (but not forever) even across full Simulator relaunches. (Or device reboots as the case may be.)