Several OpenTap.Python code paths create owning pythonnet PyObject wrappers and then drop them without deterministic disposal. In pythonnet, those wrappers keep Python references alive until Dispose() or later finalization, so repeated plan-load, discovery, annotation, and serialization activity can accumulate refcount retention, finalizer backlog, and observable memory growth. ([GitHub]6)
Affected locations in the current dev branch:
-
OpenTap.Python/PythonTypeDataWrapper.cs
CreateInstance() does mem.ToPython().AsManagedObject(innerType.Type) without disposing the temporary PyObject. ([GitHub]2)
-
OpenTap.Python/PythonPluginProvider.cs
Search() creates multiple wrappers with no Dispose(), including Py.Import("opentap"), Py.Import(moduleName), module.GetAttr("__dict__"), new PyDict(files), new PyType(_item), and pyType.GetAttr("__module__"). This is especially relevant if plugin discovery is re-run in-process. ([GitHub]3)
-
OpenTap.Python/Annotations/PyObjectAnnotator.cs and OpenTap.Python/Annotations/PythonEnumValueProperty.cs
The annotator leaks temporaries (Py.Import, GetAttr, etc.) and also stores enum-member PyObjects inside PythonEnumValueProperty. That retains Python references for at least the annotation lifetime. ([GitHub]4)
-
OpenTap.Python/PyObjectSerializer.cs
Serialize() and Deserialize() leak more than just Py.Import("pickle") / Py.Import("codecs"); they also allocate undisposed temporaries from ToPython() and InvokeMethod(...) chains. ([GitHub]5)
Suggested fix direction:
- Use
using var for every temporary PyObject / PyType / PyDict that does not intentionally escape the current scope.
- In
PythonTypeDataWrapper.CreateInstance(), wrap the temporary conversion object:
using var pyObj = mem.ToPython(); return pyObj.AsManagedObject(innerType.Type); ([GitHub]2)
- In
PythonPluginProvider.Search(), avoid storing disposable wrappers in HashSet<PyType> if a managed identity (Type, module/name tuple, or handle snapshot) is sufficient. ([GitHub]3)
- In
PyObjectAnnotator, either keep the retained enum values bounded and explicitly intentional, or replace per-annotation List<PyObject> creation with a safer cached/managed representation. ([GitHub]4)
- In
PyObjectSerializer, dispose all non-escaping intermediates; caching pickle / codecs alone is not enough. ([GitHub]5)
Expected behavior:
All temporary pythonnet wrappers should be deterministically released inside the same lexical scope in which they are created. Only deliberately long-lived Python references should escape, and those should be bounded by design rather than recreated on every search/annotation/serialization pass. ([GitHub]6)
Several
OpenTap.Pythoncode paths create owning pythonnetPyObjectwrappers and then drop them without deterministic disposal. In pythonnet, those wrappers keep Python references alive untilDispose()or later finalization, so repeated plan-load, discovery, annotation, and serialization activity can accumulate refcount retention, finalizer backlog, and observable memory growth. ([GitHub]6)Affected locations in the current
devbranch:OpenTap.Python/PythonTypeDataWrapper.csCreateInstance()doesmem.ToPython().AsManagedObject(innerType.Type)without disposing the temporaryPyObject. ([GitHub]2)OpenTap.Python/PythonPluginProvider.csSearch()creates multiple wrappers with noDispose(), includingPy.Import("opentap"),Py.Import(moduleName),module.GetAttr("__dict__"),new PyDict(files),new PyType(_item), andpyType.GetAttr("__module__"). This is especially relevant if plugin discovery is re-run in-process. ([GitHub]3)OpenTap.Python/Annotations/PyObjectAnnotator.csandOpenTap.Python/Annotations/PythonEnumValueProperty.csThe annotator leaks temporaries (
Py.Import,GetAttr, etc.) and also stores enum-memberPyObjects insidePythonEnumValueProperty. That retains Python references for at least the annotation lifetime. ([GitHub]4)OpenTap.Python/PyObjectSerializer.csSerialize()andDeserialize()leak more than justPy.Import("pickle")/Py.Import("codecs"); they also allocate undisposed temporaries fromToPython()andInvokeMethod(...)chains. ([GitHub]5)Suggested fix direction:
using varfor every temporaryPyObject/PyType/PyDictthat does not intentionally escape the current scope.PythonTypeDataWrapper.CreateInstance(), wrap the temporary conversion object:using var pyObj = mem.ToPython(); return pyObj.AsManagedObject(innerType.Type);([GitHub]2)PythonPluginProvider.Search(), avoid storing disposable wrappers inHashSet<PyType>if a managed identity (Type, module/name tuple, or handle snapshot) is sufficient. ([GitHub]3)PyObjectAnnotator, either keep the retained enum values bounded and explicitly intentional, or replace per-annotationList<PyObject>creation with a safer cached/managed representation. ([GitHub]4)PyObjectSerializer, dispose all non-escaping intermediates; cachingpickle/codecsalone is not enough. ([GitHub]5)Expected behavior:
All temporary pythonnet wrappers should be deterministically released inside the same lexical scope in which they are created. Only deliberately long-lived Python references should escape, and those should be bounded by design rather than recreated on every search/annotation/serialization pass. ([GitHub]6)