Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8083f3b
Finalizer for PyObject
amos402 Jun 23, 2018
af33e74
Avoid test interdependency
amos402 Jun 24, 2018
6d9f897
Add source to .csproj
amos402 Jun 24, 2018
7140fd0
Make sure recover the environment
amos402 Jun 24, 2018
f66697d
Add StackTrace of C# exception
amos402 Jun 24, 2018
799d37e
Clean up the test and interface
amos402 Jun 24, 2018
cb55163
Update CHANGELOG.md
amos402 Jun 24, 2018
bfc0392
Mono doesn't have GC.WaitForFullGCComplete
amos402 Jun 24, 2018
59b614d
Fixed PythonException leak
amos402 Jun 25, 2018
569cd94
Merge branch 'master' into pyobject-finalizer
den-run-ai Jul 5, 2018
f4f5032
Fixed nPython.exe crash on Shutdown
amos402 Jul 10, 2018
0967a12
Merge branch 'master' into pyobject-finalizer
filmor Jul 17, 2018
f071c55
Add error handler
amos402 Aug 4, 2018
cfda491
Merge branch 'master' into pyobject-finalizer
amos402 Aug 4, 2018
f6c6e42
Merge branch 'master' into pyobject-finalizer
filmor Aug 30, 2018
0d96641
Make collect callback without JIT
amos402 Sep 6, 2018
34713f7
Merge branch 'master' into pyobject-finalizer
filmor Oct 16, 2018
b4e30ac
Merge branch 'master' into pyobject-finalizer
filmor Oct 17, 2018
f836ffa
Merge remote-tracking branch 'remotes/upstream/master' into pyobject-…
amos402 Oct 20, 2018
a4bb82d
Add pending marker
amos402 Oct 23, 2018
21da86d
Merge branch 'master' into pyobject-finalizer
amos402 Oct 24, 2018
5254c65
Remove PYTHONMALLOC setting
amos402 Oct 25, 2018
916e85e
Merge remote-tracking branch 'remotes/upstream/master' into pyobject-…
amos402 Nov 22, 2018
eee3683
Fix ref count error
amos402 Nov 22, 2018
cee8e17
Add ref count check for helping discover the bugs of decref too much
amos402 Nov 22, 2018
247e2d9
Fix ref count error
amos402 Nov 25, 2018
90c67ca
typo error
amos402 Nov 26, 2018
6d68d70
Merge branch 'master' into pyobject-finalizer
filmor Mar 6, 2019
4eff81e
Merge branch 'master' into pyobject-finalizer
filmor Mar 29, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add ref count check for helping discover the bugs of decref too much
  • Loading branch information
amos402 committed Nov 22, 2018
commit cee8e17a51c1d37078b20b3c71066d3fe8ad3fbe
42 changes: 41 additions & 1 deletion src/embed_tests/TestFinalizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ namespace Python.EmbeddingTest
{
public class TestFinalizer
{
private string _PYTHONMALLOC = string.Empty;
private int _oldThreshold;

[SetUp]
Expand Down Expand Up @@ -195,5 +194,46 @@ public void ErrorHandling()
Finalizer.Instance.ErrorHandler -= handleFunc;
}
}

[Test]
public void ValidateRefCount()
{
if (!Finalizer.Instance.RefCountValidationEnabled)
{
Assert.Pass("Only run with FINALIZER_CHECK");
}
IntPtr ptr = IntPtr.Zero;
bool called = false;
Finalizer.IncorrectRefCntHandler handler = (s, e) =>
{
called = true;
Assert.AreEqual(ptr, e.Handle);
Assert.AreEqual(2, e.ImpactedObjects.Count);
// Fix for this test, don't do this on general environment
Runtime.Runtime.XIncref(e.Handle);
return false;
};
Finalizer.Instance.IncorrectRefCntResovler += handler;
try
{
ptr = CreateStringGarbage();
FullGCCollect();
Assert.Throws<Finalizer.IncorrectRefCountException>(() => Finalizer.Instance.Collect());
Assert.IsTrue(called);
}
finally
{
Finalizer.Instance.IncorrectRefCntResovler -= handler;
}
}

private static IntPtr CreateStringGarbage()
{
PyString s1 = new PyString("test_string");
// s2 steal a reference from s1
PyString s2 = new PyString(s1.Handle);
return s1.Handle;
}

}
}
8 changes: 4 additions & 4 deletions src/runtime/Python.Runtime.15.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@
<DefineConstants Condition="'$(CustomDefineConstants)' == ''">$(DefineConstants);PYTHON3;$(Python3Version);$(PythonMonoDefineConstants)</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'DebugMono'">
<DefineConstants Condition="'$(CustomDefineConstants)' == ''">$(DefineConstants);PYTHON2;$(Python2Version);$(PythonMonoDefineConstants);TRACE;DEBUG</DefineConstants>
<DefineConstants Condition="'$(CustomDefineConstants)' == ''">$(DefineConstants);PYTHON2;$(Python2Version);$(PythonMonoDefineConstants);FINALIZER_CHECK;TRACE;DEBUG</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'DebugMonoPY3'">
<DefineConstants Condition="'$(CustomDefineConstants)' == ''">$(DefineConstants);PYTHON3;$(Python3Version);$(PythonMonoDefineConstants);TRACE;DEBUG</DefineConstants>
<DefineConstants Condition="'$(CustomDefineConstants)' == ''">$(DefineConstants);PYTHON3;$(Python3Version);$(PythonMonoDefineConstants);FINALIZER_CHECK;TRACE;DEBUG</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'ReleaseWin'">
<DefineConstants Condition="'$(CustomDefineConstants)' == ''">$(DefineConstants);PYTHON2;$(Python2Version);$(PythonWinDefineConstants)</DefineConstants>
Expand All @@ -80,10 +80,10 @@
<DefineConstants Condition="'$(CustomDefineConstants)' == ''">$(DefineConstants);PYTHON3;$(Python3Version);$(PythonWinDefineConstants)</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'DebugWin'">
<DefineConstants Condition="'$(CustomDefineConstants)' == ''">$(DefineConstants);PYTHON2;$(Python2Version);$(PythonWinDefineConstants);TRACE;DEBUG</DefineConstants>
<DefineConstants Condition="'$(CustomDefineConstants)' == ''">$(DefineConstants);PYTHON2;$(Python2Version);$(PythonWinDefineConstants);FINALIZER_CHECK;TRACE;DEBUG</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'DebugWinPY3'">
<DefineConstants Condition="'$(CustomDefineConstants)' == ''">$(DefineConstants);PYTHON3;$(Python3Version);$(PythonWinDefineConstants);TRACE;DEBUG</DefineConstants>
<DefineConstants Condition="'$(CustomDefineConstants)' == ''">$(DefineConstants);PYTHON3;$(Python3Version);$(PythonWinDefineConstants);FINALIZER_CHECK;TRACE;DEBUG</DefineConstants>
</PropertyGroup>

<ItemGroup Condition=" '$(PythonInteropFile)' != '' ">
Expand Down
7 changes: 6 additions & 1 deletion src/runtime/delegatemanager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ A possible alternate strategy would be to create custom subclasses
too "special" for this to work. It would be more work, so for now
the 80/20 rule applies :) */

public class Dispatcher : IDisposable
public class Dispatcher : IPyDisposable
{
public IntPtr target;
public Type dtype;
Expand Down Expand Up @@ -276,6 +276,11 @@ public object TrueDispatch(ArrayList args)
Runtime.XDecref(op);
return result;
}

public IntPtr[] GetTrackedHandles()
{
return new IntPtr[] { target };
}
}


Expand Down
157 changes: 141 additions & 16 deletions src/runtime/finalizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,48 @@ struct PendingArgs
private delegate int PendingCall(IntPtr arg);
private readonly PendingCall _collectAction;

private ConcurrentQueue<IDisposable> _objQueue = new ConcurrentQueue<IDisposable>();
private ConcurrentQueue<IPyDisposable> _objQueue = new ConcurrentQueue<IPyDisposable>();
private bool _pending = false;
private readonly object _collectingLock = new object();
private IntPtr _pendingArgs;

#region FINALIZER_CHECK

#if FINALIZER_CHECK
private readonly object _queueLock = new object();
public bool RefCountValidationEnabled { get; set; } = true;
#else
public readonly bool RefCountValidationEnabled = false;
#endif
// Keep these declarations for compat even no FINALIZER_CHECK
public class IncorrectFinalizeArgs : EventArgs
{
public IntPtr Handle { get; internal set; }
public ICollection<IPyDisposable> ImpactedObjects { get; internal set; }
}

public class IncorrectRefCountException : Exception
{
public IntPtr PyPtr { get; internal set; }
private string _message;
public override string Message => _message;

public IncorrectRefCountException(IntPtr ptr)
{
PyPtr = ptr;
IntPtr pyname = Runtime.PyObject_Unicode(PyPtr);
string name = Runtime.GetManagedString(pyname);
Runtime.XDecref(pyname);
_message = $"{name} may has a incorrect ref count";
}
}

public delegate bool IncorrectRefCntHandler(object sender, IncorrectFinalizeArgs e);
public event IncorrectRefCntHandler IncorrectRefCntResovler;
public bool ThrowIfUnhandleIncorrectRefCount { get; set; } = true;

#endregion

private Finalizer()
{
Enable = true;
Expand Down Expand Up @@ -72,7 +109,7 @@ public List<WeakReference> GetCollectedObjects()
return _objQueue.Select(T => new WeakReference(T)).ToList();
}

internal void AddFinalizedObject(IDisposable obj)
internal void AddFinalizedObject(IPyDisposable obj)
{
if (!Enable)
{
Expand All @@ -84,7 +121,12 @@ internal void AddFinalizedObject(IDisposable obj)
// for avoiding that case, user should call GC.Collect manual before shutdown.
return;
}
_objQueue.Enqueue(obj);
#if FINALIZER_CHECK
lock (_queueLock)
#endif
{
_objQueue.Enqueue(obj);
}
GC.ReRegisterForFinalize(obj);
if (_objQueue.Count >= Threshold)
{
Expand All @@ -96,7 +138,7 @@ internal static void Shutdown()
{
if (Runtime.Py_IsInitialized() == 0)
{
Instance._objQueue = new ConcurrentQueue<IDisposable>();
Instance._objQueue = new ConcurrentQueue<IPyDisposable>();
return;
}
Instance.DisposeAll();
Expand Down Expand Up @@ -175,21 +217,29 @@ private void DisposeAll()
{
ObjectCount = _objQueue.Count
});
IDisposable obj;
while (_objQueue.TryDequeue(out obj))
#if FINALIZER_CHECK
lock (_queueLock)
#endif
{
try
{
obj.Dispose();
Runtime.CheckExceptionOccurred();
}
catch (Exception e)
#if FINALIZER_CHECK
ValidateRefCount();
#endif
IPyDisposable obj;
while (_objQueue.TryDequeue(out obj))
{
// We should not bother the main thread
ErrorHandler?.Invoke(this, new ErrorArgs()
try
{
obj.Dispose();
Runtime.CheckExceptionOccurred();
}
catch (Exception e)
{
Error = e
});
// We should not bother the main thread
ErrorHandler?.Invoke(this, new ErrorArgs()
{
Error = e
});
}
}
}
}
Expand All @@ -202,5 +252,80 @@ private void ResetPending()
_pendingArgs = IntPtr.Zero;
}
}

#if FINALIZER_CHECK
private void ValidateRefCount()
{
if (!RefCountValidationEnabled)
{
return;
}
var counter = new Dictionary<IntPtr, long>();
var holdRefs = new Dictionary<IntPtr, long>();
var indexer = new Dictionary<IntPtr, List<IPyDisposable>>();
foreach (var obj in _objQueue)
{
IntPtr[] handles = obj.GetTrackedHandles();
foreach (var handle in handles)
{
if (handle == IntPtr.Zero)
{
continue;
}
if (!counter.ContainsKey(handle))
{
counter[handle] = 0;
}
counter[handle]++;
if (!holdRefs.ContainsKey(handle))
{
holdRefs[handle] = Runtime.Refcount(handle);
}
List<IPyDisposable> objs;
if (!indexer.TryGetValue(handle, out objs))
{
objs = new List<IPyDisposable>();
indexer.Add(handle, objs);
}
objs.Add(obj);
}
}
foreach (var pair in counter)
{
IntPtr handle = pair.Key;
long cnt = pair.Value;
// Tracked handle's ref count is larger than the object's holds
// it may take an unspecified behaviour if it decref in Dispose
if (cnt > holdRefs[handle])
{
var args = new IncorrectFinalizeArgs()
{
Handle = handle,
ImpactedObjects = indexer[handle]
};
bool handled = false;
if (IncorrectRefCntResovler != null)
{
var funcList = IncorrectRefCntResovler.GetInvocationList();
foreach (IncorrectRefCntHandler func in funcList)
{
if (func(this, args))
{
handled = true;
break;
}
}
}
if (!handled && ThrowIfUnhandleIncorrectRefCount)
{
throw new IncorrectRefCountException(handle);
}
}
// Make sure no other references for PyObjects after this method
indexer[handle].Clear();
}
indexer.Clear();
}
#endif
}
}
29 changes: 28 additions & 1 deletion src/runtime/pyobject.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Dynamic;
using System.Linq.Expressions;

namespace Python.Runtime
{
public interface IPyDisposable : IDisposable
{
IntPtr[] GetTrackedHandles();
}

/// <summary>
/// Represents a generic Python object. The methods of this class are
/// generally equivalent to the Python "abstract object API". See
/// PY2: https://docs.python.org/2/c-api/object.html
/// PY3: https://docs.python.org/3/c-api/object.html
/// for details.
/// </summary>
public class PyObject : DynamicObject, IEnumerable, IDisposable
public class PyObject : DynamicObject, IEnumerable, IPyDisposable
{
#if TRACE_ALLOC
/// <summary>
/// Trace stack for PyObject's construction
/// </summary>
public StackTrace Traceback { get; private set; }
#endif

protected internal IntPtr obj = IntPtr.Zero;
private bool disposed = false;
private bool _finalized = false;
Expand All @@ -31,19 +44,29 @@ public class PyObject : DynamicObject, IEnumerable, IDisposable
public PyObject(IntPtr ptr)
{
obj = ptr;
#if TRACE_ALLOC
Traceback = new StackTrace(1);
#endif
}

// Protected default constructor to allow subclasses to manage
// initialization in different ways as appropriate.

protected PyObject()
{
#if TRACE_ALLOC
Traceback = new StackTrace(1);
#endif
}

// Ensure that encapsulated Python object is decref'ed appropriately
// when the managed wrapper is garbage-collected.
~PyObject()
{
if (obj == IntPtr.Zero)
{
return;
}
if (_finalized || disposed)
{
return;
Expand Down Expand Up @@ -158,6 +181,10 @@ public void Dispose()
GC.SuppressFinalize(this);
}

public IntPtr[] GetTrackedHandles()
{
return new IntPtr[] { obj };
}

/// <summary>
/// GetPythonType Method
Expand Down
Loading