Skip to content

Alternative attempt at fixing memleak by being explicit about copies#2722

Closed
filmor wants to merge 3 commits into
masterfrom
alternative-memleak-fix
Closed

Alternative attempt at fixing memleak by being explicit about copies#2722
filmor wants to merge 3 commits into
masterfrom
alternative-memleak-fix

Conversation

@filmor
Copy link
Copy Markdown
Member

@filmor filmor commented May 11, 2026

No description provided.

MethodBinding and OverloadMapper held PyObject `target` references that
were not disposed during tp_clear, leaving Python-side refcount drops to
wait on the multi-hop .NET finalizer chain. They also shared the same
C# PyObject instance across mp_subscript/Overloads paths, so freeing one
could free the underlying Python object out from under the others.

- ExtensionType: add virtual OnClear() hook called from tp_clear before
  the GCHandle is released, letting subclasses eagerly drop owned
  Python references.
- MethodBinding/OverloadMapper: override OnClear to dispose `target`.
  (`targetType` is intentionally not disposed since Python types are
  long-lived and tracked by other caches.)
- Take an independent INCREF'd PyObject copy at every site that hands a
  shared target into a new MethodBinding or OverloadMapper, so each
  wrapper owns its own reference.

Result: the three _does_not_leak_memory tests drop from ~485 MB delta
to ~10 KB delta on Python 3.14.
The previous 90% threshold (0.9 MB/iter against a 1 MB allocation)
documented the issue but did not reproduce it: master leaks
~600-765 KB/iter, which the 0.9 MB threshold accepts as passing.

Drop the threshold to 10% (104 KB/iter). On the 2026-05-09 verification
run with Python 3.14 GIL on linux-aarch64:

  Without fix (master):   ~572-765 KB/iter (FAIL)
  With fix (this branch): ~-500 B/iter     (PASS)

Margin is roughly 6x in either direction across .NET 8 and .NET 10, so
the threshold cleanly separates buggy from fixed states without being
sensitive to GC noise.
Comment thread src/runtime/PythonTypes/PyObject.cs Outdated
Finalizer.Instance.ThrottledCollect();
}

public PyObject Copy() => new(this);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name of this function is too generic and therefore misleading

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is it misleading? This is exactly what it does (at least in Python terms). The current behaviour, being able to freely pass the object around, is the misleading thing IMO.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not make a copy of an object. It makes a separate living reference to it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.NewReference()?

@filmor filmor force-pushed the alternative-memleak-fix branch 2 times, most recently from 603a5d9 to ec0a494 Compare May 12, 2026 16:21
- Handle the `PyType` reference in `OverloadMapper` and `MethodBinding` in the
  same way as the object reference
- Unconditionally store the `PyType` of the object
- Introduce `NewReference` helper function for the object and type
  passing
- Fix the remaining missing reference count bump for the type
  (`MethodObject`)
- As the count is now correct, `Dispose` the type as well
@filmor filmor force-pushed the alternative-memleak-fix branch from e205ad0 to 11bebe4 Compare May 12, 2026 18:58
@filmor filmor closed this May 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants