Excessive CPU use and memory leak with Tilemaps


#1

Dear OpenFL Developers,

When starting to use Tilemaps, I noticed that whenever a Tilemap is attached to the DisplayList, it consumes a large amount of CPU time and also appears to continuously leak memory. I’ve tried to isolate the issue into a minimal test app (derived from the OpenFL Bunnymark example). The test application has two textfield “buttons”. Clicking these will attach or detach two Sprites, both of which have (unless otherwise specified) 1000 DisplayObject children. On the first Sprite, the children are all empty Tilemaps. On the second Sprite, the children are all empty Bitmaps (the second Sprite is used to check that the issue is not related to all DisplayObjects, only Tilemaps).

I have run the test program on Windows and Linux. On Linux, using Neko, when the program starts (‘openfl test neko’), memory usage is stable at 38 Mb and CPU utilization around 0-1 %. If the Bitmaps are attached, there is no detectable difference in CPU or memory usage. However, if the Tilemaps are attached CPU usage grows to 25% and memory usage starts to increase by around 50 Mb per second. Detaching the Tilemaps drops CPU usage back to 0-1% and memory usage does not increase any more, but the memory is not released either. With the Tilemaps attached, the memory consumption keeps increasing steadily, I let it continue to around 6 Gb, which it reached in a couple of minutes.

With HTML5 on Linux (‘openfl test html5’) the empty Tilemaps increase (the browser’s) CPU usage to 15% and memory usage increases at about 10 Mb/sec. With CPP the effect is a lot smaller, tested with 10000 empty objects (‘openfl test cpp -args 10000’) the empty Tilemaps cause CPU to go from 0% to 6% with memory usage increasing at 10 Kb/sec.

The issue appears similar on Windows, on Neko the empty Tilemaps push CPU usage to 18% and memory usage increases at a rate of aroung 5 Mb per second. Using HTML5 on Windows the empty Tilemaps increase CPU usage to 7% with memory usage increasing at 5 Mb per second.

The test program is very lightweight, so I have used at least 1000 empty Tilemaps to highlight the issue. In any case, using a corresponding amount of other empty DisplayObjects (Bitmaps in this case) does not increase CPU or memory usage, so whatever is happening, it appears to be related to Tilemaps. I assume that there shouldn’t be any additional CPU overhead for empty Tilemaps as opposed to other DisplayObjects (and in any case the memory usage should stay constant).

Both of the computers I tested on have an Intel Core i3-3220 CPU and an MSI GeForce GT 610 GPU.

My Linux machine has the following setup:

OS: Ubuntu 16.04 LTS (64-bit)
Haxe Compiler 3.4.7
NekoVM 2.2.0
openfl: 8.0.2
lime: 6.3.1

My Windows machine has the following setup:

OS: Windows 7 Professional SP1 (32-bit)
Haxe Compiler 3.4.7
NekoVM 2.1.0
openfl: 8.0.2
lime: 6.3.1

The test program (replaces Main.hx in the Bunnymark sample):

package;

import openfl.display.FPS;
import openfl.display.Sprite;
import openfl.display.Bitmap;
import openfl.display.Tilemap;
import openfl.events.MouseEvent;
import openfl.text.TextField;

class Main extends Sprite 
{
	private var fps:FPS;
	private var tilemapContainer:Sprite;
	private var bitmapContainer:Sprite;

	private var fieldTilemaps:TextField;
	private var fieldBitmaps:TextField;
	
	public function new () 
	{
		super ();

		// Define the number of objects
		var numObjects:Int = 1000;
		var args = Sys.args();
		if (args.length > 0 && Std.parseInt(args[0]) != null)
			numObjects = Std.parseInt(args[0]);
		trace("Using " + numObjects + " objects.");
		
		// Initialize the empty tilemaps
		tilemapContainer = new Sprite();
		for (i in 0...numObjects)
		{
			var tilemap = new Tilemap (stage.stageWidth, stage.stageHeight, null);
			tilemapContainer.addChild (tilemap);
		}

		// Initialize the empty bitmaps
		bitmapContainer = new Sprite();
		for (i in 0...numObjects)
		{
			var bitmap = new Bitmap();
			bitmapContainer.addChild (bitmap);
		}

		// Add a textfield "button" for attaching/detaching the tilemaps
		fieldTilemaps = new TextField();
		fieldTilemaps.selectable = false;
		fieldTilemaps.x = 100; fieldTilemaps.y = 200;
		fieldTilemaps.background = true; fieldTilemaps.backgroundColor = 0xC0C0C0;
		addChild(fieldTilemaps);
		fieldTilemaps.addEventListener (MouseEvent.MOUSE_UP, toggleTiles);
		
		// Add a textfield "button" for attaching/detaching the bitmaps
		fieldBitmaps = new TextField();
		fieldBitmaps.selectable = false;
		fieldBitmaps.x = 400; fieldBitmaps.y = 200;
		fieldBitmaps.background = true; fieldBitmaps.backgroundColor = 0xC0C0C0;
		addChild(fieldBitmaps);
		fieldBitmaps.addEventListener (MouseEvent.MOUSE_UP, toggleBitmaps);	

		// Update the initial state
		updateTexts();

		// Add the FPS counter
		fps = new FPS ();
		addChild (fps);
	}

	private function updateTexts()
	{
		fieldTilemaps.text = tilemapContainer.parent != null ? "Tilemaps\nattached" : "Tilemaps\ndetached";
		fieldBitmaps.text = bitmapContainer.parent != null ? "Bitmaps\nattached" : "Bitmaps\ndetached";
	}
	
	// Event Handlers
	
	private function toggleTiles(event:MouseEvent):Void 
	{
		if (tilemapContainer.parent != null)
			removeChild(tilemapContainer);
		else
			addChildAt(tilemapContainer, 0);
		updateTexts();
	}

	private function toggleBitmaps(event:MouseEvent):Void 
	{
		if (bitmapContainer.parent != null)
			removeChild(bitmapContainer);
		else
			addChildAt(bitmapContainer, 0);
		updateTexts();
	}
}

#2

When you add or remove a Tilemap, you are toggling whether it is renderable. When it no longer becomes renderable, we allow for certain internal objects to be garbage collected. When (or if) they are garbage collected is up to the implementation. HXCPP has a threshold where it will not a run a garbage collection at all until memory usage reaches a certain size. You can force garbage collection to get a better view of the actual memory used, but this behavior (wrongfully) makes it look like a leak is a occurring when it is not. When you make a Tilemap renderable it needs to allocate the GL buffers and objects to render a batch draw again. Rendering Bitmap does not require these objects because Bitmaps have a static buffer that is retained for the lifetime of the BitmapData.


#3

Thank you for the information! I’m just starting to understand how OpenFL works with OpenGL, and my experience with OpenGL is many years out of date, so apologies if I’m misunderstanding some things here. In any case, the increase in memory usage does not appear to come from adding and removing the Tilemap (at least not primarily).

I ran again my above test program (‘openfl test neko’), and clicked only once to add the Sprite which has the 1000 empty Tilemaps as children (after this point, I no longer interact with the program, and in the program itself, there is nothing happening in my code). After adding the Sprite with the Tilemap children, processor usage immediately climbs to around 25% and memory usage keeps growing consistently. In around 15 minutes the memory usage grows to 7,1 Gb (my machine has a total of 8 Gb) at which point the program crashes with the message:

Too many heap sections: Increase MAXHINCR or MAX_HEAP_SECTS
Aborted (core dumped)

For some reason, those 1000 Tilemaps, even when they are completely empty and have no Tiles in them, consume 25% of the entire CPU when rendering at 60 FPS, and also continuously consume more and more memory until the program crashes. No-one would probably need 1000 Tilemaps in an actual application, but the large amount I’m using here highlights a behaviour that I assume shouldn’t be happening at all?

As another test, I created a completely unmodified OpenFL Bunnymark (‘openfl create BunnyMark’) and ran it (‘openfl test neko’). I never interacted with the program, just let the initial 100 bunnies jump on screen, but in 30 minutes the memory consumption had increased from 38 Mb to 80 Mb. I’m not sure what can be causing this, since no Tiles are added or removed, shouldn’t the corresponding OpenGL structures and the memory usage stay constant?


#4

It should, it sounds something funny is going on here. I’ll look into this and see what I can find