How to debug a memory leak (caused by dynamically resizing font in RichTextLabel)

Godot Version

4.2.2

Question

TL;DR

How can I see where is the memory going while running a game? I see that the Memory usage keeps growing indefinitely, but how can I know where is it going? Is it Resources? Objects? Nodes? I don’t see these 3 going up, just the Static Memory Usage…

Full Context

I was developing a small game/app for Android, and suddenly one day I noticed that after using it for a while it closed unexpectedly with no logs whatsoever. I replicated the issue after again using it for a while (there are no certain steps to reproduce it happens after a while after executing lots of actions) and I was with the debugger connected to the Godot Editor to my pc and using adb logcat to catch any errors but nothing.

I went back to my computer and debugged it to see what might be causing this issue, and I’ve noticed that the Memory/Static usage jumps from the main menu having around 100MB usage to 500MB and continues going up to 700~1000MB as long as i keep advancing.

The thing that puzzles me is that going back to the main menu doesn’t clear the memory usage, it remains on whatever last value was on the scene that was after the main menu, it never goes back to 100MB (it also never goes down, only up)

How can I know where is the memory leak going?

EDIT: Just in case it helps somehow, my app/game is just 2 scenes

  • A main menu
  • A Scene where everything happens

This second scene ramps up using a lot of RAM the moment I play some WAV files using AudioStreamPlayer. I obtained these WAV files from the internet and processed them with Audacity, then exported them to WAV again with that program. I don’t know if this is related but on rare occassions, I’ve noticed that opening the folder with the WAV files in Windows makes the Windows explorer a bit sluggish.

EDIT2: In case it helps, in the scene where everything happens, there are a lot of tweens. I create them using “create_tween”, each one interpolates around 30 things, when i run these tweens is when the huge memory spikes appear

2 Likes

It’s impossible to know what leaked memory unless you know your code. Typically start with systems that you know can create memory requests, checking to make sure they are cleaning up.

Typically you can try and log creation and deleting behavior and see if there would be an expected change in memory.

Also peak at the remote scene tree to see if nodes are being added but never removed.

Anything that inherits Object (all node classes) need to be freed manually. Typically with queue_free function. Only using remove_child will leak memory.

Anything that is refcounted/Resource class (tween) will delete itself once all references are gone. Although, this isn’t to say that the ref count could have a bug and miss count. Or you hold on to a reference in an array by accident.

And very large arrays and dictionarys that can grow indefinitely. But these should clean up if they fall out of scope. Unless it’s in an auto load which will be held for the entire process lifetime.

1 Like

This is what leaked memory is, you have allocated the memory, but lost the reference. Like owning a car, but you forgot where it was parked. Memory will not be reclaimed until the process has exited.

This is also a hint that it is happening in your game scene

1 Like

At the rate your memory usage is growing, my guess is that you are creating either new versions of the main menu and/or game scene when you switch to them, and aren’t freeing up the old versions.

Try this (having a second monitor may help):

  1. Run your game in the editor.
  2. Go back to the editor while the game is running.
  3. In the Scene tab, you will see two new tabs above the node list: Remote and Local.
  4. Click the Remote tab.
  5. Play your game.
  6. Watch how many objects get created, and which get duplicated in the Remote tab.

I’ve found the issue, there seems to be a memory leak when resizing the text within RichTextLabels dynamically. In my project I have some big numbers being drawn with RichTextLabels, and when scoring, I have set up some tweens that temporarily increase the size of those RichTextLabels.

These RichTextLabels have a very big font size (> 200).

I’ve made a demo project.
Perhaps there’s a better way to do this that I’m missing?
Or maybe is it a bug?

EDIT: This demo project was made with Godot 4.3 because I wanted to test if it was fixed on the latest version, but it happens on both 4.2.2 and 4.3

EDIT2: Should I rename the topic now that I’ve found the issue…?

It is a not-a-bug quirk.
This very thing was discussed recently.
And the bug report

1 Like

@sancho2 Thanks for the useful info, I did fix it taking a look at the info provided in both links and looking deeply into TextServer and FontFile documentation.

My solution is as follows

  1. Create a Singleton with the following code
extends Node

# This will hold references to all CanvasItems added to the game
# I do this to keep a reference to every node that potentially might
# be using the font affected by the memory leak
# (there might be a more optimal solution)
var canvases : Dictionary = {}

# Connect to tree signals so we know when a node is added to the tree
func _ready():
	get_tree().node_added.connect(_on_node_added)
	get_tree().node_removed.connect(_on_node_removed)

# Public function used at the end of the tweens that modify the
# size of the font in the RichTextLabels that i use
func font_size_cache_clear(font : FontFile):
	for i : int in range(font.get_cache_count()):
		for size_cache : Vector2i in font.get_size_cache_list(i):
			font.remove_size_cache(i, size_cache)
	_canvas_redraw()

# This will force a redraw of all CanvasItems because if you clear all the size
# cache, blank squares are drawn instead of characters
func _canvas_redraw():
	for canvas : CanvasItem in canvases.values():
		canvas.queue_redraw()

# When a CanvasItem is added to the tree, add it to our cache
func _on_node_added(node : Node):
	var canvas : CanvasItem = node as CanvasItem
	if not canvas:
		return
	canvases[canvas.get_instance_id()] = canvas

# When a CanvasItem is removed from the tree, remove it from our cache
func _on_node_removed(node : Node):
	var canvas : CanvasItem = node as CanvasItem
	if not canvas:
		return
	canvases.erase(canvas.get_instance_id())
  1. On the tweens that I use to interpolate the font size values, at the end simply add
    tween.tween_callback(FontSizeCacheCleaner.font_size_cache_clear.bind(my_font))

EDIT: I improved my above solution by running automatically in the background instead of having to call it manually

extends Node

const MAX_FONT_SIZE_CACHE_SIZE : int = 50
const SECONDS_TO_CHECK_CACHE : float = 1.0
const FONTS_TO_CHECK : Array[FontFile] = [...font preloads...]

var canvases : Dictionary = {}

func _ready():
	get_tree().node_added.connect(_on_node_added)
	get_tree().node_removed.connect(_on_node_removed)
	get_tree().create_timer(SECONDS_TO_CHECK_CACHE).timeout.connect(_check_excessive_usage)

func _check_excessive_usage():
	var need_redraw : bool = false
	for font : FontFile in FONTS_TO_CHECK:
		for i : int in range(font.get_cache_count()):
			var size_cache_list : Array[Vector2i] = font.get_size_cache_list(i)
			if size_cache_list.size() > MAX_FONT_SIZE_CACHE_SIZE:
				Log.debug("Excessive usage for font " + str(font.font_name) + ", cleaning up")
				for size_cache : Vector2i in size_cache_list:
					font.remove_size_cache(i, size_cache)
				need_redraw = true
	
	if need_redraw:
		_canvas_redraw()
	
	get_tree().create_timer(SECONDS_TO_CHECK_CACHE).timeout.connect(_check_excessive_usage)

func _canvas_redraw():
	for canvas : CanvasItem in canvases.values():
		canvas.queue_redraw()

func _on_node_added(node : Node):
	var canvas : CanvasItem = node as CanvasItem
	if not canvas:
		return
	canvases[canvas.get_instance_id()] = canvas

func _on_node_removed(node : Node):
	var canvas : CanvasItem = node as CanvasItem
	if not canvas:
		return
	canvases.erase(canvas.get_instance_id())

2 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.