Skip to content

Python module not finalized or too late / Memory leak #715

Description

@Enyium
  • OS: Windows 11 Home 25H2
  • Talon version: 0.4.0 public

First: Please guarantee that an old Python module object is always quickly garbage-collected. I.e., if you're not already doing this, please call gc.collect() in the context of a script reload. This can, e.g., be important to abort an old thread that the new Python module restarts. See also the todo at the start of my windows_toolkit_talon's reload_resilience.py (fixed commit). (You can read it with wrapped lines by using GitHub's "Raw" button.)


Please consider the two todos at the start of windows_toolkit_talon's /pymod_termination/index.py (fixed commit) that are directed at you, the Talon author.


The "private notes" of the first todo refer to these reproduction steps:

  • Create bug.talon:

    debug set event: user.dbg_set_event()

    If needed, this allows you to differentiate old module objects.

  • Create bug.py:

    Click to view.
    import sys
    import threading
    import weakref
    
    import talon
    
    _mod = talon.Module()
    _event = threading.Event()
    
    @_mod.action
    def dbg_set_event():
        """..."""
        _event.set()
    
    _pymod_name = __name__
    weakref.finalize(
        sys.modules[_pymod_name],
        lambda name=_pymod_name: print(f"Python module `{name}` finalized. Event set: {_event.is_set()}"),
    )
    
    _pymod_dict_representative = frozenset()  # Something weakly referenceable.
    weakref.finalize(
        _pymod_dict_representative,
        lambda name=_pymod_name: print(f"`__dict__` of module `{name}` finalized."),
    )

    Note that module finalizers are officially supported as per the bottom of this page.

  • On the first execution, nothing is printed. — Correct, because there was no old module to be finalized.

  • On the second execution (by changing the file), something is printed. — Correct, because the module from the first execution was finalized.

  • Do any number of reloads with the use or omission of the voice command that calls event.set() to get a feeling of what's to be expected.

  • Bug: On any bug.py-related file-remove action ([-] in log)—also by moving or renaming bug.py—the module isn't finalized. Only when a file-add action ([+] in log) for the same file path occurs (even with a zero-length file) does the tardy finalizer run. (When moving the file into and out of a subdirectory, this leads to the finalizers lagging one file-move step behind.)


The memory leak the second todo talks about has to do with the fact that a Python module object's __dict__ is also held by module globals like, e.g., functions and indirectly methods, which reference it in their respective __globals__ attributes. This creates a reference cycle that keeps the module __dict__ (not the module object) of every module version along the track of reloads alive. The above bug.py should demonstrate that `__dict__` of module `….bug` finalized. is never printed, even when running gc.collect().

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions