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.)

That test project still crashes—still at wildly unpredictable intervals—as of 2.7.2. (Built using 2.7.2 SDK, running on Simulator 2.7.2.)

First run: smooth sailing for about an hour, then crash.

Second run a few minutes later: ran fine for a couple minutes (I did not wait longer).

Third run: crashed instantly on launch.

Fourth run: ran for about a minute then crashed.

So—the same odd pattern as before: across multiple launches over a long time, it has “good times” when you can’t make it crash, and then “bad times” when it crashes a lot.

Tentative results with SDK 2.7.5:

  • Still crashes the Simulator every time—still after wildly varying durations (from almost instant to 20 minutes, across my 10 or so test runs). Past experience suggests sometimes it could be hours. (No need to force quit when it happens: I just hit Stop in Nova. But I did relaunch the Sim in the middle of those runs just to see if it helped. It didn’t.)

  • I have NOT seen any crashes on hardware yet, after many long hours of testing both this test app (built fresh on 2.7.5, two runs totaling 25 hours) and my game (existing pdx from an early 2.x SDK, two runs totaling 45 hours). Promising!

  • Of course, the crash might still happen on hardware: in the past, it’s sometimes been fine for hours and even days and then has started crashing like mad. So I’ll report back if hardware crashes again next time I’m at work on my game. But so far so good! (Crashing on the Simulator still makes development really hard though.)

(I wish I could recall whether I tested 2.7.2 on hardware. I did not test at all between then and 2.7.5.)

Here is the Simulator’s error output to Nova from a crash. (I know it’s actually memory-related, not an invalid source, because the source loads and plays multiple times before failing. And then I can watch the malloc map suddenly fill up, over maybe 10 seconds leading up to each crash. BTW the crash also occurs with the malloc window closed, or set to not auto-update, or manually cleared every few seconds.)

main.lua:24: bad argument #1 to 'addSource' (valid source type)
stack traceback:
	[C]: in method 'addSource'
	main.lua:24: in function 'nextSound'
	CoreLibs/timer.lua:300: in local 'callEndedCallback'
	CoreLibs/timer.lua:342: in field 'updateTimers'
	main.lua:13: in function <main.lua:12>

I’ve been wondering whether the issue could somehow be time or date connected—since the same app will run great for days and then start crashing within minutes every time. SOMETHING seems to persist across reboots, but what? Date and time were my only thought on that. However, my testing on device was going fine at the same time as I was crashing up a storm on the Simulator. Their clocks matched.

FWIW I’ve attached slightly updated reproduction code—better suited than the original to testing on actual hardware, since I added one line to prevent system sleep. This is the version I used for my 10 recent crashes in the 2.7.5 Simulator, and the recent good runs on hardware.

Sample Player Memory Test 1.2.zip (2.9 MB)

2.7.5 still NOT crashing on device, still crashing on Simulator, after enough testing to be pretty confident that’s a real pattern.