Do coroutines throttle loading, or imagetable?

I'm doing a basic look into how coroutines work, and have a question about performance.

As has been noted elsewhere, in general we want to optimize our Playdate code to not require any lengthy loading screens when possible... but in this case it's a simple application that needs all of its image data loaded first before it can be functional.

In this example, I'm using the :setImage() function to load sequential image table data manually in a for loop (instead of using imagetable.new() directly to load all at once), just so that I have access to the progress of the loading and can expose that in a progress bar of some sort.

The issue is performance. On device testing:

  • with a folder containing 720 images, about 8mb worth of 250x250 12kb images
  • Without using a coroutine, it takes the Playdate about 3 seconds or less to load the imagetable.new() into memory
  • Using the coroutine and :setImage() instead, it takes the Playdate 25 seconds to complete (equivalent performance on Simulator as well, if that's relevant information)

So the obvious question is, does :setImage(i, path) run notably slower than imagetable.new(), or is the coroutine/yield causing the slowdown?

Partial test code below. If anyone has any insight into this, let me know. Thank you!

function playdate.update()
	if not imagesLoaded then

		for i=1, 720 do
			local progress = i/720
			local path = gfx.image.new('assets/images/images-test/image' .. i)
			imageTable:setImage(i, path)
			
			gfx.drawArc(kDisplayWidth/2, kDisplayHeight/2, 105, 0, playdate.math.lerp(0, 360, progress))
	
			coroutine.yield()
		end
		imagesLoaded = true
	else
		gfx.sprite.update()
		-- ... rest of update code
	end
end

Going to bump this once, just in case some people missed it. Would really love some insight into this !

Thanks for bumping this, I missed it the first time around. When you call coroutine.yield() the system jumps back to the run loop and does the normal end-of-frame stuff, runs the GC a bit according to the minimum GC time setting, and pauses to maintain the frame rate. I wouldn't expect that either, so I'll file it as a bug. For now, you can avoid that by setting the refresh rate and the GC min to 0, e.g.:

		playdate.display.setRefreshRate(0)
		playdate.setMinimumGCTime(0)
		for i = 1,100 do
			print(i..":"..playdate.getCurrentTimeMilliseconds())
			coroutine.yield()
		end
		playdate.display.setRefreshRate(30)
		playdate.setMinimumGCTime(5)

Without setting the refresh and GC time this takes 3.3s, 100 frames at 30 fps. After setting those I get anywhere from 400 to 800 ms in the simulator--timers on the application run loop on macOS are pretty irregular--but on the device it's a pretty consistent 184 ms.

or if you want to hide that away you can do

local _yield = coroutine.yield
coroutine.yield = function()
	playdate.display.setRefreshRate(0)
	playdate.setMinimumGCTime(0)
	_yield()
	playdate.display.setRefreshRate(30)
	playdate.setMinimumGCTime(5)
end

Thank you @dave ! Very much appreciated