-
Notifications
You must be signed in to change notification settings - Fork 546
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Resolve some concurrency issues #1200
Conversation
internal static readonly ConcurrentDictionary<Type, ConstructorInfo> constructors = new ConcurrentDictionary<Type, ConstructorInfo> (); | ||
internal static readonly Dictionary<IntPtr, WeakReference> instances = new Dictionary<IntPtr, WeakReference> (); | ||
|
||
internal static readonly ReaderWriterLockSlim instancesLock = new ReaderWriterLockSlim (); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of pure locking and concurrent dictionaries, just use a reader/writer lock as that is what we actually want. And, when reading and we find that there is no handle, then we can upgrade in place.
internal static class HandleDictionary | ||
{ | ||
private static readonly Type IntPtrType = typeof (IntPtr); | ||
private static readonly Type BoolType = typeof (bool); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Caching on .NET Core is a major perf boost. Major as in 3-4x, but still in ns.
} | ||
|
||
var type = typeof (TSkiaImplementation); | ||
var constructor = constructors.GetOrAdd (type, t => GetConstructor (t)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anonymous delegates are cached rather than direct method references.
binding/Binding/SKObject.cs
Outdated
internal ConcurrentDictionary<IntPtr, SKObject> OwnedObjects => | ||
ownedObjects ??= new ConcurrentDictionary<IntPtr, SKObject> (); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lazy-instantiate this as it is hardly ever used. Typically for documents and some for some streams, codecs and typefaces.
@@ -12,18 +12,13 @@ | |||
<PackageReference Include="xunit" Version="2.4.1" /> | |||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> | |||
<PackageReference Include="XunitXml.TestLogger" Version="2.1.26" /> | |||
<PackageReference Include="xunit.assemblyfixture" Version="2.0.3" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This never worked for .NET Core anyway. Use the xUnit-approved way from the xUnit samples.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might be missing something, but at first sight getting OwnedObjects
and KeepAliveObjects
is not thread-safe, and can suffer from a race condition?
You could use double checked locking:
https://en.m.wikipedia.org/wiki/Double-checked_locking
Or maybe just Interlocked.CompareExchange
I could be wrong, I just briefly looked at the code, maybe some higher level locking is performed
Thanks for the review! Technically, the objects should only be referenced from a single thread at any given time. However, I will add some locking to prevent any future issues. |
- Lazy<T> still allocates, so not useful - Technically not needed due to objects not being thread-safe
It is highly likely that some parent object provides a reference to another child object, but then the parent destroys and replaces the child. In this case, the child will not own the handle. If there is a managed object that owns its handle, then it should throw.
binding/Binding/SKBitmap.cs
Outdated
return SkiaApi.sk_bitmap_peek_pixels (Handle, pixmap.Handle); | ||
var result = SkiaApi.sk_bitmap_peek_pixels (Handle, pixmap.Handle); | ||
if (result) | ||
Referenced (pixmap, this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The GC sometimes likes to collect the bitmap in use by a pixmap. This case is important because the bitmap OWNS the pixmap, and the reference is not counted. So a disposal destroys the pixmap.
@@ -251,6 +251,7 @@ public void SaveTo (Stream target) | |||
} finally { | |||
ArrayPool<byte>.Shared.Return (buffer); | |||
} | |||
GC.KeepAlive (this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just prevent the GC from going to town, because we are not actually using the object, but a value from one of the properties.
#if THROW_OBJECT_EXCEPTIONS | ||
internal static readonly ConcurrentBag<Exception> exceptions = new ConcurrentBag<Exception> (); | ||
#endif | ||
private readonly object locker = new object (); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A single locker as this doesn't need to be too complicated.
I think this is it. Fixed some other bugs. Concurrency due to construction and destruction, but also when the GC starts to collect things too soon. |
I have pushed what I think is now a good version to the preview feed: https://aka.ms/skiasharp-eap/index.json |
Will have a look shortly 👍 |
@Gillibald have you been able to have a look and see if things are better? I am thinking of just merging and pushing to nuget.org so that the mapsui folks can check. But, I just want to make sure that I get your opinion/hands on the pie here. |
Did not have time today. Could do tomorrow morning. In a few hours 😀. My current test creates and disposes millions of SKObjects. So minimal improvements should come out clearly. Mostly text related objects. |
Ah, millions? Have you noticed any random crashes or weird behavior at all 😄 The issues that mostly come up is with concurrent execution because objects were being created with the same handle as an object being destroyed. As a result, the managed side got all mixed up as it was busy nuking things and then it is added back, or worse the reverse. Most of the cases coming now are when people use the GL views because that is usually for high frequency updates and often on separate threads. So this would be interesting to see if you were seeing weird things, or even having to do some weird things to prevent weird things 😄 |
Luckily I only have to deal with single threaded processing. I might want to put text processing on a dedicated thread in the near future. |
The preview runs just fine. After this is merged I will work on the changes to SKObject.GetObject Do we really need to keep track of all SKObject instances? Ideally, we could just turn off caching. |
We have to track them because if I call I know @kekekeks was asking for a pointer-only API: #1205 But, this is not to say we can't actually have a look at certain APIs and see if we can just store things in a field. For example, a surface will always have the same canvas, so no need to even go through the interop. But this is not true for Overall, the slowest point of this library is the lookup. And this is made harder because some objects are reference counted, others are not. And then, some are reference counted, but not used like one. The API is not quite perfect. |
Thanks for having a look. Merging this now so I can get a preview out. |
Description of Change
Resolve some concurrency issues. In some cases the construction and/or destruction overlap and this causes invalid states. This PR improves the locking to avoid this, but still preserves the read and write level of locking.
It also includes a few other improvements, such as making sure objects that require their "parent" object to not be GC'ed add a reference to it. In most cases it is simple, such as a canvas being owned by a surface. But, in the case of a pixmap, the ownership is a little different in that they are copies, but actually require the parent object to stick around. So, the ownership is "reversed".
There is also a few other tweaks to improve general GC reliability/interop performance.
Bugs Fixed
API Changes
None.
Behavioral Changes
Less crashes due to correct object management.
PR Checklist