| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 25e0ccf commit ca323cc
7 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -93,6 +93,12 @@ internal PyObject(in StolenReference reference) | |||
| 93 | 93 | Finalizer.Instance.ThrottledCollect(); | |
| 94 | 94 | } | |
| 95 | 95 | ||
| 96 | + /// <summary> | ||
| 97 | + /// Create a new PyObject instance of this object, bumping the reference | ||
| 98 | + /// count. | ||
| 99 | + /// </summary> | ||
| 100 | + public PyObject NewReference() => new(this); | ||
| 101 | + | ||
| 96 | 102 | // Ensure that encapsulated Python object is decref'ed appropriately | |
| 97 | 103 | // when the managed wrapper is garbage-collected. | |
| 98 | 104 | ~PyObject() | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -35,6 +35,12 @@ internal PyType(in StolenReference reference, bool prevalidated = false) : base( | |||
| 35 | 35 | throw new ArgumentException("object is not a type"); | |
| 36 | 36 | } | |
| 37 | 37 | ||
| 38 | + /// <summary> | ||
| 39 | + /// Create a new PyType instance of this object, bumping the reference | ||
| 40 | + /// count. | ||
| 41 | + /// </summary> | ||
| 42 | + public new PyType NewReference() => new(this); | ||
| 43 | + | ||
| 38 | 44 | protected PyType(SerializationInfo info, StreamingContext context) : base(info, context) { } | |
| 39 | 45 | ||
| 40 | 46 | internal new static PyType? FromNullableReference(BorrowedReference reference) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -84,8 +84,18 @@ public unsafe static void tp_dealloc(NewReference lastRef) | |||
| 84 | 84 | DecrefTypeAndFree(lastRef.Steal()); | |
| 85 | 85 | } | |
| 86 | 86 | ||
| 87 | + /// <summary> | ||
| 88 | + /// Called during tp_clear before the GCHandle is released. | ||
| 89 | + /// Override to eagerly dispose Python object references (PyObject fields) | ||
| 90 | + /// held by the subclass, preventing the multi-hop .NET finalizer chain | ||
| 91 | + /// from delaying Python-side refcount decrements. | ||
| 92 | + /// </summary> | ||
| 93 | + protected virtual void OnClear() { } | ||
| 94 | + | ||
| 87 | 95 | public static int tp_clear(BorrowedReference ob) | |
| 88 | 96 | { | |
| 97 | + (GetManagedObject(ob) as ExtensionType)?.OnClear(); | ||
| 98 | + | ||
| 89 | 99 | var weakrefs = Runtime.PyObject_GetWeakRefList(ob); | |
| 90 | 100 | if (weakrefs != null) | |
| 91 | 101 | { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -18,14 +18,12 @@ internal class MethodBinding : ExtensionType | |||
| 18 | 18 | internal MaybeMethodInfo info; | |
| 19 | 19 | internal MethodObject m; | |
| 20 | 20 | internal PyObject? target; | |
| 21 | - internal PyType? targetType; | ||
| 21 | + internal PyType targetType; | ||
| 22 | 22 | ||
| 23 | - public MethodBinding(MethodObject m, PyObject? target, PyType? targetType = null) | ||
| 23 | + public MethodBinding(MethodObject m, PyObject? target, PyType targetType) | ||
| 24 | 24 | { | |
| 25 | 25 | this.target = target; | |
| 26 | - | ||
| 27 | - this.targetType = targetType ?? target?.GetPythonType(); | ||
| 28 | - | ||
| 26 | + this.targetType = targetType; | ||
| 29 | 27 | this.info = null; | |
| 30 | 28 | this.m = m; | |
| 31 | 29 | } | |
@@ -54,7 +52,7 @@ public static NewReference mp_subscript(BorrowedReference tp, BorrowedReference | |||
| 54 | 52 | } | |
| 55 | 53 | ||
| 56 | 54 | MethodObject overloaded = self.m.WithOverloads(overloads); | |
| 57 | - var mb = new MethodBinding(overloaded, self.target, self.targetType); | ||
| 55 | + var mb = new MethodBinding(overloaded, self.target?.NewReference(), self.targetType.NewReference()); | ||
| 58 | 56 | return mb.Alloc(); | |
| 59 | 57 | } | |
| 60 | 58 | ||
@@ -141,7 +139,7 @@ public static NewReference tp_getattro(BorrowedReference ob, BorrowedReference k | |||
| 141 | 139 | // FIXME: deprecate __overloads__ soon... | |
| 142 | 140 | case "__overloads__": | |
| 143 | 141 | case "Overloads": | |
| 144 | - var om = new OverloadMapper(self.m, self.target); | ||
| 142 | + var om = new OverloadMapper(self.m, self.target?.NewReference(), self.targetType.NewReference()); | ||
| 145 | 143 | return om.Alloc(); | |
| 146 | 144 | case "__signature__" when Runtime.InspectModule is not null: | |
| 147 | 145 | var sig = self.Signature; | |
@@ -249,7 +247,6 @@ public static NewReference tp_call(BorrowedReference ob, BorrowedReference args, | |||
| 249 | 247 | } | |
| 250 | 248 | } | |
| 251 | 249 | ||
| 252 | - | ||
| 253 | 250 | /// <summary> | |
| 254 | 251 | /// MethodBinding __hash__ implementation. | |
| 255 | 252 | /// </summary> | |
@@ -281,5 +278,12 @@ public static NewReference tp_repr(BorrowedReference ob) | |||
| 281 | 278 | string name = self.m.name; | |
| 282 | 279 | return Runtime.PyString_FromString($"<{type} method '{name}'>"); | |
| 283 | 280 | } | |
| 281 | + | ||
| 282 | + protected override void OnClear() | ||
| 283 | + { | ||
| 284 | + target?.Dispose(); | ||
| 285 | + targetType.Dispose(); | ||
| 286 | + target = null; | ||
| 287 | + } | ||
| 284 | 288 | } | |
| 285 | 289 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -197,7 +197,7 @@ public static NewReference tp_descr_get(BorrowedReference ds, BorrowedReference | |||
| 197 | 197 | && self.type.Value.IsInstanceOfType(obj.inst)) | |
| 198 | 198 | { | |
| 199 | 199 | var basecls = ReflectedClrType.GetOrCreate(self.type.Value); | |
| 200 | - return new MethodBinding(self, new PyObject(ob), basecls).Alloc(); | ||
| 200 | + return new MethodBinding(self, new PyObject(ob), basecls.NewReference()).Alloc(); | ||
| 201 | 201 | } | |
| 202 | 202 | ||
| 203 | 203 | return new MethodBinding(self, target: new PyObject(ob), targetType: new PyType(tp)).Alloc(); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -10,11 +10,13 @@ namespace Python.Runtime | |||
| 10 | 10 | internal class OverloadMapper : ExtensionType | |
| 11 | 11 | { | |
| 12 | 12 | private readonly MethodObject m; | |
| 13 | - private readonly PyObject? target; | ||
| 13 | + private PyObject? target; | ||
| 14 | + readonly PyType targetType; | ||
| 14 | 15 | ||
| 15 | - public OverloadMapper(MethodObject m, PyObject? target) | ||
| 16 | + public OverloadMapper(MethodObject m, PyObject? target, PyType targetType) | ||
| 16 | 17 | { | |
| 17 | 18 | this.target = target; | |
| 19 | + this.targetType = targetType; | ||
| 18 | 20 | this.m = m; | |
| 19 | 21 | } | |
| 20 | 22 | ||
@@ -42,7 +44,7 @@ public static NewReference mp_subscript(BorrowedReference tp, BorrowedReference | |||
| 42 | 44 | return Exceptions.RaiseTypeError(e); | |
| 43 | 45 | } | |
| 44 | 46 | ||
| 45 | - var mb = new MethodBinding(self.m, self.target) { info = mi }; | ||
| 47 | + var mb = new MethodBinding(self.m, self.target?.NewReference(), self.targetType.NewReference()) { info = mi }; | ||
| 46 | 48 | return mb.Alloc(); | |
| 47 | 49 | } | |
| 48 | 50 | ||
@@ -54,5 +56,12 @@ public static NewReference tp_repr(BorrowedReference op) | |||
| 54 | 56 | var self = (OverloadMapper)GetManagedObject(op)!; | |
| 55 | 57 | return self.m.GetDocString(); | |
| 56 | 58 | } | |
| 59 | + | ||
| 60 | + protected override void OnClear() | ||
| 61 | + { | ||
| 62 | + target?.Dispose(); | ||
| 63 | + targetType.Dispose(); | ||
| 64 | + target = null; | ||
| 65 | + } | ||
| 57 | 66 | } | |
| 58 | 67 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -983,9 +983,10 @@ def test_getting_generic_method_binding_does_not_leak_memory(memory_usage_tracki | |||
| 983 | 983 | bytesAllocatedPerIteration = pow(2, 20) # 1MB | |
| 984 | 984 | bytesLeakedPerIteration = processBytesDelta / iterations | |
| 985 | 985 | ||
| 986 | - # Allow 90% threshold - this shows the original issue is fixed, which leaks the full allocated bytes per iteration | ||
| 987 | - # Increased from 50% to ensure that it works on Windows with Python >3.13 | ||
| 988 | - failThresholdBytesLeakedPerIteration = bytesAllocatedPerIteration * 0.9 | ||
| 986 | + # Tight 10% threshold: with the fix the per-iteration leak is essentially | ||
| 987 | + # zero, while the bug retains the bulk of the 1 MB payload (~600 KB/iter | ||
| 988 | + # on 3.14 GIL). 100 KB/iter cleanly distinguishes the two states. | ||
| 989 | + failThresholdBytesLeakedPerIteration = bytesAllocatedPerIteration * 0.1 | ||
| 989 | 990 | ||
| 990 | 991 | assert bytesLeakedPerIteration < failThresholdBytesLeakedPerIteration | |
| 991 | 992 | ||
@@ -1025,8 +1026,8 @@ def test_getting_overloaded_method_binding_does_not_leak_memory(memory_usage_tra | |||
| 1025 | 1026 | bytesAllocatedPerIteration = pow(2, 20) # 1MB | |
| 1026 | 1027 | bytesLeakedPerIteration = processBytesDelta / iterations | |
| 1027 | 1028 | ||
| 1028 | - # Allow 90% threshold - this shows the original issue is fixed, which leaks the full allocated bytes per iteration | ||
| 1029 | - failThresholdBytesLeakedPerIteration = bytesAllocatedPerIteration * 0.9 | ||
| 1029 | + # Tight 10% threshold; see test_getting_generic_method_binding_does_not_leak_memory. | ||
| 1030 | + failThresholdBytesLeakedPerIteration = bytesAllocatedPerIteration * 0.1 | ||
| 1030 | 1031 | ||
| 1031 | 1032 | assert bytesLeakedPerIteration < failThresholdBytesLeakedPerIteration | |
| 1032 | 1033 | ||
@@ -1068,8 +1069,8 @@ def test_getting_method_overloads_binding_does_not_leak_memory(memory_usage_trac | |||
| 1068 | 1069 | bytesAllocatedPerIteration = pow(2, 20) # 1MB | |
| 1069 | 1070 | bytesLeakedPerIteration = processBytesDelta / iterations | |
| 1070 | 1071 | ||
| 1071 | - # Allow 90% threshold - this shows the original issue is fixed, which leaks the full allocated bytes per iteration | ||
| 1072 | - failThresholdBytesLeakedPerIteration = bytesAllocatedPerIteration * 0.9 | ||
| 1072 | + # Tight 10% threshold; see test_getting_generic_method_binding_does_not_leak_memory. | ||
| 1073 | + failThresholdBytesLeakedPerIteration = bytesAllocatedPerIteration * 0.1 | ||
| 1073 | 1074 | ||
| 1074 | 1075 | assert bytesLeakedPerIteration < failThresholdBytesLeakedPerIteration | |
| 1075 | 1076 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments