Skip to content

LogStream memory leak issue (orphan Nodes) #27

@gergely-g

Description

@gergely-g

Hi, first of all, thanks for the addon, I use it a lot. :)
I’ve been working on a strategy game with heavy non-UI, data-only logic where I instantiate custom LogStreams from my classes like this:

class_name Pathfinding
extends RefCounted

var _log := LogStream.new("Pathfinding", LogStream.LogLevel.DEBUG)

This is very similar to what I'm used to from Java/Python which I appreciate a lot. :)

However I noticed that my GUT unit tests complain about orphan nodes:

==============================================
= 43 Orphans
==============================================
res://test/unit/test_pathfinding.gd
    - Outside Tests
        * ...
test_pathfinding.gd
    - test_path_on_plains_is_cheapest
        * [LogStream[Pathfinding](log-stream.gd)]
    - test_path_avoids_costly_terrain_like_forest
        * [LogStream[Pathfinding](log-stream.gd)]

GUT explanation for orphans link:

  • Any Node (or a subclass of Node) that is not currently in the tree is considered an orphan. Children of orphaned Nodes are also considered orphans.

Technical background

This is what I learned after looking into this (I'm still new to Godot).

Godot’s memory model (docs here):

  • Node and other Object-derived classes are not reference-counted.
  • They remain in memory until explicitly freed (free() / queue_free()).
  • Only RefCounted instances are freed automatically when the last reference is dropped.

Because the current version add-on encourages creating streams like this:

var network_log = LogStream.new("Networking")
network_log.warn("No internet")

the result is a permanently orphaned Node (not attached to the scene tree, never freed).

This means every such stream instance will stay in memory until the engine shuts down,
or the developer manually frees it -- which is not mentioned in the documentation.

This can be especially bad if it's on an object that has many instances, like an enemy unit in the game for example.

My suggestion: consider switching LogStream to extend RefCounted

From what I understand, changing LogStream to extend RefCounted instead of Node would eliminate the orphan problem.

This would make each stream self-managed and automatically freed once there are no references.

Draft steps

  1. Remove _ready() logic
    RefCounted objects don’t receive _ready().
    The current _ready() in log-stream.gd connects a tree_exiting cleanup signal,
    but this is already handled by plugin.gd when the plugin is enabled:

    # plugin.gd
    tree_exiting.connect(_LogInternalPrinter._cleanup)

    So this code can safely be removed or moved to the plugin initialization.

  2. Verify signal/callback behavior
    All Node signal connections (e.g. tree_exiting) that assume a scene-tree context
    should be removed or replaced with manual disconnection when appropriate.

Risks?

I might be missing some lifecycle stuff and RefCounted can still lead to memory leaks if
there are circular references.

So we should be careful.

If LogStream keeps deriving from Node

If you prefer to keep LogStream as a Node, the docs should at least recommend a cleanup pattern. Something like this maybe (?):

var log = LogStream.new("MyLogger")
get_tree().root.add_child(log)
# or explicitly free when done
log.queue_free()

and note that otherwise, it will stay in memory.

I'd be happy to help with the implementation, please let me know what you think.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions