Garbage collector not behaving as expected?

I've been getting to grips with the SDK for a couple of weeks and had some unexpected behaviour with the garbage collector today. I've been getting some pretty significant performance drops on device due to the garbage collector running, which obviously isn't unusual on the face of it. However, I've had trouble getting the garbage collector to respect the settings I'm providing through the API methods.

playdate.setMinimumGCTime(ms) seems to work as expected; I can set it to an arbitrarily high number and see the effect in the sampler.

However playdate.setGCScaling(min, max) doesn't quite work as I'd expect. My understanding was that playdate.setGCScaling(1, 1) and playdate.setMinimumGCTime(0) would effectively disable the garbage collector since it wouldn't actually run unless the device was literally about to OOM. But I still see a GC step line logged in the sampler in a nasty spike.

Even setting playdate.setCollectsGarbage(false) doesn't seem to make a difference, and the garbage collector still seems to run when it feels like.

Am I missing something here? Does GC mean something different to GC step in this context, and is there anything I can do to get more control over the situation?

EDIT: wrote this quite late, just to clarify some things in the light of day. My motivation for understanding this is for a game that is much more CPU intensive than it is memory intensive, so getting some control over the garbage collector and reclaiming those CPU cycles would be nice. I understand the general concepts behind garbage collection (i.e. two or more separate processes for 'identifying garbage' and 'collecting garbage', e.g. mark and sweep), and my initial thought was the GC step would be the 'mark' equivalent and GC was the 'sweep' equivalent. However, it seems odd that:

  1. GC step still runs even if GC won't, and:
  2. GC step takes such a long time and freezes the whole game when collectgarbage("count") indicates only a few MB of memory usage.
3 Likes

I've similarly noticed playdate.setCollectsGarbage(false) doesn't seem to actually stop the automatic GC. My game was in a similar boat as your (heavy CPU but barely any RAM usage) & I was hoping to manually GC between levels. No dice.

Wish I had more information for you, but just wanted to +1 this post

EDIT: this topic should probably be recategorized as SDK Bug report

2 Likes

Category changed to bug report

1 Like

Hello, is there a way of squashing this bug? Or does anyone know why it happens? (Or, even better, does anyone managed to somehow work around the bug?) It feels like turning off GC wakes up some elder memory guardian gods.

I have a few more observations (with current firmware/SDK, 100% Lua code):

After I turn GC off with playdate.setCollectsGarbage(false) (my goal is to collect garbage only in-between levels), I still see spikes in CPU use in parallel with visibly freed memory. Strangely, there’s no reported GC activity in the Device Info chart. The peaks look like this:

An interesting thing is that with the GC off, the System category of tasks now runs at 1-2% instead of 0.5% with GC on (it makes it actually visible on the chart without zooming into a screenshot). Here’s the difference on chart (the right half of the chart is the same code, just without disabled GC).

(GC on the right chart has minimumGCTime set to zero but it still keeps eating about 7–9% of cpu.)

Just one more detail — the frequency of spikes is dependent on the refreshRate that is set. With 50fps it’s about 40 seconds, and with 30fps it’s around 65 seconds (I used a stopwatch).

The spike takes down cca 4–5 frames in my case, but that will be probably dependent on the specific memory use.

I was seeing this behavior as well, and came across a workaround. Calling collectgarbage('stop') instead of playdate.setCollectsGarbage(false) gives the expected behavior on simulator and device in 2.1.0. Hopefully this helps someone in the future. My simple example:

import 'CoreLibs/graphics'

local gfx <const> = playdate.graphics

playdate.display.setRefreshRate(50)

-- Doesn't work as expected, still auto GCs multiple times before max
-- playdate.setCollectsGarbage(false)

-- Works as expected, no auto GC until RAM maxed out
collectgarbage('stop')

function playdate.update()
	for	i = 1, 5 do
		gfx.image.new(128, 128)
	end

	playdate.drawFPS(382, 225)
end
6 Likes